Frida inlineHook原理分析及簡單設計一款AArch64 inlineHook工具
近期突然發現64位APP分析需求激增,然而手邊好用的 inlineHook 只有 Frida 一款,所以打算稍微研究下 Frida 的思路,以作借鑒,然后寫一款滿足簡單自用需求的 AArch64 inlineHook 工具。
一
Frida inlineHook 思路分析
根據之前開發 AArch32 inlineHook 框架的經驗,總結 inlineHook 框架開發的幾個關鍵點大抵如下:
- 動態替換需要 Hook 的指令片段為一段經過設計的跳板指令,即 trampoline ,目標為我們設計好的一段 shellCode
- 在內存中設計并生成一段 shellCode ,這是我們的可控 shellCode ,在該 shellCode 中需要實現 Hook 的功能函數(即打印/替換-參數/結果)
- shellCode 的設計原則是保持 Hook 前后的棧平衡,并保護寄存器狀態(即Hook結束后,保持與Hook開始前一致的棧布局與寄存器狀態)
- 在 shellCode 中完成原函數的執行工作,被替換的掉的指令中若包含計算 PC-relative address ( 如 Branch 指令 ),需要對其正確解析執行
對我來說一個簡單的工具只要滿足前3點就足夠了,第4點待后續優化的時候再行完善,所以我們接下來看看 Frida 是如何完成以上這幾點的。
Step1:
首先我們簡單編寫一個 com.example.x64 應用作為目標 APP,且在 libx64.so 中放置一個 native 函數: Java_com_example_x64_JNI_aal ,馬上使用 Frida Hook
存在以下兩種情況:
1、Frida Hook 函數開頭指令(即直接 Hook 導出函數)
2、Hook 函數中間指定位置的指令

Frida 代碼如下:
//## hookTest1: Hook 導出函數->Java_com_example_x64_JNI_aalfunction hookTest1() { var helloAddr = Module.findExportByName("libx64.so", "Java_com_example_x64_JNI_aal"); console.log(helloAddr); if(helloAddr != null){ Interceptor.attach(helloAddr,{ onEnter: function(args){ console.log("hook1 on enter"); }, onLeave: function(retval){ console.log("hook1 on leave"); } }); }}//## hookTest2: Hook 指定位置->0x000000000000BBA0function hookTest2() { var libutilityAddr = Module.findBaseAddress("libx64.so"); var getOriginalStringAddr = libutilityAddr.add(0x000000000000BBA0); console.log(getOriginalStringAddr); if(getOriginalStringAddr != null){ Interceptor.attach(getOriginalStringAddr,{ onEnter: function(args){ console.log("hook2 on enter"); }, onLeave: function(retval){ console.log("hook2 on leave"); } }); }}
Hook 完畢,執行結果如下:

不知道為什么打出來了兩次 hook1 on leave ,之后我使勁檢查代碼,確定并沒有寫錯。猜測原因或許是Hook點2在最終返回值的設置上出現了什么問題吧。我們暫時忽略上面的問題,接下來分析這兩個地址上的指令發生了甚么變化。
Step2:
掛上我們的調試器,首先對 Hook1 進行分析:Hook1 對應 Java_com_example_x64_JNI_aal 函數的入口位置( 0x7fac430b70 ),可以看到前16字節已經被替換掉,新指令為利用 x16 寄存器制作的一個跳板(trampoline),其目標為 0x7face7c600:

Hook2 情況與 Hook1 類似,也是生成了16字節的跳板指令(依然使用 x16 寄存器)來替換掉 0xBBA0 位置的16字節原始指令,在此不做展示。
P.S. > 在后續多次測試中發現,偶爾也會出現使用單條 Branch 指令(4字節)來替換掉被 Hook 地址的單條指令(4字節)的情況發生,如下圖所示。

因為 Branch 指令存在跳轉范圍(+-128MB),所以 Frida 使用這種形式的 trampoline 需要對被 Hook 地址前后 128MB 范圍進行檢測,尋找空閑地址,不過這對本文實現一個簡單的 inlineHook 模型并無太大影響,故不做深入討論。
其實 Frida 還有一種跳轉范圍擴大至 +-4GB 大小的 trampoline 生成規則,在此也不做討論了,因為在原理上大同小異,單純屬于細節優化問題。
另外還有不得不提的一點,當 trampoline 使用 x16 寄存器作為跳板寄存器時,Hook 結束后 x16 寄存器無疑會被污染,然而事實上 Frida 同時使用了 x16 與 x17 寄存器,那么關于這兩個寄存器有什么說法呢?官方對這兩個寄存器作用的描述如下:

描述中提到 x16、x17 寄存器作為內部過程調用中的臨時寄存器,結合下圖便能更好的理解官方的定義。

關于 trampoline 的研究就到此為止了,接下來我們看他生成的 shellCode。
Step3:
接下來我們開始分析 shellcode 部分,以 Hook1 為例。
Java_com_example_x64_JNI_aal 函數入口: 0x7fac430b70
入口處 trampoline 匯編代碼如下:

進入 0x7face7c600 位置,分析如下圖:

首先 mmap 了一段匿名內存( 7face7c000-7face83000 rwxp ),在 0x7face7c600 位置放置了以下幾條匯編指令構成第二段跳板。
> ldr x17, =0x7facec12e0
> ldr x16, =0x7face7c000
> br x16
其中 x17 寄存器裝載了一個地址( 0x7facec12e0 ),這個地址內部保存著 0x7fac430b70 ,正是 Java_com_example_x64_JNI_aal 函數入口地址。
而 x16 寄存器裝載了此番生成的 shellCode 的地址( 0x7face7c000 ),將該段內存 dump 下來,拖入 ida 進行分析:

綠色、藍色部分合并完成了棧平衡、寄存器保護與恢復工作。
我們在外部用 JS 編寫的 Hook 功能代碼( onEnter 部分 ),由 BLR X4 ( 0x7F7D8D8360 ) 跳轉至 frida-agent-64.so (見下圖)來完成。
在 JS 中可以打印,甚至修改函數入參的原因是因為入參(前8個在 X0-X7 寄存器上,后面的在棧上)已全部由綠色塊指令壓入棧中保存,所以在 BLR X4 進行函數調用時,合理設置 X0-X3 寄存器,使其正確的指向棧上某位置尤為關鍵。

我們接下來在 shellcode 最后一條 BR X16 指令上插入斷點,分析函數的運行情況。

當斷點觸發時 BR X16 欲跳轉至內存 0x7face7c630,其對應的匯編代碼如上圖所見,其中包含 Java_com_example_x64_JNI_aal 函數開頭被替換的4條原始指令。
之后再次使用 x16 寄存器跳轉至 0x7fac430b80,即函數 Java_com_example_x64_JNI_aal 開頭偏移 0x10 的位置,以完成原函數的執行動作。
此時 hook1 on enter 打印完畢,但 hook1 on leave 還未打印,所以注意到 x30 寄存器中保存的返回地址是 0x7face7c60c,即前文中暫未分析的第三段跳板指令,匯編代碼如下:
> ldr x17, =0x7facec12e0
> ldr x16, =0x7face7c100
> br x16
x17 寄存器行為與之前一致,x16 寄存器裝載了第二段 shellCode 的地址( 0x7face7c100 ),剛才已經一起 dump 下來了,直接在 ida 分析。

