不同的android hook姿势

PREFACE:被拷打一排甚至没听过,之前确实一点不懂这个,爬来快速过一下

# 一、GOT 表 HOOK

参考:PLT HOOK - 知乎 (zhihu.com)

仅能 hook got 表中引用的函数,替换某个 SO 的外部调用,通过将外部函数调用跳转成我们的目标函数。

当需要使用一个 Native 库(.so 文件)的时候,我们需要调用 dlopen (“libname.so”) 来加载这个库。在我们调用了 dlopen (“libname.so”) 之后,系统首先会检查缓存中已加载的 ELF 文件列表。如果未加载则执行加载过程,如果已加载则计数加一,忽略该调用。然后系统会用从 libname.so 的 dynamic 节区中读取其所依赖的库,按照相同的加载逻辑,把未在缓存中的库加入加载列表。

Untitled

  • Relocation Outputs(输出)
  1. .got.plt - 外部函数的绝对地址。
  2. .data,.data.rel.ro - 外部数据(包括函数指针)的绝对地址。
  • Relocation Tables(基本信息)
  1. .rel.plt,.rela.plt 用于 “关联”.dynsym 和.got.plt。
  2. .rel.dyn,.rela.dyn,.rel.dyn.aps2,.rela.dyn.aps2 用于 “关联”.dynsym 和.data,.data.rel.ro。
  3. .relr.dyn 是 Android 11 新增的,仅用于 ELF 的内部相对 relocation(基地址重写)
  • 符号信息(.dynsym 和 .dynstr)
  1. .dynstr 是 “字符串池”,保存了动态链接过程中用到的所有字符串信息,比如:函数名,全局变量名。
  2. .dynsym 中包含了与符号关联的各种 “索引” 信息,起到 “关联” 和 “描述(符号类型 func/ifunc/object 等等)” 的作用。
  3. .dynsym 中的符号分为 “导入符号” 和 “导出符号”。SHN_UNDEF == st_shndx 为导入符号,SHN_UNDEF != st_shndx 为导出符号。

加载 ELF 文件:

  1. 读 ELF 的程序头部表,把所有 PT_LOAD 的节区 mmap 到内存中。
  2. 从 “.dynamic” 中读取各信息项,计算并保存所有节区的虚拟地址,然后执行重定位操作。
  3. 最后 ELF 加载成功,引用计数加一。

重定位:

  • The Global Offset Table (GOT)。简单来说就是在数据段的地址表,假定我们有一些代码段的指令引用一些地址变量,编译器会引用 GOT 表来替代直接引用绝对地址,因为绝对地址在编译期是无法知道的,只有重定位后才会得到 ,GOT 自己本身将会包含函数引用的绝对地址。
  • The Procedure Linkage Table (PLT)。PLT 不同于 GOT,它位于代码段,动态库的每一个外部函数都会在 PLT 中有一条记录,每一条 PLT 记录都是一小段可执行代码。 一般来说,外部代码都是在调用 PLT 表里的记录,然后 PLT 的相应记录会负责调用实际的函数。我们一般把这种设定叫作 “蹦床”(Trampoline)。

PLT 和 GOT 记录是一一对应的,并且 GOT 表第一次解析后会包含调用函数的实际地址。既然这样,那 PLT 的意义究竟是什么呢?PLT 从某种意义上赋予我们一种懒加载的能力。当动态库首次被加载时,所有的函数地址并没有被解析。

![Untitled (1)](不同的 android-hook 姿势 / Untitled (1).png)

# 实现(待自行探究)

**(1)** 在内存中找到目标 ELF

方法 1:dl_iterate_phdr

优点:

  • Linux 的标准 dl API,NDK 提供了支持,使用方便。

缺点:

  • arm32 中,Android version >= 5.0(API level 21)时 NDK 才支持。
  • Android 5.0 和 5.1(API level 21 和 22),dl_iterate_phdr 的实现不持 linker 全局锁,需要自己找 linker 的符号(__dl__ZL10g_dl_mutex)自己加锁。
  • x86 平台 Android 4.x 的 dl_iterate_phdr () 也不持锁,而且 Android 4.x 的 linker 全局锁符号未导出。
  • Android < 8.1(API level 27)时,不能通过 dl_iterate_phdr 遍历到 linker /linker64。(aosp 从 8.0 开始已经包含了 linker/linker64,但是大量的其他厂商的设备是从 Android 8.1 开始包含 linker/linker64 的)
  • 部分 Android 4.x 和 Android 5.x 设备的 dl_iterate_phdr 只能返回 ELF 的 basename,而不是 pathname。

