[Android] Meet Android Native Hook(了解安卓原生钩子技术)

Posted by xiuyuantech on 2025-01-20

在目前的安卓APP开发、测试、APM中对于Native Hook的需求越来越大,越来越多的APP开始逐渐使用NDK来开发核心或者敏感代码逻辑。
安全的考虑:各大APP越来越注重安全性,NDK所编译出来的so库逆向难度明显高于java代码产生的dex文件。越是敏感的加密算法与数据就越是需要用NDK进行开发。
性能的追求:NDK对于一些高性能的功能需求是java层无法比拟的。
手游的兴起:Unity等引擎开发的手游中都有大量包含游戏逻辑的so库。
Android Native Hook工具目前有两大路线: 1、PLT Hook 2、Inline Hook

认识hook

“Hook”(钩子)是计算机编程中的一种技术,它允许开发者拦截、修改或扩展软件或系统的行为。通过使用钩子,开发者可以在特定事件发生时注入自定义代码,以便修改程序的行为或响应程序的特定事件。

钩子的使用场景大概有这些:

键盘和鼠标事件:拦截键盘和鼠标输入,用于实现自定义的快捷键、鼠标手势或输入法。
窗口消息:拦截和处理窗口消息,用于实现窗口管理、界面定制等功能。
函数调用:拦截特定函数的调用,用于实现调试、性能分析、代码注入等功能。
文件操作和网络请求:拦截文件操作和网络请求,用于实现文件监控、安全检测等功能。

钩子技术可以提供很大的灵活性和功能扩展性,但也需要谨慎使用,因为不正确的使用钩子可能会导致程序崩溃、安全漏洞或不稳定的行为。

GOT/PLT Hook

Android 是基于Linux的操作系统,因此在Android开发平台上,ELF是原生支持的可执行文件格式;ELF文件格式除了作为可执行文件,还可以作为共享库格式,也就是我们常见的so文件, 以及 object文件 (.o)、core dumps文件等。

GOT/PLT HOOK 是ELF 文件函数hook的一种实现机制,GOT/PLT Hook 主要用于实现替换某个SO的外部调用,它的优点是非常稳定,因此在生产环境通常使用这种实现方案。

GOT/PLT Hook的方案命名主要是因为该方案主要是通过修改 ELF 文件结构中的 GOT (The Global Offset Table) 和 PLT(The Procedure Linkage Table) 段的地址来实现将原始函数调用指向自定义的函数或者跳转到其他代码段,从而实现对函数行为的修改或拦截。

  • PLT(Procedure Linkage Table)在Android中的作用主要是实现函数的延迟绑定(lazy binding)。PLT存储了指向函数的指针,但这些指针并不是直接指向函数的实际实现,而是指向一个桩函数(如_dl_runtime_resolve)。当第一次调用一个动态链接的函数时,会执行这个桩函数,它负责将控制权转移给动态链接器,以解析函数地址并将其存储在.got.plt(或.got的一个特定部分)中。后续的调用将直接通过.got.plt中的地址访问函数,避免了再次调用桩函数的开销‌。

  • .got (Global Offset Table)
    got 位于数据段, 作用是用来记录代码中引用到的外部符号的地址映射。这里的符号包括,变量、函数等。

    这里存在一个问题,如果一个引用的函数是在共享库中,而共享库在加载的时候是没有固定地址的,所以在got表中无法直接保存该符号的地址,此时就需要引入 plt表

  • .plt (Procedure Linkage Table)
    PLT 位于代码段,动态库中的每一个外部函数都会在PLT 中有一条对应的记录,每一条PLT记录都是一小段可执行的代码。可以说,PLT是由代码片段组成的表,每个代码片段由会跳转到GOT表中的一个具体的函数调用

  • -fPIC说明
    使用 PIC 参数作用于编译阶段,是告诉编译生成与 位置无关(Position-Independent Code)的代码, 对于共享库来说,如果不加 -fPIC,则.so文件的代码段在被加载时,代码段引用的数据对象需要重新定位,重定位会修改代码段的内容,这就总爱称每个使用这个.so文件代码段的进程在内核中都需要生成这个.so文件代码段的副本,每个副本都不一样,具体取决于这个.so文件代码和数据段内存映射的位置。 当添加 PIC参数后,则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。 因此对于动态库来说,一般产生位置无关的代码。

