一, 概述
在使用 YAHFA 框架的过程中, 遇到了些问题, 为了解决这些问题在 YAHFA 的基础上写了 FastHook 框架. 本文分析内容基于 Android 8.1.
项目地址: FastHook: https://github.com/turing-technician/FastHook
二, YAHFA
2.1 YAHFA 原理
首先我们来看看 YAHFA 框架基本流程, 再分析其实现原理.
Target 方法 EntryPoint 替换为 HookTrampoline. 当执行 Target 方法时, 实际执行 HookTrampoline, 从而实现方法 Hook.
HookTrampoline 先将 r0 设置为 Hook 方法, 再跳转到 Hook 方法 EntryPoint 执行 Hook 方法.
Hook 方法参数与 Target 方法参数须一致, 在 Hook 方法里调用 Backup 方法达到间接调用 Target 方法的目的.
Backup 方法必须是 static 方法(如果 Target 方法不是 static 方法, Backup 方法第一个参数必须为 this,Hook 方法不需要一定是 static 方法, 只要保证参数一致即可).static 方法是静态分派的, 这可以保证调用的是 Backup 方法本身.
Backup 方法必须要完全备份 Target 方法, ART 需要知道 native code 与 dex code 的映射关系, 例如一条 native 指令对应哪条 dex 指令, 这个映射关系需要 EntryPoint 来计算, 而为了实现 Hook, 我们替换了 Target 方法的 EntryPoint. 所以我们必须完全复制 Backup 方法, 此时我们执行的还是 Backup 方法, 只是这个 Backup 方法的内容跟 Target 完全一样, 这样间接达到调用 Target 方法的目的.
2.2 YAHFA 缺陷
方法执行效率低. YAHFA 通过禁止 JIT 和 AOT 编译来规避一些问题, 但是同时也极大降低了方法执行效率.
Backup 方法不能被再次解析. 由于 Backup 方法已备份为 Target 方法, 因而再此被解析将引发 NoSuchMethod 异常.
Moving GC 引发的空指针异常. 当 Target 方法的某些成员 (例如 Class) 被移动时, 由于 Backup 方法是备份得到的, 因而不会更新到新地址, 导致空指针异常.
方法内联导致 Hook 失效. Hook 是通过替换方法 EntryPoint 实现的, 因此当方法被内联时就不会用到 EntryPoint, 这是 Hook 将失效.
三, FastHook
FastHook 提供了两种方案, 一种类似 Native Inline Hook, 另一个依旧是 Entrypoint 替换.
3.1 Inline 模式
Inline 模式由 5 部分组成:
JumpTrampoline:Hook 链表的头节点, 一段跳转指令, 覆盖原方法前几字节, 将会跳转到 HookTrampoline.
HookTrampoline: 判断是否需要 Hook, 如果是, 设置 r0 为 Hook 方法并跳转到 Hook 方法, 否则跳转到下一个 HookTrampoline(多个相似的不同方法可能会共用相同的指令, 因此多个方法 Hook 将形成一个链表结构).
OriginalTrampoline:Hook 链表的尾节点, 用于恢复原方法执行流.
Hook 方法: 执行想要的逻辑, 修改原方法参数, 屏蔽原方法调用(Hook 方法通过调用 Forward 方法来实现原方法调用).
Forward 方法: 一个静态的 native 方法. 没有方法体, 也不会被实际调用, 如其名仅仅起到 Forward 的作用, 方法 EntryPoint 将会被 TargetTrampoline 替换.
TargetTrampoline: 用于执行原方法, 设置 r0 为原方法并恢复原方法执行流.
综上可知, 原方法没有任何修改, 而 Forward 方法仅仅修改了 EntryPoint, 从理论上解决了方法解析和 Moving GC 所带来的问题.
3.1.1 方法编译
Inline 模式要求方法必须有编译后的机器代码, 而 7.0 之后默认不会进行 AOT 编译, 因而必须找到一个能编译方法的方案. 幸运的是 Android 默认的 JIT 便提供了这样的方法:"jit_compile_method". 该方法由 libart-compile.so 导出, 可以利用 dlsym 获取 (7.0 之后限制了 dlsym, 改用 enhanced_dlsym 代替, 不仅支持. dynsym(动态符号表) 查询, 还支持. symtab(符号表)查询). 值得注意的是, JIT 编译会改变线程状态, 为了线程保持正确的状态, 编译完成后需要恢复线程状态.
3.1.2 指令对齐
对于 Thumb2 指令集, JumpTrampoline 是 8 字节 , 但 Thumb 有 16 位和 32 位两种模式, 也就是说 JumpTrampoline 覆盖掉的指令有可能是不完整的, 因此需要做指令判断, 复制完整的指令, 可能是 8 字节, 也可能是 10 字节.
3.1.3 PC 相关指令
覆盖的指令若包含 PC 相关指令, 需要进行指令恢复, 不然计算出来的地址将是错误的. FastHook 并不做实际修复, 仅判断覆盖的指令是否包含有 PC 相关指令, 如果包含就使用 EntryPoint 模式.
3.1.4 Hook 限制
下列几种情况下将 Hook 失败:
JIT 编译失败.
编译后的指令长度小于 JumpTrampoline 的长度.
Native 方法(没有实际方法体因此也不能 Hook).
当 Inline 模式 Hook 失败将自动转换为 EntryPoint 模式.
3.2 EntryPoint 替换模式
EntryPoint 模式由 4 个部分组成:
HookTrampoline: 设置 r0 为 Hook 方法并跳转到 Hook 方法.
Hook 方法: 与 Inline 模式一致.
Forward 方法: 与 Inline 模式一致.
TargetTrampoline: 用于执行原方法, 与 Inline 模式不同的是, 原方法将固定以解释模式执行.
综上可知, 虽然原方法 EntryPoint 被修改了, 但其将固定以解释模式执行, 虽然牺牲了性能, 但是也彻底解决了方法解析与 Moving GC 所带来的问题.
3.2.1 InterpreterToInterpreter
在 8.0 之后, 如果在 Debug 编译版本, 使用 EntrypPoint 替换模式会出现 Hook 失效的情况, 方法调用进入 InterpreterTointerpreter, 不会用到 EntryPoint, 这里采用 YAHFA 的方案, Target 方法设置 kAccNative 来规避, 只在 Debug 版本下修改, Release 版本不受影响, 不修改.
3.3 Hook 安全
无论 Inline 模式还是 EntryPoint 模式, 都要求 EntryPoint 不能改变. 下列几种情况会改变方法 EntryPoint:
dex 文件加载.
类初始化.
JIT 编译.
JIT 垃圾回收(类似 Mark-Sweep, 设置为 QuickToInterpreterBridge).
解释执行(如果存在 JIT 入口则设置为 JIT 入口 ).
当进行 Hook 时, 方法所在类一定是初始化了的. 所以只需要处理 JIT, 要准确的判断出当前方法的 JIT 状态. 如果其等待 JIT 编译或者正在 JIT 编译, 则需待其编译完成再 Hook, 其他情况可安全 Hook.
3.4 方法内联
无论 Inline 模式还是 EntryPoint 模式, 方法内联都会导致 Hook 失效, 因此需要想方法禁止方法内联. 先看看什么情况下会进行内联.
- // 代理方法不内联
- if (method->IsProxyMethod()) {
- return false;
- }
- // 递归超过限制不内联
- if (CountRecursiveCallsOf(method)> kMaximumNumberOfRecursiveCalls) {
- return false;
- }
- const DexFile::CodeItem* code_item = method->GetCodeItem();
- //native 方法不内联
- if (code_item == nullptr) {
- return false;
- }
- // 方法指令大小超过 nline_max_code_units 不内联
- size_t inline_max_code_units = compiler_driver_->GetCompilerOptions().GetInlineMaxCodeUnits();
- if (code_item->insns_size_in_code_units_> inline_max_code_units) {
- return false;
- }
- // 有异常捕获不内联
- if (code_item->tries_size_ != 0) {
- return false;
- }
- // 设置了 kAccCompileDontBother, 这里没有返回 false, 所以并不能阻止内联
- if (!method->IsCompilable()) {
- }
- //Verifiy 失败不内联
- if (!method->GetDeclaringClass()->IsVerified()) {
- uint16_t class_def_idx = method->GetDeclaringClass()->GetDexClassDefIndex();
- if (Runtime::Current()->UseJitCompilation() ||
- !compiler_driver_->IsMethodVerifiedWithoutFailures(
- method->GetDexMethodIndex(), class_def_idx, *method->GetDexFile())) {
- return false;
- }
- }
- // 静态方法或私有方法关联 < clinit > 不内联
- if (invoke_instruction->IsInvokeStaticOrDirect() &&
- invoke_instruction->AsInvokeStaticOrDirect()->IsStaticWithImplicitClinitCheck()) {
- return false;
- }
考虑到修改方法属性可能会其他未知的风险, 因此选择修改 inline_max_code_units.inline_max_code_units 是 CompilerOptions 的成员, CompilerOptions 是 jit_compile_handle 的成员, jit_compile_handle 是一个全局静态变量, 因此可以通过 dlsym 获取. 通过修改其为 0 来禁止 JIT 编译. 这种方式只能阻止 JIT 内联, 对 AOT 无效. AOT 编译的时候会新建立 Runtime 环境, 而我们只能修改当前 Runtime 环境. 对 OSR 也无能为力.
3.5 小结
简而言之, FastHook 方案就是: Hook 方法 Hook 原方法, 原方法 Hook Forward 方法, Hook 方法调用 Forward 方法来实现调用原方法.
四, 使用 FastHook
4.1 提供 HookInfo
- private static String[] mHookItem = {
- "mode",
- "targetClassName","targetMethodName","targetParamSig",
- "hookClassName","hookMethodName","hookParamSig",
- "forwardClassName","forwardMethodName","forwardParamSig"
- };
- public static String[][] HOOK_ITEMS = {
- mHookItem
- };
HookInfo 类可以是任意类, 但是必须存在一个名为 HOOK_ITEMS 的静态二维数组成员变量.
HookItem 的格式是固定的, 如上图所示, mode 有两个取值:"1":Inline 模式;"2":EntryPoint 替换模式, 特别注意, sig 要求的是参数签名而不是完整的方法签名.
4.2 Hook 接口
- /**
- *
- *@param hookInfoClassName HookInfo 类名
- *@param hookInfoClassLoader HookInfo 类所在的 ClassLoader, 如果为 null, 代表当前 ClassLoader
- *@param targetClassLoader Target 方法所在的 ClassLoader, 如果为 null, 代表当前 ClassLoader
- *@param hookClassLoader Hook 方法所在的 ClassLoader, 如果为 null, 代表当前 ClassLoader
- *@param forwardClassLoader Forward 方法所在的 ClassLoader, 如果为 null, 代表当前 ClassLoader
- *@param jitInline 是否内联, false, 禁止内联; true, 允许内联
- *
- */
- public static void doHook(String hookInfoClassName, ClassLoader hookInfoClassLoader, ClassLoader targetClassLoader, ClassLoader hookClassLoader, ClassLoader forwardClassLoader, boolean jitInline)
1. 插件式 Hook: 建议在 attachBaseContext 方法里调用.
- // 插件式 Hook, 需要提供插件的 ClassLoader
- FastHookManger.doHook("hookInfoClassName",pluginsClassloader,null,pluginsClassloader,pluginsClassloader,false);
2. 内置 Hook, 建议在 attachBaseContext 方法里调用.
- // 内置 Hook, 都位于当前 ClassLoader
- FastHookManger.doHook("hookInfoClassName",null,null,null,null,false);
3. Root Hook, 建议在 handleBindApplication 方法里合适的地方调用, 一般在加载 apk 后, 调用 attachBaseContext 前.
- //Root Hook, 需要体供插件的 ClassLoader 和 apk 的 ClassLoader
- FastHookManger.doHook("hookInfoClassName",pluginsClassloader,apkClassLoader,pluginsClassloader,pluginsClassloader,false);
4.3 支持的 Android 版本
5.0 ~ 9.0
4.4 支持的架构
Thumb2 Arm64
参考
- YAHFA: https://github.com/rk700/YAHFA
- Enhanced_dlfunctions:
来源: https://juejin.im/post/5c94d6205188252d9d66b996