[轉]Android Arm Inline Hook


原文鏈接: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。
其基本原理請參閱網上其他資料。

需要解決的問題

  1. Arm模式與Thumb模式的區別
  2. 跳轉指令的構造
  3. PC相關指令的修正
  4. 線程處理
  5. 其他一些細節

下面我將結合源碼對這幾個問題進行解決。

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
#define SET_BIT0(addr) (addr | 1)
// 設置bit[0]的值為0
#define CLEAR_BIT0(addr) (addr & 0xFFFFFFFE)
// 測試bit[0]的值,若為1則返回真,若為0則返回假
#define TEST_BIT0(addr) (addr & 1)

 

跳轉指令的構造

跳轉指令主要分為以下兩種:

  • 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個處理分支:

  1. BLX_ARM、BL_ARM、B_ARM、BX_ARM
  2. ADD_ARM
  3. ADR1_ARM、ADR2_ARM、LDR_ARM、MOV_ARM
  4. 其他指令

BLX_ARM、BL_ARM、B_ARM、BX_ARM指令的修正

即為B系列指令(BLX <label>BL <label>B <label>BX PC)的修正,其中BLX_ARMBL_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下,使用GetThreadContextSetThreadContext枚舉所有線程,遷移context到搬遷后的指令中。然而在Linux+Arm環境下,並沒有直接提供相同功能的API,不過可以使用ptrace完成,主要流程如下:

  1. 解析/proc/self/task目錄,獲取所有線程id
  2. 創建子進程,父進程等待。子進程枚舉所有線程,PTRACE_ATTACH線程,遷移線程PC寄存器,枚舉完畢后,子進程給自己發SIGSTOP信號,等待父進程喚醒
  3. 父進程檢測到子進程已經SIGSTOP,完成Inline Hook工作,向子進程發送SIGCONT信號,同時等待子進程退出
  4. 子進程枚舉所有線程,PTRACE_DETACH線程,枚舉完畢后,子進程退出
  5. 父進程繼續其他工作

這里使用子進程完成線程處理工作,實際上是迫不得已的。因為,如果直接使用本進程PTRACE_ATTACH線程,會出現operation not permitted,即使賦予root權限也是同樣的錯誤,具體原因不得而知。
具體代碼請參考freezeunFreeze兩個函數。

其他一些細節

    1. 頁保護
      頁面大小為4096字節,使用mprotect函數修改頁面屬性,修改為PROT_READ | PROT_WRITE | PROT_EXEC
    2. 刷新緩存
      對於ARM處理器來說,緩存機制作用明顯,內存中的指令已經改變,但是cache中的指令可能仍為原有指令,所以需要手動刷新cache中的內容。采用cacheflush即可實現。
    3. 一個已知的BUG
      雖然本庫已經把大部分工作放在了registerInlineHook函數中,但是在inlineHookinlineUnHook函數中還是不可避免的使用了部分libc庫的API函數,例如:mprotectmemcpymunmapfreecacheflush等。如果使用本庫對上述API函數進行Hook,可能會失敗甚至崩潰,這是因為此時原函數的指令已經被破壞,或者其邏輯已經改變。解決這個Bug有兩個方案,第一是采用其他Hook技術;第二將本庫中的這些API函數全部采用內部實現,即不依賴於libc庫,可采用靜態鏈接libc庫,或者使用匯編直接調相應的系統調用號。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM