首页
社区
课程
招聘
[分享] 爱奇艺APP使用的 native PLT hook 库开源了,经过了“亿级”线上设备的稳定性兼容性考验
发表于: 2018-5-31 11:16 12956

[分享] 爱奇艺APP使用的 native PLT hook 库开源了,经过了“亿级”线上设备的稳定性兼容性考验

2018-5-31 11:16
12956

https://github.com/iqiyi/xhook

作者居然还写了一篇安卓PLT hook的小白入门文章,还有配套的演示代码,原文在:https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md

你始终可以从 这里 访问本文的最新版本。

文中使用的示例代码可以从 这里 获取。文中提到的 xhook 开源项目可以从 这里 获取。

我们有一个新的动态库:libtest.so。

头文件 test.h

源文件 test.c

say_hello 的功能是在终端打印出 hello\n 这6个字符(包括结尾的 \n)。

我们需要一个测试程序:main。

源文件 main.c

编译它们分别生成 libtest.so 和 main。运行一下:

太棒了!libtest.so 的代码虽然看上去有些愚蠢,但是它居然可以正确的工作,那还有什么可抱怨的呢?赶紧在新版 APP 中开始使用它吧!

遗憾的是,正如你可能已经发现的,libtest.so 存在严重的内存泄露问题,每调用一次 say_hello 函数,就会泄露 1024 字节的内存。新版 APP 上线后崩溃率开始上升,各种诡异的崩溃信息和报障信息跌撞而至。

幸运的是,我们修复了 libtest.so 的问题。可是以后怎么办呢?我们面临 2 个问题:

如果我们能对动态库中的函数调用做 hook(替换,拦截,窃听,或者你觉得任何正确的描述方式),那就能够做到很多我们想做的事情。比如 hook malloccallocreallocfree,我们就能统计出各个动态库分配了多少内存,哪些内存一直被占用没有释放。

这真的能做到吗?答案是:hook 我们自己的进程是完全可以的。hook 其他进程需要 root 权限(对于其他进程,没有 root 权限就没法修改它的内存空间,也没法注入代码)。幸运的是,我们只要 hook 自己就够了。

ELF(Executable and Linkable Format)是一种行业标准的二进制数据封装格式,主要用于封装可执行文件、动态库、object 文件和 core dumps 文件。

使用 google NDK 对源代码进行编译和链接,生成的动态库或可执行文件都是 ELF 格式的。用 readelf 可以查看 ELF 文件的基本信息,用 objdump 可以查看 ELF 文件的反汇编输出。

ELF 格式的概述可以参考 这里,完整定义可以参考 这里。其中最重要的部分是:ELF 文件头、SHT(section header table)、PHT(program header table)。

ELF 文件的起始处,有一个固定格式的定长的文件头(32 位架构为 52 字节,64 位架构为 64 字节)。ELF 文件头以 magic number 0x7F 0x45 0x4C 0x46 开始(其中后 3 个字节分别对应可见字符 E L F)。

libtest.so 的 ELF 文件头信息:

ELF 文件头中包含了 SHT 和 PHT 在当前 ELF 文件中的起始位置和长度。例如,libtest.so 的 SHT 起始位置为 12744,长度 40 字节;PHT 起始位置为 52,长度 32字节。

ELF 以 section 为单位来组织和管理各种信息。ELF 使用 SHT 来记录所有 section 的基本信息。主要包括:section 的类型、在文件中的偏移量、大小、加载到内存后的虚拟内存相对地址、内存中字节的对齐方式等。

libtest.so 的 SHT:

比较重要,且和 hook 关系比较大的几个 section 是:

.rel.dyn:除 .rel.plt 以外的重定位信息。(比如通过全局函数指针来调用外部函数)

ELF 被加载到内存时,是以 segment 为单位的。一个 segment 包含了一个或多个 section。ELF 使用 PHT 来记录所有 segment 的基本信息。主要包括:segment 的类型、在文件中的偏移量、大小、加载到内存后的虚拟内存相对地址、内存中字节的对齐方式等。

libtest.so 的 PHT:

所有类型为 PT_LOAD 的 segment 都会被动态链接器(linker)映射(mmap)到内存中。

我们关心的 hook 操作,属于动态形式的内存操作,因此主要关心的是执行视图,即 ELF 被加载到内存后,ELF 中的数据是如何组织和存放的。

这是一个十分重要和特殊的 section,其中包含了 ELF 中其他各个 section 的内存位置等信息。在执行视图中,总是会存在一个类型为 PT_DYNAMIC 的 segment,这个 segment 就包含了 .dynamic section 的内容。

无论是执行 hook 操作时,还是动态链接器执行动态链接时,都需要通过 PT_DYNAMIC segment 来找到 .dynamic section 的内存位置,再进一步读取其他各项 section 的信息。

libtest.so 的 .dynamic section:

安卓中的动态链接器程序是 linker。源码在 这里

动态链接(比如执行 dlopen)的大致步骤是:

