更新
简易Android ARM&ARM64 GOT Hook (二)
基本思路为:基于执行视图,解析内存中的ELF,查找导入符号并替换函数地址
概述
本文以Hook公共库libc.so的getpid函数为例,基于ELF的链接视图(Linking View),讨论Android ARM&ARM64架构的GOT/PLT Hook。
原理
程序加载后,在执行之前,需要先进行动态链接,并进行重定位。
调用外部函数时,需要先跳转到PLT(Procedure Link Table 程序链接表,位于代码段),再跳转到GOT(Global Offset Table 全局偏移表,位于数据段),执行目标函数。
延迟绑定(Lazy Binding):当外部函数被调用时,才进行地址解析和重定位
由于Android ARM架构不支持延迟绑定,在linker重定位后,GOT已被填充为内存地址 (可使用IDA动态调试验证)
因此,可以通过比对函数地址,修改指定模块的对应GOT表项,实现对外部导入函数的Hook
具体思路
编写so库,在so加载的构造函数(linker会主动调用)中完成以下操作:定位目标模块基址、基于链接视图解析ELF文件,得到GOT表地址及大小、遍历GOT表替换目标函数地址。
注入方式
使用LIEF修改ELF文件,导入该动态库。
编译环境
Android Studio 2020.3.1、Gradle 7.0.1、CMake 3.18.1、NDK 23.0.7599858ABI: armeabi-v7a,arm64-v8a
PS:其实也可以脱离Android Studio手动编译,见使用CMake交叉编译Android ARM程序
编码
使用Android Studio创建Native C++项目,然后增加两个Android Native Library模块,分别命名为victim和inject
待注入程序
victim.cpp
1 |
|
调用getpid获取进程id(IDA动态调试时使用getchar,方便查看内存)
CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.18.1) |
编译为可执行文件
注入动态库
1 | // 基于链接视图解析ELF |
注意:Android 7.0以上,dlopen只能加载公共库,加载非公共库需要绕过命名空间限制。
子函数
获取模块基址
1 | uintptr_t getModuleBase(const char *modulePath) { |
遍历/proc/self/maps文件内容,根据权限及模块路径找到基址。
获取GOT表地址及其大小
篇幅所限,此处仅给出思路(以ARM为例),完整代码见AndroidGotHook
根据模块路径打开ELF文件(本例为/data/local/tmp/victim-patch-arm),解析ELF文件结构(图片为010Editor ELF模板解析结果):
首先从elf header中得到section header table的起始偏移(e_shoff)、字符串表索引(e_shstrndx)、section header大小(e_shentsize)和总section header个数(e_shnum)
然后计算出字符串表section header的偏移地址(e_shoff + e_shstrndx * e_shentsize),从而得到字符串表的偏移值(s_offset)及大小(s_size)
再遍历section header table,查找sh_type为SHT_PROGBITS 且 section名(通过sh_name查询字符串表)为.got的section header,得到GOT表偏移值(s_offset)及大小(s_size)
最终将GOT表偏移值与模块基址相加,得到GOT表地址。
查找目标函数地址
1 | uintptr_t getSymAddrInGOT(uintptr_t GOTBase, int GOTSize, uintptr_t ori) { |
遍历GOT表,查找目标函数地址。
替换函数地址
1 | void replaceFunction(uintptr_t addr, uintptr_t replace, uintptr_t ori) { |
首先判断函数是否已被替换,然后与期望值相比较,如果一致,则进行以下操作:
将该地址权限设置为可读可写,然后替换函数地址,并清空指令缓存。
适配ARM64
添加宏,将Elf32_w替换为ELFW(w):
1 |
根据架构使用不同的路径:
1 |
使用LIEF添加依赖
编译完成后,打开victim/build/intermediates/cmake/debug/obj/CPU架构/,使用LIEF注入victim
1 | import lief,sys |
测试
打开inject/build/intermediates/cmake/debug/obj/CPU架构/,
使用adb将生成的libinject.so和victim-patch-arm发送到手机的/data/local/tmp/目录(so需要重命名)
1 | adb push libinject.so /data/local/tmp/libinject-arm.so |
设置可执行权限后,运行victim-patch-arm
1 | adb shell chmod +x /data/local/tmp/victim-patch-arm |
(ARM64同理)
运行结果

成功实现对getpid函数的GOT Hook。
日志
ARM

ARM64

存在的问题
- 未绕过
dlopen命名空间限制,在Android 7以上无法打开非公共库 - 未hook
dlopen,无法实时修改加载模块的GOT表 - 解析maps获取模块基址,兼容性可能存在一定问题
- 基于链接视图静态解析ELF,无法处理加壳的so
- 静态注入可执行文件,无法绕过完整性检测
- 未提供卸载函数,无法恢复GOT表
- 模块路径硬编码,通用性不足
- 未处理全局函数指针调用
- …
总结
通过本项目,学习了GOT Hook原理和ELF文件结构,并适配了ARM64架构,目的基本达到。虽然功能还不够完善,但短期内应该不会再改动了(俗话说得好:不要重复造轮子)。
实际应用可以考虑使用字节的bhook
参考
android中基于plt/got的hook实现原理
聊聊Linux动态链接中的PLT和GOT(2)——延迟重定位
constructor属性函数在动态库加载中的执行顺序
Android7.0以上命名空间详解(dlopen限制)
Android中GOT表HOOK手动实现
Android GOT Hook