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。