等一下!我们似乎发现了什么!再看一遍重定位操作(relocate)的部分。难道我们只要从这些 .relxxx 中获取到“目标地址”,然后在“目标地址”中重新填上一个新的函数地址,这样就完成 hook 了吗?也许吧。

静态分析验证一下还是很容易的。以 armeabi-v7a 架构的 libtest.so 为例。先看一下 say_hello 函数对应的汇编代码吧。

找到了!say_hello 在地址 f61,对应的汇编指令体积为 60(10 进制)字节。用 objdump 查看 say_hello 的反汇编输出。

malloc 函数的调用对应于指令 blx dd4。跳转到了地址 dd4。看看这个地址里有什么吧:

果然,跳转到了 .plt 中,经过了几次地址计算,最后跳转到了地址 3f90 中的值指向的地址处,3f90 是个函数指针。

稍微解释一下:因为 arm 处理器使用 3 级流水线,所以第一条指令取到的 pc 的值是当前执行的指令地址 + 8
于是:dd4 + 8 + 3000 + 1b4 = 3f90

地址 3f90 在哪里呢:

果然,在 .got 里。

顺便再看一下 .rel.plt

malloc 的地址居然正好存放在 3f90 里,这绝对不是巧合啊!还等什么,赶紧改代码吧。我们的 main.c 应该改成这样:

编译运行一下:

思路是正确的。但之所以还是失败了,是因为这段代码存在下面的 3 个问题:

我们需要解决这些问题。

在进程的内存空间中,各种 ELF 的加载地址是随机的,只有在运行时才能拿到加载地址,也就是基地址。我们需要知道 ELF 的基地址,才能将相对地址换算成绝对地址。

没有错,熟悉 Linux 开发的聪明的你一定知道,我们可以直接调用 dl_iterate_phdr。详细的定义见 这里

嗯,先等等,多年的 Android 开发被坑经历告诉我们,还是再看一眼 NDK 里的 linker.h 头文件吧:

为什么?!ARM 架构的 Android 5.0 以下版本居然不支持 dl_iterate_phdr!我们的 APP 可是要支持 Android 4.0 以上的所有版本啊。特别是 ARM,怎么能不支持呢?!这还让不让人写代码啦!

幸运的是,我们想到了,我们还可以解析 /proc/self/maps:

maps 返回的是指定进程的内存空间中 mmap 的映射信息,包括各种动态库、可执行文件(如:linker),栈空间,堆空间,甚至还包括字体文件。maps 格式的详细说明见 这里

我们的 libtest.so 在 maps 中有 3 行记录。offset 为 0 的第一行的起始地址 b6ec6000绝大多数情况下就是我们寻找的基地址

maps 返回的信息中已经包含了权限访问信息。如果要执行 hook,就需要写入的权限,可以使用 mprotect 来完成:

注意修改内存访问权限时,只能以“页”为单位。mprotect 的详细说明见 这里

注意 .got.data 的 section 类型是 PROGBITS,也就是执行代码。处理器可能会对这部分数据做缓存。修改内存地址后,我们需要清除处理器的指令缓存,让处理器重新从内存中读取这部分指令。方法是调用 __builtin___clear_cache

注意清除指令缓存时,也只能以“页”为单位。__builtin___clear_cache 的详细说明见 这里

我们把 main.c 修改为:

重新编译运行:

是的,成功了!我们并没有修改 libtest.so 的代码,甚至没有重新编译它。我们仅仅修改了 main 程序。

libtest.so 和 main 的源码放在 github 上,可以从 这里 获取到。(根据你使用的编译器不同,或者编译器的版本不同,生成的 libtest.so 中,也许 malloc 对应的地址不再是 0x3f90,这时你需要先用 readelf 确认,然后再到 main.c 中修改。)

当然,我们已经开源了一个叫 xhook 的工具库。使用 xhook,你可以更优雅的完成对 libtest.so 的 hook 操作,也不必担心硬编码 0x3f90 导致的兼容性问题。

xhook 支持 armeabi, armeabi-v7a 和 arm64-v8a。支持 Android 4.0 (含) 以上版本 (API level >= 14)。经过了产品级的稳定性和兼容性验证。可以在 这里 获取 xhook

总结一下 xhook 中执行 PLT hook 的流程:

可以。而且对于格式解析来说,读文件是最稳妥的方式,因为 ELF 在运行时,原理上有很多 section 不需要一直保留在内存中,可以在加载完之后就从内存中丢弃,这样可以节省少量的内存。但是从实践的角度出发,各种平台的动态链接器和加载器,都不会这么做,可能它们认为增加的复杂度得不偿失。所以我们从内存中读取各种 ELF 信息就可以了,读文件反而增加了性能损耗。另外,某些系统库 ELF 文件,APP 也不一定有访问权限。

正如你已经注意到的,前面介绍 libtest.so 基地址获取时,为了简化概念和编码方便,用了“绝大多数情况下”这种不应该出现的描述方式。对于 hook 来说,精确的基地址计算流程是:

绝大多数的 ELF 第一个 PT_LOAD segment 的 p_vaddr 都是 0

