allhookinone

struct HookInfo{
    char * classDesc;//被hook方法聲明所在的類.
    char * methodName;//被hook方法名字
    char * methodSig;//被hook方法的參數和返回值例如,如果被hook方法是`private String getString(int a,String b)`則此時對應的methodSig為`(ILjava/lang/String;)Ljava/lang/String;`
    
    //for dalvik jvm
    bool isStaticMethod;//
    void * originalMethod;
    void * returnType;
    void * paramTypes;

    //for art jvm
    const void * nativecode;
    const void * entrypoint;
}

java hook思路

在hook之前需要準備的信息

  1. 需要知道被hook方法的名字以及被hook方法定義所在的類
  2. 通過classname.class.getDeclaredMethod("methodname",args[i].class)方法獲取被hook方法的Method對象;
  3. 得到被hook方法的Method對象之后,可以通過Method類提供的方法獲取更多的被hook方法的信息。具體是clasdes(被Hook方法的聲明所在的類,通過method.getDeclaringClass().getName()方法獲得),methodname(通過method.getName()方法獲得),methodsig(methodsig中存儲的是被hook方法的參數和返回值).

hook流程

在進行hook時,主要思路是修改被hook方法對應的ArtMethod對象中的EntryPointFromCompiledCode()即被hook方法的本地機器指令入口。修改該入口,使得該入口指向我們編寫的函數,在執行完成之后跳轉到被hook函數,從而完成整個hook過程。
由于java代碼在生成本地機器指令時,是通過dex2oat程序完成的,而native層代碼生成本地機器指令時則是通過gcc生成,這兩套工具生成的機器指令規則不同,所以在相互調用時,需要去調整寄存器中的內容。所以在修改被hook方法時,需要對寄存器中內容進行調整。
兩種工具生成的本地機器指令對應的寄存器的內容如下所示

r0=method pointer                                                  |r0=method pointer
r1=thread pointer                                                  |r1=arg1
r2=args array                                                      |r2=arg2
r3=old sp                                                          |r3=arg3
[sp]=entrypoint                                                    |r9=thread pointer
        1                                                         |[sp]=method pointer
                                                                   |[sp+4]=addr of thiz
                                                                   |[sp+8]=addr of arg1
                                                                   |[sp+12]=addr of arg2
                                                                   |[sp+16]=addr of arg3 
                                                                                2

具體代碼分析

代碼入口點hookMethodNative(String clsdes,String methodname,String methodsig,boolean isstatic)
對應的native層方法是java_com_example_allhookinone_HookUtils_hookMethodNative(JNIEnv *env,jobject thiz,jstring cls,jstring methodname,jstring methodsig,jboolean isstatic)方法
java語言中的String對應到native層是jstring,所以整體的代碼邏輯是

  1. 將jstring類型的變量都轉換成char *類型,存入到HookInfo結構體中。這一步會對HookInfo結構體中的classDesc,methodName,methodSig,isStaticMethod進行賦值
  2. 獲取android虛擬機運行過程中加載的so文件,以此來判斷android虛擬機是art模式還是dalvik模式。這里假定androidVM此時是ART運行時,往下繼續分析代碼。
  3. 從HookInfo結構體中可以獲取到被hook方法的相關信息,包括classDesc(被hook方法定義所在的類),methodName(被hook方法的名字),methodSig(被hook方法的參數和返回值),isStaticMethod(被hook方法是否是靜態方法)。
  4. 獲取當前程序對應的JNIEnv。然后通過JNIEnv中提供的JNI方法得到被hook方法對應的ArtMethod對象。具體的獲取方法是
    4.1 通過env->FindClass(classDesc)方法獲取被hook方法所在的類,存儲在jclass類型的變量claxx中。
    4.2 判斷被hook方法是否是靜態方法(通過第一步中獲取的isStaticMethod判斷),如果是靜態方法則通過env->GetStaticMethodID(claxx,methodName,methodSig)獲取被hook方法的MethodID,存儲在jmethodID類型的變量methid中;如果是非靜態方法,則通過env->GetMethodID(claxx,methodName,methodSig)獲取被hook方法的MethodID,同樣存儲在jmethodID類型的變量methid中。
  5. 獲取到被hook方法的methodID之后,通過reinterpret_cast<ArtMethod *>(methid)獲取被hook方法,存儲在ArtMethod*類型的變量artmeth中
  6. 獲取ArtMethod類型的對象之后,通過GetEntryPointFromCompiledCode()獲取被hook方法原本的本地機器指令入口,然后將這個入口與我們hook之后指向的入口做比較,如果此時被hook方法指定的本地機器指令入口不是我們hook之后指向的入口,則繼續走下邊的流程;如果兩個入口相同,則說明該方法已經被hook,程序運行結束。
  7. 被hook方法對應的artmethod對象中指定的本地機器指令入口不是hook之后指向的入口
    7.1 獲取此時被hook方法對應的artmethod對象中指定的本地機器指令入口,存儲在新定義的變量entrypoint中
    7.2 將entrypoint賦值給HookInfo結構體中的entrypoint變量
    7.3 將當前被hook方法對應的artmethod對象中指定的NativeMethod賦值給HookInfo結構體中的nativecode
    7.4重新設置被hook方法對應的artmethod對象中的EntryPointFromCompiledCode,將這個值設置為art_quick_dispatcher。
    7.5重新設置artmethod對象中的nativemethod。