方法 2:dlopen (“libdl.so”) 返回 linker 内部的 struct soinfo list header,自己遍历

优点:

  • 能支持 Android 4.x。

缺点:

  • Android 4.x 的 linker 全局锁(gDlMutex)没有符号导出,直接遍历 struct soinfo list 容易挂。
  • 有一定的兼容性风险。
  • 部分 Android 4.x 和 Android 5.x 设备只能返回 ELF 的 basename,而不是 pathname。

方法 3:读 maps 自己解析 (/proc/self/maps)

优点:

  • 不用考虑 Android 4.x linker 全局锁的问题。

缺点:

  • 有一定的兼容性风险。

最佳实践

Android 4.x:

  • 解析 maps,使用 “权限 r-xp” + “offset == 0” 来过滤,再检查 ELF magic header。(4.x 上 ELF 结构还是比较保守的,目前没有发现 r-xp 判定失败的情况)
  • 也可以先使用 dlopen (“libdl.so”) 的方式,但是兼容性需要更多的测试,如果读取失败,需要回到读 maps 的方式来处理。

Android >=5.0:

  • 使用 dl_iterate_phdr。
  • 对于 5.0/5.1,自己用__dl__ZL10g_dl_mutex 加锁。
  • 发现 ELF 名称为 basename 时,读 maps,从 maps 中查找对应的 pathname。
  • 需要 linker/linker64 的话,< 8.1 时需要从 maps 中读取。
  1. hook 导入表,即 “调用方”。如果需要 hook 进程中对于某个函数的所有调用,这种方法是比较麻烦的,需要逐个 hook 内存中已经加载的所有 ELF,还需要监控 dlopen 和 android_dlopen_ext(以便感知到新加载的 ELF,再对它执行导入表 hook)。
  2. hook 导出表,即修改被调用方对应函数符号的 offset 值(.dynsym 中对应表项的 st_value),使 linker 通过修改后的新 st_value 来查找对应函数符号的内存绝对地址时,实际查找到的是内存中外部 ELF 的 hook 函数的地址,这样 linker 对新加载的 ELF 执行完 relocation 操作后,新 ELF 的相应调用就自然被 hook 到了我们指定的函数。
  • 查找导入表符号的方法

当存在 SYSV hash 时,先尝试通过 SYSV hash 来查找。先后按顺序尝试查找:

.hash -> .dynsym -> .dynstr,时间复杂度:O(x) + O(1) + O(1) .dynsym -> .dynstr,时间复杂度:O(n) + O(1)
  • 查找导出表符号的方法

优先使用 GNU hash,比 SYSV hash 更高效。先后按顺序尝试查找:

.gnu.hash -> .dynsym -> .dynstr,时间复杂度:O(x) + O(1) + O(1) .hash -> .dynsym -> .dynstr,时间复杂度:O(x) + O(1) + O(1)
  • 查找 .got.plt 中函数地址的方法
  1. 逐项遍历 .rel.plt 和.rela.plt 表,用上面已经找到的符号信息去比对,对应到即找到了地址的 offset(r_offset 项)
  2. 将 r_offset 加上 ELF 的 “内存加载基地址”(load_bias)即为所得。
  • 查找 .data 和 .data.rel.ro 中数据地址的方法
  1. 与上面查找 .got.plt 的过程相同,只是改为了遍历 .rel.dyn 和 .rela.dyn,以及 .rel.dyn.aps2 和 .rela.dyn.aps2。
  • 修改数据(hook)
  1. 修改内存权限:使用 mprotect 将目标地址所在内存页改为可读可写
  2. 修改数据:

** 方法 1:** 直接赋值,再使用 __builtin___clear_cache 清除目标地址的 CPU cache。

** 方法 2:** 使用 atomic 方式来赋值,比如:__atomic_store_n ((uintptr_t *) got_addr, (uintptr_t) new_func, __ATOMIC_SEQ_CST);

# 二、LDPRELOAD_HOOK

虚假的导入函数

# 三、inline hook

这个在 frida 源码里面看的比较多,这里简单看一下怎么检测

# 检测手段

CRC 校验:inline hook 会改函数写跳板,这样的话一个常见的思路就是对函数

# 四、异常 hook

# 五、依赖库篡改 hook

# 六、linker hook

# 七、UNICOR

https://github.com/acbocai/vergil

参考:PLT HOOK - 知乎 (zhihu.com)

[原创] 7 种 Android Native Anti Hook 的实现思路 - Android 安全 - 看雪 - 安全社区 | 安全招聘 | kanxue.com