更新
简易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.7599858
ABI
: 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