在前面的几篇文章里,我们已经把注入器和 Java Hook 这两部分大致梳理了一遍。继续往下走,一个比较自然的问题就是:如果目标不再是 Java 方法,而是 so 里的 native 函数,那 Hook 又该怎么做?Native Hook 这件事如果再往下拆,其实又可以分成几条路。最常见的两类,一类是直接改机器码的 Inline Hook,另一类是利用动态链接过程留下来的导入表/重定位信息做 PLT Hook。相较之下,PLT Hook 更适合作为一个 Native Hook 框架的第一步:它不用上来就硬改目标函数入口,而是优先利用 ELF 和动态链接器已经准备好的信息。这篇文章我们先把目标定在实现一个可用的plt hook demo。项目地址:5a2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6^5x3h3q4G2L8U0q4F1k6#2)9J5c8V1&6G2L8$3D9`.读完之后,我希望至少能把下面这些问题讲明白:在 Android/Linux 里,native 动态库本质上就是 ELF 文件。libxxx.so 被加载进进程后,并不是简单把文件原样搬到内存里,而是由动态链接器按照 ELF 中的 program header、dynamic segment、relocation 信息等内容完成装载和重定位。如果只从 Hook 的角度去看,ELF 里最重要的几类信息是:后面 的 PLT Hook,本质上就是围绕这些信息展开。导入函数,简单说就是:当前模块里“要调用,但实现不在自己这个模块里”的函数。 比如 libnative-lib.so 里写了:如果 strcmp 和 malloc 的实现都不在 libnative-lib.so 自己内部,而是在别的 so 里,比如 libc.so,那对 libnative-lib.so 来说,strcmp、malloc 就是导入函数。对应的另一边就是导出函数,我理解的概念大概是:如果一个函数定义在某个 so 里,并且它的符号对外可见、能被别的模块链接和调用,那它就是这个 so 的导出函数。了解这个概念后,我们就可以回答上面的问题:PLT Hook就是在Hook导入函数。当一个 so 调用另一个 so 里的导入函数时,编译器通常不会把调用点直接写成最终的真实地址。原因很简单:编译时还不知道这个函数在目标进程里最终会被映射到哪里。于是就有了两层非常重要的中间结构:一个很粗略但够用的理解是:一旦动态链接器完成重定位,GOT 里的某个槽位就会被写成对应导入函数的真实地址。此后,调用链就会顺着这个槽位跳到真正的目标函数里。所以,所谓 PLT Hook,从运行时视角看,本质上并不是去改 PLT 机器码,而是去改“PLT/GOT 这条调用链最终依赖的那个槽位里的函数指针”。所以PLT Hook的本质就是在修改重定位的结果。如果说 GOT 槽位是最终要改的目标,那么 relocation entry 就是“告诉你该改哪里”的索引。一个重定位条目里,最关键的通常是三个字段:对 PLT Hook 来说,最核心的问题其实就是:当地址被替换掉后,自然的就走到了我们的Hook逻辑的,这就是PLT Hook的核心。ELFIO 解析的是磁盘上的 ELF 文件,而真正的 Hook 动作发生在已经加载到进程内存中的 so 映像上。文件里的 relocation offset 只是“相对于 ELF 映像布局”的偏移,不是可以直接拿来写内存的真实地址。所以中间必须经过一步 runtime bias 换算。最常见的一种写法是:而 runtime_bias 的求法,通常要结合 PT_LOAD 段和运行时模块基址一起算出来:GOT/PLT 对应的内存页在运行时往往不是天然可写的,很多时候只有读权限,甚至还会带执行权限。想要在上面改指针,就得先把对应页临时改成可写:这两类 Hook 最大的区别不在“Hook 的函数都是 native 函数”,而在“改的是哪一层”。PLT Hook 改的是导入调用链路上的目标槽位,特点是:Inline Hook 改的是函数入口处的机器码,特点是:假设有一个目标模块 libnative-lib.so,它内部调用了 strcmp。编译和链接完成后,运行时这个调用大致会依赖某个 relocation 对应的 GOT 槽位。一开始,槽位里装的是原始的 strcmp 地址:如果我们把这个槽位改成自己的 hooked_strcmp:那么后续只要 libnative-lib.so 仍然通过这条导入链路调用 strcmp,执行流就会先进到 hooked_strcmp。而如果在改写前,我们先把槽位里原本保存的函数地址读出来存到 original,后续在 hooked_strcmp 里就还可以继续调用原始 strcmp。先看一下当前项目里和 PLT Hook 相关的目录划分:这几层各自负责的事情大概是:先把整条调用链串起来,再分别讲细节。一次 hook_symbol() 大致会经历下面这些步骤,这只是针对当前项目,一个简单的PLT Hook实际并不需要这么复杂:也就是说,对外看起来只是一个:但内部实际上完成了“模块定位 -> 文件解析 -> 重定位筛选 -> 地址换算 -> 内存页修改 -> 指针改写”这一整套动作,即:先看公开头文件:对外 API 非常薄,真正的核心在 NookPltHookSymbol() 里。它做的事情主要有三类:对应代码:可以看到,这一层本身完全不碰 ELF 头、不碰 relocation,也不碰 mprotect。它只负责把这次 Hook 需要的策略拼起来,然后把执行权交给内部调度器。在真正解析 ELF 之前,首先得回答一个问题:目标模块当前在进程里到底被加载到了哪里?这个问题其实在之前的文章中也多次提到,这里再简单讲一下。当前的做法很传统,也很直接,就是扫描 /proc/self/maps。get_module_info() 的核心逻辑可以概括成:代码逻辑大致如下:到了这一步,我们已经拿到了两份非常关键的信息:接下来 ELFIO 路径要解决的问题就比较纯粹了:只从磁盘上的 ELF 文件里,把“这个符号对应哪些 relocation”找出来。ElfioImageParser 负责的事情大致可以拆成三件:它会先拿到 .dynsym,然后逐项遍历:一旦拿到 symbol_index,后面的 relocation 过滤就有了抓手。CollectRelocationsForSymbol() 不会只盯 .plt 相关段,而是会遍历所有 SHT_REL/SHT_RELA section:然后只保留 relocation_symbol == symbol_index 的那些条目,并把 offset、type、section_name 等信息记录下来。这一点很重要:虽然我们习惯把这类方案叫 PLT Hook,但 Nook 当前的主路径实现并不只看 .plt,而是把引用该符号的 relocation 全部纳入候选。这使它既能覆盖典型 JUMP_SLOT 场景,也
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290921.htm
[原创]从0到1构建一个Hook工具之PLT Hook篇(四)
116 浏览
10 回复
学习。
感谢分享!
感谢分享
学习。
学习。
学习
看看
666学习一下
感谢分享
nice