是什么
hook是在原有的程序执行流程中插入我们自己的代码逻辑。比如callback;比如代理;比如字节码插桩...
PLT hook是native hook中的一种,也是其中相对最常用的。
那如何实现Plt HOOK呢?
可以分为三步:
- 找到目标函数的地址
- 实现自定义函数
- 将目标函数的地址替换为自定义函数的地址
这里面比较核心的是第一步,下面的内容也主要是围绕如何找到目标函数。
首先我们了解下so的文件格式:ELF
ELF
ELF全称 executable linkable format,可执行可链接的文件格式。常见的比如静态链接库.a以及动态链接库.so。
ELF有两个比较重要的表:PHT(Program Header Table)跟SHT(Section Header Table)。
这里我把这两个头表理解为目录。so中有很多不同类型的内容,比如字符常量,代码段等,这些不同的内容存放在不同的区域,这些区域的起始地址,长度等信息会统一存放在头表中,这样通过查看头表就能管理so的所有内容。
那为啥需要两个头表呢?它们一个对应的是未加载到内存中的文件组织形式(sht),通常会称它为链接视图,一个对应的是加载到内存中的组织形式(pht),通常称它为执行视图。
链接视图-SHT
在链接视图,ELF是使用section组织的,SHT会存放section的起始位置等基本信息。可以通过readelf -S查看,对于native hook来说有几个比较重要的section,分别是:
- .dynamic : 包含了该elf依赖的外部调用,比如依赖了哪些so;包含了其他section的起始位置信息,也就是通过这个section可以查到其他section的位置
- .got : 存放外部调用的内存绝对地址(动态链接器执行完重定位之后)
- .plt : 外部调用的跳板。通过执行plt的指令会跳转到got表。
- .relxxx : 重定位条目,以外部调用为例,会存放外部调用的方法名称,以及got表的地址
- .dynsym :符号信息
- .dynstr :字符串常量信息
- .text : 代码编译之后的机器指令
运行视图-PHT
在运行视图,ELF是使用segment组织的,一个segment会包含多个section。可以通过readelf -l查看
C:\Users\wangzeqi\Desktop>readelf -l libnativelib.so
Elf file type is DYN (Shared object file)
Entry point 0x0
There are 8 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00000034 0x00000034 0x00100 0x00100 R 0x4
LOAD 0x000000 0x00000000 0x00000000 0x17332 0x17332 R E 0x1000
LOAD 0x017584 0x00018584 0x00018584 0x01a8c 0x01ca5 RW 0x1000
DYNAMIC 0x018ca8 0x00019ca8 0x00019ca8 0x00108 0x00108 RW 0x4
NOTE 0x000134 0x00000134 0x00000134 0x000bc 0x000bc R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
EXIDX 0x0131f8 0x000131f8 0x000131f8 0x00e68 0x00e68 R 0x4
GNU_RELRO 0x017584 0x00018584 0x00018584 0x01a7c 0x01a7c RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .note.android.ident .note.gnu.build-id .dynsym .dynstr .gnu.hash .gnu.version .gnu.version_d .gnu.version_r .rel.dyn .rel.plt .plt .text .ARM.exidx .ARM.extab .rodata
02 .data.rel.ro .fini_array .dynamic .got .data .bss
03 .dynamic
04 .note.android.ident .note.gnu.build-id
05
06 .ARM.exidx
07 .data.rel.ro .fini_array .dynamic .got
了解完so的文件格式之后,我们再来看下动态链接,理解了动态链接的流程,基本上也就掌握了如何寻找目标函数地址。
动态链接
举个例子我们理解一下大体过程:
假设我们写了一个native库,需要调用到另一个so的方法。编译的时候是不知道这个方法的内存地址的,那在运行的时候我们怎么才能执行到这个方法呢?那我们是不是可以把这个方法的信息先存起来,等到运行时so完成加载之后,我们再根据存放的方法信息找到对应的地址。
相应的其实编译之后会在调用该方法的地方生成一条跳转指令,执行这个指令会跳转到plt表中的一个条目,执行plt条目中的指令之后会先跳转到got表。
第一次执行(红线)的时候,got表中存放的是寻找真实地址的方法的地址,再次跳转就会开始寻找目标函数的地址,寻找完成之后会执行目标函数并把真实地址写入got表。
当第二次执行的时候,还是会执行跳转指令到plt,之后从plt跳到got表。不过此时got表存放的就是目标函数的地址了,这样我们就直接调到了目标函数。如下图:
回顾下我们的目的:找到目标函数的真实地址。
到这里我想大家应该有思路了:如果我们可以拿到对应got表的地址,也就可以找到目标函数的地址了。
再回顾下relxxx section中存放的内容:外部调用的方法名称,以及got表的地址。
下面我举个例子来验证下通过动态链接找到的地址跟relxxx section中存放的地址是一样的。
举个例子
调用方法__android_log_print,查看通过plt跳转的got表地址跟重定位条目中的地址是否一致?
#include <jni.h>
#include <string>
#include <android/log.h>
int test_elf(){
__android_log_print(ANDROID_LOG_DEBUG, "MyTag", "This is a debug message from JNI");
return -1;
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_xmkj_plugin_nativelib_NativeLib_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
test_elf();
return env->NewStringUTF(hello.c_str());
}
- 找到test_elf的地址:readelf -s 查看符号表信息
C:\Users\wangzeqi\Desktop>readelf -s libnativelib.so | findstr test_elf
419: 000081f1 40 FUNC GLOBAL DEFAULT 12 _Z8test_elfv
9433: 000081f1 40 FUNC GLOBAL DEFAULT 12 _Z8test_elfv
81f1
- 查看test_elf的汇编代码: objdump -d libnativelib.so > a.txt
000081f0 <_Z8test_elfv>:
81f0: b580 push {r7, lr}
81f2: 466f mov r7, sp
81f4: b082 sub sp, #8
81f6: 4906 ldr r1, [pc, #24] ; (8210 <_Z8test_elfv+0x20>)
81f8: 4479 add r1, pc
81fa: 4a06 ldr r2, [pc, #24] ; (8214 <_Z8test_elfv+0x24>)
81fc: 447a add r2, pc
81fe: 2003 movs r0, #3
8200: f7ff ed32 blx 7c68 <__android_log_print@plt>
8204: f04f 31ff mov.w r1, #4294967295
8208: 9001 str r0, [sp, #4]
820a: 4608 mov r0, r1
820c: b002 add sp, #8
820e: bd80 pop {r7, pc}
8210: 0000ce6c .word 0x0000ce6c
8214: 0000ce6e .word 0x0000ce6e
blx 7c68 <__android_log_print@plt>跳转到plt
- 查看对应plt的指令
00007c68 <__android_log_print@plt>:
7c68: e28fc600 add ip, pc, #0, 12
7c6c: e28cca12 add ip, ip, #73728 ; 0x12000
7c70: e5bcf1d4 ldr pc, [ip, #468]! ; 0x1d4
arm 3级流水(pc = 当前执行指令 + 8;pc存放取指指令地址)计算最终的跳转地址:7c68 + 8 + 12000 + 1d4 = 19e44
执行plt跳转到19e44
- 查看重定位条目:readelf -r
C:\Users\wangzeqi\Desktop>readelf -r libnativelib.so | findstr __android
00019e44 00000a16 R_ARM_JUMP_SLOT 00000000 __android_log_print
可以看到重定位条目relxxx中的地址也是19e44
简短总结
再回到最初的问题,我们看下链接器是如何找到目标函数的地址(重定向)。
- 遍历所有的relxxxx(重定向)条目(包含外部方法的相关信息,以及got表的地址)
- 根据外部方法的信息找到真实地址(此时依赖的so已经加载完毕)
- 将真实地址写入got表中
那XHook是如何找目标函数的地址的呢?简单理解其实就是遍历so的所有重定位条目,这些条目里面包含了外部方法的信息以及got表的地址。根据方法信息就能找到got表地址,got表里面就是目标函数的真实地址。
XHook完整流程:来自 juejin.cn/post/684490…
- 读 maps,获取 ELF 的内存首地址(start address)。
- 验证 ELF 头信息。
- 从 PHT 中找到类型为
PT_LOAD
且 offset 为0
的 segment。计算 ELF 基地址。 - 从 PHT 中找到类型为
PT_DYNAMIC
的 segment,从中获取到.dynamic
section,从.dynamic
section中获取其他各项 section 对应的内存地址。 - 在
.dynstr
section 中找到需要 hook 的 symbol 对应的 index 值。 - 遍历所有的
.relxxx
section(重定位 section),查找 symbol index 和 symbol type 都匹配的项,对于这项重定位项,执行 hook 操作。hook 流程如下:- 读 maps,确认当前 hook 地址的内存访问权限。
- 如果权限不是可读也可写,则用
mprotect
修改访问权限为可读也可写。 - 如果调用方需要,就保留 hook 地址当前的值,用于返回。
- 将 hook 地址的值替换为新的值。(执行 hook)
- 如果之前用
mprotect
修改过内存访问权限,现在还原到之前的权限。 - 清除 hook 地址所在内存页的处理器指令缓存。
应用
使用plt hook 获取anr的trace文件。具体可以查看另一篇文章:todo(anr)