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之前需要准备的信息
- 需要知道被hook方法的名字以及被hook方法定义所在的类
- 通过classname.class.getDeclaredMethod("methodname",args[i].class)方法获取被hook方法的Method对象;
- 得到被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,所以整体的代码逻辑是
- 将jstring类型的变量都转换成char *类型,存入到HookInfo结构体中。这一步会对HookInfo结构体中的classDesc,methodName,methodSig,isStaticMethod进行赋值
- 获取android虚拟机运行过程中加载的so文件,以此来判断android虚拟机是art模式还是dalvik模式。这里假定androidVM此时是ART运行时,往下继续分析代码。
- 从HookInfo结构体中可以获取到被hook方法的相关信息,包括classDesc(被hook方法定义所在的类),methodName(被hook方法的名字),methodSig(被hook方法的参数和返回值),isStaticMethod(被hook方法是否是静态方法)。
- 获取当前程序对应的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中。 - 获取到被hook方法的methodID之后,通过
reinterpret_cast<ArtMethod *>(methid)
获取被hook方法,存储在ArtMethod*类型的变量artmeth中 - 获取ArtMethod类型的对象之后,通过
GetEntryPointFromCompiledCode()
获取被hook方法原本的本地机器指令入口,然后将这个入口与我们hook之后指向的入口做比较,如果此时被hook方法指定的本地机器指令入口不是我们hook之后指向的入口,则继续走下边的流程;如果两个入口相同,则说明该方法已经被hook,程序运行结束。 - 被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
从这段汇编可以看出,在运行汇编之前,寄存器中的内容分布是第二种
这段汇编代码的逻辑是
- 保存环境,以便运行完成之后返回,执行下一条指令。
- 保存r1,r2,r3寄存器中的内容
- r0中的值不变,在两种存储规则中都是存储的method pointer
- 从r9中获取thread pointer,存储到r1中
- r2中存储的是参数指针,
- r3中存储原来的栈定sp
- 调用artQuickToDispatcher方法。
- 运行结束,返回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。