常用的hook方式主要有導(dǎo)入表hook、導(dǎo)出表hook和inline hook三種。
一,導(dǎo)入表hook
首先需要了解延時(shí)重定位的過(guò)程,下面這張圖很好的解釋了整個(gè)過(guò)程

即程序調(diào)用目標(biāo)函數(shù)時(shí),函數(shù)地址才會(huì)被運(yùn)行時(shí)函數(shù)解析出來(lái)寫到got表中。
導(dǎo)入表hook就是基于修改got實(shí)現(xiàn)對(duì)目標(biāo)函數(shù)hook的,同時(shí)導(dǎo)入表hook也是基于ptrace注入的,前面在總結(jié)注入的時(shí)候有個(gè)案例,將包含hello函數(shù)的so注入到target進(jìn)程中,這里需要將包含修改got表的函數(shù)注入到target中;
got表的修改,原理很簡(jiǎn)單,主要是got表定位的過(guò)程有很多方法,可以根據(jù)自己喜好,通過(guò)dlopen函數(shù)打開(kāi)so文件,返回的是
solist信息,這里可以直接解析出so文件的section中的字符串表的索引,然后通過(guò)對(duì)比section的名字去確定got位置,因?yàn)槲覀冏⑷氲臅r(shí)機(jī)已經(jīng)加載過(guò)目標(biāo)函數(shù)了,所以目標(biāo)函數(shù)的地址是已經(jīng)重定位過(guò)的,可以直接調(diào)用目標(biāo)函數(shù)從而獲取目標(biāo)函數(shù)在內(nèi)存中的地址,然后去got表中對(duì)比,命中后直接修改即可。
這里有個(gè)想法沒(méi)有實(shí)際驗(yàn)證過(guò),就是如果我們注入的實(shí)際更早一些,got表沒(méi)有進(jìn)行重定位,我們可以通過(guò)動(dòng)態(tài)加載段去找到動(dòng)態(tài)字符串和hash表,然后計(jì)算出目標(biāo)函數(shù)在got表中的地址,然后直接修改該地址應(yīng)該也是可以的。
這樣的hook原理實(shí)際上是有很大局限性的:
1,比如,目標(biāo)函數(shù)不在目標(biāo)進(jìn)程中的時(shí)候,目標(biāo)進(jìn)程通過(guò)dlopen去打開(kāi)其他so,然后dlsym調(diào)用目標(biāo)函數(shù)的時(shí)候,我們對(duì)目標(biāo)進(jìn)程的got表修改是無(wú)效的;
2,對(duì)于so內(nèi)部自定義的函數(shù)也是無(wú)法修改的;
3,修改got表后影響的是整個(gè)目標(biāo)進(jìn)程,無(wú)法精確到hook某次調(diào)用;
二,導(dǎo)出表hook
符號(hào)表中每個(gè)符號(hào)的結(jié)構(gòu)如下:
typedef struct elf32_sym{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
其中的st_info中包含type字段,用STB_GLOBAL、STB_LOCAL和STB_WEAK等字段來(lái)標(biāo)識(shí)是全局符號(hào)還是本地符號(hào)。

然后來(lái)看下linker加載so的過(guò)程,http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp
源碼中可以看到,linker將so加載到內(nèi)存之后,會(huì)符號(hào)進(jìn)行重定位,通過(guò)檢測(cè)符號(hào)中的st_info字段判斷外部符號(hào)還是本地符號(hào),如果是外部符號(hào)就會(huì)去解析
NEEDED獲取外部符號(hào)的地址,
if (reloc == sym_addr) {
Elf32_Sym *src = soinfo_do_lookup(NULL, sym_name, &lsi, needed);
返回Elf32_Sym,從這個(gè)Elf32_Sym中的st_value字段得到函數(shù)的虛地址。
typedef struct {
Elf32_Word st_name; /* Symbol name (.strtab index) */
Elf32_Word st_value; /* value of symbol */
Elf32_Word st_size; /* size of symbol */
Elf_Byte st_info; /* type / binding attrs */
Elf_Byte st_other; /* unused */
Elf32_Half st_shndx; /* section index of symbol */
} Elf32_Sym;
那么修改NEEDED 中的這個(gè)符號(hào) st_value 字段,即可實(shí)現(xiàn)導(dǎo)出表 HOOK。
流程如下:以 libc.so 中的 unlink 函數(shù)為例
- 注入 zygote 進(jìn)程;
dlopen libc.so,找到unlink符號(hào);- 解析此符號(hào),得到其
st_value地址;- 修改此地址的值為:
NewFunc – BaseAddr(libc.so 加載的基地址)。
第四步中之所以要改成偏移地址而不是絕對(duì)地址是是因?yàn)?code>st_value保存的本身就是是偏移地址;
導(dǎo)出表 HOOK的局限性就在于只能HOOK導(dǎo)出的符號(hào)。
三,inline hook
inline hook的原理是在在匯編指令層做修改,下面這兩張圖很好的解釋了具體操作,其中第二張更詳細(xì)一些:


一,匯編指令構(gòu)造:
說(shuō)到跳轉(zhuǎn)指令我們首先想到的是B指令,但是在32位arm中,地址占4字節(jié),B指令跳轉(zhuǎn)范圍是有限的,因此我們還可以使用LDR指令將立即數(shù)賦值給pc寄存器的方式:


其中修改后的第二條指令是目標(biāo)函數(shù)的地址,這是是默認(rèn)識(shí)別成了指令,實(shí)際并沒(méi)有這條匯編指令,
修改后的第一條指令ldr是其實(shí)原指令應(yīng)該是
ldr pc,[pc,#-4]這樣就看出了ldr指令尋址其實(shí)是通過(guò)第一個(gè)寄存器參數(shù)的偏移來(lái)尋址的,并不是直接跳轉(zhuǎn)到某地址,這就解釋了為什么mov、b、bl、ldr等指令尋址是有范圍的了,這里為什么是#-4而不是```#4``,這與匯編指令的三級(jí)流水線相關(guān),執(zhí)行第一條指令的時(shí)候?qū)嶋H上pc指向的第三條指令,所以是減4而不是加4。
在構(gòu)造匯編指令時(shí)可以使用https://armconverter.com/將匯編代碼轉(zhuǎn)化成16進(jìn)制,https://onlinedisassembler.com/odaweb/將16進(jìn)制轉(zhuǎn)換成匯編代碼。
實(shí)現(xiàn)代碼為:
// 設(shè)置bit[0]的值為1
#define SET_BIT0(addr) (addr | 1)
// 設(shè)置bit[0]的值為0
#define CLEAR_BIT0(addr) (addr & 0xFFFFFFFE)
// 測(cè)試bit[0]的值,若為1則返回真,若為0則返回假
#define TEST_BIT0(addr) (addr & 1)
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;
}
二,inst1指令修正
上面再構(gòu)造跳轉(zhuǎn)指令的時(shí)候覆蓋了兩條指令,所以在目標(biāo)函數(shù)運(yùn)行結(jié)束后還是需要恢復(fù)的,但是,如果要恢復(fù)的這兩條指令是包含PC寄存器的,那么需要注意,此時(shí)的pc寄存器的值與原環(huán)境中的pc寄存器的值是不一樣的,需要針對(duì)不同的指令做不同的修復(fù)。
首先,對(duì)涉及pc寄存器操作的指令進(jìn)行分類:
static int getTypeInArm(uint32_t instruction)
{
if ((instruction & 0xFE000000) == 0xFA000000) {
return BLX_ARM;
}
if ((instruction & 0xF000000) == 0xB000000) {
return BL_ARM;
}
if ((instruction & 0xF000000) == 0xA000000) {
return B_ARM;
}
if ((instruction & 0xFF000FF) == 0x120001F) {
return BX_ARM;
}
if ((instruction & 0xFEF0010) == 0x8F0000) {
return ADD_ARM;
}
if ((instruction & 0xFFF0000) == 0x28F0000) {
return ADR1_ARM;
}
if ((instruction & 0xFFF0000) == 0x24F0000) {
return ADR2_ARM;
}
if ((instruction & 0xE5F0000) == 0x41F0000) {
return LDR_ARM;
}
if ((instruction & 0xFE00FFF) == 0x1A0000F) {
return MOV_ARM;
}
return UNDEFINE;
}
1, 若是B系列指令,包括b、bx、bl、blx等指令,
if (type == BLX_ARM || type == BL_ARM || type == B_ARM || type == BX_ARM) {
uint32_t x;
int top_bit;
uint32_t imm32;
uint32_t value;
if (type == BLX_ARM || type == BL_ARM) {
trampoline_instructions[trampoline_pos++] = 0xE28FE004; // ADD LR, PC, #4
}
trampoline_instructions[trampoline_pos++] = 0xE51FF004; // LDR PC, [PC, #-4]
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;
}
2,若是LDR、ADR、MOV等指令,同樣首先解析指令,得到value,然后用于構(gòu)造修復(fù)指令,代碼如下:
else if (type == ADD_ARM) {
int rd;
int rm;
int r;
rd = (instruction & 0xF000) >> 12;
rm = instruction & 0xF;
for (r = 12; ; --r) {
if (r != rd && r != rm) {
break;
}
}
trampoline_instructions[trampoline_pos++] = 0xE52D0004 | (r << 12); // PUSH {Rr}
trampoline_instructions[trampoline_pos++] = 0xE59F0008 | (r << 12); // LDR Rr, [PC, #8]
trampoline_instructions[trampoline_pos++] = (instruction & 0xFFF0FFFF) | (r << 16);
trampoline_instructions[trampoline_pos++] = 0xE49D0004 | (r << 12); // POP {Rr}
trampoline_instructions[trampoline_pos++] = 0xE28FF000; // ADD PC, PC
trampoline_instructions[trampoline_pos++] = pc;
}
else if (type == ADR1_ARM || type == ADR2_ARM || type == LDR_ARM || type == MOV_ARM) {
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;
}
trampoline_instructions[trampoline_pos++] = 0xE51F0000 | (r << 12); // LDR Rr, [PC]
trampoline_instructions[trampoline_pos++] = 0xE28FF000; // ADD PC, PC
trampoline_instructions[trampoline_pos++] = value;
}
3,理論上其他指令也可能用到pc寄存器操作,但實(shí)際中沒(méi)有,這類我們不做考慮,故默認(rèn)其他的指令都不涉及pc寄存器操作,直接將原指令寫入即可,但是如果遇到bug分析的時(shí)候不要忘了這一點(diǎn)。
4,上面講的是ARM指令環(huán)境下的hook,但是系統(tǒng)中還會(huì)遇到thumb指令,對(duì)于thumb指令的構(gòu)造和修復(fù),原理與arm相同,只是有些地方需要注意:
- Thumb 模式并沒(méi)有能表跳轉(zhuǎn)任意地址的指令,只能切換到 ARM 狀態(tài)再進(jìn)行跳轉(zhuǎn)。
- 為了盡可能的 減少替換的指令數(shù),狀態(tài)切換應(yīng)盡快,這里采用
BX PC。- ARM 指令是 4 字節(jié)對(duì)齊,
BX PC狀態(tài)切換時(shí),必須保證跳轉(zhuǎn)到的地址為4字節(jié)對(duì)齊。- 由于 T2 指令占 4 字節(jié),如果被替換指令的最后一條為T2指令,且T2指令的前2字節(jié)處于被替換指令中,而后2字節(jié)未處于其中時(shí),也是需要將后2字節(jié)歸入被替換的指令中作為一個(gè)整體。
有了上面的分析,指令的替換流程如下:
- 判定其實(shí)地址是否 4 字節(jié)對(duì)齊,如果不為 4 字節(jié)對(duì)齊,則
BX PC之前構(gòu)造 NOP 指令 - 由于預(yù)取 2 條指令,
BX PC之后2字節(jié)填充 NOP - 構(gòu)造
ARM LDR指令,占4字節(jié)。其后4字節(jié)存放跳轉(zhuǎn)絕對(duì)地址 - 判定被替換指令最后2字節(jié)是否為T2指令的前2兩字節(jié),如果是,則還需把之后2字節(jié)加入替換指令中,后2字節(jié)用
Thumb NOP填充。
參考:
項(xiàng)目源碼:Ele7enxxh大神的源碼,github收藏地址
TK大神的《SO Hook 技術(shù)匯總》
游戲安全實(shí)驗(yàn)室的Android平臺(tái)inline hook實(shí)現(xiàn)