如何編寫一個Android inline hook框架
- 2020 年 2 月 26 日
- 筆記
Android Inline_Hook
https://github.com/GToad/AndroidInlineHook_ARM64
有32和64的實現,但是是分離的,要用的話還要自己把兩份程式碼合併在一起。
缺點:1、不支援函數替換(即hook後不執行原函數),現在只能修改參數暫存器,無法修改返回值。2、不支援定義同類型的hook函數來接受處理參數,只能通過修改暫存器的方式修改參數。多餘4個/或者佔兩個位元組的參數,那麼參數還要自己從棧上撈取。所以issues中說的把mov r0,sp去掉用來接收參數也是有問題的,就是參數在棧上的情況,傳過來的時候sp不是原來的sp了。
因為以上的兩個缺點,想來沒太多人用也是情理之中了,因為自己解析參數、不能修改返回值、不能不執行原函數,局限太大了。看來要想用起來,還得自己修改程式碼。
whale
https://github.com/asLody/whale
移植好像比較簡單,記得好像移植過,但是hook了某個系統函數回調原函數就崩潰了。所以用之前可能要先幫他找一遍bug、修復。lody的程式碼bug一直很多。。。
後記:art hook的之前看過,應該說是xposed/frida的另一份程式碼,frida也是用的xposed的方式,只不過不修改系統文件、通過動態調用系統函數實現。frida是js的程式碼,這個是c/c++的實現。
內聯hook大概看了下其實也是一樣的套路,32位採用ldr pc的方式跳到hook函數,64位使用x17暫存器跳到hook函數。剩下的修復原函數也是一樣的。只是沒時間完整看一遍定位bug了。
HookZz
https://github.com/jmpews/HookZz
對於Android程式設計師來說不太友好,編譯需要cmake。windows:mkdir buildforandroidarm64 && cd buildforandroidarm64
set ANDROID_NDK=D:androidNDKandroid-ndk-r16b
C:UsersEDZAppDataLocalAndroidSdkcmake3.10.2.4988404bincmake .. -DCMAKE_TOOLCHAIN_FILE=%ANDROID_NDK%/build/cmake/android.toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DANDROID_ABI="arm64-v8a" -DANDROID_NATIVE_API_LEVEL=android-21 -DSHARED=OFF -DHOOKZZ_DEBUG=OFF -G "Unix Makefiles" -D"CMAKE_MAKE_PROGRAM:PATH=D:appmingw-w64x86_64-8.1.0-posix-seh-rt_v6-rev0mingw64binmingw32-make.exe"
需要指定-G "Unix Makefiles",默認的NMake Makefiles編譯不過,指定make,因為未設置環境變數,-D"CMAKEMAKEPROGRAM:PATH="
編譯動態庫,-DSHARED=ON;編譯靜態庫,-DSHARED=OFF。
接下來為了方便還是移植到Android工程中吧。
呃。。。基本不可用!Android:arm/arm64均crach,arm64可以修復,https://www.gitmemory.com/foundkey,在OneLibstdcxxLiteIterator.cc中函數initWithCollection添加inCollection->initIterator(innerIterator);
但是arm還是crash,
#00 pc 00025ef0 [anon:libc_malloc] 12-12 15:12:53.685 181-181/? I/DEBUG: #01 pc 000081f1 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (LiteCollectionIterator::getNextObject()+28) 12-12 15:12:53.685 181-181/? I/DEBUG: #02 pc 00007531 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (gen_thumb_relocate_code(void*, int*, unsigned int, unsigned int)+312) 12-12 15:12:53.685 181-181/? I/DEBUG: #03 pc 00007ac1 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (InterceptRouting::Prepare()+56) 12-12 15:12:53.685 181-181/? I/DEBUG: #04 pc 00007cc1 /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (FunctionInlineReplaceRouting::Dispatch()+12) 12-12 15:12:53.685 181-181/? I/DEBUG: #05 pc 00007d4d /data/app-lib/com.zhuotong.myhkzz-1/libhookzz.so (ZzReplace+120)
且通過arm64測試來看,open、fopen可以hook成功。_systemproperty_get函數,第二個參數既是入參也做返回參數的情況無法正確hook,可以hook到,但是回調原函數,無論是使用第二個參數還是new參數均無法得到值,所以肯定哪裡存在bug。
一個函數只能被hook一次,再次hook調用原函數(備份的第一個hook函數)崩潰,所以兩次hook不能再調用原函數。
基於以上種種情況,可能還是自己實現AndroidInlineHook比較好,畢竟AndroidInlineHook程式碼易懂,hookZz太多無關程式碼,沒時間看架構了。
後記:後來大概看了一下,32位也是ldr pc實現,好像也做了保存暫存器等操作,和AndroidInlineHook基本是一樣的,64位好像也是使用x17暫存器。其他也都是大同小異。所以也是沒時間完整看一遍定位bug了。
自實現inline hook
因為以上的問題,目前/或者之前用過的一些hook框架或多或少都有些較大的bug(hookzz之前的某個版本應該是可以的),而對其進行修復成本較高,還不如自己寫一個。
首先統一一些概念。把覆蓋被hook函數的指令稱為跳板0,因為這部分指令越短越好,所以其大多數情況下只是一個跳板,跳到真正的用於hook指令處,這部分指令稱為shellcode(shellcode不是必須的,比如跳板0直接跳到hook函數去執行,那麼就不需要shellcode)。
這裡假設都有一些arm指令的基礎了,或者對hook已經有些理解了。後來我想了下我這篇更偏向於怎麼寫一個穩定可用hook框架,更偏向設計、編程,所以適合已經有些基礎的,不是從0講述hook到實現,雖然接下來的部分也會有很細節的部分,但是是針對一些特定的點。建議可以先看下其他人,比如ele7enxxh、GToad寫的一些文章。
inline hook這種東西,我是感覺當你掌握彙編、自己有需求的情況下,不經過學習也是可以從0寫出一個hook框架的,確實是原理很簡單的。
最簡單的實現
最容易想到的一種實現方式,使用跳板0覆蓋一個函數的指令,當執行到這個函數的時候其實就是執行跳板0,跳板0在不修改暫存器的情況跳到執行hook函數。如果在hook函數內不需要執行原函數是最簡單的,到這hook就是一個完整的hook。
如果需要執行原函數,那麼在跳板0覆蓋指令之前先備份指令,執行原函數之前把備份的指令再覆蓋回去,執行之後再覆蓋回跳板0。確實是最簡單的方法、也確實可以,但是也有一個很大的問題,就是多執行緒的問題,在把備份的指令覆蓋回去之後,其他執行緒執行到這裡不就hook不到了,甚至crash。
因為無法加鎖(真正有效的鎖),而暫停所有執行緒的太重了,所以基本上只有自己確定某個函數不存在多執行緒問題或者無需調用原函數才可用。寫個demo,熟悉下hook還行,真的實際使用是不行的。
也是因為這個問題,基本上目前的inline hook都會避免再次覆蓋指令。不能覆蓋回去,那麼就直接執行備份的指令,執行後再跳到跳板0之後再繼續執行。
也確實是可行的,大部分指令是可以這麼做的,但是也有例外。比如備份的指令中有b/bl指令跳到一個偏移地址,跳轉到的地址等於當前地址+偏移。而備份後的指令取得當前地址肯定是不等於原來的當前地址的,所以就跳到錯誤的地址去執行了。
好在我們可以進行修復,計算出原來要跳到的絕對地址,把這條b指令替換成ldr pc指令。其他指令的一些修復也是類似的道理。
四種hook形式應用不同的場景
dump讀寫暫存器、堆棧–一種hook形式
AndroidInlineHook就是這樣的實現,只能接收讀寫暫存器和堆棧,原函數還運行。那麼其實這種方式主要作用就是讀寫參數暫存器、棧,不能讀寫函數返回值,如果是不受參數控制流程的函數就無能為力了(當然是可以逆向分析,在已經得到返回值的指令處hook,但是應用場景太小了)。
那麼實現的核心就是,跳板0->dumpshellcode。dumpshellcode把暫存器以數組的形式存放(棧就是天然的數組),把這個數組傳遞給dump函數,dump函數接收處理暫存器(未生效)。dump函數返回shellcode,恢複數組數據到暫存器,完成恢復或者修改。執行backupshellcode(備份的原函數的開頭部分,修復pc,跳回原函數之後的部分)完成原函數的執行流程。
替換/攔截函數,接收處理參數,調用原函數、讀寫返回值–一種hook形式
最常用、最符合使用習慣的的方式。實現的核心,跳板0->funshellcode。funshellcode可以在上面的dumpshellcode基礎上實現也可以全新實現。
在dumpshellcode基礎上可以取巧一些,前面的保存dump暫存器保留,把後面的執行backupshellcode換成執行hook函數。
全新實現就是不保存暫存器,那麼就可以把hook函數放在跳板shellcode中,直接跳到hook函數;也可以跳板0->funshellcode,funshellcode中再跳轉到hook函數。
之後就進入hook函數,如果不調用原函數,直接返回一個返回值或者void函數什麼都不做即可(如果是參數也做返回值的情況就修改參數)。如果調用原函數,就通過一個結構體/map等查詢被hook函數地址對應的backupshellcode,把backupshellcode轉成函數指針,傳參調用,即可完成原函數的調用、或者返回值。
dump讀寫暫存器、堆棧,調用原函數、讀寫返回值/暫存器–一種hook形式
在dump的基礎上,執行backupshellcode(備份的原函數的開頭部分,修復pc,跳回原函數之後的部分)完成原函數的執行流程之後返回到dumpshellcode,調用另一個dump函數,讀寫返回值(r0/x0,r1/x1暫存器)。不同於dump的地方在於如果要返回到dumpshellcode,那麼在調用backupshellcode之前應該備份原來的lr暫存器。考慮到多執行緒的問題,肯定是不能用一個固定的變數/地址去存儲lr暫存器的,可能被覆蓋,同一個執行緒哪怕是遞歸調用函數也是有順序的,所以每個執行緒的被hook函數使用一個順序的容器保存lr,後進先出。
dump讀寫暫存器、堆棧,讀寫返回值/暫存器–一種hook形式
在dump的基礎上,不執行backupshellcode(備份的原函數的開頭部分,修復pc,跳回原函數之後的部分),直接修改r0/x0、r1/x1暫存器,返回。和dump函數很多地方是一致的,應用於只需要返回值,並不需要原函數執行的情況。
以上四種場景應該是足夠滿足hook的需要了。
arm64 實現難點
因為無法直接操作pc,那麼實現跳轉(通用情況)需要佔用一個暫存器。要麼使用一個不會被使用的暫存器(哪有絕對不會被使用的暫存器),要麼先保存這個暫存器,通過棧保存(之前就是忽略了這個問題在固定地址保存暫存器,那麼多執行緒情況下就可能被覆蓋),跳過去之後先恢復這個暫存器。例如:
stp X1, X0, [SP, #-0x10];//保存暫存器 ... ldr x0, [sp, #-0x8];//恢復x0暫存器
對應shellcode我們很容易實現,但是如果是c/c++函數(我們的hook函數)就麻煩了,直接在函數開頭插程式碼肯定是不行的。在源碼中函數第一行內嵌彙編恢復暫存器?很可惜,這種只在無參、無返回值(空實現)、只有內嵌彙編的情況會成功,其他情況下源碼中的第一行並不是彙編中的第一行。。。
所以似乎陷入了無解的狀態,在llvm中函數開頭插指令?似乎太重了。所以回到原點了,就要考慮真的沒有不使用暫存器跳轉的可能嗎?其實還是有的,b或者bl到偏移
ARM64:
B:0x17向前跳轉,0x14向後跳轉 BL:0x97向前跳轉 0x94向後跳轉
偏移地址計算過程:
(目標地址 – 指令地址)/ 4 = 偏移
// 減8,指令流水造成。
// 除4,因為指令定長,存儲指令個數差,而不是地址差。
完整指令:
.text:000000000008CC84 8D B3 FF 97 BL jearenamalloc_hard .text:0000000000079AB8 jearenamalloc_hard
計算偏移:
(79AB8-8CC84) / 4 = FFFFFFFFFFFFB38D FFB38D | 0x97000000 = 97FFB38D
所以理論上,如果被hook的函數和hook函數/跳板/shellcode的距離在正負67108860/64m的範圍內,64m=67108864,還有0,比如BL 0=00000094。那麼這樣就可以省一個暫存器了,針對arm64不能直接操作pc的問題,這應該是一個解決方案。
那麼單指令hook除了異常的方式是不是就是指的這種方式呢?只需要覆蓋一條指令,關鍵是怎麼確保被hook函數和hook函數地址在正負64m內呢。怎麼能申請到這塊地址的記憶體呢。
也許可以查看proc/pid/maps,在要hook的so附近尋找未使用的空間,然後使用mmap申請,不確定是否可行。
暫時可用的一些方式如下(最終未採用):
自定義section,增加蹦床,可讀寫執行
實際上到記憶體中還是被和程式碼段放在一起都是可讀可執行,沒有寫的許可權。
.section ".my_sec", "awx" .global _myopen_start //.text _myopen_start: ldr x0, [sp, #-0x8]; b my_open; .end //用於手動生成蹦床程式碼,因為hook程式碼和這個蹦床一起編譯的,所以基本上不會超出加減64m,那麼就可以使用b跳轉 //到偏移,就不用佔用一個暫存器了。需要每增加一個hook函數,就加一個蹦床,相應的生成shellcode中跳轉到hook //函數的地方都要改成這個蹦床的地址。難就難在這個不好通過內嵌彙編實現,因為b跟的是個偏移值,在彙編中可以使用 //函數名稱,編譯器替換,但是內嵌彙編中不行。而自己計算
shellcode如下,
//用於演示,因為不是最終方案,不寫完整程式碼了。 //修改trampoline指向蹦床即可 .data replace_start: ldr x0, trampoline; //如果每一個函數都在源碼中新建一個shellcode的話,而不是動態複製生成,那麼這個shellcode和蹦床可以合為一個。 br x0; //不能改變lr暫存器 trampoline: //蹦床的地址 .double 0xffffffffffffffff replace_end:
awx指定讀寫執行,在elf中(ida查看)確實是讀寫執行。如果這個section中僅包含變數,那麼在記憶體中放在可讀寫的段;如果存在程式碼或者程式碼和變數均存在,都是和程式碼段一樣僅可讀執行。那麼在單獨的section中存放蹦床程式碼意義也不大了,還是需要調用mprotect修改記憶體許可權。不過考慮到如果放在僅可讀寫的段中,那麼萬一映射後的rw-p和r-xp超過了64m,不就白瞎了,所以還是以程式碼或者至少這個section中存在一個函數,保證和被hook的函數都在r-xp。
77c5545000-77c557c000 r-xp 00000000 103:37 533353 /data/app/com.zhuotong.myinhk-TRvqt0ReHRjQeeAnpXDnFQ==/lib/arm64/libInlineHook.so 77c558b000-77c558e000 r--p 00036000 103:37 533353 /data/app/com.zhuotong.myinhk-TRvqt0ReHRjQeeAnpXDnFQ==/lib/arm64/libInlineHook.so 77c558e000-77c558f000 rw-p 00039000 103:37 533353 /data/app/com.zhuotong.myinhk-TRvqt0ReHRjQeeAnpXDnFQ==/lib/arm64/libInlineHook.so
而如果直接和.text一起,主要是怕對這塊記憶體修改許可權引發什麼異常,不確定如果程式碼正在執行,修改許可權是否會出問題,所以最好能僅修改蹦床的區間。而且不確定是否有隻能修改為讀寫和讀執行的系統,所以可能要先修改成讀寫,寫了之後再修改成讀執行?這樣會不會也有幾率觸發問題,但是如果每個蹦床都分配一個頁的記憶體也不現實。。。
內嵌彙編,在自定義section中增加蹦床

這樣定義一個無參無返回的函數,倒是可以使這個函數就是內嵌彙編,但是不確定如果ollvm混淆等是否會被拆分、加入垃圾程式碼等。
而且問題在於怎麼自動生成一個蹦床函數。宏定義?那這個宏要在函數之外,不太容易自動化實現,包括蹦床的函數名稱、b後面的函數名稱。和上面的彙編中添加一個蹦床一樣的問題,除非自己實現預處理之類的自動插入彙編程式碼、宏等,似乎不太現實。。。
似乎很難實現自動化,手動寫程式碼太麻煩且和arm的部分介面、行為不一致。但是如果自己簡單測試、不在乎這些也是可以的。
在自定義section中預留蹦床空間
unsigned int hkArry[256*2] __attribute__ ((section(".my_sec")));
//下面為偽程式碼,通過這樣也可以實現不修改/保存暫存器、不修改hook函數,動態生成蹦床完成hook。 //優點是不用保存暫存器,缺點是因為正負64m的限制,hook函數應該和hook庫是一起編譯成動態庫/可執行文件的。不能單獨使用hook庫。 //256個蹦床,實際使用可能要考慮這個hkArry記憶體對齊的問題(如果編譯器未做記憶體對齊)。 //unsigned int hkArry[256 * 2] __attribute__ ((section(".my_sec"))); //void test_trampoline(){ //僅用於演示,應該先設置記憶體為可讀寫/執行, // hkArry[0] = 0xf85f83e0;//或者memcpy, ldr x0, [sp, #-0x8]; //例如這樣計算偏移,組合b指令。 // unsigned long offset = (unsigned long) my_open - ((unsigned long) hkArry + 1 * 4); // hkArry[1] = 0x14000000;//計算偏移,生成指令,b 0; //}
256個蹦床。動態生成蹦床程式碼,第一條指令固定,第二條指令計算生成。需要hkArry修改為讀寫執行。

未實現
so的r-xp中應該是有未使用的多餘的記憶體的,為了對齊、頁面等,所以怎麼確定多餘的空間大小和位置,然後蹦床程式碼存放其中。
為什麼盡量不使用x16、x17等不用保存的暫存器?
因為種種限制,所以最後採用的還是保存一個暫存器,而使用哪個暫存器呢,我是選的lr暫存器。為什麼不用x16、x17等不用保存的暫存器?這裡就涉及到一個標準和規範的問題。
理論上編譯器編譯的c/c++函數是遵守這個規範的,要麼不使用x16、x17暫存器,要麼只是臨時中轉,不會在調用其他函數之後再從x16、x17暫存器取值(因為其他函數可能改變x16、x17),但是內嵌彙編(雖然指定不使用x16、x17暫存器,但還是被編譯器使用了)或者人工寫的彙編,雖然不常見,但確實存在。而最常見的是plt函數內都是使用x16、x17做中轉。所以使用一個不一定會被保存的暫存器不如使用一個會被保存的暫存器。而因為lr暫存器的特殊性,一般使用的話都會先壓棧保存,結束恢復(32位不一定恢復,64位是會恢復lr暫存器的,因為不能直接操作pc,多數都是恢復lr,再ret)
所以其實主要還是兼容性考慮,盡量採用一些繞彎的方式不改變任何暫存器,實在沒辦法的情況下或者標準c/c++函數、非函數任意位置hook的情況下才使用x16、x17暫存器。
解析暫存器、棧取出參數,調用hook函數
難點在於:1、不知道參數個數、參數類型。2、需要確定各種類型參數占幾個暫存器、可變參數等
其實在源碼中定義hook函數的時候是有函數原型的,但是運行時拿不到。忽然想到"c++"函數名的規則,裡面包含參數、返回值類型,似乎可行,但是很多情況並不希望導出函數,而且也並不一定都是c++實現的。
那麼如果定義一個介面,傳入函數原型也不是不行,基本類型就用相應的字元或者枚舉標識,其他所有的都是void*指針。可還是怕碰見可變參數函數,不確定參數個數,參數類型,只有接收/實現函數才知道。所以似乎不太可行。
而libffi似乎不太適合這個情況,也是需要明確指令參數和返回類型,還要傳輸參數。
目前的實現沒有經過自己解析參數,只是中轉,通過定義一致的函數原型,讓編譯器幫助我們解析參數。
程式碼實現arm64統一的跳板0
因為arm64限制條件最多,那麼應該先實現arm64,這樣才能更好的保證api的通用/一致性,因為arm64不能操作pc不得不這麼做,arm32同樣也可以這麼做,但是如果先實現arm32,可能就先入為主的設計一些arm32可用的api。這算是一種架構思維吧,或者多思考一下就明白了。
STP X1, X0, [SP, #-0x10];//因為要使用一個暫存器,所以先保存,當然不一定是x0 LDR X0, 8; //把shellcode/hook函數地址存到x0 BR X0; //執行shellcode/hook函數 ADDR(64) LDR X0, [SP, -0x8];//因為不能操作pc,所以跳回來的時候免不了也要使用一個暫存器
其實很簡單,注釋基本都說明了,使用了24位元組,所以如果能確定要hook的函數是標準的c/c++函數,不會使用x16、x17去保存值的話也可以這樣,使用16位元組,降低被hook函數太短失敗的概率。
LDR X17, 8; BR x17; ADDR(64)
程式碼實現arm64dump函數
.include "../../asm/base.s" //.extern _dump_start //.extern _dump_end //.extern _hk_info //.set _dump_start, r_dump_start //.set _dump_end, r__dump_end //.set _hk_info, r__hk_info //.global _dump_start //.global _dump_end //.global _hk_info //.hidden _dump_start //.hidden _dump_end //.hidden _hk_info //可用於標準的c/c++函數、非標準函數、函數的一部分(用於讀寫暫存器),前提都是位元組長度足夠 //非標準函數即非c/c++編譯的函數,那麼手寫彙編可能存在並不遵守約定的情況,比如我們使用了sp暫存器,並在未使用的棧上保存暫存器 //但是可能不是滿遞減而是反過來滿遞增,或者不遵守棧平衡,往棧上寫數據,但是並不改變sp暫存器。當然應該是很少見的。 .data _dump_start: //用於讀寫暫存器/棧,需要自己解析參數,不能讀寫返回值,不能阻止原函數(被hook函數)的執行 //從行為上來我覺得更偏向dump,所以起名為dump。 sub sp, sp, #0x20; //跳板在棧上存儲了x0、x1,但是未改變sp的值 mrs x0, NZCV str x0, [sp, #0x10]; //覆蓋跳板存儲的x1,存儲狀態暫存器 str x30, [sp]; //存儲x30 add x30, sp, #0x20 str x30, [sp, #0x8]; //存儲真實的sp ldr x0, [sp, #0x18]; //取出跳板存儲的x0 save_x0_x29://保存暫存器x0-x29 sub sp, sp, #0xf0; //分配棧空間 stp X0, X1, [SP]; //存儲x0-x29 stp X2, X3, [SP,#0x10] stp X4, X5, [SP,#0x20] stp X6, X7, [SP,#0x30] stp X8, X9, [SP,#0x40] stp X10, X11, [SP,#0x50] stp X12, X13, [SP,#0x60] stp X14, X15, [SP,#0x70] stp X16, X17, [SP,#0x80] stp X18, X19, [SP,#0x90] stp X20, X21, [SP,#0xa0] stp X22, X23, [SP,#0xb0] stp X24, X25, [SP,#0xc0] stp X26, X27, [SP,#0xd0] stp X28, X29, [SP,#0xe0] call_onPreCallBack://調用onPreCallBack函數,第一個參數是sp,第二個參數是STR_HK_INFO結構體指針 mov x0, sp; //x0作為第一個參數,那麼操作x0=sp,即操作棧讀寫保存的暫存器 ldr x1, _hk_info; ldr x3, [x1]; //onPreCallBack bl get_lr_pc; //lr為下條指令 add lr, lr, #8; //lr為blr x3的地址 str lr, [sp, #0x108]; //lr當作pc,覆蓋棧上的x0 blr x3 restore_regs://恢復暫存器 ldr x0, [sp, #0x100]; //取出狀態暫存器 msr NZCV, x0 ldp X0, X1, [SP]; //恢復x0-x29暫存器 ldp X2, X3, [SP,#0x10] ldp X4, X5, [SP,#0x20] ldp X6, X7, [SP,#0x30] ldp X8, X9, [SP,#0x40] ldp X10, X11, [SP,#0x50] ldp X12, X13, [SP,#0x60] ldp X14, X15, [SP,#0x70] ldp X16, X17, [SP,#0x80] ldp X18, X19, [SP,#0x90] ldp X20, X21, [SP,#0xa0] ldp X22, X23, [SP,#0xb0] ldp X24, X25, [SP,#0xc0] ldp X26, X27, [SP,#0xd0] ldp X28, X29, [SP,#0xe0] add sp, sp, #0xf0 ldr x30, [sp]; //恢復x30 add sp, sp, #0x20; //恢復為真實sp call_oriFun: stp X1, X0, [SP, #-0x10]; //因為跳轉還要佔用一個暫存器,所以保存 ldr x0, _hk_info; ldr x0, [x0, #8]; //pOriFuncAddr br x0 get_lr_pc: ret; //僅用於獲取LR/PC _hk_info: //結構體STR_HK_INFO .double 0xffffffffffffffff _dump_end: .end
基本上注釋已經說明了。hkinf為如下結構體的指針,onPreCallBack函數原型如下。
//hook資訊 typedef struct STR_HK_INFO{ void (*onPreCallBack)(struct my_pt_regs *, struct STR_HK_INFO *pInfo); //回調函數,在執行原函數之前獲取參數/暫存器的函數 void * pOriFuncAddr; //存放備份/修復後原函數的地址 void (*pre_callback)(struct my_pt_regs *, struct STR_HK_INFO *pInfo); //pre_callback,內部做保存lr的操作,之後回調onPreCallBack,不能被用戶操作 void (*onCallBack)(struct my_pt_regs *, struct STR_HK_INFO *pInfo); //回調函數,執行原函數之後獲取返回值/暫存器的函數 void (*aft_callback)(struct my_pt_regs *, struct STR_HK_INFO *pInfo); //aft_callback,內部做恢復lr的操作,之後回調onCallBack,不能被用戶操作 void *pHkFunAddr; //hook函數,即自定義按照被hook的函數原型構造,處理參數/返回值的函數 //以上為在shellcode中通過偏移直接或間接用到的,所以如果有變動,相應的shellcode也要跟著變動 void **ppHkFunAddr; //上面pHkFunAddr函數在shellcode中的地址,已廢棄; void **hk_infoAddr; //shellcode中HK_INFO的地址 void *pBeHookAddr; //被hook的地址/函數 void *pStubShellCodeAddr; //跳過去的shellcode stub的地址 size_t shellCodeLength; //上面pStubShellCodeAddr的位元組數 void ** ppOriFuncAddr; //shellcode 中存放備份/修復後原函數的地址,已廢棄,待去除;改成上面pHkFunAddr函數的指針,應用於解除hook void *pNewEntryForOriFuncAddr; //和pOriFuncAddr一致 BYTE szbyBackupOpcodes[OPCODEMAXLEN]; //原來的opcodes int backUpLength; //備份程式碼的長度,arm64模式下為24 int backUpFixLengthList[BACKUP_CODE_NUM_MAX]; //保存 const char* methodName; } HK_INFO; #if defined(__aarch64__) struct my_pt_regs { __u64 uregs[31]; __u64 sp; __u64 pstate; //有時間應該修復,pc在前,但是涉及到棧和生成shellcode都要改,先這麼用吧,和系統結構體有這點不同 __u64 pc; }; #define ARM_lr uregs[30] #define ARM_sp sp //#define SP (__u64*)sp //#define SP_32(i) *(__u32*)((__u64)(regs->sp) + i*4) //#define SP_32(i) *((__u32*)regs->sp+i) //#define SP_32(i) *((__u64*)regs->sp+i) #define SP(i) *((__u64*)regs->sp+i) #elif defined(__arm__) struct my_pt_regs { long uregs[18]; }; #ifndef __ASSEMBLY__ #define ARM_cpsr uregs[16] #define ARM_pc uregs[15] #define ARM_lr uregs[14] #define ARM_sp uregs[13] #define ARM_ip uregs[12] #define ARM_fp uregs[11] #define ARM_r10 uregs[10] #define ARM_r9 uregs[9] #define ARM_r8 uregs[8] #define ARM_r7 uregs[7] #define ARM_r6 uregs[6] #define ARM_r5 uregs[5] #define ARM_r4 uregs[4] #define ARM_r3 uregs[3] #define ARM_r2 uregs[2] #define ARM_r1 uregs[1] #define ARM_r0 uregs[0] #define ARM_ORIG_r0 uregs[17] #define ARM_VFPREGS_SIZE (32 * 8 + 4) #endif #define SP(i) *((__u32*)regs->ARM_sp+i) #endif
myptregs對應在棧上存放的暫存器。這裡面其他暫存器都容易保存,pc暫存器因為不能直接操作,所以要取巧一些。我是利用bl跳轉之前會把下一條指令的地址存放到lr暫存器,那麼再跳回讀取lr暫存器即可。
bl get_lr_pc; //lr為下條指令 add lr, lr, #8; //lr為blr x3的地址 str lr, [sp, #0x108]; //lr當作pc,覆蓋棧上的x0 blr x3 ... get_lr_pc: ret; //僅用於獲取LR/PC
當然這裡保存pc暫存器其實也不是必須的。
dump_demo
/** * 用戶自定義的stub函數,嵌入在hook點中,可直接操作暫存器等 * @param regs 暫存器結構,保存暫存器當前hook點的暫存器資訊 * @param pInfo 保存了被hook函數、hook函數等的結構體 */ void onPreCallBack(my_pt_regs *regs, HK_INFO *pInfo) //參數regs就是指向棧上的一個數據結構,由第二部分的mov r0, sp所傳遞。 { const char* name = "null"; if (pInfo) { if (pInfo->methodName) { name = pInfo->methodName; } else { char buf[20]; sprintf(buf, "%p", pInfo->pBeHookAddr); name = buf; } } // LE("tid=%d onPreCallBack:%s", gettid(), name); #if defined(__aarch64__) LE("tid=%d, onPreCallBack:%s, " "x0=0x%llx, x1=0x%llx, x2=0x%llx, x3=0x%llx, x4=0x%llx, x5=0x%llx, x6=0x%llx, x7=0x%llx, x8=0x%llx, x9=0x%llx, x10=0x%llx," " x11=0x%llx, x12=0x%llx, x13=0x%llx, x14=0x%llx, x15=0x%llx, x16=0x%llx, x17=0x%llx, x18=0x%llx, x19=0x%llx, x20=0x%llx, " "x21=0x%llx, x22=0x%llx, x23=0x%llx, x24=0x%llx, x25=0x%llx, x26=0x%llx, x27=0x%llx, x28=0x%llx, x29/FP=0x%llx, x30/LR=0x%llx, " "cur_sp=%p, ori_sp=%p, ori_sp/31=0x%llx, NZCV/32=0x%llx, x0/pc/33=0x%llx, cur_pc=%llx, arg8=%x, arg9=%x, arg10=%x, arg11=%x, " "arg12=%x, arg13=%x;" , gettid(), name, regs->uregs[0], regs->uregs[1], regs->uregs[2], regs->uregs[3], regs->uregs[4], regs->uregs[5], regs->uregs[6], regs->uregs[7], regs->uregs[8], regs->uregs[9], regs->uregs[10], regs->uregs[11], regs->uregs[12], regs->uregs[13], regs->uregs[14], regs->uregs[15], regs->uregs[16], regs->uregs[17], regs->uregs[18], regs->uregs[19], regs->uregs[20], regs->uregs[21], regs->uregs[22], regs->uregs[23], regs->uregs[24], regs->uregs[25], regs->uregs[26], regs->uregs[27], regs->uregs[28], regs->uregs[29], regs->uregs[30], regs, ((char*)regs + 0x110), regs->uregs[31], regs->uregs[32], regs->uregs[33], regs->pc, SP(0), SP(1), SP(2), SP(3), SP(4), SP(5) ); #elif defined(__arm__) LE("tid=%d, onPreCallBack:%s, " "r0=0x%lx, r1=0x%lx, r2=0x%lx, r3=0x%lx, r4=0x%lx, r5=0x%lx, r6=0x%lx, r7=0x%lx, r8=0x%lx, r9=0x%lx, r10=0x%lx, r11=0x%lx, r12=0x%lx, " "cur_sp=%p, ori_sp=%p, ori_sp/13=0x%lx, lr=0x%lx, cur_pc=0x%lx, cpsr=0x%lx, " "arg4=0x%lx, arg5=0x%lx, arg4=0x%lx, arg5=0x%lx;" , gettid(), name, regs->uregs[0], regs->uregs[1], regs->uregs[2], regs->uregs[3], regs->uregs[4], regs->uregs[5], regs->uregs[6], regs->uregs[7], regs->uregs[8], regs->uregs[9], regs->uregs[10], regs->uregs[11], regs->uregs[12], regs, ((char*)regs + 0x44), regs->uregs[13], regs->uregs[14], regs->uregs[15], regs->uregs[16], regs->uregs[17], regs->uregs[18], SP(0), SP(1) ); #endif if (pInfo) { LE("onPreCallBack: HK_INFO=%p", pInfo); if (pInfo->pBeHookAddr == open && regs->uregs[0]) { const char* name = (const char *)(regs->uregs[0]); LE("onPreCallBack: open: %s , %o, %o", name, regs->uregs[1], (mode_t)regs->uregs[2]); } } } void test_dump(){ LE("open=%p, callback=%p", open, onPreCallBack); if (dump((void *)(open), onPreCallBack, NULL, "open") != success) { LE("hook open error"); } int fd = open("/system/lib/libc.so", O_RDONLY); LE("open /system/lib/libc.so, fd=%d", fd); }
自定義onPreCallBack修改暫存器即可。如果是一個函數,那麼能控制的是參數,不能阻止原函數的調用。
程式碼實現arm64dumpwithret函數
//.include "../../asm/base.s" .global r_dump_start .global r_dump_end .global r_hk_info .hidden r_dump_start .hidden r_dump_end .hidden r_hk_info .data r_dump_start: //用於讀寫暫存器/棧,需要自己解析參數,不能讀寫返回值,不能阻止原函數(被hook函數)的執行 //從行為上來我覺得更偏向dump,所以起名為dump。 sub sp, sp, #0x20; //跳板在棧上存儲了x0、x1,但是未改變sp的值 mrs x0, NZCV str x0, [sp, #0x10]; //覆蓋跳板存儲的x1,存儲狀態暫存器 str x30, [sp]; //存儲x30 add x30, sp, #0x20 str x30, [sp, #0x8]; //存儲真實的sp ldr x0, [sp, #0x18]; //取出跳板存儲的x0 save_x0_x29://保存暫存器x0-x29 sub sp, sp, #0xf0; //分配棧空間 stp X0, X1, [SP]; //存儲x0-x29 stp X2, X3, [SP,#0x10] stp X4, X5, [SP,#0x20] stp X6, X7, [SP,#0x30] stp X8, X9, [SP,#0x40] stp X10, X11, [SP,#0x50] stp X12, X13, [SP,#0x60] stp X14, X15, [SP,#0x70] stp X16, X17, [SP,#0x80] stp X18, X19, [SP,#0x90] stp X20, X21, [SP,#0xa0] stp X22, X23, [SP,#0xb0] stp X24, X25, [SP,#0xc0] stp X26, X27, [SP,#0xd0] stp X28, X29, [SP,#0xe0] call_onPreCallBack://調用onPreCallBack函數,第一個參數是sp,第二個參數是STR_HK_INFO結構體指針 mov x0, sp; //x0作為第一個參數,那麼操作x0=sp,即操作棧讀寫保存的暫存器 ldr x1, r_hk_info; ldr x3, [x1, #0x10]; //pre_callback bl get_lr_pc; //lr為下條指令 add lr, lr, 8; //lr為blr x3的地址 str lr, [sp, #0x108]; //lr當作pc,覆蓋棧上的x0 blr x3 to_call_oriFun: ldr x0, [sp, #0x100]; //取出狀態暫存器 msr NZCV, x0 ldp X0, X1, [SP]; //恢復x0-x29暫存器 ldp X2, X3, [SP,#0x10] ldp X4, X5, [SP,#0x20] ldp X6, X7, [SP,#0x30] ldp X8, X9, [SP,#0x40] ldp X10, X11, [SP,#0x50] ldp X12, X13, [SP,#0x60] ldp X14, X15, [SP,#0x70] ldp X16, X17, [SP,#0x80] ldp X18, X19, [SP,#0x90] ldp X20, X21, [SP,#0xa0] ldp X22, X23, [SP,#0xb0] ldp X24, X25, [SP,#0xc0] ldp X26, X27, [SP,#0xd0] ldp X28, X29, [SP,#0xe0] add sp, sp, #0xf0 ldr x30, [sp]; //恢復x30 add sp, sp, #0x20; //恢復為真實sp stp X1, X0, [SP, #-0x10]; //因為跳轉還要佔用一個暫存器,所以保存 ldr x0, r_hk_info; ldr x0, [x0, #8]; //pOriFuncAddr blr x0; to_aft_callback: //有時間再把這部分程式碼優化掉,是可以再跳轉到開頭的,因為這部分程式碼都是一樣的,判斷lr可以區分出來的 STP X1, X0, [SP, #-0x10] sub sp, sp, #0x20; //跳板在棧上存儲了x0、x1,但是未改變sp的值 mrs x0, NZCV str x0, [sp, #0x10]; //覆蓋跳板存儲的x1,存儲狀態暫存器 str x30, [sp]; //存儲x30 add x30, sp, #0x20 str x30, [sp, #0x8]; //存儲真實的sp ldr x0, [sp, #0x18]; //取出跳板存儲的x0 sub sp, sp, #0xf0; //分配棧空間 stp X0, X1, [SP]; //存儲x0-x29 stp X2, X3, [SP,#0x10] stp X4, X5, [SP,#0x20] stp X6, X7, [SP,#0x30] stp X8, X9, [SP,#0x40] stp X10, X11, [SP,#0x50] stp X12, X13, [SP,#0x60] stp X14, X15, [SP,#0x70] stp X16, X17, [SP,#0x80] stp X18, X19, [SP,#0x90] stp X20, X21, [SP,#0xa0] stp X22, X23, [SP,#0xb0] stp X24, X25, [SP,#0xc0] stp X26, X27, [SP,#0xd0] stp X28, X29, [SP,#0xe0] mov x0, sp; //x0作為第一個參數,那麼操作x0=sp,即操作棧讀寫保存的暫存器 ldr x1, r_hk_info; ldr x3, [x1, #0x20]; //aft_callback bl get_lr_pc; //lr為下條指令 add lr, lr, 8; //lr為blr x3的地址 str lr, [sp, #0x108]; //lr當作pc,覆蓋棧上的x0 blr x3; //執行aft_callback to_popreg: ldr x0, [sp, #0x100]; //取出狀態暫存器 msr NZCV, x0 ldp X0, X1, [SP]; //恢復x0-x29暫存器 ldp X2, X3, [SP,#0x10] ldp X4, X5, [SP,#0x20] ldp X6, X7, [SP,#0x30] ldp X8, X9, [SP,#0x40] ldp X10, X11, [SP,#0x50] ldp X12, X13, [SP,#0x60] ldp X14, X15, [SP,#0x70] ldp X16, X17, [SP,#0x80] ldp X18, X19, [SP,#0x90] ldp X20, X21, [SP,#0xa0] ldp X22, X23, [SP,#0xb0] ldp X24, X25, [SP,#0xc0] ldp X26, X27, [SP,#0xd0] ldp X28, X29, [SP,#0xe0] add sp, sp, #0xf0 ldr x30, [sp]; //恢復x30 add sp, sp, #0x20; //恢復為真實sp //巧的是下條指令也是ret或者br lr,共用一條指令。 get_lr_pc: ret; //僅用於獲取LR/PC r_hk_info: //結構體STR_HK_INFO .double 0xffffffffffffffff r_dump_end: .end
和dump不同,在原函數執行後可以對返回值進行讀寫,且可以只處理參數或者只處理返回值。
因為要讀寫返回值,所以執行原函數之前需要修改lr暫存器,而讀寫返回值之後還要恢復正常的流程,那麼lr暫存器是需要保存的。在這個shellcode或者結構體的一個欄位存儲lr都存在多執行緒覆蓋的問題,所以使用和執行緒綁定的容器存儲。那麼何時保存呢?考慮到程式碼復用和最小的更改,那麼可以在調用onPreCallBack函數內保存,但是這個函數是用戶創建的,不應該讓用戶參與保存,而且這個onPreCallBack不一定存在。所以做一次中轉,shellcode中先跳到一個一定存在的函數preCallBack,preCallBack內保存lr,並調用onPreCallBack(如果存在)。恢復lr也是同樣的思路。
//頭文件 #include <map> #include <pthread.h> #include <vector> typedef std::vector<unsigned long> LRS; //static LRS lrs; struct STR_LR { }; typedef std::map<const void*, LRS*> LR_MAP; //typedef std::map<pid_t, LR_MAP*> TID_MAP; typedef std::map<pid_t, LR_MAP*> TID_MAP; static TID_MAP * getTid_map(); static void saveLR(void* key_fun, unsigned long lr); static unsigned long getLR(void* key_fun); #endif #ifdef __cplusplus extern "C" { #endif #include "mhk.h" //因為會被導出,所以兩個靜態庫中存在兩個同名函數,另一個被覆蓋了。 //extern void pre_callback(struct my_pt_regs *regs, HK_INFO* info); static void pre_callback(struct my_pt_regs *regs, HK_INFO* info); //extern void aft_callback(struct my_pt_regs *regs, HK_INFO* info); static void aft_callback(struct my_pt_regs *regs, HK_INFO* info); typedef void (*callback)(struct my_pt_regs *regs, HK_INFO* info); extern callback d_pre_callback;// = pre_callback; extern callback d_aft_callback;// = aft_callback; #ifdef __cplusplus } #endif //頭文件結束 static TID_MAP* tid_map; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; TID_MAP * getTid_map(){ // pthread_mutex_init(&mutex, NULL); pthread_mutex_lock(&mutex); if (tid_map == NULL) { tid_map = new TID_MAP; } pthread_mutex_unlock(&mutex); return tid_map; } //因為不清楚tid分配機制,是否存在已死亡執行緒的tid重新分配給新建執行緒,所以可以ls -la /proc/8205/task/根據執行緒時間 //判斷是不是一個新執行緒,若是使用舊tid的新執行緒可以清空map,不過其實使用的棧結構存數據,理論上不區分也應該沒問題的。 void saveLR(void* key_fun, unsigned long lr){ pid_t tid = gettid(); TID_MAP *map = getTid_map(); pthread_mutex_lock(&mutex); auto it = map->find(tid); if (it == map->end()) { auto lr_map = new LR_MAP; auto ls = new LRS; ls->push_back(lr); lr_map->insert(std::make_pair(key_fun, ls)); map->insert(std::make_pair(tid, lr_map)); } else { auto lr_map = it->second; auto it_vt = lr_map->find(key_fun); if (it_vt == lr_map->end()) { auto ls = new LRS; ls->push_back(lr); lr_map->insert(std::make_pair(key_fun, ls)); } else { std::vector<unsigned long> *vt = it_vt->second; vt->push_back(lr); } } pthread_mutex_unlock(&mutex); } unsigned long getLR(void* key_fun){ unsigned long lr = 0; pid_t tid = gettid(); TID_MAP *map = getTid_map(); pthread_mutex_lock(&mutex); auto it = map->find(tid); if (it == map->end()) { LE("what's happened ? not found tid=%d", tid);//理論上不應該出現 } else { auto lr_map = it->second; auto it_vt = lr_map->find(key_fun); if (it_vt == lr_map->end()) { LE("what's happened ? not found LR for=%p in tid=%d", key_fun, tid);//理論上不應該出現 } else { std::vector<unsigned long> *vt = it_vt->second; if (!vt || /*vt->size() <= 0*/ vt->empty()) { LE("what's happened ? null LR for=%p in tid=%d", key_fun, tid); } else { unsigned long size = vt->size(); lr = (*vt)[size - 1]; vt->pop_back(); } } } pthread_mutex_unlock(&mutex); return lr; } void pre_callback(struct my_pt_regs *regs, HK_INFO* info){ LE("dump_with_ret pre_callback"); saveLR(info->pBeHookAddr, regs->ARM_lr); if (info->onPreCallBack) info->onPreCallBack(regs, info); } void aft_callback(struct my_pt_regs *regs, HK_INFO* info){ LE("dump_with_ret aft_callback"); unsigned long lr = getLR(info->pBeHookAddr); regs->ARM_lr = lr; if (info->onCallBack) info->onCallBack(regs, info); } callback d_pre_callback = pre_callback; callback d_aft_callback = aft_callback;
目前是使用"c++"容器實現的,考慮到有些項目可能不能用c++,有時間再用c實現map、vector。
dumpwithret_demo
void test_dump_with_ret(){ LE("open=%p, callback=%p", open, onPreCallBack); if (dump((void *)(open), onPreCallBack, onCallBack, "open") != success) { LE("hook open error"); } int fd = open("/system/lib/libc.so", O_RDONLY); LE("open /system/lib/libc.so, fd=%d", fd); } void test_dump_ret(){ LE("open=%p, callback=%p", open, onPreCallBack); if (dump((void *)(open), NULL, onCallBack, "open") != success) { LE("hook open error"); } int fd = open("/system/lib/libc.so", O_RDONLY); LE("open /system/lib/libc.so, fd=%d", fd); }
程式碼實現arm64dumpjustret函數
.global j_dump_start .global j_dump_end .global j_hk_info .hidden j_dump_start .hidden j_dump_end .hidden j_hk_info .data j_dump_start: sub sp, sp, #0x20; //跳板在棧上存儲了x0、x1,但是未改變sp的值 mrs x0, NZCV str x0, [sp, #0x10]; //覆蓋跳板存儲的x1,存儲狀態暫存器 str x30, [sp]; //存儲x30 add x30, sp, #0x20 str x30, [sp, #0x8]; //存儲真實的sp ldr x0, [sp, #0x18]; //取出跳板存儲的x0 sub sp, sp, #0xf0; //分配棧空間 stp X0, X1, [SP]; //存儲x0-x29 stp X2, X3, [SP,#0x10] stp X4, X5, [SP,#0x20] stp X6, X7, [SP,#0x30] stp X8, X9, [SP,#0x40] stp X10, X11, [SP,#0x50] stp X12, X13, [SP,#0x60] stp X14, X15, [SP,#0x70] stp X16, X17, [SP,#0x80] stp X18, X19, [SP,#0x90] stp X20, X21, [SP,#0xa0] stp X22, X23, [SP,#0xb0] stp X24, X25, [SP,#0xc0] stp X26, X27, [SP,#0xd0] stp X28, X29, [SP,#0xe0] mov x0, sp; //x0作為第一個參數,那麼操作x0=sp,即操作棧讀寫保存的暫存器 ldr x1, j_hk_info; ldr x3, [x1]; //onPreCallBack bl get_lr_pc; //lr為下條指令 add lr, lr, 8; //lr為blr x3的地址 str lr, [sp, #0x108]; //lr當作pc,覆蓋棧上的x0 blr x3 to_popreg: ldr x0, [sp, #0x100]; //取出狀態暫存器 msr NZCV, x0 ldp X0, X1, [SP]; //恢復x0-x29暫存器 ldp X2, X3, [SP,#0x10] ldp X4, X5, [SP,#0x20] ldp X6, X7, [SP,#0x30] ldp X8, X9, [SP,#0x40] ldp X10, X11, [SP,#0x50] ldp X12, X13, [SP,#0x60] ldp X14, X15, [SP,#0x70] ldp X16, X17, [SP,#0x80] ldp X18, X19, [SP,#0x90] ldp X20, X21, [SP,#0xa0] ldp X22, X23, [SP,#0xb0] ldp X24, X25, [SP,#0xc0] ldp X26, X27, [SP,#0xd0] ldp X28, X29, [SP,#0xe0] add sp, sp, #0xf0 ldr x30, [sp]; //恢復x30 add sp, sp, #0x20; //恢復為真實sp get_lr_pc: ret; //僅用於獲取LR/PC j_hk_info: //結構體STR_HK_INFO .double 0xffffffffffffffff j_dump_end: .end
和dump不同,不再執行原函數,可以直接修改x0、x1暫存器返回值或者什麼都不做。這是四種方式中最簡單的一種。
dumpjustret_demo
void test_dump_just_ret(){ LE("open=%p, callback=%p", open, onPreCallBack); if (dumpRet((void *)(open), onCallBack, "open") != success) { LE("hook open error"); } int fd = open("/system/lib/libc.so", O_RDONLY); LE("open /system/lib/libc.so, fd=%d", fd); }
程式碼實現arm64replace函數
.global replace_start .global replace_end .global p_hk_info .hidden replace_start .hidden replace_end .hidden p_hk_info .data //這種方式盡量用於標準的c/c++函數,因為通過hook函數再調用原函數,只能保證參數暫存器和lr暫存器是一致的,其他暫存器可能被修改。 replace_start: //如果只是替換/跳到hook函數,其實是不用保存暫存器的,只是重新寫比較麻煩,所以在之前的基礎上 sub sp, sp, #0x20; //跳板在棧上存儲了x0、x1,但是未改變sp的值 mrs x0, NZCV str x0, [sp, #0x10]; //覆蓋跳板存儲的x1,存儲狀態暫存器 str x30, [sp]; //存儲x30 add x30, sp, #0x20 str x30, [sp, #0x8]; //存儲真實的sp ldr x0, [sp, #0x18]; //取出跳板存儲的x0 sub sp, sp, #0xf0; //分配棧空間 stp X0, X1, [SP]; //存儲x0-x29 stp X2, X3, [SP,#0x10] stp X4, X5, [SP,#0x20] stp X6, X7, [SP,#0x30] stp X8, X9, [SP,#0x40] stp X10, X11, [SP,#0x50] stp X12, X13, [SP,#0x60] stp X14, X15, [SP,#0x70] stp X16, X17, [SP,#0x80] stp X18, X19, [SP,#0x90] stp X20, X21, [SP,#0xa0] stp X22, X23, [SP,#0xb0] stp X24, X25, [SP,#0xc0] stp X26, X27, [SP,#0xd0] stp X28, X29, [SP,#0xe0] mov x0, sp; //x0作為第一個參數,那麼操作x0=sp,即操作棧讀寫保存的暫存器 ldr x1, p_hk_info; ldr x3, [x1, #0x10]; //pre_callback,保存lr bl get_lr_pc; //lr為下條指令 add lr, lr, 8; //lr為blr x3的地址 str lr, [sp, #0x108]; //lr當作pc,覆蓋棧上的x0 blr x3 to_call_hkFun: ldr x0, [sp, #0x100]; //取出狀態暫存器 msr NZCV, x0 ldp X0, X1, [SP]; //恢復x0-x29暫存器 ldp X2, X3, [SP,#0x10] ldp X4, X5, [SP,#0x20] ldp X6, X7, [SP,#0x30] ldp X8, X9, [SP,#0x40] ldp X10, X11, [SP,#0x50] ldp X12, X13, [SP,#0x60] ldp X14, X15, [SP,#0x70] ldp X16, X17, [SP,#0x80] ldp X18, X19, [SP,#0x90] ldp X20, X21, [SP,#0xa0] ldp X22, X23, [SP,#0xb0] ldp X24, X25, [SP,#0xc0] ldp X26, X27, [SP,#0xd0] ldp X28, X29, [SP,#0xe0] add sp, sp, #0xf0 ldr x30, [sp]; //恢復x30 add sp, sp, #0x20; //恢復為真實sp ldr lr, p_hk_info; ldr lr, [lr, #0x28]; //pHkFunAddr blr lr; //既跳到pHkFunAddr執行,也設置了lr to_aft_callback: //其實這裡可能存在問題,即如果hook函數或者其調用了原函數,非標準函數(非c/c++)未實現棧平衡 //比如手寫的精心構造的彙編函數,可能存在覆蓋棧上數據 STP X1, X0, [SP, #-0x10] sub sp, sp, #0x20; //跳板在棧上存儲了x0、x1,但是未改變sp的值 mrs x0, NZCV str x0, [sp, #0x10]; //覆蓋跳板存儲的x1,存儲狀態暫存器 str x30, [sp]; //存儲x30 add x30, sp, #0x20 str x30, [sp, #0x8]; //存儲真實的sp ldr x0, [sp, #0x18]; //取出跳板存儲的x0 sub sp, sp, #0xf0; //分配棧空間 stp X0, X1, [SP]; //存儲x0-x29 stp X2, X3, [SP,#0x10] stp X4, X5, [SP,#0x20] stp X6, X7, [SP,#0x30] stp X8, X9, [SP,#0x40] stp X10, X11, [SP,#0x50] stp X12, X13, [SP,#0x60] stp X14, X15, [SP,#0x70] stp X16, X17, [SP,#0x80] stp X18, X19, [SP,#0x90] stp X20, X21, [SP,#0xa0] stp X22, X23, [SP,#0xb0] stp X24, X25, [SP,#0xc0] stp X26, X27, [SP,#0xd0] stp X28, X29, [SP,#0xe0] mov x0, sp; //x0作為第一個參數,那麼操作x0=sp,即操作棧讀寫保存的暫存器 ldr x1, p_hk_info; ldr x3, [x1, #0x20]; //aft_callback, 恢復lr暫存器 bl get_lr_pc; //lr為下條指令 add lr, lr, 8; //lr為blr x3的地址 str lr, [sp, #0x108]; //lr當作pc,覆蓋棧上的x0 blr x3; //執行aft_callback to_popreg: ldr x0, [sp, #0x100]; //取出狀態暫存器 msr NZCV, x0 ldp X0, X1, [SP]; //恢復x0-x29暫存器 ldp X2, X3, [SP,#0x10] ldp X4, X5, [SP,#0x20] ldp X6, X7, [SP,#0x30] ldp X8, X9, [SP,#0x40] ldp X10, X11, [SP,#0x50] ldp X12, X13, [SP,#0x60] ldp X14, X15, [SP,#0x70] ldp X16, X17, [SP,#0x80] ldp X18, X19, [SP,#0x90] ldp X20, X21, [SP,#0xa0] ldp X22, X23, [SP,#0xb0] ldp X24, X25, [SP,#0xc0] ldp X26, X27, [SP,#0xd0] ldp X28, X29, [SP,#0xe0] add sp, sp, #0xf0 ldr x30, [sp]; //恢復x30 add sp, sp, #0x20; //恢復為真實sp get_lr_pc: br lr; //僅用於獲取LR/PC,最後相當於br lr nop; p_hk_info: //結構體STR_HK_INFO .double 0xffffffffffffffff replace_end: .end
和dumpwithret大部分是一致的,執行原函數換成執行hook函數。因為這個hook函數不是shellcode,所以沒有好辦法在開頭加入恢復暫存器的指令,那麼就要使用一個無關的暫存器,因為要返回所以lr暫存器被保存了也改變了,所以這裡就使用lr暫存器即可,blr lr,既跳到lr保存的地址去執行,也會把下條指令的地址存到lr。
replace_demo
typedef int (*old_open)(const char* pathname,int flags,/*mode_t mode*/...); typedef int (*old__open)(const char* pathname,int flags,int mode); typedef int (*old__openat)(int fd, const char *pathname, int flags, int mode); typedef FILE* (*old_fopen)(const char* __path, const char* __mode); old_open *ori_open; old__open *ori__open; //可變參數的函數,需要自己按照被hook函數的邏輯,解析出參數再傳遞給原函數。 //因為並不清楚參數個數/類型,如果不改變參數的情況下還有方法不解析參數調用原函數。 //但是如果改變了參數,比如printf中的fmt,那麼理論上後面的參數類型個數也應該改變,這種情況下應該是使用者 //已經清楚共用多少參數和類型,應該自己調用,而如果只改fmt,應該會出bug的。 //所以如果只是想列印明確類型的參數,不改變參數直接調用原函數的情況,應該實現下參數的解析重組/傳遞,待實現 int my_open(const char* pathname,int flags, .../*int mode*/){ mode_t mode = 0; if (needs_mode(flags)) { va_list args; va_start(args, flags); mode = static_cast<mode_t>(va_arg(args, int)); va_end(args); } LE("hk: open %s , %d, %d", pathname ? pathname : "is null", flags, mode); //測試解除hook // HK_INFO *pInfo = isHookedByHkFun((void *) (my_open)); // unHook(pInfo); int fd = -1; if (!ori_open) {//理論上只有粗心沒有給ori_open賦值。 LE("ori_open null !"); exit(1); } if (!(*ori_open)) {//理論上應該是hook取消了,但是如果是自己忘了給ori_open賦值或者賦值為NULL,那就是自己的鍋了,太粗心了,會陷入死循環。 LE("unhook open"); old_open tmp = open; ori_open = &tmp; } fd = (*ori_open)(pathname, flags, mode); LE("ori: open %s , fd=%d", pathname ? pathname : "is null", fd); return fd; } void test_replace(){ LE("open=%p, callback=%p", open, onPreCallBack); const RetInfo info = dump_replace((void *) (open), (void *) (my_open), onPreCallBack, onCallBack, "open"); if (info.status != success) { LE("hook open error"); } else { //考慮到解除hook的問題,不能用getOriFun直接獲取備份的函數,應該獲取函數的指針 //不然直接返回函數,free函數後無法知道。建議不用自己保存原函數,使用api獲取,當然如果沒有unhook的情況,就不需要考慮這些問題。 ori_open = (old_open*)(getPoriFun(info.info)); } int fd = open("/system/lib/libc.so", O_RDONLY); LE("open /system/lib/libc.so, fd=%d", fd); unHook(info.info); } FILE* my_fopen(const char* pathname,const char* mode){ LE("hk: fopen %s , %s", pathname ? pathname : "is null", mode); FILE* fd = NULL; //理論上有可能存在取消hook了,但是hook函數還要執行,所以應該可以再回調原函數了。 //所以如果要絕對安全,那麼hook和取消hook時暫停執行緒,檢查函數調用棧是必須的。 auto ori_open = (old_fopen)(getOriFunByHkFun((void *)(my_fopen))); if (!ori_open) { ori_open = (old_fopen)fopen; } fd = ori_open(pathname, mode); LE("ori: fopen %s , fd=%p", pathname ? pathname : "is null", fd); return fd; } //9.0上arm64未導出__open函數,而arm下實現為: //.text:0006FEDC //.text:0006FEDC EXPORT __open //.text:0006FEDC __open ; DATA XREF: LOAD:0000254C↑o //.text:0006FEDC ; __unwind { //.text:0006FEDC PUSH {R7,LR} //.text:0006FEDE BLX j_abort //.text:0006FEDE ; } // starts at 6FEDC //.text:0006FEDE ; End of function __open //open函數不再走__open,而是__openat,9.0上arm64未導出__openat函數(只是隱藏了符號,可以自己根據字元串解析出地址),arm導出。 void test_justReplace(){ LE("open=%p, callback=%p", open, onPreCallBack); // const RetInfo info = dump_replace(dlsym(RTLD_DEFAULT, "__openat"), (void *) (my__open), NULL, // NULL, "__open"); //android高版本沒有__open了,__openat也隱藏符號了(只有64位隱藏了) // const RetInfo info = dump_replace(dlsym(RTLD_DEFAULT, "__openat"), (void *) (my__openat), NULL, // NULL, "__openat"); const RetInfo info = dump_replace((void*)fopen, (void *) (my_fopen), NULL, NULL, "fopen"); if (info.status != success) { LE("hook fopen error=%d", info.status); } FILE *pFile = fopen("/system/lib/libc.so", "re"); LE("fopen /system/lib/libc.so, fd=%p", pFile); unHook(info.info); }
api
以上就是4種hook方式和對應的實現。提供給用戶的api介面如下:
#ifdef __cplusplus #include <map> #include <pthread.h> #include <vector> typedef std::vector<HK_INFO*> INFOS; //typedef std::map<const void*, HK_INFO*> LR_MAP; #endif enum hk_status{ success, hooked, error }; struct RetInfo { enum hk_status status; HK_INFO *info; }; //列印暫存器 /** * 用戶自定義的stub函數,嵌入在hook點中,可直接操作暫存器等 * @param regs 暫存器結構,保存暫存器當前hook點的暫存器資訊 * @param pInfo 保存了被hook函數、hook函數等的結構體 */ void default_onPreCallBack(my_pt_regs *regs, HK_INFO *pInfo); /** * 用戶自定義的stub函數,嵌入在hook點中,可直接操作暫存器等 * @param regs 暫存器結構,保存暫存器當前hook點的暫存器資訊 * @param pInfo 保存了被hook函數、hook函數等的結構體 */ void default_onCallBack(my_pt_regs *regs, HK_INFO *pInfo); /** * 真實函數執行前調用onPreCallBack,執行後調用onCallBack,通過onPreCallBack控制參數,通過onCallBack控制返回值 * @param pBeHookAddr 要hook的地址,必須 * @param onPreCallBack 要插入的回調函數(讀寫參數暫存器), 可以為NULL(onCallBack不為空),當和onCallBack都為NULL的情況使用默認的列印暫存器的函數default_onPreCallBack,因為什麼都不做為什麼hook? * @param onCallBack 要插入的回調函數(讀寫返回值暫存器),可以為NULL,如果只關心函數執行後的結果 * @param methodName 被hook的函數名稱,可以為NULL。 * @return success:成功;error:錯誤;hooked:已被hook; */ hk_status dump(void *pBeHookAddr, void (*onPreCallBack)(struct my_pt_regs *, HK_INFO *pInfo), void (*onCallBack)(struct my_pt_regs *, struct STR_HK_INFO *pInfo) = NULL, const char* methodName = NULL); /** * 不執行真實函數,直接操作暫存器,之後恢復暫存器返回,理論上也是可以在onCallBack其中執行真實函數的,但是需要自己封裝參數,調用後自己解析暫存器 * @param pBeHookAddr 要hook的地址,必須 * @param onCallBack 要插入的回調函數(讀寫參數暫存器),必須 * @param methodName 被hook的函數名稱,可以為NULL。 * @return success:成功;error:錯誤;hooked:已被hook; */ hk_status dumpRet(void *pBeHookAddr, void (*onCallBack)(struct my_pt_regs *, HK_INFO *pInfo), const char* methodName = NULL); /** * 針對標準函數,最常用的hook介面。定義一個和被hook函數原型一致的函數接收處理參數,可直接返回或者調用被備份/修復的原函數 * @param pBeHookAddr 要hook的地址,必須 * @param pHkFunAddr 和被hook函數原型一致的函數,接收處理參數,可直接返回或者調用被備份/修復的原函數,必須 * @param onPreCallBack 要插入的回調函數(讀寫參數暫存器), 可以為NULL * @param onCallBack 要插入的回調函數(讀寫返回值暫存器),可以為NULL,如果只關心函數執行後的結果 * @param methodName 被hook的函數名稱,可以為NULL * @return 因為既要區分三種狀態,還存在返回備份/修復的原函數的情況,使用結構體存儲兩個欄位,參考demo。 */ RetInfo dump_replace(void *pBeHookAddr, void*pHkFunAddr, void (*onPreCallBack)(struct my_pt_regs *, HK_INFO *pInfo) = NULL, void (*onCallBack)(struct my_pt_regs *, struct STR_HK_INFO *pInfo) = NULL, const char* methodName = NULL); /** * 通過被hook函數獲取數據結構體 * @param pBeHookAddr 被hook函數 * @return 返回HK_INFO結構體 */ HK_INFO *isHooked(void* pBeHookAddr); /** * 通過hook函數獲取數據結構體 * @param hkFun hook函數 * @return 返回HK_INFO結構體 */ HK_INFO *isHookedByHkFun(void* hkFun); /** * 獲取備份/修復的被hook函數,主要是不清楚結構體欄位的用戶或者透明指針的情況 * @param info * @return 返回備份/修復的被hook函數 */ void* getOriFun(HK_INFO* info); /** * 獲取備份/修復的被hook函數的指針,二級指針;可用於自己保存,推薦存在取消hook的情況下調用getOriFunByHkFun函數 * @param info * @return 返回指向存儲備份/修復的被hook函數的指針 */ void** getPoriFun(HK_INFO* info); /** * 通過hook函數獲取被hook的函數 * @param hkFun hook函數 * @return 返回被hook函數,如果被取消hook或者未被hook返回NULL */ void* getOriFunByHkFun(void* hkFun); /** * 取消hook,釋放shellcode/備份的原方法佔用的空間並還原原方法 * @param info 如果成功會釋放這個結構體,所以之後這個結構體/指針不能再用 * @return 取消成功true,否則false */ bool unHook(HK_INFO* info); /** * 取消所有的hook * @return */ bool unHookAll();
arm/thumb實現和難點
以上就是arm64的實現和定義的api介面。arm的只需要依葫蘆畫瓢即可,比arm64簡單多了。
shellcode統一使用arm
shellcode理論上是arm還是thumb都沒有關係,而且Android的編譯鏈中的彙編器不支援大部分的32位Thumb-2指令(默認情況下),
.code 16 //.thumb //和上面效果一致 //.force_thumb //強制thumb shell_code: push {r0, r1, r2, r3} //r0=r0, r1=sp(push之前的sp), r2=r14/lr, r3=cpsr mrs r0, cpsr str r0, [sp, #0xC] str r14, [sp, #8]
第四條指令編譯報錯:Error: lo register required — `str r14,[sp,#8]'
因為thumb16中能直接操作的暫存器是r0-r7;r8-r12、r14暫存器不能被str、ldr等,ldr pc暫存器也不行,但是可以ldr到r0-r7,再mov到sp暫存器。而thumb模式下的跳板0也是LDR PC, [PC, #0],是32位的thumb32/thumb-2指令,所以理論上armv5和以下的cpu應該是不支援指令的。
因為shellcode中彙編指令超出了thumb16的暫存器範圍,使用thumb32/thumb-2指令,彙編器不支援:
Error: unexpected character `w' in type specifier Error: bad instruction `str.w r14,[sp,#8]'
所以不用在Android.mk中指定LOCALARMMODE := arm,因為這樣整個module都是arm指令、4位元組,浪費了空間,所以默認就行。奇怪的是編譯出的文件包含arm和thumb32,混合的。沒有細看是什麼情況,編譯c/c++使用的彙編器不一致嗎?
呃,後來看到:經過Google工程師的提醒,對於ARM GCC的彙編器,在彙編文件最上面加入.syntax unified之後,Thumb-2 T3 encoding彙編也能正常使用了,比如:
.syntax unified .text .align 4 .globl helloThumb .thumb .thumb_func helloThumb: add.w r0, r0, r1, lsl #2 bx lr
所以在彙編文件中加入
.syntax unified .force_thumb
可以把操作r0-r7暫存器之外的指令寫成,ldr.w、str.w、add.w等,也可以不修改,編譯器會自動轉成thumb32/thumb-2指令。
編譯後就是thumb32/thumb-2指令了,

因為這個shellcode變成thumb了,所以構建跳板shellcode時(LDR PC, [PC, #0]/hook thumb函數或者LDR PC, [PC, #-4]/hook arm函數,指令下面的地址要加1)。
所以hook arm函數也可以構建thumb的shellcode,同樣的thumb函數也可以構建arm的shellcode。但是為了方便、不考慮四位元組對齊,乾脆還是都用arm的shellcode吧,只要不指定為thumb(默認可能就是)、不指定.syntax unified就始終編譯出來的就是arm的shellcode。畢竟沒有那麼嚴重的強迫症,也占不了多少空間。
LDR PC, [PC, #0]/hook thumb函數或者LDR PC, [PC, #-4]/hook arm函數,理論上在armv5及以上就可以,所以無論是arm到thumb還是thumb到thumb,LDR到PC的地址+1就可以了。同樣到arm就保證是非奇數的地址即可。
至於這個shellcode編譯到.text還是.data都可以。不同的就是.data中是數據,.text中為函數。
跳板0統一使用ldr pc
只要cpu是armv5以上就行,因為目前很少armv5及以下的cpu了,所以不考慮支援問題。ldr pc, [pc, #0/#4],可被編譯成arm或者thumb32/thumb-2指令。這裡可能有人會有些誤解,比如我們要hook的一個so是armeabi的,那麼thumb函數只支援thumb16,所以是沒有ldr pc這樣的指令的,那麼對於這樣的函數還能使用ldr pc覆蓋嗎?其實是可以的,因為這條指令能不能被正確解析執行是取決於cpu,只要cpu支援thumb32/thumb-2指令(armv5以上的cpu)即可。thumb32/thumb-2指令更像是thumb16的一個超集(16位的指令應該都是相同的,32位的指令會不同),所以不影響後面的thumb16的指令的解析執行。
所以這裡我們不是對當前函數的thumb做兼容性適配而是針對cpu的,而如果要適配armv5以下,就多繞一下好了。
程式碼實現arm/thumb統一的跳板0
arm:
LDR PC, [PC, #-4];流水線,pc為addr下面的指令,所以-4 addr:0x12345678
thumb32/thumb-2:
LDR PC, [PC, #0];流水線,pc為addr指令 addr:0x12345678
都是佔用8條指令,如果thumb函數地址不是4位元組對齊的話就加nop或者bx #-2佔用10位元組。
程式碼實現armdump函數
.global _dump_start .global _dump_end .global _hk_info .global _oriFuc .hidden _dump_start .hidden _dump_end .hidden _hk_info .hidden _oriFuc //可用於標準的c/c++函數、非標準函數、函數的一部分(用於讀寫暫存器),前提都是位元組長度足夠 //非標準函數即非c/c++編譯的函數,那麼手寫彙編可能存在並不遵守約定的情況,比如我們使用了sp暫存器,並在未使用的棧上保存暫存器 //但是可能不是滿遞減而是反過來滿遞增,或者不遵守棧平衡,往棧上寫數據,但是並不改變sp暫存器。當然應該是很少見的。 .data _dump_start: //用於讀寫暫存器/棧,需要自己解析參數,不能讀寫返回值,不能阻止原函數(被hook函數)的執行 //從行為上來我覺得更偏向dump,所以起名為dump。 push {r0-r4} //r0=r0,中轉用 r1=sp(push之前的sp), r2=r14/lr, r3=pc, r4=cpsr mrs r0, cpsr str r0, [sp, #0x10] //r4的位置存放cpsr str r14, [sp, #8] //r2的位置存放lr add r14, sp, #0x14 str r14, [sp, #4] //r1的位置存放真實sp pop {r0} //恢復r0 push {r0-r12} //保存r-r12,之後是r13-r16/cpsr mov r0, sp ldr r1, _hk_info; ldr r3, [r1]; //onPreCallBack str pc, [sp, #0x3c]; //存儲pc,arm模式,所以pc+8,指向ldr r0, [sp, #0x40] blx r3 ldr r0, [sp, #0x40] //cpsr msr cpsr, r0 ldmfd sp!, {r0-r12} //恢復r0-r12 ldr r14, [sp, #4] //恢復r14/lr ldr sp, [r13] //恢復sp(push之前的sp) ldr pc, _oriFuc _oriFuc: //備份/修復的原方法 .word 0x12345678 _hk_info: //結構體STR_HK_INFO .word 0x12345678 _dump_end: .end
這裡和arm64比有點不同的是多了一個標號/4個位元組存放oriFuc/原函數的地址,這麼做的原因是比較無奈的,因為hkinfo取到的是HKINFO,還需要再計算一次才能得到pOriFuncAddr,這樣就需要佔用一個暫存器去計算,pc暫存器是不能這麼用的,所以還是在shellcode中存儲吧。當然其實還可以讓hkinfo存放的是pOriFuncAddr這個變數的地址,那麼可以一次取到pc暫存器中,但是其他shellcode都要變且不如原來的直觀/簡單了,同樣的方式還可以把pOriFuncAddr放在結構體頭部,然後hkinfo存放pOriFuncAddr變數的地址/HKINFO結構體的地址,也是可以的,但是也是要變動太多。而且其實只有這一個shellcode需要多定義一個oriFuc,其他是不需要的,所以折衷後就這樣了。
demo和arm64一致,api也是一致的。arm和thumb共用這一個shellcode,注意的一點就是如果被hook的函數是thumb,那麼_oriFuc存放的地址+1即可。
程式碼實現armdumpwithret函數
.global r_dump_start .global r_dump_end .global r_hk_info .hidden r_dump_start .hidden r_dump_end .hidden r_hk_info .data r_dump_start: //用於讀寫暫存器/棧,需要自己解析參數,不能讀寫返回值,不能阻止原函數(被hook函數)的執行 //從行為上來我覺得更偏向dump,所以起名為dump。 push {r0-r4} //r0=r0,中轉用 r1=sp(push之前的sp), r2=r14/lr, r3=pc, r4=cpsr mrs r0, cpsr str r0, [sp, #0x10] //r4的位置存放cpsr str r14, [sp, #8] //r2的位置存放lr add r14, sp, #0x14 str r14, [sp, #4] //r1的位置存放真實sp pop {r0} //恢復r0 push {r0-r12} //保存r-r12,之後是r13-r16/cpsr mov r0, sp ldr r1, r_hk_info; ldr r3, [r1, #8]; //pre_callback str pc, [sp, #0x3c]; //存儲pc blx r3 to_call_oriFun: ldr r0, [sp, #0x40] //cpsr msr cpsr, r0 ldmfd sp!, {r0-r12} //恢復r0-r12 ldr r14, [sp, #4] //恢復r14/lr ldr sp, [r13] //恢復sp(push之前的sp) ldr lr, r_hk_info ldr lr, [lr, #4]; //pOriFuncAddr blx lr; to_aft_callback: push {r0-r4} //r0=r0,中轉用 r1=sp(push之前的sp), r2=r14/lr, r3=pc, r4=cpsr mrs r0, cpsr str r0, [sp, #0x10] //r4的位置存放cpsr str r14, [sp, #8] //r2的位置存放lr add r14, sp, #0x14 str r14, [sp, #4] //r1的位置存放真實sp pop {r0} //恢復r0 push {r0-r12} //保存r-r12,之後是r13-r16/cpsr mov r0, sp ldr r1, r_hk_info; ldr r3, [r1, #0x10]; //aft_callback str pc, [sp, #0x3c]; //存儲pc blx r3 to_popreg: ldr r0, [sp, #0x40] //cpsr msr cpsr, r0 ldmfd sp!, {r0-r12} //恢復r0-r12 ldr r14, [sp, #4] //恢復r14/lr ldr sp, [r13] //恢復sp(push之前的sp) mov pc, lr; r_hk_info: //結構體STR_HK_INFO .word 0x12345678 r_dump_end: .end
demo和arm64一致,這裡不用多定義一個_oriFuc,因為保存了lr,不是必須使用pc暫存器了。也是讓lr暫存器做了很多事情。
程式碼實現armdumpjustret函數
.global j_dump_start .global j_dump_end .global j_hk_info .hidden j_dump_start .hidden j_dump_end .hidden j_hk_info .data j_dump_start: push {r0-r4} //r0=r0,中轉用 r1=sp(push之前的sp), r2=r14/lr, r3=pc, r4=cpsr mrs r0, cpsr str r0, [sp, #0x10] //r4的位置存放cpsr str r14, [sp, #8] //r2的位置存放lr add r14, sp, #0x14 str r14, [sp, #4] //r1的位置存放真實sp pop {r0} //恢復r0 push {r0-r12} //保存r-r12,之後是r13-r16/cpsr mov r0, sp ldr r1, j_hk_info; ldr r3, [r1]; //onPreCallBack str pc, [sp, #0x3c]; //存儲pc blx r3 ldr r0, [sp, #0x40] //cpsr msr cpsr, r0 ldmfd sp!, {r0-r12} //恢復r0-r12 ldr r14, [sp, #4] //恢復r14/lr ldr sp, [r13] //恢復sp(push之前的sp) mov pc, lr j_hk_info: //結構體STR_HK_INFO .word 0x12345678 j_dump_end: .end
demo和arm64一致。
程式碼實現armreplace函數
.global replace_start .global replace_end .global p_hk_info .hidden replace_start .hidden replace_end .hidden p_hk_info .data //這種方式盡量用於標準的c/c++函數,因為通過hook函數再調用原函數,只能保證參數暫存器和lr暫存器是一致的,其他暫存器可能被修改。 replace_start: //如果只是替換/跳到hook函數,其實是不用保存暫存器的,只是重新寫比較麻煩,所以在之前的基礎上 push {r0-r4} //r0=r0,中轉用 r1=sp(push之前的sp), r2=r14/lr, r3=pc, r4=cpsr mrs r0, cpsr str r0, [sp, #0x10] //r4的位置存放cpsr str r14, [sp, #8] //r2的位置存放lr add r14, sp, #0x14 str r14, [sp, #4] //r1的位置存放真實sp pop {r0} //恢復r0 push {r0-r12} //保存r-r12,之後是r13-r16/cpsr mov r0, sp ldr r1, p_hk_info; ldr r3, [r1, #8]; //pre_callback,保存lr str pc, [sp, #0x3c]; //存儲pc blx r3 to_call_oriFun: ldr r0, [sp, #0x40] //cpsr msr cpsr, r0 ldmfd sp!, {r0-r12} //恢復r0-r12 ldr r14, [sp, #4] //恢復r14/lr ldr sp, [r13] //恢復sp(push之前的sp) ldr lr, p_hk_info ldr lr, [lr, #0x14]; //pHkFunAddr blx lr; to_aft_callback: push {r0-r4} //r0=r0,中轉用 r1=sp(push之前的sp), r2=r14/lr, r3=pc, r4=cpsr mrs r0, cpsr str r0, [sp, #0x10] //r4的位置存放cpsr str r14, [sp, #8] //r2的位置存放lr add r14, sp, #0x14 str r14, [sp, #4] //r1的位置存放真實sp pop {r0} //恢復r0 push {r0-r12} //保存r-r12,之後是r13-r16/cpsr mov r0, sp ldr r1, p_hk_info; ldr r3, [r1, #0x10]; //aft_callback str pc, [sp, #0x3c]; //存儲pc blx r3 to_popreg: ldr r0, [sp, #0x40] //cpsr msr cpsr, r0 ldmfd sp!, {r0-r12} //恢復r0-r12 ldr r14, [sp, #4] //恢復r14/lr ldr sp, [r13] //恢復sp(push之前的sp) mov pc, lr; p_hk_info: //結構體STR_HK_INFO .word 0x12345678 replace_end: .end
demo和arm64一致。
細節的優化
框架已經搭好了且已經可以運行了,接下來就是一些細節的優化。
記憶體對齊
shellcode、備份/修復的原函數不能使用malloc等非記憶體對齊的函數,原因在下面的AndroidInlineHook中較嚴重的一些bug有提到。但是每個shellcode都佔用一頁記憶體也太浪費了,理想情況是一頁用完或者放不下一個shellcode再申請,但是考慮到解除hook釋放shellcode等問題,這麼做實現起來很複雜,要監聽整個流程,暫時沒精力寫這麼完美,所以就把shellcode和備份/修復的原函數放在同一頁記憶體,當然還是用不滿,但是至少節約了一些,這樣解除hook直接釋放這頁記憶體都可以,當然還可以進一步把HKINFO這個結構體也放在這一頁內,但是不確定是否確實有些系統只允許記憶體許可權為rw或者rp,暫時沒有把HKINFO混在一起。
刷新指令快取
void test_args_11(int a0, int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10, int a11){ LE("a0=%d, a1=%d, a2=%d, a3=%d, a4=%d, a5=%d," " a6=%d, a7=%d, a8=%d, a9=%d, a10=%d. a11=%d", a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11); } void nullCallBack(my_pt_regs *regs, HK_INFO *pInfo){ } void test_args_for_cache(){ //測試多參數傳遞情況 dump((void *)(test_args_11), nullCallBack, /*onCallBack*/NULL); // dumpRet((void *) (test_args_11), onPreCallBack, "test_args_11"); test_args_11(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); HK_INFO *pInfo = isHooked((void*)test_args_11); // unHook(pInfo); } //拷貝回原指令後不刷新快取。機器不同、cpu性能、快取大小不同等,這個閾值需要自己測試。 //性能好的機器,循環100次基本都可以100%復現,差的機器200次也只有幾次復現。 void test_cache(){ test_args_for_cache(); LE("test_args_11=%p", test_args_11); for (int i = 0; i < 188; ++i) { if (i == 178) { HK_INFO *pInfo = isHooked((void *) test_args_11); unHook(pInfo); } test_args_11(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); LE("i=%d", i); // usleep(1); } LE("end test cache"); }
為了測試不刷新快取的情況,使用了循環,因為實測發現想觸發快取不刷新crash概率太低了。我能想到的就是循環執行一段指令,觸發jit,提高指令被快取的概率。然後修改指令後不刷新快取,觸發crash。關於如何刷新快取在下面的AndroidInlineHook中較嚴重的一些bug有提到。
既是入參也是出參的函數
只是確認下,因為程式碼寫的沒問題,所以應該是不會有問題的。
int (*ori__system_property_get)(const char *name, char *value); //注意操作參數之前最好都檢查參數是否為null,如果原函數也是crash處理還行,如果不是就改變了函數邏輯、進程中斷等 //所以細節都要注意,參數和返回值也有對應關係 int my__system_property_get(const char *name, char *value){ LE("hk: __system_property_get(%s, %p)", name ? name : "is null", value); ori__system_property_get = (int (*)(const char *, char *))(getOriFunByHkFun( (void *) (my__system_property_get))); if (!ori__system_property_get) { ori__system_property_get = __system_property_get; } int ret = ori__system_property_get(name, value); LE("ori: __system_property_get(%s, %s)=%d", name ? name : "is null", ret > 0 ? value : "is null", ret); if (name && !strncmp("ro.serialno", name, strlen("ro.serialno"))) { strcpy(value, "12345678"); return (int)(strlen("12345678")); } return ret; } //測試既是參數也做返回值的函數 void test__system_property_get() { const RetInfo info = dump_replace((void*)__system_property_get, (void *) (my__system_property_get), NULL, NULL, "__system_property_get"); if (info.status != success) { LE("hook __system_property_get error=%d", info.status); } char sn[256]; int ret = __system_property_get("ro.serialno", sn); LE("serialno=%s", ret > 0 ? sn : "is null"); //2019-12-23 17:10:14.810 22213-22213/? E/zhuo: hk: __system_property_get(ro.serialno, 0x7fd55a2088) //2019-12-23 17:10:14.810 22213-22213/? E/zhuo: ori: __system_property_get(ro.serialno, 8acd10de)=8 //2019-12-23 17:10:14.810 22213-22213/? E/zhuo: lr aft_callback //2019-12-23 17:10:14.810 22213-22213/? E/zhuo: serialno=12345678 }
測試不存在hookzz的問題。
todo
基本上上面已經是大部分的框架和部分細節,剩下的對用戶api的具體實現都是很靈活的,只要保持和api函數原型和定義行為一致即可(已實現)。
跳板、shellcode、api都實現了,還剩下一個對備份的原函數進行修復。還沒有重新開始寫,暫時用的AndroidInlineHook修復pc相關的指令。在寫arm的vmp,到時抽取出指令解析和生成的程式碼就好了,加一些簡單的語義分析。
執行緒暫停的問題,抽時間加上,因為一般自己常用的場景是fork一個進程還未執行應用程式碼的時候,基本上不會出現多執行緒導致的crach,不過還是應該提供介面,畢竟有些場景需要,但是怕是也不能百分百解決執行緒問題,還需要用戶自己判斷函數行為。
可變參數的傳遞,如果不修改只是調用原函數倒是有思路了,但是如果是對明確的參數進行修改了,還是要抽時間寫解析構造參數的部分。
已知bug
1、arm64指令修復
.text:0000000000000EF8 dlopen ; DATA XREF: LOAD:0000000000000508↑o .text:0000000000000EF8 .text:0000000000000EF8 var_s0 = 0 .text:0000000000000EF8 .text:0000000000000EF8 ; __unwind { .text:0000000000000EF8 STP X29, X30, [SP,#-0x10+var_s0]! .text:0000000000000EFC MOV X29, SP .text:0000000000000F00 MOV X2, X30 .text:0000000000000F04 BL .__loader_dlopen .text:0000000000000F08 LDP X29, X30, [SP+var_s0],#0x10 .text:0000000000000F0C RET .text:0000000000000F0C ; } // starts at EF8 .text:0000000000000F0C ; End of function dlopen
例如其中的BL指令並未修復,這個其實還容易修復,可以替換成LDR LR, 8(應該放到最後);BLR LR;因為既然是BL指令那麼LR暫存器肯定是會被修改,那就說明LR暫存器已經保存了,所以我們就使用LR暫存器也是沒有關係的。
如果是B指令才麻煩,因為不能操作pc,那麼就要至少使用一個暫存器且還不能是LR,只能使用x16、x17了。
後面看了下AndroidInlineHook寫了B指令的修復(是錯的),BL指令未實現,後來自己寫了BL指令的實現,只是暫時用,不準備在這個基礎上再寫了,缺少的待修復的指令還有不少,且已實現的也有一些錯誤,待重構。


修復之後Android9.0 arm64的dlopen可以hook了。
2、arm64從備份/修復的原方法跳回跳板0之後執行,採用的方式是在備份/修復的原方法後面加入:
STP X1, X0, [SP, #-0x10] LDR X0, 8 BR X0 ADDR(64) LDR X0, [SP, -0x8]
和跳板0是一樣的,但是跳板0是一個函數的開頭的概率很大,所以一般都是會棧平衡的,sp指向棧頂,所以暫時存放暫存器不會被覆蓋。而放在後面就不太確定能保證sp指向棧頂了,所以可能要考慮解析前幾條指令是否有操作sp,或者儘可能的把暫存器儲存在sp-較大的值,或者使用x16、x17暫存器。。。當然這個可能概率較小,可以先不管。
3、無法重複hook一個函數,當然這個是說在其他框架或者本框架的另一個版本(或者無法維護保存hook的容器,如果是本框架內是可以重複hook同一個函數的,比如解除hook再hook,或者再擴展成鏈表的形式,依次調用hook/dump函數),其實就是指令修復不完整,arm的應該是ldr pc沒有正確修復,這個事情排在後面,不是特別重要。
也可以參考Cydia Substrate框架,不過有點坑,只處理thumb的情況,thumb可以多次hook,arm下第二次hook不生效。直接判斷第一條指令是不是ldr pc, [pc, #-4];是的話就認為是已經被hook過的,直接返回。
if (result != NULL) { if (backup[0] == A$ldr_rd_$rn_im$(A$pc, A$pc, 4 - 8)) { *result = reinterpret_cast<void *>(backup[1]); return; }
可見只有第一次hook生效。

而thumb模式,把上次的shellcode賦給result,再把跳板中的地址替換為新的地址。
f ( (align == 0 || area[0] == T$nop) && thumb[0] == T$bx(A$pc) && thumb[1] == T$nop && arm[0] == A$ldr_rd_$rn_im$(A$pc, A$pc, 4 - 8) ) { if (result != NULL) *result = reinterpret_cast<void *>(arm[1]); SubstrateHookMemory code(process, arm + 1, sizeof(uint32_t) * 1); arm[1] = reinterpret_cast<uint32_t>(replace); return sizeof(arm[0]); }

感覺是不是寫程式碼的忘了處理arm的,因為arm也是可以同樣處理的,呃,無語的bug。例如這麼修復即可,前面再加一行SubstrateHookMemory code(process, symbol, used);修改許可權/刷新快取。

應該VirtualApp hook系統函數之後使用syscall調用,就是因為梆梆等一些殼hook了函數,而VirtualApp使用Cydia Substrate未修復該bug,所以導致hook失敗。但是一推理應該是錯誤的,VirtualApp是先hook的,殼應該是後hook的,所以只能是殼hook失敗,所以應該是某些系統函數修復指令不正確導致調用原函數失敗,和本篇其實沒多大關係,只是推理下。
其實以上bug根本原因還是指令的修復,這是最麻煩的,也是暫時沒辦法達到百分百修復的,尤其是arm64,只能抽時間逐步完善了。
AndroidInlineHook中較嚴重的一些bug
1、在init_arry函數和普通函數引用vector,但其實不是同一個vector的,導致無法保存已hook的資訊,應該用指針,不能直接引用vector。屬於程式碼邏輯bug。
2、記憶體許可權問題雖然mprotect的時候取一頁記憶體,但是因為使用的malloc申請的記憶體可能是夾縫中記憶體,前後被使用了?也可能是申請後剩餘的被其他佔用更改了許可權?雖然mprotect不報錯,修改許可權成功了,但是有幾率signal 11 (SIGSEGV), code 2 (SEGV_ACCERR)。還要一種可能是malloc返回的地址是奇數的,未測試,理論上如果是奇數的也會crash的,因為記憶體沒有對齊。所以使用
long pagesize = sysconf(_SC_PAGE_SIZE); void *pNewShellCode = NULL;// = malloc(sShellCodeLength); int code = posix_memalign(&pNewShellCode,pagesize, pagesize);
或者
mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
申請一整頁的記憶體,但是這樣肯定是浪費的,所以之後應該在這個記憶體上填充,佔滿之後再申請新的一頁記憶體。
3、修改記憶體許可權,ChangePageProperty中如果超過一頁的記憶體,第二頁和之後的都沒有設置許可權,屬於程式碼邏輯bug,但是沒有超過2頁的情況,所以不會觸發。
4、arm刷新快取錯誤/未生效,arm64未刷新快取。//通過測試:cacheflush無法刷新快取,不過不確定是不是第三個參數的問題,ICACHE, DCACHE, or BCACHE,抽時間再看吧,//不過gtoad的把地址轉成無符號指針再取值肯定是不正確的。// cacheflush(((uint32t)(info->pBeHookAddr)), info->backUpLength, 0);//測試也無法刷新快取// cacheflush(PAGESTART((uintptrt)info->pBeHookAddr), PAGESIZE, 0);//測試也無法刷新快取// cacheflush(info->pBeHookAddr, info->backUpLength, 0);//測試也無法刷新快取
//而確實有效的刷新快取的是:可以直接傳地址和結束地址,也可以刷新一頁,效果來看都成功了。// if(TESTBIT0((uint32t)info->pBeHookAddr)){//builtinclearcache((char)info->pBeHookAddr – 1, (char)info->pBeHookAddr – 1 + info->backUpLength);// } else {//builtinclearcache((char ) info->pBeHookAddr,// (char ) info->pBeHookAddr + info->backUpLength);// }//builtinclearcache(PAGESTART((uintptrt)info->pBeHookAddr), PAGEEND((uintptrt)info->pBeHookAddr + info->backUpLength));
//後記://測試cacheflush可以,但是和int cacheflush(longaddr, longnbytes, long _cache);不一致的是,從命名來看第二個參數應該是大小,不應該是//結束地址,而且linux man中Some or all of the address range addr to (addr+nbytes-1) is not accessible。哎奇怪,抽時間再逆向看看吧。/if(TESTBIT0((uint32_t)info->pBeHookAddr)){ cacheflush((char)info->pBeHookAddr – 1, (char)info->pBeHookAddr – 1 + info->backUpLength, 0);} else { cacheflush((char ) info->pBeHookAddr, (char ) info->pBeHookAddr + info->backUpLength, 0);}/
//目前來看使用builtinclearcache吧,應該是都實現了且確實可用,如果某些系統不生效在摳出系統調用吧,先使用函數吧。
寫在後面
為了避免不必要的糾紛,前面列舉的框架只是為了引出為什麼在寫一個的原因,沒有其他的意思。只是想寫一個讓大多數人更容易使用、了解實現細節、能更容易參與開發完善的Android inline hook項目。所以暫時不考慮程式碼的藝術,而盡量的寫成易懂的程式碼,且程式碼寫了較多、完整的注釋。
也是希望不止是為了使用,而能讓使用者能自己了解原理,自己就能解決錯誤,這方面感謝下Android Inline Hook。之前也寫過一些自己個人用的臨時hook,但是時間久了程式碼都找不到了,且也都是最簡單的實現,臨時用下的,所以也打算重新實現下,沒時間一直拖著,看到AndroidInlineHook,發現很多思路和我之前是一樣的,程式碼也易懂,看一下就全明白了,所以一開始在AndroidInlineHook上面修了一些bug湊合用,不過後來修的多了發現不如重新實現,因為很多地方不兼容、架構有衝突了。
整理下,最近開源了,希望各位大佬多參與,完善修復bug,多謝!
https://github.com/zhuotong/Android_InlineHook