Android Native Hook技术你知道多少?

做者:xiangzhihong,地址:http://suo.im/6xocWunode



Hook 直译过来就是“钩子”的意思,是指截获进程对某个 API 函数的调用,使得 API 的执行流程转向咱们实现的代码片断,从而实现咱们所须要得功能,这里的功能能够是监控、修复系统漏洞,也能够是劫持或者其余恶意行为。
程序员

相信许多新手第一次接触 Hook 时会以为这项技术十分神秘,只能被少数高手、黑客所掌握,那 Hook 是否是真的难以掌握?但愿今天的文章能够打消你的顾虑。web

Native Hook 的不一样流派

对于 Native Hook 技术,咱们比较熟悉的有 GOT/PLT Hook、Trap Hook 以及 Inline Hook,下面我来逐个讲解这些 Hook 技术的实现原理和优劣比较。编程

1. GOT/PLT Hook

在Chapter06-plus中,咱们使用了 PLT Hook 技术来获取线程建立的堆栈。先来回顾一下它的整个流程,咱们将 libart.so 中的外部函数 pthread_create 替换成本身的方法 pthread_create_hook。缓存


你能够发现,GOT/PLT Hook 主要是用于替换某个 SO 的外部调用,经过将外部函数调用跳转成咱们的目标函数。GOT/PLT Hook 能够说是一个很是经典的 Hook 方法,它很是稳定,能够达到部署到生产环境的标准。安全

那 GOT/PLT Hook 的实现原理到底是什么呢?你须要先对 SO 库文件的 ELF 文件格式和动态连接过程有所了解。微信

ELF 格式

ELF(Executableand Linking Format)是可执行和连接格式,它是一个开放标准,各类 UNIX 系统的可执行文件大多采用 ELF 格式。虽然 ELF 文件自己就支持三种不一样的类型(重定位、执行、共享),不一样的视图下格式稍微不一样,不过它有一个统一的结构,这个结构以下图所示。网络


网上介绍 ELF 格式的文章很是多,你能够参考《ELF 文件格式解析》。顾名思义,对于 GOT/PLT Hook 来讲,咱们主要关心“.plt”和“.got”这两个节区:架构

  • .plt。该节保存过程连接表(Procedure Linkage Table)。app

  • .got。该节保存着全局的偏移量表。

咱们也可使用readelf -S来查看 ELF 文件的具体信息。

连接过程

接下来咱们再来看看动态连接的过程,当须要使用一个 Native 库(.so 文件)的时候,咱们须要调用dlopen("libname.so")来加载这个库。

在咱们调用了dlopen("libname.so")以后,系统首先会检查缓存中已加载的 ELF 文件列表。若是未加载则执行加载过程,若是已加载则计数加一,忽略该调用。而后系统会用从 libname.so 的dynamic节区中读取其所依赖的库,按照相同的加载逻辑,把未在缓存中的库加入加载列表。

你可使用下面这个命令来查看一个库的依赖:

readelf -d <library> | grep NEEDED

下面咱们大概了解一下系统是如何加载的 ELF 文件的。

  • 读 ELF 的程序头部表,把全部 PT_LOAD 的节区 mmap 到内存中。

  • 从“.dynamic”中读取各信息项,计算并保存全部节区的虚拟地址,而后执行重定位操做。

  • 最后 ELF 加载成功,引用计数加一。

可是这里有一个关键点,在 ELF 文件格式中咱们只有函数的绝对地址。若是想在系统中运行,这里须要通过重定位。这实际上是一个比较复杂的问题,由于不一样机器的 CPU 架构、加载顺序不一样,致使咱们只能在运行时计算出这个值。不过还好动态加载器(/system/bin/linker)会帮助咱们解决这个问题。

