hook

hook在Android的应用框架层分为:JAVA层和Native层

常见实现方式为:

框架层次 Hook手段
Java层 动态代理,代码、字节码织入(AspectJ、ASM等)
Native层 GOT/PLT Hook,Trap Hook,Inline Hook

其中Native层的三种hook手段在应用范围、实现难度、性能等维度上有以下区别:

比较维度 GOT/PLT Hook Trap Hook Inline Hook
实现原理 修改延时绑定表 SIGTRAP断点信号 运行时指令替换
粒度 方法级 指令级 指令级
作用域 广 广
性能
难度 极高

在实际环境中应用较多的是GOT/PLT Hook,这种方法知识在ELF动态链接的默认流程上稍作修改,入侵性低,但能保证性能,可以方便的对so库进行hook,缺点是只能作用于绑定表中存在的方法,作用域有一定限制。

Inline hook是终极hook手段,通过直接修改运行时内存的方式替换指令,完全手工的完成hook及跳回操作,理论上可以实现任意位置的hook,不过手写指令时需要考虑abi兼容等众多因素,实现难度很高,实际应用的场景不多。

Inline Hook

指令级别的hook跟高级语言层面的实现方式在感官上有很大区别,高级语言中不管借助什么手段,只需将hook代码织入到目标代码之中即可,但这种方式在指令级别是行不通的。

操作系统将程序指令成段装载到内存里,我们手动把若干指令插入到某个位置就是改动了程序装载后的内存结构,这意味着程序需要重新做地址重定位才能正常运行,这本该由链接器完成的工作换成人工来计算几乎是不可能的,所以这肯定不是实现hook的正确方式。为了保持内存结构不变,正确的方法是使用指令替换而不是指令插入的方式来实现hook。

流程

跳转指令

ARM的常用指令集

类型 功能 举例
跳转 跳转到目标地址执行 B, BL, BLX, BX
数据处理 数据传送、算术、比较等 MOV, CMP, ADD, MUL
加载/存储 读取/写入寄存器 LDR, LDRB, LDRH
访问状态寄存器 读取/写入程序状态寄存器 MRS, MSR
访问协处理器 操作协处理器 CDP, LDC
异常/中断 产生软件中断 SWI, BKPT
伪指令 - -

以B开头的指令是专门的跳转指令,不过在这里不适用inline hook的场景,因为它们只用来完成32MB以内的相对地址的跳转,而我们无法保证hook方法在这个范围内。如何实现绝对地址的跳转呢?回忆下,还记得PC这个特殊地位的寄存器吗?它存储着程序当前执行的指令地址,换句话说,CPU执行的指令是从PC指向的地址取出来的,那么我们将一个目标地址写入PC就实现了绝对地址的跳转,对应的是写入寄存器的指令:LDR。

如果涉及到指令长度问题,则需要用到寄存器间接寻址

然后就是翻译成机器码

跳回指令

跳回指令和跳转指令格式一样,只是将目标地址从hook方法的起始地址改为原函数继续执行的地址:

虚拟地址 内容
0x00008000 0xe51ff004
0x00008004 return address

其中return address = 目标方法起始地址 + 替换指令长度 = 目标方法起始地址 + 8字节

指令修复

完成了跳转和跳回指令,剩下的操作就只有补充执行原函数中被替换的原始指令了。

回忆下前面提到的PC相对寻址,实际上这种寻址方式应用相当广泛,带来的结果就是指令往往与当前的PC值强绑定。当我们手动修改程序流程,跳到hook方法再回头执行原始指令时,PC已不再是原始指令预期的值,毫无疑问会执行异常。所以执行原始指令前要进行指令修复,修复方法就是将指令中PC的值修改为预期的值(注意并不是修改PC,只是修改指令中的表示PC值的那几位数据)。

指令修复需要涵盖PC相关的所有指令类型,这里只用ADD指令来举例说明:

ADD Rd, [PC, Rm]

对上面指令进行修复,可以预见指令的机器码中第一个操作数那几位肯定是1111(即r15=PC),我们需要将其改为一个其他寄存器Rx,而Rx中存入该指令预期的PC值,即指令被替换前的PC值。代码如下:

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
//执行到原方法时,pc值是原方法起始地址+8字节
uint32_t pc = target_addr + 8;

int rd;
int rm;
int r;

//用位运算提取出指令中用到的Rd和Rm寄存器编号
rd = (instruction & 0xF000) >> 12;
rm = instruction & 0xF;

//找出一个闲置寄存器r(既不是Rd也不是Rm),用来保存hook前的pc
for (r = 12; ; --r) {
if (r != rd && r != rm) {
break;
}
}

//将Rr的值入栈暂存
trampoline_instructions[trampoline_pos++] = 0xE52D0004 | (r << 12); // PUSH {Rr}
//将原始pc值写入Rx
trampoline_instructions[trampoline_pos++] = 0xE59F0008 | (r << 12); // LDR Rr, [PC, #8]
//用Rx编号替换指令中的PC编号
trampoline_instructions[trampoline_pos++] = (instruction & 0xFFF0FFFF) | (r << 16);
//暂存值出栈到Rx
trampoline_instructions[trampoline_pos++] = 0xE49D0004 | (r << 12); // POP {Rr}
//跳越4字节执行
trampoline_instructions[trampoline_pos++] = 0xE28FF000; // ADD PC, PC
trampoline_instructions[trampoline_pos++] = pc;

这样就完成了加法指令的修复,其他类型指令的修复方式大同小异,基本思想都是PC值替换。

三方库

实现inline hook的三方库很稀缺,已知的有Cydia Substrate,并已停止开源,

官网:http://www.cydiasubstrate.com/

总结

修改got表和内联hook比较:

1.修改got表拦截比内联hook拦截容易,只需要知道elf文件中调用外部符号的地址。

参考

Android Arm Inline Hook

Android逆向Hook学习——第二篇:Android inline Hook

Android Inline Hook 详解前言原理分析

Android inline hook之实现原理