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(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(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。

你可能感兴趣的:(allhookinone)