一種簡單的java函數hook思路


一種簡單的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都沒有問題了,也不好奔潰

hook 多個函數

但是當hook多個函數的時候就有問題了,我用的是全局變量,第二次hook的時候會覆蓋第一次的baocun()和huifu()函數,所以我又采用了數組的方式來保存,這部分代碼很簡單就直接貼了

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);

...
}

總結

這樣一個java層的hook框架就完成了,開始寫的時候感覺比inlinehook難,完成之后發現代碼量好少,原理就是從書上得到的,比較簡單,主要獲得ArtMethod不需要hook就節省了許多,jni方法太好用了</artmethod*>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM