前言
目标写出一个通用的android hook库,开发在AS下进行,方便运行和调试
先为了看懂:https://github.com/xiaobaiyey/dexload
钩子(hook)与注入(inject):
hook目的是修改执行逻辑,我直接理解为类似AOP的思想,不过这个更注重实现。
注入目的是非己内容加入自己代码,分为静态与动态,前者运行前修改可执行文件,后者通过ptrace等。
实现部分交集互用,有时不区分。一般hook目标需要注入为前提。
之前写过一篇注入的,但没有实现完整:https://xn--74q78i15hxv3arigm4e.cn/2018/05/26/android-hook/
猜测失败原因是Android 7.0之后对于非公开API的调用限制:
开发只要保证:连接不在apk中的库时只能连接公开的NDK,自己使用的so一定要放到apk中打包。
AOSP 7.0以后关于NDK限制机制的核心代码是在 /bionic/linker/linker.cpp,自己环境下可以修改源码来方便注入,其它环境下可以先注入处理掉检测部分,再注入自己的so。
Java Hook
Static Field Hook:静态成员hook
Method Hook:函数hook
Native So Hook
GOT Hook:全局偏移表hook
SYM Hook:符号表hook
Inline Hook:函数内联hook
本篇分析native的hook,下一篇java下针对art hook。
一个cpu是可以支持一系列指令的,比如我的n6p:
ro.product.cpu.abi=arm64-v8a
ro.product.cpu.abilist=arm64-v8a,armeabi-v7a,armeabi
ro.product.cpu.abilist32=armeabi-v7a,armeabi
ro.product.cpu.abilist64=arm64-v8a
ARM64有俩种执行状态:
AArch64 ——64 位执行状态,包括该状态的异常模型、内存模型、程序员模型和指令集支持
AArch32 ——32 位执行状态,包括该状态的异常模型、内存模型、程序员模型和指令集支持
这些执行状态支持三个主要指令集:
A32(或 ARM):32 位固定长度指令集,通过不同架构变体增强部分 32 位架构执行环境现在称为 AArch32。
T32 (Thumb) 是以 16 位固定长度指令集的形式引入的,随后在引入 Thumb-2 技术时增强为 16 位和 32 位混合长度指令集。部分 32 位架构执行环境现在称为 AArch32。
A64:提供与 ARM 和 Thumb 指令集类似功能的 32 位固定长度指令集。随 ARMv8-A 一起引入,它是一种 全新的AArch64 指令集。
hook实现分析
几个推荐的hook框架:
https://github.com/jmpews/HookZz/tree/master
https://github.com/iqiyi/xHook
GToad的实现
https://github.com/GToad/Android_Inline_Hook
https://github.com/GToad/Android_Inline_Hook_ARM64
他分别写了32与64的hook,都很新。
同时在他的博客https://gtoad.github.io/ 写了很多相关的文章。
接下来对其精要
PLT-Hook&Inline-Hook
PLT Hook通过修改GOT表,使得在调用该共享库的函数时跳转到的是用户自定义的Hook功能代码。
只能修改指定模块单向使用其它模块的地方。got是代码模块导入其它模块的重定位点。
用于:统一hook目标模块针对同一外部的所有调用,如运行库、系统调用等。
无法hook某次精确调用,无法hook模块内自定义的函数。
Inline Hook在代码段中插入跳转指令,从而把程序执行流程引向自定义代码。
基本流程
图1
几乎应对全部情况,可以针对库函数修改达到批量hook。
GOT-hook
原理实现:https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md
曾经的一篇实现:https://xn--74q78i15hxv3arigm4e.cn/2018/06/29/android%E5%BA%94%E7%94%A8%E5%8A%A0%E5%9B%BA2/
linker对so的连接处理就是将rel指定的内容用其他符号指定的值填写,符号表标识的名字有自己定义的和需要的外部的名字,rel指向了这些外部的名字。自己的代码执行时链接已经完成,此时其他模块使用此模块与此模块使用其他模块的got已经填写完毕,因此got hook,只能动每个模块中got表部分-使用其他模块的调用。
也可以直接用xhook,还挺好用的。
32位Inline-hook
64兼容能用,实际这个版本更通用。
32位示例在n6p测试成功,不过实际使用有一点小心,当lib中存在v8版本64位机会优先选择64位版本使用,此时加入32位的so是错误的。不能同时加载32bit和64bit的so库。实际上64位进程中加载32位so就是错误的。
apk安装时,解析其中lib的类型,确定使用的虚拟机。64位设备提供32与64位俩种虚拟机,根据apk的不同fork使用,优先使用64位的。因此注意使用时abi的完全一致,apk安装时决定版本也要与使用so一致。
使用:hook框架产出so文件,加载so后自动执行hook逻辑。用constructor属性init_array中调用(destructor属性卸载时用)调用顺序与声明顺序一致。
需要的工具:https://github.com/keystone-engine/keystone 多架构汇编程序框架。在这里用于看机器码。
arm32基本步骤
1.根据/proc/self/maps中获取so的基地址。
|
|
从上面一个so的三种状态看结构,分别是:进程中maps下的信息,elf中段的信息,ida中解析出的segmentation
可见,maps下描述的是进程或线程中连续虚拟内存的区域。据说由mmap创建—这需要看完linker才能了解具体的加载过程了。
elf中段是文件如何映射,即文件偏移到VA的转化。
ida中则直接解析出了VA的信息。
对应关系:
maps是实际基址开始的三种权限段VA地址,ida中的是假设从基址0开始的VA,elf则是给出了文件部分的大小与位置所放到VA的大小与位置。
实际的权限与文件要求的权限并不一致,依赖段的内容决定。因此通过权限判别并不可靠。
显而易见,文件so内偏移不是VA偏移,通过文件偏移在内存中找代码位置要依靠段的位置换算。这一过程ida完成了,通过ida分析窗口的地址可以直接看VA偏移,此偏移加上内存中文件基址就是实际的文件中内容在内存的地址。
2.基地址加VA偏移定位hook点,保存hook点内容。
3.构造stub代码,以提前编好的汇编代码为模板填充构建。跳转到自定义的函数,通过寄存器远跳
4.将保存的原地址内容构造成可执行备份模块,并修复指令,最后跳回hook点。继续填充stub的尾部,跳到原内容备份处,通过地址数据改PC远跳。
备份的指令执行不一定能还原原语句功能,此时需要修补指令:
取PC的值进行计算的指令,因为PC改变了。包括相对寻址
跳转到hook点部分的指令,因为跳到中间会造成不完整的hook,跳到hook开始处没事。
主要有:
BLX, BL, B, BX-绝对跳转没问题,相对时需要修正,计算实际要跳转的地址使用修改PC跳转,有L时将原地址下一条保存到LR
ADD-可能使用PC,寻找其他寄存器替代
ADR, LDR, MOV-可能使用PC间接寻址,计算绝对地址获取值,将值赋给指定寄存器。
这里需要复习下arm32指令的格式:
cond 00 X opcode S Rn Rd So
条件码 00 X 指令编码 是否影响CPSR 第一个操作数寄存器编码 目标寄存器编码 第二个操作数
5.修改hook点代码,指向stub
用LDR PC, [PC, #-4] ,想远跳必须绝对地址。B立即数只能相对跳。
由于三级流水,执行时PC是正在执行指令+8的位置,只有直接使用PC的指令要当心这个问题。
Thumb-2方案
图2
https://gtoad.github.io/2018/07/06/Android-Native-Hook-Practice/
暂时先放弃对Thumb的支持….因为我还用不到….
64位Inline-hook
测试并分析64位的
ARM64切换到ARM32处理器模式来处理ARM32和Thumb-2需要产生Ring1的异常才行,因此普通进程并不会混用指令。
ARM64指令集中PC,SP不再是通用寄存器X0-X31中的一员。在ARM32中,R13就是SP,R14就是LR,R15就是PC。在ARM64中,X29是栈帧寄存器,X30是LR,而PC,SP是独立的寄存器。这也导致了对它们的操控和读写有了更多限制。没有了PUSH/POP和LDM/STM指令。
图3
基本流程与32位一致。
注意一下寄存器的恢复时机。占用的寄存器应该在跳至的代码中恢复,这里他是做的对的。
|
|
我的hook方案与实现
64用hook,接口完善。
一次性hook,不用修复指令,完美复原。
不限次数hook,修复指令。
项目地址:https://github.com/imbaya2466/android-hook
含分析项目的注释代码及自己的实践hook。
待完成:
- 优化接口,改善数据结构 ok
- 修复bug-重复hook ok
- 删除hook功能 ok
- 一次性hook -失败
- 使hook函数提前结束(不使用BLR而使用BR,用户函数的ret将直接将hook函数返回)
无法一次性hook-arm64下。
因为不可修改pc,只能使用寄存器跳转,此时跳转回去必须使用一个寄存器但无代码可修复寄存器,因此失败。
一些坑:
- Cmake尝试失败,最后又用回了ndk-build
- 一定要在build.gradle中加abiFilters,否则会将arm代码视为别的abi编译产生错误。在application.mk中指定集合到studio没用。即含有汇编代码时需要指定平台编译!!
- AS build出apk里的so与AS调试用的so不一致,即使是debug编译出的。因此找偏移地址时要确定目标so不变。
- native方法不是System.loadLibrary时绑定,是调用native方法时才寻找符号表中符合的,也可以手动绑定。需要看art的native方法oat后的样子才能确定。
关于多线程安全
hook时暂停所有线程,并检查是否有线程位于hook点,有时就将其运行随机小时间,之后再次暂停,直到hook点没有执行线程,进行hook。hook后恢复所有线程。