在后面的分析中会经常使用到 native hook,有必要先对 native hook 有个大概的了解。native hook 可以分为 3 类,GOT/PLT Hook、inline Hook 和 Trap Hook。了解的不到,只能聊一聊第一个,给出一些自己的理解。
inline hook
inline hook 是将函数调用开始时的指令更替为跳转指令,使得原函数直接跳转到 Hook 的目标函数函数,并保存调用原函数时的寄存器和堆栈,在 Hook 的目标函数执行完成后,在原函数调用的下面几条指令使用跳转指令跳回原函数继续执行并保证原函数的这几条指令已经执行。可能说的不是很明白,配合一张图片来说明。
1.将原函数的指令1改成跳转指令,并将指令1时的寄存器保存下来,这样指令以就跳转开始执行目标函数并将可以正常使用寄存器。
2.在目标函数执行完成之后跳转回原函数执行并恢复寄存器,比如跳转回原函数的指令2开始执行,但是现在执行的并不是正常执行过程中的原函数,因为指令1被改成了跳转指令,如果原函数的指令1操作了寄存器或者其他的,那就会造成原函数执行的环境不一致。所以先要模拟原函数执行,使得在后面的执行指令环境与原函数正常执行环境一致(修改寄存器、堆栈等与原函数执行一样),可能到指令4或者5时就可以使原函数正常接下去执行了。
msg:实际操作难度高
GOT/PLT Hook
利用 ELF 文件中的导入表,在一个 so 被正确加载之前,导入表只有对于函数的符号,而没有函数的真正执行地址,在被 linker 连接之后导入表就有了相对应的函数符号及地址。所以可以动态修改导入表中的函数真正执行地址。当调用该函数时,实际执行的时修改过后的函数执行地址。
上面就是我对 GOT/PLT Hook 的理解,当然说的很不严谨。对于 GOT/PLT Hook 来说 ELF 文件格式是最重要的 ,理解了 ELF 的文件格式及加载到内存的过程就理解了 GOT/PLT Hook,所以可以先去了解 ELF。
下面介绍下 xHook 使用及原理 。
int ProxyOpen(const char *path, int flags, mode_t mode) {
__android_log_print(ANDROID_LOG_ERROR, "TAG", "监听到文件打开:%s", path);
int fd = open(path, flags, mode);
return fd;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_dabaicai_moniter_NativeBridge_xhookInit(JNIEnv *env, jclass clazz) {
/**
* int xhook_register(const char *pathname_regex_str, //匹配规则
const char *symbol, //需要 hook 的函数名
void *new_func, //hook 后的函数
void **old_func); //hook 的原函数
*/
xhook_register("libopenjdkjvm.so", "open", (void *) ProxyOpen, NULL);
xhook_register("libjavacore.so", "open", (void *) ProxyOpen, NULL);
xhook_register("libopenjdk.so", "open", (void *) ProxyOpen, NULL);
xhook_register("libopenjdkjvm.so", "open64", (void *) ProxyOpen, NULL);
xhook_register("libjavacore.so", "open64", (void *) ProxyOpen, NULL);
xhook_register("libopenjdk.so", "open64", (void *) ProxyOpen, NULL);
// 执行真正的 hook 操作
xhook_refresh(1);
}
以上就实现了对打开文件操作的 hook。
除此外还有几个 api
// 忽略部分 hook 信息
int xhook_ignore(const char *pathname_regex_str,
const char *symbol);
// 清除缓存
void xhook_clear();
// 启用/禁用 调试信息 给 flag 参数传 1 表示启用调试信息,传 0 表示禁用调试信息。 (默认为:禁用)
// 调试信息将被输出到 logcat,对应的 TAG 为:`xhook`。
void xhook_enable_debug(int flag);
// 启用/禁用 SFP (段错误保护)
// 给 flag 参数传 1 表示启用 SFP,传 0 表示禁用 SFP。 (默认为:启用)
void xhook_enable_sigsegv_protection(int flag);
- 原理
爱奇艺 xHook 原理介绍,在这篇文章中很详细的分析 so 导入表中 malloc 函数的地址修改,也就不多说了。我换一个角度来看看是怎么找到 malloc 的地址为 3f90。
在连接文章中查找 malloc 地址的步骤:
- 1.验证 ELF 头信息。
- 2.在 PHT 中找到类型为
PT_DYNAMIC
的 segment,从中获取到.dynamic
section,从.dynamic
section中获取其他各项 section 对应的内存地址。 - 3.在
.dynstr
section 中找到需要 hook 的 symbol 对应的 index 值。 - 4.遍历所有的
.relxxx
section(重定位 section),查找 symbol index 和 symbol type 都匹配的项,对于这项重定位项,执行 hook 操作。
详情点击连接查看
换一个角度看:
- 1.查找 ELF 头信息。
- 2.在 PHT 中找类型为
PT_DYNAMIC
的 segment。
0200 0000 642e 0000 643e 0000
643e 0000 0801 0000 0801 0000 0600 0000
0400 0000
type 0200 0000
offset 642e 0000 文件偏移
virtual addr 643e 0000
physical addr 643e 0000 没用
file length 0801 0000 264(0108)
mem length 0801 0000 264
RWX 0600 0000 6
align 0400 0000 对齐
- 3.通过偏移 2e64 找到
PT_DYNAMIC
的 segment 。
0300 0000 7c3f 0000
0200 0000 f000 0000 1700 0000 b80c 0000
1400 0000 1100 0000 1100 0000 780c 0000
1200 0000 4000 0000 1300 0000 0800 0000
faff ff6f 0300 0000 0600 0000 f001 0000
0b00 0000 1000 0000 0500 0000 9005 0000
0a00 0000 b104 0000 0400 0000 440a 0000
0100 0000 1500 0000 0100 0000 9304 0000
0100 0000 9b04 0000 0100 0000 a804 0000
0e00 0000 1d00 0000 1a00 0000 3c3e 0000
1c00 0000 0800 0000 1900 0000 443e 0000
1b00 0000 0400 0000 1e00 0000 0800 0000
fbff ff6f 0100 0000 f0ff ff6f c80b 0000
fcff ff6f 3c0c 0000 fdff ff6f 0100 0000
feff ff6f 580c 0000 ffff ff6f 0100 0000
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
上面中的 0300 0000 7c3f 0000 为一组数据,其中 0300 0000 为type ,7c3f 0000 为其type 类型在 ELF 文件中的地址。
- 4.解析
PT_DYNAMIC
的 segment 中的字符串表
0500 0000 9005 0000
找到 type 为 5 的,地址为 9005 0000,即 0x950 开始的是字符串表的内容,没有提供字符串表的长度。这里就取得长一点
005f 5f63 7861 5f66 696e 616c 697a 6500
4c49 4243 006c 6962 632e 736f 006c 6962
7465 7374 2e73 6f00 736e 7072 696e 7466
006d 616c 6c6f 6300 5f5f 6378 615f 6174
6578 6974 0070 7269 6e74 6600 7361 795f
6865 6c6c 6f00 5f5f 6165 6162 695f 756e
7769 6e64 5f63 7070 5f70 7231 005f 6564
6174 6100 6162 6f72 7400 6d65 6d63 7079
005f 656e 6400 5f5f 676e 755f 556e 7769
6e64 5f46 696e 645f 6578 6964 7800 5f5f
6273 735f 7374 6172 7400 5f5f 6165 6162
695f 756e 7769 6e64 5f63 7070 5f70 7230
005f 5f61 6561 6269 5f75 6e77 696e 645f
6370 705f 7072 3200 5f5f 676e 755f 556e
7769 6e64 5f52 6573 746f 7265 5f56 4650
5f44 005f 5f67 6e75 5f55 6e77 696e 645f
5265 7374 6f72 655f 5646 5000 5f5f 676e
- 5.找到符号表
0600 0000 f001 0000
符号表是地址和字符串的对应关系,type= 6.
type 为 6,地址为 0x1f0
0000 0000 0000 0000 0000 0000 0000 0000 第 0 项不使用
0100 0000 0000 0000 0000 0000 1200 0000
0100 0000 在string的偏移 590+1=591 __cxa_atexit
0000 0000 地址 导入表之前为0
0000 0000 函数或者变量大小
1200 0000 附加的(不知道什么用)
2800 0000 0000 0000 0000 0000 1200 0000
3100 0000 0000 0000 0000 0000 1200 0000
...
- 6.找到导入表
导入表大小
0200 0000 f000 0000
导入表
1700 0000 b80c 0000
导入表的 type = 17 ,导入表大小的 type = 2
所以导入表开始地址为 0xcb8,大小为 f0 / 8 = 240/8 = 30 项
883f 0000 1604 0000
883f 0000 导入地址
16 导入表类型
04 0000 符号表索引
8c3f 0000 1601 0000 903f 0000 1603 0000
943f 0000 1602 0000 983f 0000 1605 0000
9c3f 0000 160c 0000 a03f 0000 1610 0000
....
- 7.逆向查找
(8c3f 0000 1601 0000)正向查找:找到符号表索引为 1 的
0100 0000 0000 0000 0000 0000 1200 0000
0100 0000 在字符串的偏移 0x590 + 0x1= 0x591 查找为 __cxa_atexit
0000 0000 地址 导入表之前为0
0000 0000 函数或者变量大小
1200 0000 附加的(不知道什么用)
结论 :函数名 __cxa_atexit 导入地址 0x3f8c
逆向查找:
已知函数名为 malloc 。
-
查找在字符串表中的地址为 0x5c1。
malloc.png 减去字符串表起始地址 0x590 ,0x5c1 - 0x590 = 0x31
在符号表中查找在字符串的偏移为 0x31 的项
3100 0000 0000 0000 0000 0000 1200 0000
该项为符号表的第 3 项
- 在导入表中查找符号表索引为 3 的项
903f 0000 1603 0000
结论 :函数名 malloc 导入地址 0x3f90
解析完毕,理论可行。
Android Native Hook技术你知道多少?
字节跳动开源 Android PLT hook 方案 bhook