hook原理小結(jié)

常用的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ù)為例

  1. 注入 zygote 進(jìn)程;
  2. dlopen libc.so,找到unlink符號(hào);
  3. 解析此符號(hào),得到其st_value地址;
  4. 修改此地址的值為: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相同,只是有些地方需要注意:

  1. Thumb 模式并沒(méi)有能表跳轉(zhuǎn)任意地址的指令,只能切換到 ARM 狀態(tài)再進(jìn)行跳轉(zhuǎn)。
  2. 為了盡可能的 減少替換的指令數(shù),狀態(tài)切換應(yīng)盡快,這里采用BX PC。
  3. ARM 指令是 4 字節(jié)對(duì)齊,BX PC狀態(tài)切換時(shí),必須保證跳轉(zhuǎn)到的地址為4字節(jié)對(duì)齊。
  4. 由于 T2 指令占 4 字節(jié),如果被替換指令的最后一條為T2指令,且T2指令的前2字節(jié)處于被替換指令中,而后2字節(jié)未處于其中時(shí),也是需要將后2字節(jié)歸入被替換的指令中作為一個(gè)整體。

有了上面的分析,指令的替換流程如下:

  1. 判定其實(shí)地址是否 4 字節(jié)對(duì)齊,如果不為 4 字節(jié)對(duì)齊,則 BX PC 之前構(gòu)造 NOP 指令
  2. 由于預(yù)取 2 條指令,BX PC之后2字節(jié)填充 NOP
  3. 構(gòu)造ARM LDR指令,占4字節(jié)。其后4字節(jié)存放跳轉(zhuǎn)絕對(duì)地址
  4. 判定被替換指令最后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)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容