若是你理解了动态连接的过程,咱们再回头来思考一下“.got”和“.plt”它们的具体含义。

  • The Global Offset Table (GOT)。简单来讲就是在数据段的地址表,假定咱们有一些代码段的指令引用一些地址变量,编译器会引用 GOT 表来替代直接引用绝对地址,由于绝对地址在编译期是没法知道的,只有重定位后才会获得 ,GOT 本身自己将会包含函数引用的绝对地址。

  • The Procedure Linkage Table (PLT)。PLT 不一样于 GOT,它位于代码段,动态库的每个外部函数都会在 PLT 中有一条记录,每一条 PLT 记录都是一小段可执行代码。通常来讲,外部代码都是在调用 PLT 表里的记录,而后 PLT 的相应记录会负责调用实际的函数。咱们通常把这种设定叫做“蹦床”(Trampoline)。

PLT 和 GOT 记录是一一对应的,而且 GOT 表第一次解析后会包含调用函数的实际地址。既然这样,那 PLT 的意义到底是什么呢?PLT 从某种意义上赋予咱们一种懒加载的能力。当动态库首次被加载时,全部的函数地址并无被解析。下面让咱们结合图来具体分析一下首次函数调用,请注意图中黑色箭头为跳转,紫色为指针。

  • 咱们在代码中调用 func,编译器会把这个转化为 func@plt,并在 PLT 表插入一条记录。

  • PLT 表中第一条(或者说第 0 条)PLT[0] 是一条特殊记录,它是用来帮助咱们解析地址的。一般在类 Linux 系统,这个的实现会位于动态加载器,就是专栏前面文章提到的 /system/bin/linker。

  • 其他的 PLT 记录都均包含如下信息:

-- 跳转 GOT 表的指令(jmp *GOT[n])。
-- 为上面提到的第 0 条解析地址函数准备参数。
-- 调用 PLT[0],这里 resovler 的实际地址是存储在 GOT[2] 。

在解析前 GOT[n] 会直接指向 jmp *GOT[n] 的下一条指令。在解析完成后,咱们就获得了 func 的实际地址,动态加载器会将这个地址填入 GOT[n],而后调用 func。

若是你对上面的这个调用流程还有疑问,你能够参考《GOT 表和 PLT 表》这篇文章,它里面有一张图很是清晰。

当第一次调用发生后,以后再调用函数 func 就高效简单不少。首先调用 PLT[n],而后执行 jmp *GOT[n]。GOT[n] 直接指向 func,这样就高效的完成了函数调用。

总结一下,由于不少函数可能在程序执行完时都不会被用到,好比错误处理函数或一些用户不多用到的功能模块等,那么一开始把全部函数都连接好实际就是一种浪费。为了提高动态连接的性能,咱们可使用 PLT 来实现延迟绑定的功能。

对于函数运行的实际地址,咱们依然须要经过 GOT 表获得,整个简化过程以下:

看到这里,相信你已经有了如何 Hack 这一过程的初步想法。这里业界一般会根据修改 PLT 记录或者 GOT 记录区分为 GOT Hook 和 PLT Hook,但其本质原理十分接近。

GOT/PLT Hook 实践

GOT/PLT Hook 看似简单,可是实现起来也是有一些坑的,须要考虑兼容性的状况。通常来讲,推荐使用以下业界的成熟方案。

微信 Matrix 开源库的ELF Hook,它使用的是 GOT Hook,主要使用它来作性能监控。
爱奇艺开源的的xHook,它使用的也是 GOT Hook。
Facebook 的PLT Hook。

若是不想深刻它内部的原理,咱们只须要直接使用这些开源的优秀方案就能够了。由于这种 Hook 方式很是成熟稳定,除了 Hook 线程的建立,咱们还有不少其余的使用范例。

  • “I/O 优化”中使用matrix-io-canary Hook 文件的操做。

  • “网络优化”中使用 Hook 了 Socket 的相关操做。

这种 Hook 方法也不是万能的,由于它只能替换导入函数的方式。有时候咱们不必定能够找到这样的外部调用函数。若是想 Hook 函数的内部调用,这个时候就须要用到咱们的 Trap Hook 或者 Inline Hook 了。