綠色、藍色部分代碼作用不變,由 BLR X3 ( 0x7F7D8D86C8 ) 跳轉至 frida-agent-64.so 來完成外部 JS 寫的 Hook 功能代碼中 onLeave 的部分。
最后由 BR X16 返回 Java_com_example_x64_JNI_aal 函數被調用時真正的 LR。
至此 shellcode 部分也大體分析完畢了,此時我們應該能夠寫出一款簡單的 AArch64 inlineHook 工具模型了。
二
AArch64 inlineHook 開發
結合前文的分析,我們的 inlineHook 應該具備以下這幾點功能:
- Hook 導出函數:即在函數開頭進行 Hook ,能夠執行原函數,并提供 onEnter 以及 onLeave 兩層代碼注入點,達到類似 Frida 那種 "代碼托管" 一樣的效果
- Hook 函數內指定地址:Hook 指定位置的匯編指令,僅提供 onEnter 一層代碼注入點,因為考慮到在指定位置上 X30( LR ) 寄存器可能已經發生變化,此時用該寄存器做返回判斷并不準確,故放棄 onLeave
- 在 onEnter 中提供入參的打印/修改操作 ( 本質是寄存器/堆棧內存打印/修改操作 )
- 在 onLeave 中提供返回值的打印/修改操作 ( 本質是寄存器/堆棧內存打印/修改操作 )
有了以上幾點需求,我們現在可以開始開發了 ( 源碼下載見文章末尾 )
Step1:
我們首先來設計 shellcode 部分,在本簡易版工具中,我們的跳板指令選擇使用 x16 寄存器的 16 字節 trampoline ,代碼如下:
_trampoline_: LDR X16, x64code0 BR X16x64code0:_jmp_addr_: .dword 0x1111111111111111
接下需要做參數和返回地址入棧工作,以及全寄存器狀態保護,代碼如下:

接下來調用 Hook 功能函數的 onEnter 部分,并恢復寄存器及棧狀態,最后取出返回地址并返回原函數執行。