PLT Hook步骤

  • 总体
    • 定位目标函数:确定要 hook 的目标函数,通过读取/proc/self/maps文件找到其在动态链接库中的地址。

    • 修改 PLT 表:读取动态库,解析 ELF 文件,找到符号。通过计算目标函数绝对地址(目标进程函数绝对地址 = 动态库基地址 + 函数地址)修改 PLT 表中目标函数对应的条目,将其指向自定义的函数或者其他代码段。

    • 处理原始函数调用:在自定义函数中可以执行一些额外的操作,然后再调用原始的目标函数,或者完全替换原始函数的行为。

    • 恢复原始调用:有时候需要在自定义函数中调用原始的目标函数,以保持程序的正常行为。

  • 具体
    • 获取动态库的基地。
    • 计算 so 文件中的 program header table 程序头地址。
    • 遍历程序头部表,获取动态段(.dynamic)地址。
    • 找到 GOT 表地址。
    • 修改内存属性为可写。
    • 遍历 got 表,修改要替换的函数。
    • 恢复内存属性为可读可执行。

Inline Hook

Inline Hook基本原理是允许开发者在不修改原函数代码的情况下在代码段中插入跳转指令,从而把程序执行流程引向用户需要的功能代码中去,以此达到Hook的效果。

  • 完全不受函数是否在PLT表中的限制,直接在目标so中的任意代码位置都可进行Hook。这个Hook精准度是汇编指令级的。这对于逆向分析人员和安全测试人员来说是个非常好的特性!
  • 可以介入任意函数的操作。由于汇编指令级的Hook精度,以及不受PLT表的限制,Inline Hook技术可以去函数执行中的任意代码行间进行Hook功能操作,从而读取或修改任意寄存器,使得函数的操作流程完全可以被控制。
  • 对Hook功能函数的限制较小。由于在第二步调用Hook功能函数前已经把所有之前的寄存器状态都进行保存了,因此此时的Hook功能函数几乎就是个独立的函数,它无需受限于原本目标函数的参数形式,完全都由自己说了算。并且执行完后也完全是一个正常的函数退出形式释放栈空间。
  • 对于PLT Hook的强制批量Hook的特性,Native Hook要灵活许多。当想要进行批量Hook一些系统API时也可以直接去找内存里对应的如libc.so这些库,对它们中的API进行Hook,这样的话,所有对这个API的调用也就都被批量Hook了。

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

  • 由于需要直接解析和修改 ELF 中的机器指令(汇编码),对于不同架构的处理器、处理器指令集、编译器优化选项、操作系统版本可能存在不同的兼容性和稳定性问题。
  • 发生问题后可能难以分析和定位,一些知名的 inline hook 方案是闭源的。
  • 实现起来相对复杂,难度也较大。
  • 未知的坑相对较多,这个可以自行google。

Inline Hook步骤

arm下最基本的hook流程图:
arm-hook

  • 查找目标函数的地址‌:在想要Hook的目标代码处备份下面的几条指令,然后插入跳转指令,把程序流程转移到一个stub段上去。
  • 修改目标函数的开头‌:在stub代码段上先把所有寄存器的状态保存好,并调用用户自定义的Hook功能函数,然后把所有寄存器的状态恢复并跳转到备份代码处。
  • 保存原始指令‌:在备份代码处把当初备份的那几条指令都执行一下,然后跳转到当初备份代码位置的下面接着执行程序。

总结

Name PLT Hook Inline Hook
精准度 函数级 汇编级
范围 出现在PLT表中的动态链接函数 目标so内全部可执行代码
灵活性 只能批量 单次或批量都可以
技术难度 涉及内存地址计算和修改等 涉及寄存器计算、手写汇编、指令修复等

PLT Hook和Inline Hook两种技术在原理和适用场景上的差别是相当大的。如果只对于系统调用有参数或者性能上的监控需求,那可以考虑采用PLT Hook技术路线。一般适合APP的开发。 而如果是希望应对各种各样APP自己独有的NDK函数或者代码段的话,目前只能选择Inline Hook。适合APP逆向人员,软件分析人员,CTF Android逆向解题等。其他参考三方库xhookbhook详细介绍。
建议如果 PLT hook 够用的话,就不必尝试 inline hook了。