2. Trap Hook

对于函数内部的 Hook,你能够先从头想一下,会发现调试器就具有一切 Hook 框架具备的能力,能够在目标函数前断住程序,修改内存、程序段,继续执行。相信不少同窗都会使用调试器,可是对调试器如何工做却知之甚少。下面让咱们先了解一下软件调试器是如何工做的。

ptrace

通常软件调试器都是经过 ptrace 系统调用和 SIGTRAP 配合来进行断点调试,首先咱们来了解一下什么是 ptrace,它又是如何断住程序运行,而后修改相关执行步骤的。

所谓合格的底层程序员,对于未知知识的了解,第一步就是使用man命令来查看系统文档。

The ptrace() system call provides a means by which one process (the 
tracer”) may observe and control the execution of another process (the
tracee”), and examine and change the tracees memory and registers. It
is primarily used to implement breakpoint debugging and system call
tracing.

这段话直译过来就是,ptrace 提供了一种让一个程序(tracer)观察或者控制另外一个程序(tracee)执行流程,以及修改被控制程序内存和寄存器的方法,主要用于实现调试断点和系统调用跟踪。
咱们再来简单了解一下调试器(GDB/LLDB)是如何使用 ptrace 的。首先调试器会基于要调试进程是否已启动,来决定是使用 fork 或者 attach 到目标进程。当调试器与目标程序绑定后,目标程序的任何 signal(除 SIGKILL)都会被调试器作先拦截,调试器会有机会对相关信号进行处理,而后再把执行权限交由目标程序继续执行。能够你已经想到了,这其实已经达到了 Hook 的目的。

如何 Hook

但更进一步思考,若是咱们不须要修改内存或者作相似调试器同样复杂的交互,咱们彻底能够不依赖 ptrace,只须要接收相关 signal 便可。这时咱们就想到了句柄(signal handler)。对!咱们彻底能够主动 raise signal,而后使用 signal handler 来实现相似的 Hook 效果。

业界也有很多人将 Trap Hook 叫做断点 Hook,它的原理就是在须要 Hook 的地方想办法触发断点,并捕获异常。通常咱们会利用 SIGTRAP 或者 SIGKILL(非法指令异常)这两种信号。下面以 SIGTRAP 信号为例,具体的实现步骤以下。

  • 注册信号接收句柄(signal handler),不一样的体系结构可能会选取不一样的信号,咱们这里用 SIGTRAP。

  • 在咱们须要 Hook 得部分插入 Trap 指令。

  • 系统调用 Trap 指令,进入内核模式,调用咱们已经在开始注册好的信号接收句柄(signal handler)。

  • 执行咱们信号接收句柄(signal handler),这里须要注意,全部在信号接收句柄(signal handler)执行的代码须要保证async-signal-safe。这里咱们能够简单的只把信号接收句柄看成蹦床,使用 logjmp 跳出这个须要 async-signal-safe(正如我在“崩溃分析”所说的,部分函数在 signal 回调中使用并不安全)的环境,而后再执行咱们 Hook 的代码。

  • 在执行完 Hook 的函数后,咱们须要恢复现场。这里若是咱们想继续调用原来的函数 A,那直接回写函数 A 的原始指令并恢复寄存器状态。

Trap Hook 实践

Trap Hook 兼容性很是好,它也能够在生产环境中大规模使用。可是它最大的问题是效率比较低,不适合 Hook 很是频繁调用的函数。
对于 Trap Hook 的实践方案,可使用 Facebook 的Profilo,它就是经过按期发送 SIGPROF 信号来实现卡顿监控的。

3. Inline Hook

跟 Trap Hook 同样,Inline Hook 也是函数内部调用的 Hook。它直接将函数开始(Prologue)处的指令更替为跳转指令,使得原函数直接跳转到 Hook 的目标函数函数,并保留原函数的调用接口以完成后续再调用回来的目的。