另外,之所以要在 maps 里找 offset 为 0 的行,是因为我们在执行 hook 之前,希望对内存中的 ELF 文件头进行校验,确保当前操作的是一个有效的 ELF,而这种 ELF 文件头只能出现在 offset 为 0 的 mmap 区域。

可以在 Android linker 的源码中搜索“load_bias”,可以找到很多详细的注释说明,也可以参考 linker 中对 load_bias_ 变量的赋值程序逻辑。

会有一些影响。

对于外部函数的调用,可以分为 3 中情况:

一般情况下,产品级的 ELF 很少会使用 -O0 进行编译,所以也不必太纠结。但是如果你希望你的 ELF 尽量不被别人 PLT hook,那可以试试使用 -O0 来编译,然后尽量早的将外部函数的指针赋值给局部函数指针变量,之后一直使用这些局部函数指针来访问外部函数。

总之,查看 C/C++ 的源代码对这个问题的理解没有意义,需要查看使用不同的编译选项后,生成的 ELF 的反汇编输出,比较它们的区别,才能知道哪些情况由于什么原因导致无法被 PLT hook。

我们有时会遇到这样的问题:

可能的原因是:

问题分析:

先明确一个观点:不要只从应用层程序开发的角度来看待段错误,段错误不是洪水猛兽,它只是内核与用户进程的一种正常的交流方式。当用户进程访问了无权限或未 mmap 的虚拟内存地址时,内核向用户进程发送 SIGSEGV 信号,来通知用户进程,仅此而已。只要段错误的发生位置是可控的,我们就可以在用户进程中处理它。

解决方案:

具体代码可以参考 xhook 中的实现,在源码中搜索 siglongjmpsigsetjmp

我们这里介绍的 hook 方式为 PLT hook,不能做 ELF 内部函数之间调用的 hook。

inline hook 可以做到,你需要先知道想要 hook 的内部函数符号名(symbol name)或者地址,然后可以 hook。

有很多开源和非开源的 inline hook 实现,比如:

inline hook 方案强大的同时可能带来以下的问题:

建议如果 PLT hook 够用的话,就不必尝试 inline hook 了。

caikelun#qiyi.com (请用 @ 替换 #)

Copyright (c) 2018, 爱奇艺, Inc. All rights reserved.

本文使用 Creative Commons 许可证 授权。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2019-2-2 14:11 被admin编辑 ,原因: 图片本地化
收藏
免费 2
支持
分享
最新回复 (23)
雪    币: 144
活跃值: (335)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
碉堡了啊,占个前排
2018-5-31 11:21
0
雪    币: 183
活跃值: (563)
能力值: ( LV9,RANK:150 )
在线值:
发帖
回帖
粉丝
3
赞一个!
2018-5-31 11:28
0
雪    币: 1
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
是爱奇艺很稳定的一个PLT  Hook库,经过亿级用户验证。
2018-5-31 11:51
0
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
非常给力,先star一把
2018-5-31 11:53
0
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
......
最后于 2020-4-7 20:49 被junkboy编辑 ,原因: ......
2018-5-31 12:09
0
雪    币: 76
活跃值: (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
先star一把 
2018-5-31 13:36
0
雪    币: 5310
活跃值: (3699)
能力值: ( LV13,RANK:283 )
在线值:
发帖
回帖
粉丝
8
多谢分享
2018-5-31 14:10
0
雪    币: 729
活跃值: (1306)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
9
多谢分享
2018-5-31 17:54
0
雪    币: 1
活跃值: (743)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
2018-6-1 12:55
0
雪    币: 614
活跃值: (883)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
11
好东西,顶一个。
2018-6-1 16:05
0
雪    币: 10017
活跃值: (3457)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
hook  内部函数还是得用inline  hook
2018-6-2 11:28
0
雪    币: 144
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
爱奇艺做了好事
2018-6-2 12:19
0
雪    币: 23
活跃值: (10)
能力值: ( LV4,RANK:43 )
在线值:
发帖
回帖
粉丝
14
这个很赞
2018-6-2 15:02
0
雪    币: 102
活跃值: (2045)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
15
mark..........
2018-6-2 22:01
0
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
16
fishhook也挺好用
2018-6-3 13:03
0
雪    币: 249
活跃值: (72)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
m
2018-6-4 09:59
0
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
2018-6-4 23:53
0
雪    币: 2039
活跃值: (1458)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
19
这种Hook一直在用,原理很简单。但是楼主能搞的这么详细全面,还有文档,这可能就是我这种小作坊程序猿,和大公司工程师的差距吧。
2018-6-5 10:00
0
雪    币: 1380
活跃值: (116)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
20
这种hook方法也叫GOT  hook吧。
2018-6-5 21:28
0
雪    币: 210
活跃值: (631)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
21
顶~~~
2018-6-22 09:46
0
雪    币: 120
活跃值: (1597)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
深度好文
2018-7-14 00:34
0
雪    币: 574
活跃值: (405)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
很详细,很全面,超赞
2018-12-12 15:17
0
雪    币: 20
活跃值: (105)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
hook java层的哪个好用
2018-12-15 22:57
0
游客
登录 | 注册 方可回帖
返回
//