hook原理小结

常用的hook方式主要有导入表hook、导出表hook和inline hook三种。

一,导入表hook

首先需要了解延时重定位的过程,下面这张图很好的解释了整个过程



即程序调用目标函数时,函数地址才会被运行时函数解析出来写到got表中。
导入表hook就是基于修改got实现对目标函数hook的,同时导入表hook也是基于ptrace注入的,前面在总结注入的时候有个案例,将包含hello函数的so注入到target进程中,这里需要将包含修改got表的函数注入到target中;

got表的修改,原理很简单,主要是got表定位的过程有很多方法,可以根据自己喜好,通过dlopen函数打开so文件,返回的是solist信息,这里可以直接解析出so文件的section中的字符串表的索引,然后通过对比section的名字去确定got位置,因为我们注入的时机已经加载过目标函数了,所以目标函数的地址是已经重定位过的,可以直接调用目标函数从而获取目标函数在内存中的地址,然后去got表中对比,命中后直接修改即可。

这里有个想法没有实际验证过,就是如果我们注入的实际更早一些,got表没有进行重定位,我们可以通过动态加载段去找到动态字符串和hash表,然后计算出目标函数在got表中的地址,然后直接修改该地址应该也是可以的。

这样的hook原理实际上是有很大局限性的:

1,比如,目标函数不在目标进程中的时候,目标进程通过dlopen去打开其他so,然后dlsym调用目标函数的时候,我们对目标进程的got表修改是无效的;
2,对于so内部自定义的函数也是无法修改的;
3,修改got表后影响的是整个目标进程,无法精确到hook某次调用;

二,导出表hook

符号表中每个符号的结构如下:

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等字段来标识是全局符号还是本地符号。


然后来看下linker加载so的过程,http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp
源码中可以看到,linker将so加载到内存之后,会符号进行重定位,通过检测符号中的st_info字段判断外部符号还是本地符号,如果是外部符号就会去解析NEEDED获取外部符号的地址,

if (reloc == sym_addr) {
                Elf32_Sym *src = soinfo_do_lookup(NULL, sym_name, &lsi, needed);

返回Elf32_Sym,从这个Elf32_Sym中的st_value字段得到函数的虚地址。

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 中的这个符号 st_value 字段,即可实现导出表 HOOK。

流程如下:以 libc.so 中的 unlink 函数为例

  1. 注入 zygote 进程;
  2. dlopen libc.so,找到unlink符号;
  3. 解析此符号,得到其st_value地址;
  4. 修改此地址的值为:NewFunc – BaseAddr(libc.so 加载的基地址)

第四步中之所以要改成偏移地址而不是绝对地址是是因为st_value保存的本身就是是偏移地址;
导出表 HOOK的局限性就在于只能HOOK导出的符号。

三,inline hook

inline hook的原理是在在汇编指令层做修改,下面这两张图很好的解释了具体操作,其中第二张更详细一些:



一,汇编指令构造:

说到跳转指令我们首先想到的是B指令,但是在32位arm中,地址占4字节,B指令跳转范围是有限的,因此我们还可以使用LDR指令将立即数赋值给pc寄存器的方式:

原指令

修改后指令

其中修改后的第二条指令是目标函数的地址,这是是默认识别成了指令,实际并没有这条汇编指令,

修改后的第一条指令ldr是其实原指令应该是ldr pc,[pc,#-4]这样就看出了ldr指令寻址其实是通过第一个寄存器参数的偏移来寻址的,并不是直接跳转到某地址,这就解释了为什么mov、b、bl、ldr等指令寻址是有范围的了,这里为什么是#-4而不是```#4``,这与汇编指令的三级流水线相关,执行第一条指令的时候实际上pc指向的第三条指令,所以是减4而不是加4。

在构造汇编指令时可以使用https://armconverter.com/将汇编代码转化成16进制,https://onlinedisassembler.com/odaweb/将16进制转换成汇编代码。
实现代码为:

// 设置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)
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指令修正

上面再构造跳转指令的时候覆盖了两条指令,所以在目标函数运行结束后还是需要恢复的,但是,如果要恢复的这两条指令是包含PC寄存器的,那么需要注意,此时的pc寄存器的值与原环境中的pc寄存器的值是不一样的,需要针对不同的指令做不同的修复。

首先,对涉及pc寄存器操作的指令进行分类:

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,然后用于构造修复指令,代码如下:

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寄存器操作,但实际中没有,这类我们不做考虑,故默认其他的指令都不涉及pc寄存器操作,直接将原指令写入即可,但是如果遇到bug分析的时候不要忘了这一点。

4,上面讲的是ARM指令环境下的hook,但是系统中还会遇到thumb指令,对于thumb指令的构造和修复,原理与arm相同,只是有些地方需要注意:

  1. Thumb 模式并没有能表跳转任意地址的指令,只能切换到 ARM 状态再进行跳转。
  2. 为了尽可能的 减少替换的指令数,状态切换应尽快,这里采用BX PC
  3. ARM 指令是 4 字节对齐,BX PC状态切换时,必须保证跳转到的地址为4字节对齐。
  4. 由于 T2 指令占 4 字节,如果被替换指令的最后一条为T2指令,且T2指令的前2字节处于被替换指令中,而后2字节未处于其中时,也是需要将后2字节归入被替换的指令中作为一个整体。

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

  1. 判定其实地址是否 4 字节对齐,如果不为 4 字节对齐,则 BX PC 之前构造 NOP 指令
  2. 由于预取 2 条指令,BX PC之后2字节填充 NOP
  3. 构造ARM LDR指令,占4字节。其后4字节存放跳转绝对地址
  4. 判定被替换指令最后2字节是否为T2指令的前2两字节,如果是,则还需把之后2字节加入替换指令中,后2字节用Thumb NOP填充。

参考:
项目源码:Ele7enxxh大神的源码,github收藏地址
TK大神的《SO Hook 技术汇总》
游戏安全实验室的Android平台inline hook实现

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352