与 GOT/PLT Hook 相比,Inline Hook 能够不受 GOT/PLT 表的限制,几乎能够 Hook 任何函数。不过其实现十分复杂,我至今没有见过能够用在生产环境的实现。而且在 ARM 体系结构下,没法对叶子函数和很短的函数进行 Hook。

在深刻“邪恶的”细节前,咱们须要先对 Inline Hook 的大致流程有一个简单的了解。

如图所示,Inline Hook 的基本思路就是在已有的代码段中插入跳转指令,把代码的执行流程转向咱们实现的 Hook 函数中,而后再进行指令修复,并跳转回原函数继续执行。这段描述看起来是否是十分简单并且清晰?

对于 Trap Hook,咱们只须要在目标地址前插入特殊指令,而且在执行结束后把原始指令写回去就能够了。可是对 Inline Hook 来讲,它是直接进行指令级的复写与修复。怎么理解呢?就至关于咱们在运行过程当中要去作 ASM 的字节码修改。

固然 Inline Hook 远远比 ASM 操做更加复杂,由于它还涉及不一样 CPU 架构带来的指令集适配问题,咱们须要根据不一样指令集来分别进行指令复写与跳转。下面我先来简单说明一下 Android 常见的 CPU 架构和指令集:

  • x86 和 MIPS 架构。这两个架构已经基本没有多少用户了,咱们能够直接忽视。通常来讲咱们只关心主流的 ARM 体系架构就能够了。

  • ARMv5 和 ARMv7 架构。它的指令集分为 4 字节对齐的定长的 ARM 指令集和 2 字节对齐的变长 Thumb/Thumb-2 指令集。Thumb-2 指令集虽为 2 字节对齐,但指令集自己有 16 位也有 32 位。其中 ARMv5 使用的是 16 位的 Thumb16,在 ARMv7 使用的是 32 位的 Thumb32。不过目前 ARMv5 也基本没有多少用户了,咱们也能够放弃 Thumb16 指令集的适配。

  • ARMv8 架构。64 位的 ARMv8 架构能够兼容运行 32 位,因此它在 ARM32 和 Thumb32 指令集的基础上,增长了 ARM64 指令集。关于它们具体差别,你能够查看ARM 的官方文档。

ARM64 目前我尚未适配,不过 Google Play 要求全部应用在 2019 年 8 月 1 日以前须要支持 64 位。但它们的原理基本相似,下面我以最主流的 ARMv7 架构为例,为你庖丁解牛 Inline Hook。

ARM32 指令集

ARMv7 中有一种广为流传的 PC+8的说法。这是指 ARMv7 中的三级流水线(取指、解码、执行),换句话说 PC寄存器的值老是比当前指令地址要大 8。


是否是感受有些复杂,其实这是为了引出 ARM 指令集的经常使用跳转方法:


