一、ELF(Executable and Linkable Format)
1.1、ELF(Executable and Linkable Format)格式
ELF是一种行业标准的二进制数据封装格式,主要用于封装可执行文件、动态库、object 文件和 core dumps 文件。
1.2、ELF 解析工具
使用 google NDK 对源代码进行编译和链接,生成的动态库或可执行文件都是 ELF 格式的。用 readelf 可以查看 ELF 文件的基本信息,用 objdump 可以查看 ELF 文件的反汇编输出。
工具的位置
arm-linux-androideabi-readelf 和arm-linux-androideabi-objdump 位于Android的NDK目录下
已我的mac为例,路径如下
/Users/baixuefei/Library/Android/sdk/ndk/16.1.4479499/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/
- arm-linux-androideabi-readelf -h libtestxhook.so 查看ELF 头信息
baixuefei@baixuefeideMacBook-Pro xhook % ./arm-linux-androideabi-readelf -h libtestxhook.so
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: ARM
Version: 0x1
Entry point address: 0x12728
Start of program headers: 52 (bytes into file)
Start of section headers: 1146224 (bytes into file)
Flags: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 35
Section header string table index: 33
- arm-linux-androideabi-objdump 查看so的反汇编代码
./arm-linux-androideabi-objdump -D libtestxhook.so
00012928 <Java_com_example_testxhook_XHookUtils_sayHello>:
12928: b580 push {r7, lr}
1292a: 466f mov r7, sp
1292c: b082 sub sp, #8
1292e: 9001 str r0, [sp, #4]
12930: 9100 str r1, [sp, #0]
12932: f011 ec5e blx 241f0 <__ThumbV7PILongThunk__ZNSt9exceptionD2Ev+0x104>
12936: b002 add sp, #8
12938: bd80 pop {r7, pc}
1.3ELF 格式文件构成
二、动态链接过程
2.1、静态链接和动态链接
程序对于外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,链接的方式分为静态链接和动态链接。
- 静态链接得到的可执行文件包含外部函数的全部代码,
- 动态链接得到的可执行文件中并不包含外部函数的代码,而是运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,再在发生调用时去链接库定位所需的函数。
2.2、 链接过程
当需要使用一个 Native 库(.so 文件)的时候,我们需要调用dlopen("libname.so")来加载这个库。
在我们调用了dlopen("libname.so")之后,系统首先会检查缓存中已加载的 ELF 文件列表。如果未加载则执行加载过程,如果已加载则计数加一,忽略该调用。然后系统会用从 libname.so 的dynamic节区中读取其所依赖的库,按照相同的加载逻辑,把未在缓存中的库加入加载列表。
下面命令来查看一个库的依赖:
readelf -d <library> | grep NEEDED
可以看到libtestxhook.so 依赖liblog.so、libm.so、libdl.so、libc.so四个外部库
baixuefei@baixuefeideMacBook-Pro xhook % ./arm-linux-androideabi-readelf -d ./libtestxhook.so
Dynamic section at offset 0x2632c contains 27 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [liblog.so]
0x00000001 (NEEDED) Shared library: [libm.so]
0x00000001 (NEEDED) Shared library: [libdl.so]
0x00000001 (NEEDED) Shared library: [libc.so]
0x0000000e (SONAME) Library soname: [libtestxhook.so]
0x0000001e (FLAGS) BIND_NOW
0x6ffffffb (FLAGS_1) Flags: NOW
0x00000011 (REL) 0x9fcc
0x00000012 (RELSZ) 11744 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffa (RELCOUNT) 1002
0x00000017 (JMPREL) 0xe234
0x00000002 (PLTRELSZ) 1200 (bytes)
0x00000003 (PLTGOT) 0x274a0
0x00000014 (PLTREL) REL
0x00000006 (SYMTAB) 0x210
0x0000000b (SYMENT) 16 (bytes)
0x00000005 (STRTAB) 0x4e74
0x0000000a (STRSZ) 20824 (bytes)
0x6ffffef5 (GNU_HASH) 0x2cc8
0x00000004 (HASH) 0x3b8c
0x0000001a (FINI_ARRAY) 0x27324
0x0000001c (FINI_ARRAYSZ) 8 (bytes)
0x6ffffff0 (VERSYM) 0x27d0
0x6ffffffe (VERNEED) 0x2c88
0x6fffffff (VERNEEDNUM) 2
0x00000000 (NULL) 0x0
2.2.1、 系统是如何加载的 ELF 文件的
- 读 ELF 的程序头部表,把所有 PT_LOAD 的节区 mmap 到内存中。
- 从“.dynamic”中读取各信息项,计算并保存所有节区的虚拟地址,然后执行重定位操作。
- 最后 ELF 加载成功,引用计数加一。
2.2.2、 “.got”和“.plt”它们的具体含义
- The Global Offset Table (GOT)。用来保存外部函数的的绝对地址
简单来说就是在数据段的地址表,假定我们有一些代码段的指令引用一些地址变量,编译器会引用 GOT 表来替代直接引用绝对地址,因为绝对地址在编译期是无法知道的,只有重定位后才会得到 ,GOT 自己本身将会包含函数引用的绝对地址。
- The Procedure Linkage Table (PLT)。
PLT 不同于 GOT,它位于代码段,动态库的每一个外部函数都会在 PLT 中有一条记录,每一条 PLT记录都是一小段可执行代码。一般来说,外部代码都是在调用 PLT 表里的记录,然后 PLT 的相应记录会负责调用实际的函数。我们一般把这种设定叫作“蹦床”(Trampoline)。
2.2.3、首次加载外部函数
PLT 和 GOT 记录是一一对应的,并且 GOT 表第一次解析后会包含调用函数的实际地址。
[图片上传失败...(image-ea86bd-1710343090921)]
- 我们在代码中调用 func,编译器会把这个转化为 func@plt,并在 PLT 表插入一条记录。
- PLT 表中第一条(或者说第 0 条)PLT[0] 是一条特殊记录,它是用来帮助我们解析地址的。通常在类 Linux 系统,这个的实现会位于动态加载器。/system/bin/linker。
- 其余的 PLT 记录都均包含以下信息:
(1)跳转 GOT 表的指令(jmp *GOT[n])
(2)为上面提到的第 0 条解析地址函数准备参数。
(3)调用 PLT[0],这里 resovler 的实际地址是存储在 GOT[2] 。
在解析前 GOT[n] 会直接指向 jmp *GOT[n] 的下一条指令。在解析完成后,我们就得到了 func 的实际地址,动态加载器会将这个地址填入 GOT[n],然后调用 func。
2.2.4、第二次加载外部函数
[图片上传失败...(image-8843a2-1710343090921)]
当第一次调用发生后,之后再调用函数 func 就高效简单很多。首先调用 PLT[n],然后执行 jmp *GOT[n]。GOT[n] 直接指向 func
三、PLT Hook基本原理
PLT Hook就是改变了原来的relocation后的地址。主要流程:
- 通过符号名,在hash table中找到对应的符号信息
- 再找到对应的PLT信息
- 最后找到GOT表中的绝对地址的值
- 修改这个绝对地址的值,为我们的“代理函数”的地址
四、运行时加载(dlopen、dlsysm、dlerror、dlclose)
运行时加载so库,让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。
动态库的装载通过4个函数完成:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库(dlclose)。
这几个API的实现是在/lib/libdl.so.2里面,它们的声明和相关常量在系统标准头文件<dlfcn.h>。
4.1、dlopen()
dlopen()函数用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程,它的原型定义为
void *dlopen(const char *filename, int flags);
dlopen的返回值是被加载的模块的句柄,这个句柄在后面使用dlsym或者dlclose时要用到。如果加载模块失败,则返回NULL。如果被加载的模块之间有依赖关系,比如模块A依赖于模块B,那么程序员必须手工加载被依赖的模块,比如先加载B,再加载A。
(1)第一个参数是被加载动态库的路径,如果这个路径是绝对路径(以”/”开始的路径),则该函数会将尝试直接打开动态库;如果是相对路径,那么dlopen()会尝试在以一定的顺序查找该动态库文件
(2)第二个参数flag表示函数符号的解析方式,其中RTLD_LAZY和RTLD_NOW必须要设置其中一种
- 常量RTLD_LAZY表示使用延迟绑定,当函数第一次被用到时才进行绑定,即PLT机制;
- RTLD_NOW表示当模块被加载时即完成所有的函数绑定工作,如果有任何未定义的符号引用的绑定工作没法完成,那么dlopen()就返回错误。
- RTLD_GLOBAL 表示将加载的模块的全局符号合并到进程的全局符号表中,使得以后加载的模块可以使用这些符号。
- RTLD_LOCAL RTLD_LOCAL则与RTLD_GLOBAL相反,使以后加载的模块不能使用这些符号,如果没有定义,默认就是RTLD_LOCAL。
- RTLD_NODELETE 在dlclose()期间不要卸载共享对象。因此,如果稍后使用dlopen()重新加载对象,则共享对象的静态变量不会被重新初始化。
- RTLD_DEEPBIND 将这个共享对象中符号的查找作用域放在全局作用域之前。这意味着包含对象将优先使用自己的符号,而不是已经加载的其他对象中包含的具有相同名称的全局符号。
4.2、dlsym()
dlsym函数就是运行时装载的核心,通过这个函数找到所需要运行的符号,函数原型如下:
void *dlsym(void *handle, const char *symbol);
两个参数,第一个参数是有dlopen()返回的动态库的句柄;第二个参数即所查找的符号的名字,一个以\0结尾的C字符串。
如果查找的符号是个函数,那么它返回函数的地址;如果是个变量,它返回变量的地址;如果这个符号是个常量,那么它返回的是该常量的值。
4.3、dlerror()
每次我们调用dlopen()、dlsym()或dlclose()以后,都可以调用dlerror()函数来判断上一次调用是否成功。dlerror()的返回值类型是char*,如果返回NULL,则表示上一次调用成功,如果不是,则返回相应的错误消息。
4.4、dlclose()
dlclose()的作用跟dlopen()刚好相反,它的作用是将上一个已经加载的模块卸载。系统会维持一个加载引用计数器,每次使用dlopen()加载某模块时,相应的计数器加一;每次使用dlclose()卸载某模块时,相应计数器减一。只有当计数器值减到0时,模块才被真正的卸载掉。
参考文章
xhook
PLT Hook基本原理
PLT HOOK
GOT表和PLT表
[Android Native Hook技术你知道多少?](https://zhuanlan.zhihu.com/p/13269