【轉】Android Native Hook工具實踐
來自專欄智能移動開發方法5 人贊了文章
本文章所對應項目長期維護與更新,因為在我自己的幾台測試機上用得還挺順手的。本項目作為作者本人的一個學習項目將會長期更新以修復當前可能存在的Bug以及跟進以後Android NDK可能出現的主流彙編模式。
本文為GToad作者本人原創,轉載請註明出處:GToad Blog 或 銀河安全實驗室
前言
在目前的安卓APP測試中對於Native Hook的需求越來越大,越來越多的APP開始逐漸使用NDK來開發核心或者敏感代碼邏輯。個人認為原因如下:
1. 安全的考慮。各大APP越來越注重安全性,NDK所編譯出來的so庫逆向難度明顯高於java代碼產生的dex文件。越是敏感的加密演算法與數據就越是需要用NDK進行開發。2. 性能的追求。NDK對於一些高性能的功能需求是java層無法比擬的。3. 手游的興起。虛幻4,Unity等引擎開發的手游中都有大量包含遊戲邏輯的so庫。因此,本人調查了一下Android Native Hook工具目前的現狀。儘管Java層的Hook工具多種多樣,但是Native Hook的工具卻非常少並且在安卓5.0以上
的適配工具更是寥寥無幾。(文末說明1)而目前Native Hook主要有兩大技術路線:
這兩種技術路線本人都實踐了一下,關於它們的對比,我在《Android Native Hook技術路線概述》中有介紹,所以這裡就不多說了。最終,我用了Inline Hook
來做這個項目。
本文篇幅已經較長,因此寫了一些獨立的學習筆記來對其中的細節問題進行解釋:
1. 《Android Native Hook技術路線概述》2. 《Android Inline Hook中的指令修復》3. 項目倉庫4. 項目案例——Arm325. 項目案例——Thumb-2目標效果
根據本人自身的使用需求提出了如下幾點目標:
- 工具運行原理中不能涉及調試目標APP,否則本工具在遇到反調試措施的APP時會失效。儘管可以先去逆向調試patch掉反調試功能,但是對於大多數情況下只是想看看參數和返回值的Hook需求而言,這樣的前期處理實在過於麻煩。
- 依靠現有的各大Java Hook工具就能運行本工具,換句話說就是最好能用類似這些工具的插件的形式載入起本工具從而獲得Native Hook的能力。由於Java Hook工具如Xposed、YAHFA等對於各個版本的Android都做了不錯的適配,因此利用這些已有的工具即可向目標APP的Native層中注入我們的Hook功能將會方便很多小夥伴的使用。
- 既然要能夠讓各種Java Hook工具都能用本工具得到Native Hook的能力,那就這個工具就要有被載入起來以後自動執行自身功能邏輯的能力!而不是針對各個Java Hook工具找調用起來的方式。
- 要適配Android NDK下的armv7和thumb-2指令集。由於現在默認編譯為thumb-2模式,所以對於thumb16和thumb32的Native Hook支持是重中之重。
- 修復Inline Hook後的原本指令。
- Hook目標的最小單位至少是函數,最好可以是某行彙編代碼。
最終方案
最後完成項目的方案是:本工具是一個so庫。用Java Hook工具在APP的入口Activity運行一開始的onCreate方法處Hook,然後載入本so。
載入後,自動開始執行Hook邏輯。為了方便敘述,接下來的Java Hook工具我就使用目前這類工具里最流行的Xposed
,本項目的生成文件名為libautohook.so
。
自動執行
我們只是用Xposed載入了這個libautohook.so,那其中的函數該怎麼自動執行呢?
目前想到兩個方法:- 利用JniOnload來自動執行。該函數是NDK中用戶可以選擇性自定義實現的函數。如果用戶不實現,則系統默認使用NDK的版本為1.1。但是如果用戶有定義這個函數,那Android VM就會在System.loadLibrary()載入so庫時自動先執行這個函數來獲得其返回的版本號。儘管該函數最終要返回的是NDK的版本號,但是其函數可以加入任意其它邏輯的代碼,從而實現載入so時的自動執行。這樣就能優先於所有其它被APP NDK調用的功能函數被調用,從而進行Hook。目前許多APP加固工具和APP初始化工作都會用此方法。
- 本文採用的是第二種方法。該方法網路資料中使用較少。它是利用了
__attribute__((constructor))
屬性。使用這個constructor屬性編譯的普通ELF文件被載入入內存後,最先執行的不是main函數,而是具有該屬性的函數。同樣,本項目中利用此屬性編譯出來的so文件被載入後,儘管so里沒有main函數,但是依然能優先執行,且其執行甚至在JniOnload之前。於是逆向分析了一下編譯出來的so庫文件。發現具有constructor
屬性的函數會被登記在.init_array中。(相對應的destructor
屬性會在ELF卸載時被自動調用,這些函數會被登記入.fini_array)
值得一提的是,constructor
屬性的函數是可以有多個的,對其執行順序有要求的同學可以通過在代碼中對這些函數聲明進行排序從而改變其在.init_array中的順序,二者是按順序對應的。而執行時,會從.init_array中自上而下地執行這些函數。所以圖中的自動優先執行順序為:main5->main3->main1->main2->main4。並且後面會說到,從+1可以看出這些函數是thumb模式編譯的。
方案設計
先說一下使用的工具:
- 使用
keystone
查找指定架構下彙編指令的機器碼 - 使用
MS VISIO
製作了下面的設計圖 - 調試工具用的是
IDA pro
Arm32方案
現在我們的代碼可以在一開始就執行了,那該如何設計這套Inline Hook方案呢?目標是thumb-2和arm指令集下是兩套相似的方案。我參考了騰訊遊戲安全實驗室的一篇教程,其中給出了一個初步的armv7指令集下的Native Hook方案,整理後如下圖:
Arm 第1步
根據/proc/self/map中目標so庫的內存載入地址與目標Hook地址的偏移計算出實際需要Hook的內存地址。將目標地址處的2條ARM32彙編代碼(8 Bytes)進行備份,然後用一條LDR PC指令和一個地址(共計8 Bytes)替換它們。這樣就能(以arm模式)將PC指向圖中第二部分stub代碼所在的位置。由於使用的是LDR而不是BLX,所以lr寄存器不受影響。關鍵代碼如下:
//LDR PC, [PC, #-4]對應的機器碼為:0xE51FF004BYTE szLdrPCOpcodes[8] = {0x04, 0xF0, 0x1F, 0xE5};//將目的地址拷貝到跳轉指令下方的4 Bytes中memcpy(szLdrPCOpcodes + 4, &pJumpAddress, 4);
Arm 第2步
構造stub代碼。構造思路是先保存當前全部的寄存器狀態到棧中。然後用BLX命令(以arm模式)跳轉去執行用戶自定義的Hook後的函數。執行完成後,從棧恢復所有的寄存器狀態。最後(以arm模式)跳轉至第三部分備份代碼處。關鍵代碼如下:
_shellcode_start_s: push {r0, r1, r2, r3} mrs r0, cpsr str r0, [sp, #0xC] str r14, [sp, #8] add r14, sp, #0x10 str r14, [sp, #4] pop {r0} push {r0-r12} mov r0, sp ldr r3, _hookstub_function_addr_s blx r3 ldr r0, [sp, #0x3C] msr cpsr, r0 ldmfd sp!, {r0-r12} ldr r14, [sp, #4] ldr sp, [r13] ldr pc, _old_function_addr_s
Arm 第3步
構造備份代碼。構造思路是先執行之前備份的2條arm32代碼(共計8 Btyes),然後用LDR指令跳轉回Hook地址+8bytes的地址處繼續執行。此處先不考慮PC修復,下文會說明。構造出來的彙編代碼如下:
備份代碼1備份代碼2LDR PC, [PC, #-4]HOOK_ADDR+8
Thumb-2方案
以上是本工具在arm指令集上的Native Hook基本方案。那麼在thumb-2指令集上該怎麼辦呢?我決定使用多模式切換來實現(文末解釋2),整理後如下圖:
雖然這部分內容與arm32很相似,但由於細節坑較多,所以我認為下文重新梳理詳細思路是必要的。
Thumb-2 第1步
第一步,根據/proc/self/map中目標so庫的內存載入地址與目標Hook地址的偏移計算出實際需要Hook的內存地址。將目標地址處的X Bytes的thumb彙編代碼進行備份。然後用一條LDR.W PC指令和一個地址(共計8 Bytes)替換它們。這樣就能(以arm模式)將PC指向圖中第二部分stub代碼所在的位置。由於使用的是LDR.W而不是BLX,所以lr寄存器不受影響。
細節1
:為什麼說是X Bytes?參考了網上不少的資料,發現大部分代碼中都簡單地將arm模式設置為8 bytes的備份,thumb模式12 bytes的備份。對arm32來說很合理,因為2條arm32指令足矣,上文處理arm32時也是這麼做的。而thumb-2模式則不一樣,thumb-2模式是thumb16(2 bytes)與thumb32(4 bytes)指令混合使用。本人在實際測試中出現過2+2+2+2+2+4>12的情形,這種情況下,最後一條thumb32指令會被截斷,從而在備份代碼中執行了一條只有前半段的thumb32,而在4->1的返回後還要執行一個只有後半段的thumb32。因此,本項目最初在第一步備份代碼前會檢查最後第11和12byte是不是前半條thumb32,如果不是,則備份12 byte。如果是的話,就備份10 byte。但是後來發現也不行,因為Thumb32指令的低16位可能會被誤判為新Thumb32指令的開頭。因此,最終通過統計末尾連續「疑似」Thumb32高16位的數量,當數量為單數則備份10 bytes,數量為偶數則備份12 bytes。這麼做的原因如下:如果這個16位符合Thumb32指令的高16位格式,那它肯定不是Thumb16,只可能是Thumb32的高16位或低16位。因為Thumb16是不會和Thumb32有歧義的。那麼,當它前面的16位也是類似的「疑似」Thumb32的話,可能是它倆共同組成了一個Thumb32,也可能是它們一個是結尾一個是開頭。所以,如果結尾出現1條疑似Thumb32,則說明這是一條截斷的,出現2條疑似Thumb32,說明它倆是一整條,出現3條,說明前2條是一條thumb32,最後一條是被截斷的前部分,依此類推。用下面這張圖可能更容易理解,總之:疑似Thumb32的2 Bytes可能是Thumb32高16位或Thumb32低16位,但不可能是Thumb16
:
細節2
:為什麼Plan B是10 byte?我們需要插入的跳轉是8 byte,但是thumb32中如果指令涉及修改PC的話,那麼這條指令所在的地址一定要能整除4,否則程序會崩潰。我們的指令地址肯定都是能被2整除的,但是能被4整除是真的說不準。因此,當出現地址不能被4整除時,我們需要先補一個thumb16的NOP指令(2 bytes)。這樣一來就需要2+8=10 Bytes了。儘管這時候選擇14 Bytes也差不多,我也沒有內存空間節省強迫症,但是選擇這10 Bytes主要還是為了提醒一下大家這邊補NOP的細節問題。
bool InitThumbHookInfo(INLINE_HOOK_INFO* pstInlineHook){ ...... uint16_t *p11; for (int k=5;k>=0;k--){ p11 = pstInlineHook->pHookAddr-1+k*2; LOGI("P11 : %x",*p11); if(isThumb32(*p11)){ is_thumb32_count += 1; }else{ break; } } //如果是的話就需要備份14byte或者10byte才能使得彙編指令不被截斷。由於跳轉指令在補nop的情況下也只需要10byte, //所以就取pstInlineHook->backUpLength為10 if(is_thumb32_count%2==1) { LOGI("The last ins is thumb32. Length will be 10."); pstInlineHook->backUpLength = 10; } else{ LOGI("The last ins is not thumb32. Length will be 12."); pstInlineHook->backUpLength = 12; } //修正:否則szbyBackupOpcodes會向後偏差1 byte memcpy(pstInlineHook->szbyBackupOpcodes, pstInlineHook->pHookAddr-1, pstInlineHook->backUpLength); ......}bool BuildThumbJumpCode(void *pCurAddress , void *pJumpAddress){ ...... //LDR PC, [PC, #0]對應的thumb機器碼為:0xf000f8df, NOP為BF00 if (CLEAR_BIT0((uint32_t)pCurAddress) % 4 != 0) { BYTE szLdrPCOpcodes[12] = {0x00, 0xBF, 0xdF, 0xF8, 0x00, 0xF0}; memcpy(szLdrPCOpcodes + 6, &pJumpAddress, 4); memcpy(pCurAddress, szLdrPCOpcodes, 10); cacheflush(*((uint32_t*)pCurAddress), 10, 0); } else{ BYTE szLdrPCOpcodes[8] = {0xdF, 0xF8, 0x00, 0xF0}; //將目的地址拷貝到跳轉指令緩存位置 memcpy(szLdrPCOpcodes + 4, &pJumpAddress, 4); memcpy(pCurAddress, szLdrPCOpcodes, 8); cacheflush(*((uint32_t*)pCurAddress), 8, 0); } ......}
Thumb-2 第2步
構造stub代碼。構造思路是先保存當前全部的寄存器狀態到棧中。然後用BLX命令(以arm模式)跳轉去執行用戶自定義的Hook後的函數。執行完成後,從棧恢復所有的寄存器狀態。最後(以thumb模式)跳轉至第三部分備份代碼處。
細節1
:為什麼跳轉到第三部分要用thumb模式?因為第三部分中是含有備份的thumb代碼的,而同一個順序執行且沒有內部跳轉的代碼段是無法改變執行模式的。因此,整個第三部分的彙編指令都需要跟著備份代碼用thumb指令來編寫。
細節2
:第二部分是arm模式,但是第三部分卻是thumb模式,如何切換?我在第一步的細節2
中提到過,無論是arm還是thumb模式,每條彙編指令的地址肯定都能整除2,因為最小的thumb16指令也需要2 Bytes。那麼這時候Arm架構就規定了,當跳轉地址是單數時,就代表要切換到thumb模式來執行;當跳轉地址是偶數時,就代表用Arm模式來執行。這個模式不是切換的概念,換句話說與跳轉前的執行模式無關。無論跳轉前是arm還是thumb,只要跳轉的目標地址是單數就代表接下來要用thumb模式執行,反之arm模式亦然。這真的是個很不錯的設定,因為我們只需要考慮接下來的執行模式就行了。這裡,本人就是通過將第三部分的起始地址+1來使得跳轉後程序以thumb模式執行。
細節3
:下方的關鍵代碼中ldr r3, _old_function_addr_s_thumb
到str r3, _old_function_addr_s_thumb
就是用來給目標地址+1的。這部分代碼不能按照邏輯緊貼著最後的ldr pc, _old_function_addr_s_thumb
來寫,而是一定要寫在恢復全部寄存器狀態的前面,否則這裡用到的r3會錯過恢復從而引起不穩定。
細節4
:那條bic指令是用來清除_old_function_addr_s_thumb
變數的最低位的。因為如果該Hook目標會被多次調用,那每次這個_old_function_addr_s_thumb
都會被+1。第一次沒有問題,成功變成了thumb模式,而第二次會以arm模式下偏2 bytes跳轉,之後偏差越來越大,模式交叉出現。因此,本人使用bic指令來清除每次Hook調用後的地址+1效果。
細節5
:用戶自定義的Hook功能函數是有一個參數的pt_regs *regs
,這個參數就是用mov r0, sp
傳遞的,此時r0指向的這個結構就是Hook跳轉前寄存器的狀態。不會受到stub或者Hook功能函數的影響。使用時regs->uregs[0]
就是R0寄存器,regs->uregs[6]
就是R6寄存器,regs->uregs[12]
就是R12寄存器,regs->uregs[13]
就是SP寄存器,regs->uregs[14]
就是LR寄存器,regs->uregs[15]
就是PSR寄存器(而不是PC寄存器,PC寄存器不備份)。
細節6
:保存寄存器的細節是怎麼樣的?棧上從高地址到低地址依次為:CPSR,LR,SP,R12,...,R0。並且在Thumb-2方案下,CPSR中的T位會先保存為第二部分所需的0,而不是原來的thumb模式下的T:1,在跳轉到第三部分時,會重新把T位變成1的。具體如下圖所示,圖中的CPSR的第6個bit就是T標誌,因此原本是0x20030030,保存在棧上的是0x20030010,最後進入第三部分時,依然能夠恢復成0x20030030。圖中R0從0x1變成了0x333隻是該次APP測試中自定義的User』s Hook Stub Function中的處理內容:regs->uregs[0]=0x333;
關鍵代碼如下:
_shellcode_start_s_thumb: push {r0, r1, r2, r3} mrs r0, cpsr str r0, [sp, #0xC] str r14, [sp, #8] add r14, sp, #0x10 str r14, [sp, #4] pop {r0} push {r0-r12} mov r0, sp ldr r3, _hookstub_function_addr_s_thumb blx r3 ldr r3, _old_function_addr_s_thumb bic r3, r3, #1 add r3, r3, #0x1 str r3, _old_function_addr_s_thumb ldr r3, [sp, #-0x34] ldr r0, [sp, #0x3C] msr cpsr, r0 ldmfd sp!, {r0-r12} ldr r14, [sp, #4] ldr sp, [r13] ldr pc, _old_function_addr_s_thumb
Thumb-2 第3步
第三步,構造備份代碼。構造思路是先執行之前備份的X Bytes的thumb-2代碼,然後用LDR.W指令來跳轉回Hook地址+Xbytes的地址處繼續執行。此處先不考慮PC修復,下文會說明。
細節1
:LDR是arm32的指令,LDR.W是thumb32的指令,作用是相同的。這裡想說的是:為什麼整個過程中都一直在用LDR和LDR.W,只有在第二步中有使用過BLX指令來進行跳轉?原因很簡單,為了保存狀態。從第一步跳轉到stub開始,如果跳轉使用了BLX,那就會影響到lr等寄存器,而如果使用LDR/LDR.W則只會改變PC來實現跳轉而已。stub中唯一的那次BLX是由於當時需要跳轉到用戶自己寫的Hook功能函數中,這是個正規的函數,它最後需要憑藉BLX設置的lr寄存器來跳轉回BLX指令的下一條指令。並且這個唯一的BLX處於保存全部寄存器的下面,恢復全部寄存器的上面,這部分的代碼就是所謂的「安全地帶」。因此,這其中改變的lr寄存器將在之後被恢復成最初始的狀態。第二步的細節3
中提及的r3寄存器的操作要放在這個「安全區」里也是這個原因。而在stub之外,我們的跳轉只能影響到PC,不可以去改變lr寄存器,所以必須使用LDR/LDR.W。
細節2
:下面的抽象圖中可以發現與arm中的不同,arm中最後是LDR PC, [PC, #-4]
,這是由於CPU三級流水的關係,執行某條彙編指令時,PC的值在arm下是當前地址+8,在thumb-2下是當前地址+4。而我們要跳轉的地址在本條指令後的4 Bytes處,因此,arm下需要PC-4,thumb下就是PC指向的地址。
構造出來的彙編代碼抽象形式如下:
備份代碼1備份代碼2備份代碼3......LDR.W PC, [PC, #0]HOOK_ADDR + X
指令修復(概述)
註:本部分內容較多且相關代碼佔了幾乎本項目開發的一半時間,故此處僅給出概述,本人之後為這部分內容獨立寫一篇文章
《Android Inline Hook中的指令修復》來詳細介紹以方便讀者更好地學習這方面內容。
在上文的處理中,我們很好地保存並恢復了寄存器原本的狀態。那麼,原本目標程序的彙編指令真的是在它原有的狀態下執行的嗎?依然不是。雖然寄存器的確一模一樣,但是那幾條被備份的指令是被移動到了另一個地址上。這樣當執行它們的時候PC寄存器的值就改變了。因此,如果這條指令的操作如果涉及到PC的值,那這條指令的執行效果就很可能和原來不一樣。所以,我們需要對備份的指令進行修復。在實際修復過程中,本人發現還有些指令也受影響,有如下幾種:
- 取PC的值進行計算的指令
- 跳轉到備份區域的指令
第一種我們已經解釋過了,而第二種則是由於我們備份區域中的代碼已經被替換了,如果有跳轉到這個區域的指令,那接下來執行的就不試原來這個位置的指令了。我們可以再把第二類細分成兩類:從備份區域跳轉到備份區域的指令
和從備份區域外跳轉到備份區域的指令
,前者本人通過計算目標代碼在備份區域中的絕對地址來代替原來的目標地址從而修復,而後者由於不知道整個程序中到底有多少條指令會跳轉過來,所以無法修復。不過個人認為這後者遇到的概率極小極小。因為我們使用Native Hook前肯定已經逆向分析過了,在IDA這類軟體中看到自己即將備份的區域里被打上了類似"loc_XXXXXX"的標籤時,一定會小心的。
這部分的修復操作參考了ele7enxxh
大神的博客和項目,裡面修復了許多可能出現的PC相關指令的情況,從中的確啟發了許多!但依然有點BUG,主要集中在BNE BEQ這些條件跳轉的指令修復上,以及CPU模式切換上容易忽略一些地址+1的問題。本項目中對這些本人已經遇到的BUG進行了修復。具體PC相關指令的修復細節本人之後會獨立寫一篇《Android Inline Hook中的指令修復》,其中也會提到我之前說的那些BUG的修復與改進。本人在此中只說一下本項目中是如何處理這個環節的:
- 遍歷備份的指令,arm32自然是一個個4 bytes的指令取走去處理就好,thumb-2則需要判斷指令是thumb16還是thumb32,把它們一條條取出來處理。
- 對每條指令進行PC修復,根據Hook目標地址和該指令在備份代碼里的偏移以及CPU的三級流水作用來計算出這條指令當時原本PC的值。從而用這個計算出來的值來代替這個指令中對當前PC的計算。
- 將每條備份代碼修復後的代碼按順序拼接(不需要修復的就用原來的指令去拼接),並在末尾拼接上原本的LDR/LDR.W跳轉指令。
於是上文第三步中構造出來的彙編代碼抽象形式如下:
備份代碼1備份代碼2涉及PC的備份代碼3的修復代碼1涉及PC的備份代碼3的修復代碼2涉及PC的備份代碼3的修復代碼3涉及PC的備份代碼3的修復代碼4涉及PC的備份代碼3的修復代碼5備份代碼4涉及PC的備份代碼5的修復代碼1涉及PC的備份代碼5的修復代碼2LDR/LDR.W PC, [PC, #-4]HOOK_ADDR + X
條件跳轉的修復方式(以Thumb為例)
在ARM32、Thumb16、Thumb32中都是有條件跳轉的指令的,本項目三套都修復了。下面來講一下Thumb16下條件跳轉的修復,作為整個指令修復
的典型代表吧。
條件跳轉指令的修復相比於其它種類的指令有一個明顯噁心的地方,看下面兩張圖可以很明顯看出來,先看第一張:
12 Bytes的備份代碼與各自對應的修復代碼自上而下一一對應,尾部再添加個跳轉回原程序的LDR。這就是上文中設想的最標準的修復方式。然而當其中混入了一條條件跳轉指令後:
我們發現按照原程序的順序和邏輯去修復條件跳轉指令的話,會導致條件跳轉指令對應的修復指令(圖中紅色部分)不是完整的一部分,而且第二部分需要出現在返回原程序跳轉的後面才能保持原來的程序邏輯。這時有兩個問題:
- 圖中X的值如何確定?我們是從上到下一條條修復備份指令然後拼接的,也就是說這條BLS指令下方的指令在修復它的時候還沒被修復。這樣這個X的值就無法確定?
- Thumb-2模式在備份時,12 Bytes最大是可能備份6條Thumb16指令的。也就是說,可能在備份指令中出現多條條件跳轉指令,這時候會出現跳轉嵌套,如下圖:
為了解決第一個問題,本人先在Hook一開始的init函數中建立一個記錄所有備份指令修復後長度的數組pstInlineHook->backUpFixLengthList
,然後當修復條件跳轉指令時,通過計算其後面修復指令的長度來得到X的值。這個方法一開始只是用來解決問題1的,當時還沒想到問題2的情況。因為這個數組中看不出後面的指令是否存在其它條件跳轉指令,所以最後的跳轉嵌套時會出錯。那第二個問題如何解決呢?本人開始意識到如果條件跳轉指令要用這種」兩段「式的修復方式的話,會使得之後的修復邏輯變得很複雜。但是按照原程序的執行邏輯順序似乎又只能這麼做...嗎?不,第一次優化方案如下所示:
這個方案通過連續的三個跳轉命令來縮小這個BXX結構,使其按照原來的邏輯跳轉到符合條件的跳轉指令去,然後再跳轉一次。至此其實已經解決了當前遇到的「兩段」式麻煩。但是最後本人又想到了一個新的優化方案:逆向思維方案
,可以簡化跳轉邏輯並在Arm32和Thumb32下減少一條跳轉指令的空間(Thumb16下由於需要補NOP所以沒有減小空間佔用),如下圖:
圖中可以看到,原來的BLS指令被轉化為了BHI指令,也就是小於等於
的跳轉邏輯變成了大於
。這樣一來,原本跳轉的目標邏輯現在就可以緊貼到BHI指令下面。從而使得條件跳轉指令的修復代碼也和其它指令一樣,成為一個連續的代碼段。並且BHI後面的參數在Thumb16中將固定為12。那麼對於多條條件跳轉指令來說呢?如下圖:
從圖中可以看出來,又回到了最初從上到下一一對應,末尾跳轉的形式。而之前新增的pstInlineHook->backUpFixLengthList
數組依然保留了,因為當跳轉的目標地址依然在備份代碼範圍內時需要用到它,《Android Inline Hook中的指令修復》中會講解,此處不再贅述。
使用說明(以Xposed為例)
使用者先找到想要Hook的目標,然後在本項目中寫自己需要的Hook功能,然後在項目根目錄使用ndk-build
進行編譯,需要注意的是本項目中需要嚴格控制arm和thumb模式,所以/jni/InlineHook/
和/jni/Interface/
目錄下的Android.mk中LOCAL_ARM_MODE := arm
不要修改,因為現在默認是編譯成thumb模式,這樣一來第二步和自定義的Hook函數就不再是設計圖中的ARM模式了。自己寫的Hook功能寫在InlineHook.cpp下,注意constructor
屬性,示例代碼如下:
//用戶自定義的stub函數,嵌入在hook點中,可直接操作寄存器等改變遊戲邏輯操作//這裡將R0寄存器鎖定為0x333,一個遠大於30的值//@param regs 寄存器結構,保存寄存器當前hook點的寄存器信息//Hook功能函數一定要有這個pt_regs *regs輸入參數才能獲取stub中r0指向的棧上保存的全部寄存器的值。void EvilHookStubFunctionForIBored(pt_regs *regs){ LOGI("In Evil Hook Stub."); //將r0修改為0x333 regs->uregs[0]=0x333;}void ModifyIBored() __attribute__((constructor));/** * 針對IBored應用,通過inline hook改變遊戲邏輯的測試函數 */void ModifyIBored(){ LOGI("In IHooks ModifyIBored."); int target_offset = 0x43b8; //想Hook的目標在目標so中的偏移 bool is_target_thumb = true; //目標是否是thumb模式? void* pModuleBaseAddr = GetModuleBaseAddr(-1, "libnative-lib.so"); //目標so的名稱 if(pModuleBaseAddr == 0) { LOGI("get module base error."); return; } uint32_t uiHookAddr = (uint32_t)pModuleBaseAddr + target_offset; //真實Hook的內存地址 //之所以人來判斷那是因為Native Hook之前肯定是要逆向分析一下的,那時候就能知道是哪種模式。而且自動識別arm和thumb比較麻煩。 if(is_target_thumb){ uiHookAddr++; LOGI("uiHookAddr is %X in thumb mode", uiHookAddr); } else{ LOGI("uiHookAddr is %X in arm mode", uiHookAddr); } InlineHook((void*)(uiHookAddr), EvilHookStubFunctionForIBored);}
本項目在有Xposed框架的測試機上運行時,可以使用一個插件在APP的起始環節就載入本項目的so。本人使用這個插件載入so就很方便啦,不用重啟手機,它會自動去系統路徑下尋找文件名符合的so然後載入到目標APP中。這個插件的關鍵代碼如下:
public class HookToast implements IXposedHookLoadPackage{ @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpp) throws Throwable { String packageName=""; String activityName=""; String soName=""; try{ packageName = "com.sec.gtoad.inline_hook_test3"; //目標app activityName = "com.sec.gtoad.inline_hook_test3.MainActivity"; //目標app的啟動activity soName = "InlineHook"; //我們so的名稱(libInlineHook.so) } catch (Exception e){ XposedBridge.log("parse result " + e.getMessage()); Log.w("GToad", "parse result " + e.getMessage()); } if(!lpp.packageName.equals(packageName)) return; XposedBridge.log("load package: " + lpp.packageName); Log.w("GToad","load package: " + lpp.packageName); hookActivityOnCreate(lpp,activityName,soName,packageName); //當啟動Activity開始創建時,就載入我們的so庫 } public static boolean loadArbitrarySo(XC_LoadPackage.LoadPackageParam lpp, String soname, String pkg) { if (lpp.packageName.equals(pkg)) { XposedBridge.log("trying to load so file: " + soname + " for " + pkg); Log.w("GToad","trying to load so file: " + soname + " for " + pkg); try { Log.w("GToad","loading1"); // /vendor/lib:/system/lib 只要把我們的so放到這些目錄之一插件就能找到 Log.w("GToad",System.getProperty("java.library.path")); System.loadLibrary(soname); Log.w("GToad","loading2"); } catch (Exception e) { XposedBridge.log("failed to load so"); Log.w("GToad","failed to load so"); return false; } XposedBridge.log("" + soname + " loaded"); Log.w("GToad","" + soname + " loaded"); return true; } XposedBridge.log("" + pkg + " not found"); Log.w("GToad","" + pkg + " not found"); return false; } private void hookActivityOnCreate(final XC_LoadPackage.LoadPackageParam lpp, final String activityName, final String soName, final String packageName){ try { XposedHelpers.findAndHookMethod(activityName, lpp.classLoader, "onCreate", Bundle.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam mhp) throws Throwable { XposedBridge.log("before " + activityName + ".onCreate"); Log.w("GToad","before " + activityName + ".onCreate"); super.beforeHookedMethod(mhp); } @Override protected void afterHookedMethod(MethodHookParam mhp) throws Throwable { XposedBridge.log("after " + activityName + ".onCreate"); Log.w("GToad","after " + activityName + ".onCreate"); loadArbitrarySo(lpp,soName,packageName); super.afterHookedMethod(mhp); } }); } catch (Throwable e) { XposedBridge.log("" + activityName + ".onCreate " + e.getMessage()); } }}
總結
本項目最終形式為一個so庫,它可以與任何一個能載入它的工具進行配合,達到Native Hook的效果。並且Hook的最小粒度單位是任意一條彙編指令,這在日常測試中作用很大。
真的非常感謝騰訊遊戲安全實驗室和ele7enxxh大牛的開源項目為本項目提供的參考。
文末說明
由於本項目的初衷是為了滿足作者自身測試需求才做的,所以關於文中的一些解釋與需求可能與別的同學的理解有偏差,這很正常。此處補充解釋一下:
- 關於目前公開的Android Native Hook工具寥寥無幾這一點我補充解釋一下:唯一一個公開且接近於Java Hook的Xposed那樣好用的工具可能就只是Cydia Substrate了。但是該項目已經好幾年沒更新,並且只支持到安卓5.0以前。還有一個不錯的Native Hook工具是Frida,但是它的運行原理涉及調試,因此遇到反調試會相當棘手。由於本人反調試遇到的情況較多,所以Frida不怎麼用。
- 為什麼不在thumb-2模式設計時都使用thumb?因為第二部分寫彙編的時候用arm寫起來容易,而且文中解釋過無論跳轉前是arm還是thumb模式,跳轉後想要用thumb模式都需要給地址+1,所以當然能用arm的地方就用arm,這樣方便。並且如果有多個不同模式的Hook目標,這時用戶自定義的Hook函數只能統一編譯成同一個模式,所以選擇ARM模式。
參考
騰訊遊戲安全實驗室
ele7enxxh的博客
推薦閱讀:
※iOS注入目標函數
※PC大佬帶你玩轉遊戲安全
※Android逆向so通過簽名校驗調用者
※狗神沙梓社評《iOS應用逆向與安全》:感謝劉培慶這樣的傻子們
※露米婭的軟體去廣告小實踐~