Android Inline Hook詳解

博客已遷移至: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í)

高級(jí)語(yǔ)言-低級(jí)語(yǔ)言轉(zhuǎn)換

平時(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)系厢蒜。

邏輯關(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í)的:

PC

一般把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í)別是行不通的,見下圖:

錯(cuò)誤做法

我們需要知道搓逾,操作系統(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污淋,見下圖:

指令級(jí)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指令格式如下:

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í)流水可以近似理解為三線程并行智什。三級(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ī)器碼轉(zhuǎn)譯規(guī)則

根據(jù)文檔將指令LDR PC, [PC, -4]翻譯為32位的二進(jìn)制機(jī)器碼:

轉(zhuǎ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/

參考:http://ele7enxxh.com/Android-Arm-Inline-Hook.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市谈秫,隨后出現(xiàn)的幾起案子扒寄,更是在濱河造成了極大的恐慌,老刑警劉巖拟烫,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件该编,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡硕淑,警方通過查閱死者的電腦和手機(jī)课竣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來置媳,“玉大人于樟,你說我怎么就攤上這事∧茨遥” “怎么了隔披?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)寂拆。 經(jīng)常有香客問我奢米,道長(zhǎng),這世上最難降的妖魔是什么纠永? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任鬓长,我火速辦了婚禮,結(jié)果婚禮上尝江,老公的妹妹穿的比我還像新娘涉波。我一直安慰自己,他們只是感情好炭序,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布啤覆。 她就那樣靜靜地躺著,像睡著了一般惭聂。 火紅的嫁衣襯著肌膚如雪窗声。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天辜纲,我揣著相機(jī)與錄音笨觅,去河邊找鬼。 笑死耕腾,一個(gè)胖子當(dāng)著我的面吹牛见剩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扫俺,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼苍苞,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了狼纬?” 一聲冷哼從身側(cè)響起羹呵,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎畸颅,沒想到半個(gè)月后担巩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡没炒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年涛癌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片送火。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拳话,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出种吸,到底是詐尸還是另有隱情弃衍,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布坚俗,位于F島的核電站镜盯,受9級(jí)特大地震影響岸裙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜速缆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一降允、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧艺糜,春花似錦剧董、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至真慢,卻和暖如春毅臊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晤碘。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工褂微, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人园爷。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓宠蚂,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親童社。 傳聞我的和親對(duì)象是個(gè)殘疾皇子求厕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355