對于 onLeave 部分的 shellcode 與之大體類似,就不貼圖展示了。
Step2:
接下來開始編寫函數完成 inlineHook 的插入
//## Hook目標函數extern "C" JNIEXPORT jstring JNICALLJava_com_cs_inline_MainActivity_stringFromJNI( JNIEnv* env, jobject /* thisobj */, jstring jstr) { std::string hello = "Hello from C++: "; hello.append(env->GetStringUTFChars(jstr, nullptr)); return env->NewStringUTF(hello.c_str());} //## 該函數內部完成了對Java_com_cs_inline_MainActivity_stringFromJNI函數的inlineHookextern "C" JNIEXPORT void JNICALLJava_com_cs_inline_MainActivity_inlineHook1(JNIEnv* env, jobject /* thisobj */){ //## Hook target函數為:Java_com_cs_inline_MainActivity_stringFromJNI u_long func_addr = (u_long)Java_com_cs_inline_MainActivity_stringFromJNI; extern u_long _shellcode_start_, _the_func_addr_, _end_func_addr_, _ori_ins_set1_, _retback_addr_, _shellcode_end_, _trampoline_, _jmp_addr_, _shellcode_part2_; //## 計算shellcode整體長度 u_long total_len = (u_long)&_shellcode_end_ - (u_long)&_shellcode_start_; LOGD(ANDROID_LOG_DEBUG, "[+] ShellCode len: %d, target func: %p", total_len, func_addr); //## 使用mmap分配匿名內存存放shellcode u_long page_size = getpagesize(); u_long shellcode_mem_start = (u_long)mmap(0, page_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); memset((void *)shellcode_mem_start, 0, page_size); memcpy((void *)shellcode_mem_start, (void *)&_shellcode_start_, total_len); LOGD(ANDROID_LOG_DEBUG, "[+] shellcode_mem_start: %p", shellcode_mem_start); //## 設置trampoline跳轉的目標地址 *(u_long*)&_jmp_addr_ = shellcode_mem_start; u_long mem_the_func_addr_ = (u_long)&_the_func_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start; u_long mem_end_func_addr_ = (u_long)&_end_func_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start; u_long mem_ori_ins_set1_ = (u_long)&_ori_ins_set1_ - (u_long)&_shellcode_start_ + shellcode_mem_start; u_long mem_retback_addr_ = (u_long)&_retback_addr_ - (u_long)&_shellcode_start_ + shellcode_mem_start; if(!off_shellcode_part2_) off_shellcode_part2_ = (u_long)&_shellcode_part2_ - (u_long)&_shellcode_start_; //## 設置onEnter及onLeave函數 *(u_long*)mem_the_func_addr_ = (u_long)on_enter_1; *(u_long*)mem_end_func_addr_ = (u_long)on_leave_1; //## 設置返回地址為距離Hook點0x10長度的指令地址,即偏移為trampoline的長度 *(u_long*)mem_retback_addr_ = (u_long)func_addr + 0x10; //## 原指令保存,并未做任何解析,PC-relative address相關指令暫不支持 *(u_long*)mem_ori_ins_set1_ = *(u_long*)func_addr; *(u_long*)(mem_ori_ins_set1_ + 8) = *(u_long*)(func_addr + 8); //## 頁權限修改并完成inlineHook u_long entry_page_start = (u_long)(func_addr) & (~(page_size-1)); mprotect((u_long*)entry_page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC); *(u_long*)func_addr = *(u_long*)&_trampoline_; *(u_long*)(func_addr + 8) = *(u_long*)(((u_long)&_trampoline_) + 8);
inlineHook1 函數主要作用是分配 shellcode 的內存及設置其中的關鍵數據,并使用 trampoline 替換原指令完成 Hook,函數內注釋較為詳細,就不做過多解釋了。
最后我們來編寫 onEnter 及 onLeave 函數。
//## 使用線程局部存儲保存原始返回地址LR(X30)u_long thread_local ori_lr = 0;u_long off_shellcode_part2_ = 0; void on_enter_1(u_long sp){ //## sp回到初始位置,取出返回地址LR sp = sp + 0x60; u_long lr = *(u_long*)(sp - 8); u_long lr_ptr = sp - 8; u_long pc = *(u_long*)(sp - 0x20); pc -= 0x20; //## 使用TLS保存LR ori_lr = lr; //## 一般來說8個參數頂天了 u_long arg1 = *(u_long*)(sp - 0x28); u_long arg2 = *(u_long*)(sp - 0x30); u_long arg3 = *(u_long*)(sp - 0x38); u_long* arg3_ptr = (u_long*)(sp - 0x38); u_long arg4 = *(u_long*)(sp - 0x40); u_long arg5 = *(u_long*)(sp - 0x48); u_long arg6 = *(u_long*)(sp - 0x50); u_long arg7 = *(u_long*)(sp - 0x58); u_long arg8 = *(u_long*)(sp - 0x60); //## sp上還有參數的話照下面這么寫 u_long arg9 = *(u_long*)(sp); u_long arg10 = *(u_long*)(sp + 0x8); //## 打印String參數 JNIEnv* env = reinterpret_cast(arg1); jstring jstr = reinterpret_cast(arg3); LOGD(ANDROID_LOG_INFO, "[+] arg3: %s", env->GetStringUTFChars(jstr, nullptr)); //## 替換String參數 jstring jstr_new = env->NewStringUTF("--This is on_enter_1 !"); *arg3_ptr = reinterpret_cast(jstr_new); //## 修改LR寄存器,保證原始函數執行完畢會回到on_leave_1函數 *(u_long*)lr_ptr = pc + off_shellcode_part2_; LOGD(ANDROID_LOG_WARN, "[+] on_enter_1: %p", on_enter_1);} void on_leave_1(u_long sp){ //## sp回到初始位置 sp = sp + 0x10; u_long x0 = *(u_long*)(sp - 8); u_long* x0_ptr = (u_long*)(sp - 8); u_long lr = *(u_long*)(sp - 0x10); u_long* lr_ptr = (u_long*)(sp - 0x10); //## do_something ... LOGD(ANDROID_LOG_DEBUG, "[+] on_leave_1: %p", on_leave_1); //## 取回LR并返回 *(u_long*)lr_ptr = ori_lr;}
在 onEnter 函數中需要保存原始函數的返回地址 LR 寄存器值至 TLS 中,并在最后設置臨時返回地址為 onLeave 函數對應的 shellcode,最后再 onLeave 中再取回真實的 LR 并返回實際的函數調用鏈中,完成整個 inlineHook 流程。
另外 Hook 指定位置匯編指令的代碼并未貼出,因為原理是一致的,僅僅在 onEnter 函數中不設置臨時返回地址即可。
三
效果展示及總結
僅開啟 Hook1 時的效果如下圖所示:

總結:借鑒 Frida 的 inlineHook 原理設計了一款簡單的 inlineHook 框架,滿足了部分常用需求;關于框架的 trampoline 優化,PC-relative address 相關指令解析執行等工作,待后續繼續開發優化。
代碼已上傳:
Gitee鏈接: https://gitee.com/zzy_cs/inline-hook
Git鏈接: https://github.com/zzyccs/inlineHook