修改完成之后,當執行被hook方法時,由于已經將被hook方法的本地機器指令入口設置為art_quick_dispatcher,所以執行到被hook方法時會去運行art_quick_dispatcher。
art_quick_dispatcher是由匯編代碼實現的。如下所示。

 ENTRY art_quick_dispatcher
    push    {r4, r5, lr}           @ sp - 12
    mov      r0, r0                @ pass r0 to method
    str   r1, [sp, #(12 + 4)]
    str      r2, [sp, #(12 + 8)]
    str      r3, [sp, #(12 + 12)]
    mov   r1, r9                   @ pass r1 to thread
    add      r2, sp, #(12 + 4)     @ pass r2 to args array
    add   r3, sp, #12              @ pass r3 to old SP
    blx      artQuickToDispatcher   @ (Method* method, Thread*, u4 **, u4 **)
    pop      {r4, r5, pc}          @ return on success, r0 and r1 hold the result
END art_quick_dispatcher

從這段匯編可以看出,在運行匯編之前,寄存器中的內容分布是第二種

這段匯編代碼的邏輯是

  1. 保存環境,以便運行完成之后返回,執行下一條指令。
  2. 保存r1,r2,r3寄存器中的內容
  3. r0中的值不變,在兩種存儲規則中都是存儲的method pointer
  4. 從r9中獲取thread pointer,存儲到r1中
  5. r2中存儲的是參數指針,
  6. r3中存儲原來的棧定sp
  7. 調用artQuickToDispatcher方法。
  8. 運行結束,返回pc,從pc中可以獲取下一條指令

artQuickToDispatcher方法

artQuickToDispatcher方法主要完成的工作包括兩點

  • 運行我們添加的before hook method,
  • 調整寄存器中的內容,將寄存器中的內容從第一種修改成第二種之后調用被hook方法原本的本地機器指令

artQuickToDispatcher方法的代碼如下所示

extern "C" uint64_t artQuickToDispatcher(ArtMethod* method, Thread *self, u4 **args, u4 **old_sp){
    HookInfo *info = (HookInfo *)method->GetNativeMethod();
    LOGI("[+] entry ArtHandler %s->%s", info->classDesc, info->methodName);

    // If it not is static method, then args[0] was pointing to this
    if(!info->isStaticMethod){
        Object *thiz = reinterpret_cast<Object *>(args[0]);
        if(thiz != NULL){
            char *bytes = get_chars_from_utf16(thiz->GetClass()->GetName());
            LOGI("[+] thiz class is %s", bytes);
            delete bytes;
        }
    }
//從這個函數開始到這里為止的這一段代碼相當于before hook方法

    const void *entrypoint = info->entrypoint;//info中存儲的是被hook方法原本的本地機器指令入口
    method->SetNativeMethod(info->nativecode); //restore nativecode for JNI method
    uint64_t res = art_quick_call_entrypoint(method, self, args, old_sp, entrypoint);//調整寄存器內容,從第一種調整成第二種,調整之后會通過blx跳轉到被hook方法原本的本地機器指令區執行

    JValue* result = (JValue* )&res;
    if(result != NULL){
        Object *obj = result->l;
        char *raw_class_name = get_chars_from_utf16(obj->GetClass()->GetName());

        if(strcmp(raw_class_name, "java.lang.String") == 0){
            char *raw_string_value = get_chars_from_utf16((String *)obj);
            LOGI("result-class %s, result-value \"%s\"", raw_class_name, raw_string_value);
            free(raw_string_value);
        }else{
            LOGI("result-class %s", raw_class_name);
        }

        free(raw_class_name);
    }

    // entrypoid may be replaced by trampoline, only once.
//  if(method->IsStatic() && !method->IsConstructor()){

    entrypoint = method->GetEntryPointFromCompiledCode();
    if(entrypoint != (const void *)art_quick_dispatcher){
        LOGW("[*] entrypoint was replaced. %s->%s", info->classDesc, info->methodName);

        method->SetEntryPointFromCompiledCode((const void *)art_quick_dispatcher);
        info->entrypoint = entrypoint;
        info->nativecode = method->GetNativeMethod();
    }

    method->SetNativeMethod((const void *)info);

//  }

    return res;
}

具體的代碼分析見注釋

art_quick_call_entrypoint是由匯編代碼實現的。如下所示。

 ENTRY art_quick_call_entrypoint
    push    {r4, r5, lr}           @ sp - 12
    sub     sp, #(40 + 20)         @ sp - 40 - 20
    str     r0, [sp, #(40 + 0)]    @ var_40_0 = method_pointer
    str     r1, [sp, #(40 + 4)]    @ var_40_4 = thread_pointer
    str     r2, [sp, #(40 + 8)]    @ var_40_8 = args_array
    str     r3, [sp, #(40 + 12)]   @ var_40_12 = old_sp
    mov     r0, sp
    mov     r1, r3
    ldr     r2, =40
    blx     memcpy                 @ memcpy(dest, src, size_of_byte)
    ldr     r0, [sp, #(40 + 0)]    @ restore method to r0
    ldr     r1, [sp, #(40 + 4)]
    mov     r9, r1                 @ restore thread to r9
    ldr     r5, [sp, #(40 + 8)]    @ pass r5 to args_array
    ldr     r1, [r5]               @ restore arg1
    ldr     r2, [r5, #4]           @ restore arg2
    ldr     r3, [r5, #8]           @ restore arg3
    ldr     r5, [sp, #(40 + 20 + 12)] @ pass ip to entrypoint
    blx     r5
    add     sp, #(40 + 20)
    pop     {r4, r5, pc}           @ return on success, r0 and r1 hold the result
 END art_quick_call_entrypoint

在運行這段匯編之前,寄存器中的內容分布是第一種

這段匯編代碼的邏輯是
1.保存環境,以便運行完成之后返回,執行下一條指令。
2.保存r0,r1,r2,r3寄存器中的內容
3.調用memcpy申請一個空間。memcpy的參數是dest,src,size_of_byte,所以在調用memcpy之前,首先要將函數的參數存儲到r0,r1,r2中。
4.重新給r0,r1,r2,r9等寄存器賦值,使得寄存器按照第二種方式。
5.調用entrypoint。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容