一種簡單的java函數hook思路
總結
本文取自《深入了解android java虛拟機art》第10章,在讀書過程中總結的一些能讓自己豁然開朗的點,通過這些點我發現了hook java函數的方法,有了此文。
LinkCode
LinkCode函數當中有個重要的過程,就是GetEntryPointFromQuickCompiledCode函數,他的傳回值也就是quick_code為空的話代表該方法沒有進行編譯,在oat檔案當中沒有對應的彙編代碼,需要進入解釋執行模式而不是jit,這裡有一個分支,如果ArtMethod是jni方法的話會将類似于elf檔案的ENTRYPOINT 設定為art_quick_generic_jni_trampoline,普通方法會設定為art_quick_to_interpreter_bridge,如下表(取自該書565頁),這裡是關鍵代表着我們可以通過修改這裡來完成java或jni函數的跳轉,也就是每次art虛拟機解釋執行某個java函數的時候,都會跳轉到該java函數的EntryPoint,那麼隻要把這裡替換成我們自己的c函數的位址,就可以實作攔截java函數了,當然也要保留原函數的 執行過程。

方案思考
了解了java函數大緻的執行流程,下一補就是實操了,那麼怎麼更改一個java函數的EntryPoint呢?這是一個困難的問題,首先提出一點
1:是否可以通過hook關鍵art函數的方式來獲得ArtMethod?
當然可以,但是我們要hook的是自身的dex,它的加載時機要早于我們的so加載進記憶體中,是以像LinkCode或者LoadMethod這種在dex加載流程上的函數是不行的,那就需要找一個系統函數,既有我們的函數的ArtMethod,又是在dex加載完成之後才進行的,這個就很難辦了,首先通過函數名過濾ArtMethod就很難,其次dex加載完成之後也很難找到覆寫所有ArtMethod的函數,是以這種方法不可行
2:是否可以像frida一樣Java choose記憶體搜尋ArtMethod?
倒是可以通過java.lang.reflect.Method這個類來進行記憶體漫遊,直接把Method類的執行個體全找出來,高版本可以再通過它父類的方法getArtMethod将它轉化為ArtMethod的指針,但是記憶體漫遊太難實作,可能一會會參考frida實作一下,目前不考慮,是以這種也不行
3:最終方案
最終的方案其實和frida差不多,回顧一下frida如何hook的java函數,先Java.use拿到類,然後用implementation直接覆寫原函數,那麼我們也可以參考jni反射執行java函數的思路,通過FindClass拿到類,再通過GetMethodID的方式拿到一個jmethodID
var MainActivity = Java.use("xxxxx");
MainActivity.xxxxx.implementation=function(){
return xxxx;
}
失敗的案例
那麼已經拿到jmethodID了,要如何轉化為ArtMethod呢,這一點可以通過安卓源碼來檢視在
http://androidxref.com/8.1.0_r33/xref/art/runtime/jni_internal.h
目錄,可以看到,它隻是做了一個強制類型轉換,那麼就相當于我們拿到了我們想hook函數對應的ArtMethod
static inline ArtMethod* DecodeArtMethod(jmethodID method_id) {
return reinterpret_cast<artmethod*>(method_id);
}
現在把他的EntryPoint改成我們想要的函數就好了,那麼就又産生了2個問題
1.EntryPoint在哪我如何能找到它
慶幸的是安卓源碼中有提供函數,也就是上面提到的GetEntryPointFromQuickCompiledCode那麼隻要通過ida檢視它的實作就好了,遺憾的是這是一個inline函數,沒有辦法使用ida檢視,隻能通過自己修改源碼,來檢視偏移了,然後直接脫出libart.so,檢視導出函數getmynative就好了
//ArtMethod.cc
extern "C" const void * getmynative(ArtMethod* m)REQUIRES_SHARED(Locks::mutator_lock_){
m->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
m->UnregisterNative();
const void* entry_point = m->GetEntryPointFromQuickCompiledCode();
if(m->IsFastNative())
return GetQuickGenericJniStub();
return entry_point;
}
這樣就知道了,偏移是40,那麼直接更改就好了
2.直接改會少很多系統函數的調用能否成功
這個從理論上來說,是一定不成功的,看書上後面還有許多的過程,包括參數、棧的儲存,都沒有做,一定不可能成功,但是也不妨試一下,沒準歪打正着了呢,第一版代碼如下
void* func(void* a,void* b,int c,int d){
__android_log_print(6,"r0ysue","i am success");
return reinterpret_cast<void *="">(5);
}
jclass a=env->FindClass(classname);
jmethodID b=env->GetMethodID(a,methodname, shoty);
*((long *)a1 + 5)= reinterpret_cast<long>(func);
居然沒拉稀,和之前猜的不太一樣,雖然成功執行了我們自己的函數,但是要思考一個問題就是如何執行原函數,這裡如果通過反射直接執行的話,我們拿不到參數是以這種方案有點垃圾,我們還需要繼續改進
進階
既然上面的方案有瑕疵,就是有很多函數沒有執行,導緻我們拿不到參數,那麼可以采取下一個,就是模仿Native函數的方式,将本函數改成Native函數,然後再使用将Native函數注冊就好了,那麼根據上面第一節的内容隻要将EntryPoint改成art_quick_generic_jni_trampoline,就好了,再次用ida打開libart.so,發現art_quick_generic_jni_trampoline是一個函數,但它不是一個導出函數,使用導出表沒有辦法找到它,那麼就用上篇文章提到的節頭表索引發來搞定,最後再用RegisterNatives注冊函數位址就好了
int so=findsym("/system/lib64/libart.so","art_quick_generic_jni_trampoline");
*((long *)a1 + 5)= reinterpret_cast<long>((char *) startr +so - 0x25000);//需要-0x25000是因為libart.so的程式頭在偏移為0x25000這裡沒周遊偷懶了
env->RegisterNatives(a,getMethods,1);
這樣就完成了Java函數的Native化,那麼試驗一下好不好使,果然不行提示不是native函數無法完成注冊,那麼就看一下如何的判斷是否是Native函數,之前編譯rom的時候我就加入了,Native相關的判斷,可以看到,公式
(~*(_DWORD *)(a1 + 4) & 0x80100)
,就是判斷是否是native函數的,隻要它不等0,那麼就可以認為我們的函數是Native函數.
那麼我們就可以構造了,直接做異或就好了
*(_QWORD *)(a1 + 4)= *(_QWORD *)(a1 + 4)^0x80100 ;
這樣準備就完成了,我們就可以像動态注冊的jni函數一樣,去實作我們的c層的add函數
,執行就正常了,這裡不給大家貼圖了,因為還有最後一個問題沒解決,就是如何執行原函數。
實作原函數
c調用java函數隻有一種方式,那就是反射,是以我們隻能用反射,那麼就要收集參數了,比如我下面的這個函數,我需要一個執行個體,一個env,2個參數
public int add2(int a,int b){
return 7;
}
幸運的是,我們是動态注冊的,動态注冊的執行個體函數會自帶env和this執行個體,是以我們直接調用就好了
void* add(void* a,void* b,int c,int d){
JNIEnv *st=(JNIEnv *)a;
jclass a2=st->FindClass("com/r0ysue/myjavahook/MainActivity");
jmethodID b2=st->GetMethodID(a2,"add2", "(II)I");
int yy= st->CallIntMethod(static_cast<jobject>(b), b2, c, d);
__android_log_print(6,"r0ysue","%x",yy);
return reinterpret_cast<void *="">(5);
}
直接死循環爆棧了........,太垃圾了,還是思路沒找好,想想也是,我沒有将函數恢複就直接反射,肯定會再次調用的,是以要想一個辦法将ArtMethod恢複為當初java函數的樣子,這裡我又用了全局變量(沒錯就是inlinehook 時候坑了我那麼久的東西,是以一會還要解決多函數的hook問題)
這裡方案是這樣的,将hook函數中儲存的jump和nativ,直接複原,但是這裡我沒想好怎麼搞,是以baocun函數要一個參數,就是ArtMethod指針,儲存完還要回複到執行Native函數的狀态
void hook(){
....
nativ=*(_QWORD *)(a1 + 4);//isNative()?
jump=*((long *)a1 + 5);//EntryPoint
....
}
void huifu(__int64 a,int n){
*((long *)a + 5)=old1;
*(_QWORD *)(a + 4)=old2;
}
void baocun(__int64 a){
old1=*((long *)a + 5);
old2=*(_QWORD *)(a + 4);
*((long *)a + 5)= jump;
*(_QWORD *)(a + 4)= nativ|0x80100;
}
void* add(void* a,void* b,int c,int d){
JNIEnv *st=(JNIEnv *)a;
jclass a2=st->FindClass("com/r0ysue/myjavahook/MainActivity");
jmethodID b2=st->GetMethodID(a2,"add2", "(II)I");
baocun((__int64)b2);
int yy= st->CallIntMethod(static_cast<jobject>(b), b2, c, d);
__android_log_print(6,"r0ysue","%x",yy);
huifu((__int64)b2)
return reinterpret_cast<void *="">(5);
}
hook 多個函數
void baocun(__int64 a,int n){
old1[n]=*((long *)a + 5);
old2[n]=*(_QWORD *)(a + 4);
*((long *)a + 5)= jump[n];
// *(_QWORD *)(a + 4)=0x1d8ee408000101;
*(_QWORD *)(a + 4)= nativ[n]|0x80100;
}
void huifu(__int64 a,int n){
*((long *)a + 5)=old1[n];
*(_QWORD *)(a + 4)=old2[n];
}
void hook(){
...
nativ[ns]=*(_QWORD *)(a1 + 4);
jump[ns]=*((long *)a1 + 5);
...
}