原文鏈接:http://ele7enxxh.com/Android-Arm-Inline-Hook.html
AndroidNativeEmu食用手冊:https://bbs.pediy.com/thread-254799.htm
著名開源項目xHook:https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md
本文將結合本項目的源代碼,詳細闡述Android Arm Inline Hook的原理與實現過程。
什么是Inline Hook
Inline Hook即內部跳轉Hook,通過替換函數開始處的指令為跳轉指令,使得原函數跳轉到自己的函數,通常還會保留原函數的調用接口。與GOT表Hook相比,Inline Hook具有更廣泛的適用性,幾乎可以Hook任何函數,不過其實現更為復雜,考慮的情況更多,並且無法對一些太短的函數Hook。
其基本原理請參閱網上其他資料。
需要解決的問題
- Arm模式與Thumb模式的區別
- 跳轉指令的構造
- PC相關指令的修正
- 線程處理
- 其他一些細節
下面我將結合源碼對這幾個問題進行解決。
Arm模式與Thumb模式的區別
本文討論的對象為基於32位的Arm架構的Inline Hook,在Arm版本7及以上的體系中,其指令集分為Arm指令集和Thumb指令集。Arm指令為4字節對齊,每條指令長度均為32位;Thumb指令為2字節對齊,又分為Thumb16、Thumb32,其中Thumb16指令長度為16位,Thumb32指令長度為32位。
在對一個函數進行Inline Hook時,首先需要判斷當前函數指令是Arm指令還是Thumb指令,指令使用目標地址值的bit[0]來確定目標地址的指令類型。bit[0]的值為1時,目標程序為Thumb指令;bit[0]值為0時,目標程序為ARM指令。其相關實現代碼為以下宏:
1
2
3
4
5
6
|
// 設置bit[0]的值為1
// 設置bit[0]的值為0
// 測試bit[0]的值,若為1則返回真,若為0則返回假
|
跳轉指令的構造
跳轉指令主要分為以下兩種:
- B系列指令:B、BL、BX、BLX
- 直接寫PC寄存器
Arm的B系列指令跳轉范圍只有4M,Thumb的B系列指令跳轉范圍只有256字節,然而大多數情況下跳轉范圍都會大於4M,故我們采用LDR PC, [PC, ?]
構造跳轉指令。另外Thumb16指令中並沒有合適的跳轉指令,如果單獨使用Thumb16指令構造跳轉指令,需要使用更多的指令完成,並且在后續對PC相關指令的修正也更加繁瑣,故綜合考慮下,決定放棄對ARMv5的支持。
另外,Arm處理器采用3級流水線來增加處理器指令流的速度,也就是說程序計數器R15(PC)總是指向“正在取指”的指令,而不是指向“正在執行”的,即PC總是指向當前正在執行的指令地址再加2條指令的地址。比如當前指令地址是0×8000, 那么當前pc的值,在thumb下面是0×8000 + 2 2, 在arm下面是0×8000 + 4 2。
對於Arm指令集,跳轉指令為:
LDR PC, [PC, #-4]
addr
|
LDR PC, [PC, #-4]
對應的機器碼為:0xE51FF004,addr
為要跳轉的地址。該跳轉指令范圍為32位,對於32位系統來說即為全地址跳轉。
對於Thumb32指令集,跳轉指令為:
LDR.W PC, [PC, #0]
addr
|
LDR.W PC, [PC, #0]
對應的機器碼為:0x00F0DFF8,addr
為要跳轉的地址。同樣支持任意地址跳轉。
其相關實現代碼為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// Arm Mode
if (TEST_BIT0(item->target_addr)) {
int i;
i =
0;
if (CLEAR_BIT0(item->target_addr) % 4 != 0) {
((
uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xBF00; // NOP
}
((
uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xF8DF;
((
uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xF000; // LDR.W PC, [PC]
((
uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = item->new_addr & 0xFFFF;
((
uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = item->new_addr >> 16;
}
// Thumb Mode
else {
((
uint32_t *) (item->target_addr))[0] = 0xe51ff004; // LDR PC, [PC, #-4]
((
uint32_t *) (item->target_addr))[1] = item->new_addr;
}
|
首先通過TEST_BIT0宏判斷目標函數的指令集類型,其中若為Thumb指令集,多了下面一個額外處理:
1
2
3
|
if (CLEAR_BIT0(item->target_addr) % 4 != 0) {
((
uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xBF00; // NOP
}
|
對bit[0]的值清零,若其值4字節不對齊,則添加一個2字節的NOP
指令,使得后續的指令4字節對齊。這是因為在Thumb32指令中,若該指令對PC寄存器的值進行了修改,則該指令必須是4字節對齊的,否則為非法指令。
PC相關指令的修正
不論是Arm指令集還是Thumb指令集,都存在很多的與PC值相關的指令,例如:B系列指令、literal系列指令等。原有函數的前幾個被跳轉指令替換的指令將會被搬移到trampoline_instructions中,此時PC值已經變動,所以需要對PC相關指令進行修正(所謂修正即為計算出實際地址,並使用其他指令完成同樣的功能)。相關修正代碼位於relocate.c文件中。其中INSTRUCTION_TYPE
描述了需要修正的指令,限於篇幅,這里僅闡述Arm指令的修正過程,對應的代碼為relocateInstructionInArm
函數。
函數原型如下:
1
2
3
4
5
6
7
8
9
10
|
/*
target_addr: 待Hook的目標函數地址,即為當前PC值,用於修正指令
orig_instructions:存放原有指令的首地址,用於修正指令和后續對原有指令的恢復
length:存放的原有指令的長度,Arm指令為8字節;Thumb指令為12字節
trampoline_instructions:存放修正后指令的首地址,用於調用原函數
orig_boundaries:存放原有指令的指令邊界(所謂邊界即為該條指令與起始地址的偏移量),用於后續線程處理中,對PC的遷移
trampoline_boundaries:存放修正后指令的指令邊界,用途與上相同
count:處理的指令項數,用途與上相同
*/
static void relocateInstructionInArm(uint32_t target_addr, uint32_t *orig_instructions, int length, uint32_t *trampoline_instructions, int *orig_boundaries, int *trampoline_boundaries, int *count);
|
具體實現中,首先通過函數getTypeInArm
判斷當前指令的類型,本函數通過類型,共分為4個處理分支:
- BLX_ARM、BL_ARM、B_ARM、BX_ARM
- ADD_ARM
- ADR1_ARM、ADR2_ARM、LDR_ARM、MOV_ARM
- 其他指令
BLX_ARM、BL_ARM、B_ARM、BX_ARM指令的修正
即為B系列指令(BLX <label>
、BL <label>
、B <label>
、BX PC
)的修正,其中BLX_ARM
和BL_ARM
需要修正LR寄存器的值,相關代碼為:
1
2
3
|
if (type == BLX_ARM || type == BL_ARM) {
trampoline_instructions[trampoline_pos++] =
0xE28FE004; // ADD LR, PC, #4
}
|
接下來構造相應的跳轉指令,即為:
1
|
trampoline_instructions[trampoline_pos++] =
0xE51FF004; // LDR PC, [PC, #-4]
|
最后解析指令,計算實際跳轉地址value
,並將其寫入trampoline_instructions
,相關代碼為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
if (type == BLX_ARM) {
x = ((instruction &
0xFFFFFF) << 2) | ((instruction & 0x1000000) >> 23);
}
else if (type == BL_ARM || type == B_ARM) {
x = (instruction &
0xFFFFFF) << 2;
}
else {
x =
0;
}
top_bit = x >>
25;
imm32 = top_bit ? (x | (
0xFFFFFFFF << 26)) : x;
if (type == BLX_ARM) {
value = pc + imm32 +
1;
}
else {
value = pc + imm32;
}
trampoline_instructions[trampoline_pos++] = value;
|
如此便完成了B系列指令的修正,關於指令的字節結構請參考Arm指令手冊。
ADD_ARM指令的修正
ADD_ARM
指的是ADR Rd, <label>
格式的指令,其中<label>
與PC相關。
首先通過循環遍歷,得到Rd寄存器,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int rd;
int rm;
int r;
// 解析指令得到rd、rm寄存器
rd = (instruction &
0xF000) >> 12;
rm = instruction &
0xF;
// 為避免沖突,排除rd、rm寄存器,選擇一個臨時寄存器Rr
for (r = 12; ; --r) {
if (r != rd && r != rm) {
break;
}
}
|
接下來是構造修正指令:
1
2
3
4
5
6
7
8
9
10
11
|
// PUSH {Rr},保護Rr寄存器值
trampoline_instructions[trampoline_pos++] =
0xE52D0004 | (r << 12);
// LDR Rr, [PC, #8],將PC值存入Rr寄存器中
trampoline_instructions[trampoline_pos++] =
0xE59F0008 | (r << 12);
// 變換原指令`ADR Rd, <label>`為`ADR Rd, Rr, ?`
trampoline_instructions[trampoline_pos++] = (instruction &
0xFFF0FFFF) | (r << 16);
//POP {Rr},恢復Rr寄存器值
trampoline_instructions[trampoline_pos++] =
0xE49D0004 | (r << 12);
// ADD PC, PC,跳過下一條指令
trampoline_instructions[trampoline_pos++] =
0xE28FF000;
trampoline_instructions[trampoline_pos++] = pc;
|
ADR1_ARM、ADR2_ARM、LDR_ARM、MOV_ARM
分別為ADR Rd, <label>
、ADR Rd, <label>
、LDR Rt, <label>
、MOV Rd, PC
。
同樣首先解析指令,得到value
,相關代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
int r;
uint32_t value;
r = (instruction &
0xF000) >> 12;
if (type == ADR1_ARM || type == ADR2_ARM || type == LDR_ARM) {
uint32_t imm32;
imm32 = instruction &
0xFFF;
if (type == ADR1_ARM) {
value = pc + imm32;
}
else if (type == ADR2_ARM) {
value = pc - imm32;
}
else if (type == LDR_ARM) {
int is_add;
is_add = (instruction &
0x800000) >> 23;
if (is_add) {
value = ((
uint32_t *) (pc + imm32))[0];
}
else {
value = ((
uint32_t *) (pc - imm32))[0];
}
}
}
else {
value = pc;
}
|
最后構造修正指令,代碼如下:
1
2
3
4
5
|
// LDR Rr, [PC]
trampoline_instructions[trampoline_pos++] =
0xE51F0000 | (r << 12);
// 跳過下一條指令
trampoline_instructions[trampoline_pos++] =
0xE28FF000; // ADD PC, PC
trampoline_instructions[trampoline_pos++] = value;
|
其他指令
事實上,還有些指令格式需要修正,例如:PUSH {PC}
、PUSH {SP}
等,雖然這些指令被Arm指令手冊標記為deprecated,但是仍然為合法指令,不過在實際匯編中並未發現此類指令,故未做處理,相關代碼如下:
1
2
|
// 直接將指令存放到trampoline_instructions中
trampoline_instructions[trampoline_pos++] = instruction;
|
處理完所有待處理指令后,最后加入返回指令:
// LDR PC, [PC, #-4]
trampoline_instructions[trampoline_pos++] = 0xe51ff004;
trampoline_instructions[trampoline_pos++] = lr;
|
Thumb指令的修正,大家可以參考這里的思路,自行閱讀源碼。
線程處理
一個完善的Inline Hook方案必須要考慮多線程環境,即要考慮線程恰好執行到被修改指令的位置。在Window下,使用GetThreadContext
和SetThreadContext
枚舉所有線程,遷移context到搬遷后的指令中。然而在Linux+Arm環境下,並沒有直接提供相同功能的API,不過可以使用ptrace
完成,主要流程如下:
- 解析/proc/self/task目錄,獲取所有線程id
- 創建子進程,父進程等待。子進程枚舉所有線程,PTRACE_ATTACH線程,遷移線程PC寄存器,枚舉完畢后,子進程給自己發SIGSTOP信號,等待父進程喚醒
- 父進程檢測到子進程已經SIGSTOP,完成Inline Hook工作,向子進程發送SIGCONT信號,同時等待子進程退出
- 子進程枚舉所有線程,PTRACE_DETACH線程,枚舉完畢后,子進程退出
- 父進程繼續其他工作
這里使用子進程完成線程處理工作,實際上是迫不得已的。因為,如果直接使用本進程PTRACE_ATTACH
線程,會出現operation not permitted,即使賦予root權限也是同樣的錯誤,具體原因不得而知。
具體代碼請參考freeze
與unFreeze
兩個函數。
其他一些細節
- 頁保護
頁面大小為4096字節,使用mprotect
函數修改頁面屬性,修改為PROT_READ | PROT_WRITE | PROT_EXEC
。 - 刷新緩存
對於ARM處理器來說,緩存機制作用明顯,內存中的指令已經改變,但是cache中的指令可能仍為原有指令,所以需要手動刷新cache中的內容。采用cacheflush
即可實現。 - 一個已知的BUG
雖然本庫已經把大部分工作放在了registerInlineHook
函數中,但是在inlineHook
、inlineUnHook
函數中還是不可避免的使用了部分libc庫的API函數,例如:mprotect
、memcpy
、munmap
、free
、cacheflush
等。如果使用本庫對上述API函數進行Hook,可能會失敗甚至崩潰,這是因為此時原函數的指令已經被破壞,或者其邏輯已經改變。解決這個Bug有兩個方案,第一是采用其他Hook技術;第二將本庫中的這些API函數全部采用內部實現,即不依賴於libc庫,可采用靜態鏈接libc庫,或者使用匯編直接調相應的系統調用號。