LDR PC, [PC, #-4] ;0xE51FF004
$TRAMPOLIN_ADDR

在了解了三级流水线之后,就不会对这个 PC-4 有什么疑惑了。

按照咱们前面描述的 Inline Hook 的基本步骤,首先插入跳转指令,跳入咱们的蹦床(Trampoline),执行咱们实现的 Hook 后函数。这里还有一个“邪恶的”细节,因为指令执行是依赖当前运行环境的,即全部寄存器的值,而咱们插入新的指令是有可能更改寄存器的状态的,因此咱们要保存当前所有的寄存器状态到栈中,使用 BLX 指令跳转执行 Hook 后函数,执行完成后,再从栈中恢复全部的寄存器,最后才能像未 Hook 同样继续执行原先函数。


在执行完 Hook 后的函数后,咱们须要跳转回原先的函数继续执行。这里不要忘记咱们在一开始覆盖的 LDR 指令,咱们须要先执行被咱们复写的指令,而后再使用以下指令,继续执行原先函数。


LDR PC, [PC, #-4]
HOOKED_ADDR+8

是否是有一种大功告成的感受?其实这里还有一个巨大的坑在等着咱们,那就是指令修复。前面我提到保存并恢复了寄存器原有的状态,已达到能够继续像原有程序同样的继续执行。但仅仅是恢复寄存器就足够么?显然答案是否认的,虽然寄存器被咱们完美恢复了,可是 2 条备份的指令被移动到了新的地址。当执行它们的时候, PC的值,那么它们将会执行出彻底不一样的结果。

Inline Hook 实践

对于 Inline Hook,虽然它功能很是强大,并且执行效率也很高,可是业界目前尚未一套彻底稳定可靠的开源方案。Inline Hook 通常会使用在自动化测试或者线上疑难问题的定位,例如“UI 优化”中说到 libhwui.so 崩溃问题的定位,咱们就是利用 Inline Hook 去收集系统信息。

业界也有一些不错的参考方案:
Cydia Substrate。在Chapter3中,咱们就使用它来 Hook 系统的内存分配函数。
adbi。支付宝在GC 抑制中使用的 Hook 框架,不过已经好几年没有更新了。

各个流派优缺点总结

最后咱们再来总结一下不一样的 Hook 方式的优缺点:

  1. GOT/PLT Hook 是一个比较中庸的方案,有较好的性能,中等的实现难度,但其只能 Hook 动态库之间的调用的函数,而且没法Hook 未导出的私有函数,并且只存在安装与卸载 2 种状态,一旦安装就会 Hook 全部函数调用。

  2. Trap Hook 最为稳定,但因为须要切换运行模式(R0/R3),且依赖内核的信号机制,致使性能不好。

  3. Inline Hook 是一个很是激进的方案,有很好的性能,而且也没有 PLT 做用域的限制,能够说是一个很是灵活、完美的方案。但其实现难度极高,我至今也没有看到能够部署在生产环境的 Inline Hook 方案,由于涉及指令修复,须要编译器的各类优化。

可是须要注意,不管是哪种 Hook 都只能 Hook 到应用自身的进程,咱们没法替换系统或其余应用进程的函数执行。

总结

总的来讲 Native Hook 是一门很是底层的技术,它会涉及库文件编译、加载、连接等方方面面的知识,并且不少底层知识是与 Android 甚至移动平台无关的。

在这一领域,作安全的同窗可能会更有发言权,我来说可能班门弄斧了。不过但愿经过这篇文章,让你对看似黑科技的 Hook 有一个大致的了解,但愿能够在本身的平时的工做中使用 Hook 来完成一些看似不可能的任务,好比修复系统 Bug、线上监控 Native 内存分配等。

Native Hook 技术的确很是复杂,即便咱们不懂得它的内部原理,咱们也应该学会使用成熟的开源框架去实现一些功能。固然对于想进一步深刻研究的同窗,推荐你学习下面这些资料。

  • 连接程序和库

  • 指南程序员的自我修养:连接、装载与库

  • 连接器和加载器 Linkers and LoadersLinux

  • 二进制分析 Learning Linux Binary Analysis

若是你对调试器的研究也很是有兴趣,强烈推荐Eli Bendersky写的博客,里面有一系列很是优秀的底层知识文章。其中一些关于 debugger 的,感兴趣的同窗能够去阅读,并亲手实现一个简单的调试器。

  • how-debuggers-work-part-1

  • how-debuggers-work-part-2-breakpoints

  • how-debuggers-work-part-3-debugging-information

---END---



推荐阅读:
JVM史上最最最完整深刻解析!万字长文!
解决CoordinatorLayout的动画抖动以及回弹问题
Java14新特性速览!
2020 年编程语言盘点展望:Java 老兵不死,Kotlin 蓄势待发


每个“在看”,我都当成真的喜欢

本文分享自微信公众号 - 技术最TOP(Tech-Android)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索