博客已遷移至:https://leeon7.github.io
Hook
Hook在Android系統(tǒng)的應(yīng)用根據(jù)框架層次可以分為兩類今膊,Java層和Native層,常見的實(shí)現(xiàn)方式如下:
框架層次 | Hook手段 |
---|---|
Java層 | 動(dòng)態(tài)代理檩咱,代碼、字節(jié)碼織入(AspectJ胯舷、ASM等) |
Native層 | GOT/PLT Hook刻蚯,Trap Hook,Inline Hook |
其中Native層的三種hook手段在應(yīng)用范圍桑嘶、實(shí)現(xiàn)難度炊汹、性能等維度上有以下區(qū)別:
比較維度 | GOT/PLT Hook | Trap Hook | Inline Hook |
---|---|---|---|
實(shí)現(xiàn)原理 | 修改延時(shí)綁定表 | SIGTRAP斷點(diǎn)信號(hào) | 運(yùn)行時(shí)指令替換 |
粒度 | 方法級(jí) | 指令級(jí) | 指令級(jí) |
作用域 | 窄 | 廣 | 廣 |
性能 | 高 | 低 | 高 |
難度 | 中 | 中 | 極高 |
這三種方式在實(shí)際環(huán)境中應(yīng)用較多的是GOT/PLT Hook,由于只是在ELF動(dòng)態(tài)鏈接的默認(rèn)流程上稍作修改逃顶,這種方式侵入性較低讨便,且能保證性能,可以方便的實(shí)現(xiàn)對(duì)so庫(kù)的方法hook口蝠,唯一的缺點(diǎn)是只能作用于綁定表中存在的方法器钟,作用域有一定限制。trap hook由于使用系統(tǒng)中斷妙蔗,在性能上表現(xiàn)不好傲霸。Inline hook是終極hook手段,通過直接修改運(yùn)行時(shí)內(nèi)存的方式替換指令,完全手工的完成hook及跳回操作昙啄,理論上可以實(shí)現(xiàn)任意位置的hook穆役,不過手寫指令時(shí)需要考慮abi兼容等眾多因素,實(shí)現(xiàn)難度很高梳凛,實(shí)際應(yīng)用的場(chǎng)景不多耿币。
匯編
在手撕匯編之前,先簡(jiǎn)單回顧下基礎(chǔ)知識(shí)
平時(shí)用來開發(fā)的高級(jí)語(yǔ)言必須轉(zhuǎn)換成低級(jí)語(yǔ)言才能被CPU執(zhí)行韧拒,根據(jù)是否有中間結(jié)果的區(qū)別淹接,完成轉(zhuǎn)換的可能是編譯器或虛擬機(jī)。和百花齊放的高級(jí)語(yǔ)言不同叛溢,低級(jí)語(yǔ)言只有兩種塑悼,匯編和機(jī)器碼(即二進(jìn)制碼),匯編是機(jī)器碼的文本化表示楷掉,兩者是一對(duì)一的對(duì)應(yīng)關(guān)系厢蒜。
邏輯上講,匯編是為了解決機(jī)器碼可讀性的產(chǎn)物烹植,匯編在執(zhí)行前需要先翻譯成機(jī)器碼斑鸦,這個(gè)過程叫assembing,所以匯編語(yǔ)言叫ASM草雕。
$ gcc -S yourfile可以將c文件編譯成匯編文件.s
寄存器
為了填平運(yùn)算組件(CPU)和存儲(chǔ)單元(硬盤)的性能溝壑巷屿,會(huì)在其間加幾個(gè)緩沖單元,從慢到快依次是RAM促绵、Cache攒庵、寄存器。一般CPU會(huì)自帶這些緩沖層败晴,其中寄存器直接跟CPU交互,是讀取速度最快的單位栽渴。隨著發(fā)展CPU的寄存器數(shù)量從幾個(gè)增長(zhǎng)到了幾十個(gè)(ARM有37個(gè))尖坤,其中前16個(gè)一般會(huì)被當(dāng)作通用寄存器使用(編號(hào)0-15),而編號(hào)15的寄存器又最為特殊闲擦,一般會(huì)把r15當(dāng)作program counter慢味,熟悉JVM的同學(xué)知道在虛擬機(jī)中也有類似的概念,只是一個(gè)是虛擬的墅冷,一個(gè)是真實(shí)的:
一般把r15當(dāng)作PC寄存器纯路,即program counter,也就是Instruction Pointer寞忿,指向程序當(dāng)前執(zhí)行到的那條指令驰唬。
Inline Hook
回到主題,指令級(jí)別的hook跟高級(jí)語(yǔ)言層面的實(shí)現(xiàn)方式在感官上有很大區(qū)別,高級(jí)語(yǔ)言中不管借助什么手段叫编,只需將hook代碼織入到目標(biāo)代碼之中即可辖佣,但這種方式在指令級(jí)別是行不通的,見下圖:
我們需要知道搓逾,操作系統(tǒng)將程序指令成段裝載到內(nèi)存里卷谈,我們手動(dòng)把若干指令插入到某個(gè)位置就是改動(dòng)了程序裝載后的內(nèi)存結(jié)構(gòu),這意味著程序需要重新做地址重定位
才能正常運(yùn)行霞篡,這本該由鏈接器完成的工作換成人工來計(jì)算幾乎是不可能的世蔗,所以這肯定不是實(shí)現(xiàn)hook的正確方式。為了保持內(nèi)存結(jié)構(gòu)不變朗兵,正確的方法是使用指令替換
而不是指令插入的方式來實(shí)現(xiàn)hook污淋,見下圖:
假設(shè)目標(biāo)方法內(nèi)有ins0, ins1, ins2三條指令,首先將起始指令(實(shí)際上是前2條指令)替換為等長(zhǎng)
的跳轉(zhuǎn)指令jump_ins矛市,jump_ins負(fù)責(zé)跳轉(zhuǎn)到hook方法執(zhí)行芙沥,而hook操作后,往往還需要保留調(diào)用原方法的能力以保證功能可用性浊吏,所以hook方法內(nèi)還有一個(gè)跳轉(zhuǎn)指令來調(diào)回原方法繼續(xù)執(zhí)行(jump ins1)而昨,調(diào)回前需要先補(bǔ)充執(zhí)行目標(biāo)方法已被替換的原始指令(圖中ins0),保證原方法完整性找田。綜上歌憨,inline hook需要完成的工作就是圖中綠色的部分,即跳轉(zhuǎn)指令的替換墩衙、補(bǔ)充執(zhí)行原指令务嫡、跳回原方法繼續(xù)執(zhí)行這三步。
跳轉(zhuǎn)指令
先簡(jiǎn)單熟悉下ARM的常用指令集
類型 | 功能 | 舉例 |
---|---|---|
跳轉(zhuǎn) | 跳轉(zhuǎn)到目標(biāo)地址執(zhí)行 | B, BL, BLX, BX |
數(shù)據(jù)處理 | 數(shù)據(jù)傳送漆改、算術(shù)心铃、比較等 | MOV, CMP, ADD, MUL |
加載/存儲(chǔ) | 讀取/寫入寄存器 | LDR, LDRB, LDRH |
訪問狀態(tài)寄存器 | 讀取/寫入程序狀態(tài)寄存器 | MRS, MSR |
訪問協(xié)處理器 | 操作協(xié)處理器 | CDP, LDC |
異常/中斷 | 產(chǎn)生軟件中斷 | SWI, BKPT |
偽指令 | - | - |
以B開頭的指令是專門的跳轉(zhuǎn)指令,不過在這里不適用inline hook的場(chǎng)景挫剑,因?yàn)樗鼈冎挥脕硗瓿?2MB以內(nèi)的相對(duì)地址的跳轉(zhuǎn)去扣,而我們無(wú)法保證hook方法在這個(gè)范圍內(nèi)。如何實(shí)現(xiàn)絕對(duì)地址的跳轉(zhuǎn)呢樊破?回憶下愉棱,還記得PC這個(gè)特殊地位的寄存器嗎?它存儲(chǔ)著程序當(dāng)前執(zhí)行的指令地址哲戚,換句話說奔滑,CPU執(zhí)行的指令是從PC指向的地址取出來的,那么我們將一個(gè)目標(biāo)地址寫入PC就實(shí)現(xiàn)了絕對(duì)地址的跳轉(zhuǎn)顺少,對(duì)應(yīng)的是寫入寄存器的指令:LDR朋其。查詢文檔王浴,LDR指令格式如下:
大括號(hào)內(nèi)的可選參數(shù)暫時(shí)不管,指令格式可歸納為LDR Rd, <destication address>
令宿,其中Rd為目標(biāo)寄存器叼耙,中括號(hào)內(nèi)為得出一個(gè)絕對(duì)地址的表達(dá)式,表達(dá)式內(nèi)部可能用到Rn和Rm兩個(gè)寄存器作為操作數(shù)粒没,也可能是一個(gè)立即數(shù)筛婉。假設(shè)想要跳轉(zhuǎn)的地址是0x11111111,那么將該地址寫入PC的指令就是LDR PC, 0x11111111
癞松,可隨即遇到一個(gè)問題爽撒,ARM下每條指令的長(zhǎng)度是32位,而地址長(zhǎng)度也是32位响蓉,將一個(gè)絕對(duì)地址寫入一個(gè)指令里顯然是不可能的硕勿,像LDR PC, 0x11111111
這樣的指令是無(wú)法寫入內(nèi)存的。那么該如何在一條指令的空間里寫入一個(gè)絕對(duì)地址的表達(dá)式呢枫甲?
寄存器間接尋址
注意到一個(gè)寄存器的容量也是32位源武,剛好能裝下一個(gè)絕對(duì)地址,所以可以把目標(biāo)地址先存到某個(gè)寄存器(Rm)中想幻,然后執(zhí)行LDR PC, Rm
就實(shí)現(xiàn)了絕對(duì)地址的跳轉(zhuǎn)粱栖,這種以某個(gè)寄存器作為基準(zhǔn)的尋址方式叫做寄存器間接尋址。再進(jìn)一步脏毯,在實(shí)際開發(fā)中我們發(fā)現(xiàn)PC寄存器就是一個(gè)天然的鉚點(diǎn)闹究,并且想要跳轉(zhuǎn)的目標(biāo)地址往往離程序當(dāng)前執(zhí)行到的地址不遠(yuǎn)
,所以索性用PC加上一個(gè)偏移量來表達(dá)一個(gè)絕對(duì)地址食店,格式為:LDR PC, [PC, offset]
渣淤,這種尋址方式又叫PC相對(duì)尋址
。使用PC相對(duì)尋址吉嫩,我們可以用8個(gè)字節(jié)(即2條指令的長(zhǎng)度)來完成一個(gè)絕對(duì)地址的跳轉(zhuǎn)操作:
虛擬地址 | 內(nèi)容 |
---|---|
0x00006000 | LDR PC, [PC, 4] |
0x00006004 | destination address |
0x00006000位置的指令含義為當(dāng)CPU執(zhí)行到此時(shí)价认,將該地址加4字節(jié)-即0x00006004地址內(nèi)的內(nèi)容寫入到PC中,而內(nèi)容就是我們事先寫入的目標(biāo)地址自娩。
到此跳轉(zhuǎn)指令似乎完成了刻伊,可實(shí)際上還需要做一個(gè)調(diào)整,由于ARM下CPU遵循三級(jí)流水
的執(zhí)行流程椒功,PC并不指向當(dāng)前指令,見下圖:
三級(jí)流水可以近似理解為三線程并行智什。三級(jí)流水將CPU運(yùn)行拆解為三個(gè)步驟:取指动漾、轉(zhuǎn)譯、執(zhí)行荠锭。取指單元在取出一條指令后旱眯,會(huì)交給下游-轉(zhuǎn)譯單元進(jìn)行翻譯,轉(zhuǎn)而繼續(xù)取下一條指令,無(wú)需等待該指令后續(xù)的步驟删豺。三個(gè)單元有各自的流水線共虑,這樣造成的結(jié)果就是PC(即取指單元)總是指向正在執(zhí)行的指令往后兩條指令的地址位置,如圖當(dāng)CPU執(zhí)行ADD指令時(shí)呀页,F(xiàn)etch已取到了CMP指令妈拌,領(lǐng)先了ADD兩條指令的距離。依據(jù)此蓬蝶,需要對(duì)上面的跳轉(zhuǎn)指令做如下調(diào)整:
虛擬地址 | 內(nèi)容 |
---|---|
0x00006000 | LDR PC, [PC, -4] |
0x00006004 | destination address |
可以看到尘分,從PC+4變成了PC-4,PC-4其實(shí)是[PC-8]+4丸氛。即當(dāng)CPU執(zhí)行到0x00006000時(shí)PC已經(jīng)指向了0x00006000+2*4的位置培愁,需要先減去8字節(jié)才得到當(dāng)前執(zhí)行位置,再加4字節(jié)便得到0x00006004缓窜。
翻譯為機(jī)器碼
將指令寫入內(nèi)存時(shí)需要翻譯為機(jī)器碼定续,根據(jù)文檔,LDR命令的機(jī)器碼格式為:
根據(jù)文檔將指令LDR PC, [PC, -4]
翻譯為32位的二進(jìn)制機(jī)器碼:
其中28-31位表示執(zhí)行條件禾锤,1110代表總是執(zhí)行私股,26-27位01表示LDR,后面到20位是6個(gè)獨(dú)立標(biāo)志位时肿,其中第23位U為0表示做減法
庇茫,Rn表示基準(zhǔn)寄存器編號(hào),1111即為15螃成,表示r15旦签,也就是PC,Rd表示目標(biāo)寄存器寸宏,也是PC宁炫,0-11位用來存儲(chǔ)立即數(shù),100就是4氮凝,這就是LDR PC, [PC, -4]
的機(jī)器碼羔巢,轉(zhuǎn)換成16進(jìn)制是0xe51ff004。最終得到跳轉(zhuǎn)hook方法地址的程序內(nèi)容如下:
虛擬地址 | 內(nèi)容 |
---|---|
0x00006000 | 0xe51ff004 |
0x00006004 | my method address |
跳回指令
跳回指令和跳轉(zhuǎn)指令格式一樣罩阵,只是將目標(biāo)地址從hook方法的起始地址改為原函數(shù)繼續(xù)執(zhí)行的地址:
虛擬地址 | 內(nèi)容 |
---|---|
0x00008000 | 0xe51ff004 |
0x00008004 | return address |
其中return address = 目標(biāo)方法起始地址 + 替換指令長(zhǎng)度 = 目標(biāo)方法起始地址 + 8字節(jié)
指令修復(fù)
完成了跳轉(zhuǎn)和跳回指令竿秆,剩下的操作就只有補(bǔ)充執(zhí)行原函數(shù)中被替換的原始指令了。這步是inline hook最復(fù)雜的一步稿壁,也是inline hook的難度所在幽钢。回憶下前面提到的PC相對(duì)尋址
傅是,實(shí)際上這種尋址方式應(yīng)用相當(dāng)廣泛匪燕,帶來的結(jié)果就是指令往往與當(dāng)前的PC值強(qiáng)綁定蕾羊。當(dāng)我們手動(dòng)修改程序流程,跳到hook方法再回頭執(zhí)行原始指令時(shí)帽驯,PC已不再是原始指令預(yù)期的值龟再,毫無(wú)疑問會(huì)執(zhí)行異常。所以執(zhí)行原始指令前要進(jìn)行指令修復(fù)尼变,修復(fù)方法就是將指令中PC的值修改為預(yù)期的值(注意并不是修改PC利凑,只是修改指令中的表示PC值的那幾位數(shù)據(jù))。
指令修復(fù)需要涵蓋PC相關(guān)的所有指令類型享甸,這里只用ADD指令來舉例說明:
ADD Rd, [PC, Rm]
對(duì)上面指令進(jìn)行修復(fù)截碴,可以預(yù)見指令的機(jī)器碼中第一個(gè)操作數(shù)那幾位肯定是1111(即r15=PC),我們需要將其改為一個(gè)其他寄存器Rx蛉威,而Rx中存入該指令預(yù)期的PC值日丹,即指令被替換前的PC值。代碼如下:
//執(zhí)行到原方法時(shí)蚯嫌,pc值是原方法起始地址+8字節(jié)
uint32_t pc = target_addr + 8;
int rd;
int rm;
int r;
//用位運(yùn)算提取出指令中用到的Rd和Rm寄存器編號(hào)
rd = (instruction & 0xF000) >> 12;
rm = instruction & 0xF;
//找出一個(gè)閑置寄存器r(既不是Rd也不是Rm)哲虾,用來保存hook前的pc
for (r = 12; ; --r) {
if (r != rd && r != rm) {
break;
}
}
//將Rr的值入棧暫存
trampoline_instructions[trampoline_pos++] = 0xE52D0004 | (r << 12); // PUSH {Rr}
//將原始pc值寫入Rx
trampoline_instructions[trampoline_pos++] = 0xE59F0008 | (r << 12); // LDR Rr, [PC, #8]
//用Rx編號(hào)替換指令中的PC編號(hào)
trampoline_instructions[trampoline_pos++] = (instruction & 0xFFF0FFFF) | (r << 16);
//暫存值出棧到Rx
trampoline_instructions[trampoline_pos++] = 0xE49D0004 | (r << 12); // POP {Rr}
//跳越4字節(jié)執(zhí)行
trampoline_instructions[trampoline_pos++] = 0xE28FF000; // ADD PC, PC
trampoline_instructions[trampoline_pos++] = pc;
這樣就完成了加法指令的修復(fù),其他類型指令的修復(fù)方式大同小異择示,基本思想都是PC值替換束凑。
三方庫(kù)
實(shí)現(xiàn)inline hook的三方庫(kù)很稀缺,已知的有Cydia Substrate栅盲,并已停止開源汪诉,官網(wǎng):http://www.cydiasubstrate.com/