FastHook——一种高效稳定、简洁易用的Android Hook框架

5,321 阅读8分钟

一、概述

在使用YAHFA框架的过程中,遇到了些问题,为了解决这些问题在YAHFA的基础上写了FastHook框架。本文分析内容基于Android 8.1。

项目地址:FastHookgithub.com/turing-tech…

二、YAHFA

2.1 YAHFA原理

首先我们来看看YAHFA框架基本流程,再分析其实现原理。

YAHFA原理.png

  1. Target方法EntryPoint替换为HookTrampoline。当执行Target方法时,实际执行HookTrampoline,从而实现方法Hook。
  2. HookTrampoline先将r0设置为Hook方法,再跳转到Hook方法EntryPoint执行Hook方法
  3. Hook方法参数与Target方法参数须一致,在Hook方法里调用Backup方法达到间接调用Target方法的目的。
  4. Backup方法必须是static方法(如果Target方法不是static方法,Backup方法第一个参数必须为this,Hook方法不需要一定是static方法,只要保证参数一致即可)。static方法是静态分派的,这可以保证调用的是Backup方法本身
  5. Backup方法必须要完全备份Target方法,ART需要知道native code与dex code的映射关系,例如一条native指令对应哪条dex指令,这个映射关系需要EntryPoint来计算,而为了实现Hook,我们替换了Target方法的EntryPoint。所以我们必须完全复制Backup方法,此时我们执行的还是Backup方法,只是这个Backup方法的内容跟Target完全一样,这样间接达到调用Target方法的目的

2.2 YAHFA缺陷

  1. 方法执行效率低。YAHFA通过禁止JIT和AOT编译来规避一些问题,但是同时也极大降低了方法执行效率。
  2. Backup方法不能被再次解析。由于Backup方法已备份为Target方法,因而再此被解析将引发NoSuchMethod异常。
  3. Moving GC引发的空指针异常。当Target方法的某些成员(例如Class)被移动时,由于Backup方法是备份得到的,因而不会更新到新地址,导致空指针异常。
  4. 方法内联导致Hook失效。Hook是通过替换方法EntryPoint实现的,因此当方法被内联时就不会用到EntryPoint,这是Hook将失效。

三、FastHook

FastHook提供了两种方案,一种类似Native Inline Hook,另一个依旧是Entrypoint替换

3.1 Inline模式

Inline模式.png
Inline模式由5部分组成:

  1. JumpTrampoline:Hook链表的头节点,一段跳转指令,覆盖原方法前几字节,将会跳转到HookTrampoline。
  2. HookTrampoline:判断是否需要Hook,如果是,设置r0为Hook方法并跳转到Hook方法,否则跳转到下一个HookTrampoline(多个相似的不同方法可能会共用相同的指令,因此多个方法Hook将形成一个链表结构)。
  3. OriginalTrampoline:Hook链表的尾节点,用于恢复原方法执行流。
  4. Hook方法:执行想要的逻辑,修改原方法参数、屏蔽原方法调用(Hook方法通过调用Forward方法来实现原方法调用)。
  5. Forward方法:一个静态的native方法。没有方法体、也不会被实际调用,如其名仅仅起到Forward的作用,方法EntryPoint将会被TargetTrampoline替换
  6. 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失败:

  1. JIT编译失败
  2. 编译后的指令长度小于JumpTrampoline的长度
  3. Native方法(没有实际方法体因此也不能Hook)

当Inline模式Hook失败将自动转换为EntryPoint模式。

3.2 EntryPoint替换模式

EntryPoint替换模式.png
EntryPoint模式由4个部分组成:

  1. HookTrampoline:设置r0为Hook方法并跳转到Hook方法。
  2. Hook方法:与Inline模式一致。
  3. Forward方法:与Inline模式一致。
  4. 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:

  1. dex文件加载
  2. 类初始化
  3. JIT编译
  4. JIT垃圾回收(类似Mark-Sweep,设置为QuickToInterpreterBridge)。
  5. 解释执行(如果存在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
};
  1. HookInfo类可以是任意类,但是必须存在一个名为HOOK_ITEMS的静态二维数组成员变量
  2. 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

参考

  1. YAHFA:github.com/rk700/YAHFA
  2. Enhanced_dlfunctions:github.com/turing-tech…

FastHook系列

  1. FastHook——巧妙利用动态代理实现非侵入式AOP
  2. FastHook——远超YAHFA的优异稳定性
  3. FastHook——如何使用FastHook免root hook微信
  4. FastHook——实现.dynsym段和.symtab段符号查询