PLT/GOT hook

75 阅读9分钟

是什么

hook是在原有的程序执行流程中插入我们自己的代码逻辑。比如callback;比如代理;比如字节码插桩...

PLT hook是native hook中的一种,也是其中相对最常用的。

那如何实现Plt HOOK呢?

可以分为三步:

  1. 找到目标函数的地址
  2. 实现自定义函数
  3. 将目标函数的地址替换为自定义函数的地址

这里面比较核心的是第一步,下面的内容也主要是围绕如何找到目标函数。

首先我们了解下so的文件格式:ELF

ELF

ELF全称 executable linkable format,可执行可链接的文件格式。常见的比如静态链接库.a以及动态链接库.so。

ELF有两个比较重要的表:PHT(Program Header Table)跟SHT(Section Header Table)。

这里我把这两个头表理解为目录。so中有很多不同类型的内容,比如字符常量,代码段等,这些不同的内容存放在不同的区域,这些区域的起始地址,长度等信息会统一存放在头表中,这样通过查看头表就能管理so的所有内容。

那为啥需要两个头表呢?它们一个对应的是未加载到内存中的文件组织形式(sht),通常会称它为链接视图,一个对应的是加载到内存中的组织形式(pht),通常称它为执行视图。

elf.png

链接视图-SHT

在链接视图,ELF是使用section组织的,SHT会存放section的起始位置等基本信息。可以通过readelf -S查看,对于native hook来说有几个比较重要的section,分别是:

  1. .dynamic : 包含了该elf依赖的外部调用,比如依赖了哪些so;包含了其他section的起始位置信息,也就是通过这个section可以查到其他section的位置
  2. .got : 存放外部调用的内存绝对地址(动态链接器执行完重定位之后)
  3. .plt : 外部调用的跳板。通过执行plt的指令会跳转到got表。
  4. .relxxx : 重定位条目,以外部调用为例,会存放外部调用的方法名称,以及got表的地址
  5. .dynsym :符号信息
  6. .dynstr :字符串常量信息
  7. .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表存放的就是目标函数的地址了,这样我们就直接调到了目标函数。如下图:

image.png

回顾下我们的目的:找到目标函数的真实地址。

到这里我想大家应该有思路了:如果我们可以拿到对应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());
}
  1. 找到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

  1. 查看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

  1. 查看对应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

image.png

执行plt跳转到19e44

  1. 查看重定位条目:readelf -r
C:\Users\wangzeqi\Desktop>readelf -r libnativelib.so | findstr __android
00019e44  00000a16 R_ARM_JUMP_SLOT   00000000   __android_log_print

可以看到重定位条目relxxx中的地址也是19e44

简短总结

再回到最初的问题,我们看下链接器是如何找到目标函数的地址(重定向)。

  1. 遍历所有的relxxxx(重定向)条目(包含外部方法的相关信息,以及got表的地址)
  2. 根据外部方法的信息找到真实地址(此时依赖的so已经加载完毕)
  3. 将真实地址写入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)

参考

juejin.cn/post/684490…

www.bilibili.com/video/BV1Gc…

blog.51cto.com/u_15333820/…

blog.csdn.net/linyt/categ…

chan-shaw.github.io/2020/04/06/…