Android ART Hook & 注入实现细节


author: Swift Gan
title: Android ART Hook
date: 2019/04/01

SandHook

Android Art Hook

Swift Gan


Agenda

  • 简介
  • ART Invoke 指令生成
  • 基本实现
  • 指令检查
  • Xposed 支持
  • inline 处理
  • Android Q
  • 架构图
  • 进程注入

简介

SandHook 是作用在 Android ART 虚拟机上的 Java 层 Hook 框架,作用于进程内是不需要 Root 的

https://github.com/ganyao114/SandHook


OS

  • 4.4(JNI 不支持 call 原方法)
  • 5.0 - 5.1
  • 6.0
  • 7.0 - 7.1
  • 8.0 - 8.1
  • 9.0
  • 10.0

ARCH

  • ARM32(基本见不到)
  • THUMb32
  • AARCH64

方法范围

  • Object Methods
  • Static Methods
  • Constructors
  • System Methods
  • JNI Methods
  • 不支持 abstract 方法

如何使用

implementation 'com.swift.sandhook:hooklib:3.4.0'
// 不使用 Xposed API 则不需要引入
implementation 'com.swift.sandhook:xposedcompat:3.4.0'
  • Annotation API
  • Xposed API

Annotation API

@HookClass(Activity.class)
//@HookReflectClass("android.app.Activity")
public class ActivityHooker {

    @HookMethodBackup("onCreate")
    @MethodParams(Bundle.class)
    //@SkipParamCheck //忽略参数匹配,如果 Hooker 里面没有同名 Hook 函数
    static Method onCreateBackup;

    @HookMethodBackup("onPause")
    static HookWrapper.HookEntity onPauseBackup;

    @HookMethod("onCreate")
    public static void onCreate(@ThisObject Activity thiz,
                                @Param("android.os.Bundle") Object bundle) throws Throwable {
        Log.e("ActivityHooker", "hooked onCreate success " + thiz);
        SandHook.callOriginByBackup(onCreateBackup, thiz, bundle);
    }

    @HookMethod("onPause")
    public static void onPause(@ThisObject Activity thiz) throws Throwable {
        Log.e("ActivityHooker", "hooked onPause success " + thiz);
        onPauseBackup.callOrigin(thiz);
    }

}

Xposed API

//setup for xposed
XposedCompat.cacheDir = getCacheDir();
XposedCompat.context = this;
XposedCompat.classLoader = getClassLoader();
XposedCompat.isFirstApplication= true;
//do hook
XposedHelpers.findAndHookMethod(Activity.class, "onResume", new XC_MethodHook() {
      @Override
      protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
          super.beforeHookedMethod(param);
          Log.e("XposedCompat", "beforeHookedMethod: " + param.method.getName());
      }

      @Override
      protected void afterHookedMethod(MethodHookParam param) throws Throwable {
          super.afterHookedMethod(param);
          Log.e("XposedCompat", "afterHookedMethod: " + param.method.getName());
      }
});

Edxposed

非官方 Xposed 框架,支持 8.0 - 9.0

https://github.com/ElderDrivers/EdXposed/tree/sandhook


with VA

免 Root Xposed 环境

https://github.com/ganyao114/SandVXposed


ART Invoke 代码生成

  • 前言
  • ArtMethod
  • Quick & Optimizing
  • Optimizing
  • Sharpening
  • 8.0 之前
  • 8.0 之后
  • 结论

前言

在正式聊 Hook 方案之前,我们需要先了解一下 ART 对 invoke 字节码的实现,因为这会决定 Hook 的部分实现。
这里的实现理论上分为:解释器实现和编译实现(JIT/AOT)
实际上解释器的实现比较稳定和单一,我们仅仅需要关注编译器实现即可。


ArtMethod

Mirror

在了解 ArtMethod 之前先了解一下这个概念:

  • Java 对象在内存中的布局可以看成一个结构体,父类的变量在开头,本身的变量紧随其后。
  • 这些对象结构体在 ART 中被映射成 mirror::Object cpp 类。
  • 有一些虚拟机比较在意的类型,例如 Class,Method,Field 这些 Art 内部所需要的类型,他们在 mirror 中是有对应的类型的
  • 成员变量的内存布局也是对应映射的

ArtMethod

  • java 层中有类 ArtMethod,Method 与之一对一, Method 中 含有 ArtMethod 的引用,而 mirror::ArtMethod 就是 java 层 ArtMethod 的映射。
  • 6.0 之后,java ArtMethod 不复存在,被完全隐藏。

// C++ mirror of java.lang.reflect.Method.
class MANAGED Method : public Executable {
  ....
}
// C++ mirror of java.lang.reflect.Executable.
class MANAGED Executable : public AccessibleObject {
  uint16_t has_real_parameter_data_;
  HeapReference<mirror::Class> declaring_class_;
  HeapReference<mirror::Class> declaring_class_of_overridden_method_;
  HeapReference<mirror::Array> parameters_;
  // ArtMethod 地址
  uint64_t art_method_;
  uint32_t access_flags_;
  uint32_t dex_method_index_;
}

GC

  • ArtMethod 以及类似的 ArtField 在 Linear 堆区,是不会 Moving GC 的。
  • 原因很简单,ArtMethod/ArtField 是有可能 JIT/AOT 在 native code 中的,如果随时变化则不好同步。

jmethodId -> ArtMethod

ALWAYS_INLINE
static inline ArtField* DecodeArtField(jfieldID fid) {
  return reinterpret_cast<ArtField*>(fid);
}

ALWAYS_INLINE
static inline jfieldID EncodeArtField(ArtField* field) {
  return reinterpret_cast<jfieldID>(field);
}

ALWAYS_INLINE
static inline jmethodID EncodeArtMethod(ArtMethod* art_method) {
  return reinterpret_cast<jmethodID>(art_method);
}

ALWAYS_INLINE
static inline ArtMethod* DecodeArtMethod(jmethodID method_id) {
  return reinterpret_cast<ArtMethod*>(method_id);
}

可以看到只是简单的 cast,jmethodId 是 ArtMethod 的透明引用。


ArtMethod 结构

  // method 所属类,是 GCRoot,Class 类是可以 Moving GC 的
  // 这点需要特别关注,影响实现
  GcRoot<mirror::Class> declaring_class_;
  // java 层的 Modifier 只有其高 16 位
  // 低 16 位用作 ART 的内部运行,在 java 层被隐藏了
  std::atomic<std::uint32_t> access_flags_;
  // 方法的 CodeItem 在 Dex 中的偏移
  uint32_t dex_code_item_offset_;
  // 方法在 Dex 中的 index
  uint32_t dex_method_index_;
  // 虚方法则为实现方法在 VTable 中的 index
  // 非虚方法则是方法在 DexCodeCache 中的 index
  uint16_t method_index_;
  // 方法的热度,JIT 的重要参考
  uint16_t hotness_count_;

  struct PtrSizedFields {
    // 公共存储区域,用不到
    void* data_;
    // 非常重要!
    // 方法的 Code 入口
    // 如果没有编译,则
    // art_quick_to_interpreter_bridge
    // art_quick_generic_jni_trampoline
    // 如果 JIT/AOT 则为编译后的 native 代码入口
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;


方法入口

在 class link 时初步确定

//获取该 OAT 方法 Code 的入口地址,表示该方法已编译成机器码
  // Install entry point from interpreter.
  const void* quick_code = method->GetEntryPointFromQuickCompiledCode();
  //获取该 Dex 方法 Code 的入口地址,表示该方法尚未编译,需要解释执行
  bool enter_interpreter = class_linker->ShouldUseInterpreterEntrypoint(method, quick_code);

  if (!method->IsInvokable()) {
    EnsureThrowsInvocationError(class_linker, method);
    return;
  }

  //如果是静态方法,并且不是构造函数,则把代码入口设置成一个桩函数的地址
  //这个函数是通用的,应为所有 static 方法都要在类初始化时候去 resolve。
  //那么先把这个方法设置成一个通用的跳板,当有其他方法调用到的时候,跳板方法将出发该类的初始化
  //在该类初始化的时候,这些跳板方法才会被替换成真正的地址 ClassLinker::InitializeClass -> ClassLinker::FixupStaticTrampolines
  if (method->IsStatic() && !method->IsConstructor()) {
    // For static methods excluding the class initializer, install the trampoline.
    // It will be replaced by the proper entry point by ClassLinker::FixupStaticTrampolines
    // after initializing class (see ClassLinker::InitializeClass method).
    method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
  }
  //如果是 JNI 方法,设置成通用的 JNI 函数跳板
  else if (quick_code == nullptr && method->IsNative()) {
    method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
  }
  //如果方法需要解释执行,则设置成解释执行的跳板
  else if (enter_interpreter) {
    // Set entry point from compiled code if there's no code or in interpreter only mode.
    method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
  }

Quick & Optimizing

ART 中的 Compiler 有两种 Backend:

  • Quick
  • Optimizing

Quick 在 4.4 就引入,直到 6.0 一直作为默认 Compiler, 直到 7.0 被移除。

Optimizing 5.0 引入,7.0 - 9.0 作为唯一 Compiler。

还有个叫 portable,基本没用过。。。

下面以 Optimizing Compiler 为例分析 ART 方法调用的生成。


Optimizing

Optimizing比Quick生成速度慢,但是会附带各种优化:

  • 逃逸分析:如果不能逃逸,则直接栈上分配
  • 常量折叠
  • 死代码块移除
  • 方法内联
  • 指令精简
  • 指令重排序
  • load/store 精简
  • Intrinsic 函数替换 。。。

其中包括 Invoke 代码生成:


Sharpening

invoke-static/invoke-direct 代码生成默认使用 Sharpening 优化


Sharpening 做了两件事情

  • 确定加载 ArtMethod 的方式和位置
  • 确定直接 blr 入口调用方法还是查询 ArtMethod -> CodeEntry 调用方法

结果保存在两个 enum 中

  • MethodLoadKind 就是 ArtMethod 加载类型
  • CodePtrLocation 就是跳转地址的类型

我们重点关注 CodePtrLocation,但是 CodePtrLocation 在 8.0 有重大变化。


8.0 之前

 // Determines the location of the code pointer.
  enum class CodePtrLocation {
    // 顾名思义,递归调用自己,此时不需要重新加载 ArtMethod
    // 直接跳转到方法开头
    kCallSelf,
    // 直接 B 到偏移地址,多见于调用附近的方法
    kCallPCRelative,
    // 可以直接知道编译完成的入口代码
    // 则可以跳过 ArtMethod->CodeEntry 查询,直接 blx entry
    // 多见于调用系统方法,这些方法中都是绝对地址,不需要重定向
    kCallDirect,
    // link OAT 文件的时候,才能确定方法在内存中的位置
    // 方法入口需要 linker 重定向,也不需要查询 ArtMethod
    kCallDirectWithFixup,
    // 此种需要在 Runtime 期间得知方法入口
    // 需要查询 ArtMethod->CodeEntry
    // 那么由此可见只有在此种情况下,入口替换的 Hook 才有可能生效
    kCallArtMethod,
  };

代码生成

void CodeGeneratorARM64::GenerateStaticOrDirectCall(HInvokeStaticOrDirect* invoke, Location temp) {

//处理 ArtMethod 加载
...........

//生成跳转代码
switch (invoke->GetCodePtrLocation()) {
    case HInvokeStaticOrDirect::CodePtrLocation::kCallSelf:
      __ Bl(&frame_entry_label_);
      break;
    case HInvokeStaticOrDirect::CodePtrLocation::kCallPCRelative: {
      relative_call_patches_.emplace_back(invoke->GetTargetMethod());
      vixl::Label* label = &relative_call_patches_.back().label;
      vixl::SingleEmissionCheckScope guard(GetVIXLAssembler());
      __ Bind(label);
      __ bl(0);  // Branch and link to itself. This will be overriden at link time.
      break;
    }
    case HInvokeStaticOrDirect::CodePtrLocation::kCallDirectWithFixup:
    case HInvokeStaticOrDirect::CodePtrLocation::kCallDirect:
      // LR prepared above for better instruction scheduling.
      DCHECK(direct_code_loaded);
      // lr()
      __ Blr(lr);
      break;
    case HInvokeStaticOrDirect::CodePtrLocation::kCallArtMethod:
      // LR = callee_method->entry_point_from_quick_compiled_code_;
      __ Ldr(lr, MemOperand(
          XRegisterFrom(callee_method),
       ArtMethod::EntryPointFromQuickCompiledCodeOffset(kArm64WordSize).Int32Value()));
      // lr()
      __ Blr(lr);
      break;
  }
}

汇编

  • call self
_functionxxx:
...
...
bl _functionxxx
  • call direct
blr lr
  • call art method
ldr lr [RegMethod(X0), #CodeEntryOffset]
blr lr

8.0 之后

或许考虑到真正优化的地方在于如何更快的加载 ArtMethod 结构体,所以 8.0 之后编译后的代码都不会再省略:

ldr lr [RegMethod(X0), #CodeEntryOffset]

这一步。


CodePtrLocation

// Determines the location of the code pointer.
  enum class CodePtrLocation {
    kCallSelf,
    kCallArtMethod,
  };

代码生成

switch (invoke->GetCodePtrLocation()) {
    case HInvokeStaticOrDirect::CodePtrLocation::kCallSelf:
      {
        // Use a scope to help guarantee that `RecordPcInfo()` records the correct pc.
        ExactAssemblyScope eas(GetVIXLAssembler(),
                               kInstructionSize,
                               CodeBufferCheckScope::kExactSize);
        __ bl(&frame_entry_label_);
        RecordPcInfo(invoke, invoke->GetDexPc(), slow_path);
      }
      break;
    case HInvokeStaticOrDirect::CodePtrLocation::kCallArtMethod:
      // LR = callee_method->entry_point_from_quick_compiled_code_;
      __ Ldr(lr, MemOperand(
          XRegisterFrom(callee_method),
          ArtMethod::EntryPointFromQuickCompiledCodeOffset(kArm64PointerSize).Int32Value()));
      {
        // Use a scope to help guarantee that `RecordPcInfo()` records the correct pc.
        ExactAssemblyScope eas(GetVIXLAssembler(),
                               kInstructionSize,
                               CodeBufferCheckScope::kExactSize);
        // lr()
        __ blr(lr);
        RecordPcInfo(invoke, invoke->GetDexPc(), slow_path);
      }
      break;
  }

invoke-virtual/interface

调用虚方法并不会使用虚方法的 ArtMethod,因为虚方法本身不含 CodeItem,无法执行。那么调用虚方法则需要从 receiver 的类中的 VTable(虚方法表) 中加载真正的实现方法并且调用。

{
    // Ensure that between load and MaybeRecordImplicitNullCheck there are no pools emitted.
    EmissionCheckScope guard(GetVIXLAssembler(), kMaxMacroInstructionSizeInBytes);
    // /* HeapReference */ temp = receiver->klass_
    __ Ldr(temp.W(), HeapOperandFrom(LocationFrom(receiver), class_offset));
    MaybeRecordImplicitNullCheck(invoke);
  }
  // Instead of simply (possibly) unpoisoning `temp` here, we should
  // emit a read barrier for the previous class reference load.
  // intermediate/temporary reference and because the current
  // concurrent copying collector keeps the from-space memory
  // intact/accessible until the end of the marking phase (the
  // concurrent copying collector may not in the future).
  GetAssembler()->MaybeUnpoisonHeapReference(temp.W());
  // temp = temp->GetMethodAt(method_offset);
  __ Ldr(temp, MemOperand(temp, method_offset));
  // lr = temp->GetEntryPoint();
  __ Ldr(lr, MemOperand(temp, entry_point.SizeValue()));
  {
    // Use a scope to help guarantee that `RecordPcInfo()` records the correct pc.
    ExactAssemblyScope eas(GetVIXLAssembler(), kInstructionSize, CodeBufferCheckScope::kExactSize);
    // lr();
    __ blr(lr);
    RecordPcInfo(invoke, invoke->GetDexPc(), slow_path);
  }

伪代码

  • Class clazz = receiver.getClass()
  • Method method = class.getMethodAt(Index);
  • ldr lr method->CodeEntry
  • blr lr

为何不 Hook Abstract

修改 VTable 是否可行?


SingleImplementation

// Set by the class linker for a method that has only one implementation for a
// virtual call.
static constexpr uint32_t kAccSingleImplementation =  0x08000000;  // method (runtime)
devirtualization
ArtMethod* single_impl = interface_method->GetSingleImplementation(pointer_size);
if (single_impl == nullptr) {
   // implementation_method becomes the first implementation for
   // interface_method.
   interface_method->SetSingleImplementation(implementation_method, pointer_size);
   // Keep interface_method's single-implementation status.
   return;
}

ArtMethod {
    // Depending on the method type, the data is
    //   - native method: pointer to the JNI function registered to this method
    //                    or a function to resolve the JNI function,
    //   - conflict method: ImtConflictTable,
    //   - abstract/interface method: the single-implementation if any,
    //   - proxy method: the original interface method or constructor,
    //   - other methods: the profiling data.
    void* data_;
}


优化步骤

CHA 优化属于内联优化

  // Try CHA-based devirtualization to change virtual method calls into
  // direct calls.
  // Returns the actual method that resolved_method can be devirtualized to.
  ArtMethod* TryCHADevirtualization(ArtMethod* resolved_method)
    REQUIRES_SHARED(Locks::mutator_lock_);

如果 ART 发现是单实现,则将指令修改为 direct calls


  private void test() {
    IDog dog = new DogImpl();
    dog.dosth();
  }

InvokeRuntime

一些特殊方法,主要服务于需要在 Runtime 时期才能确定的 Invoke,例如类初始化 函数。(kQuickInitializeType)
InvokeRuntime 会从当前 Thread 中查找 CodeEntry:


代码生成

void CodeGeneratorARM64::InvokeRuntime(int32_t entry_point_offset,
                                       HInstruction* instruction,
                                       uint32_t dex_pc,
                                       SlowPathCode* slow_path) {
  ValidateInvokeRuntime(instruction, slow_path);
  BlockPoolsScope block_pools(GetVIXLAssembler());
  __ Ldr(lr, MemOperand(tr, entry_point_offset));
  __ Blr(lr);
  RecordPcInfo(instruction, dex_pc, slow_path);
}

汇编代码

tr 就是线程寄存器,一般 ARM64 是 X19

所以代码出来一般长这样:

loc_3e6828:
mov        x0, x19
ldr        x20, [x0, #0x310]
blr        x20

Intrinsics 优化

ART 额外维护了一批系统函数的高效实现,这些高效实现利用了CPU的指令,直接跳过了方法调用。

  // System.arraycopy.
    case kIntrinsicSystemArrayCopyCharArray:
      return Intrinsics::kSystemArrayCopyChar;

    case kIntrinsicSystemArrayCopy:
      return Intrinsics::kSystemArrayCopy;

    // Thread.currentThread.
    case kIntrinsicCurrentThread:
      return Intrinsics::kThreadCurrentThread;


Thread.currentThread()

void IntrinsicCodeGeneratorARM64::VisitThreadCurrentThread(HInvoke* invoke) {
  codegen_->Load(Primitive::kPrimNot, WRegisterFrom(invoke->GetLocations()->Out()),
                 MemOperand(tr, Thread::PeerOffset<8>().Int32Value()));
}

最后出来的代码类似这样,直接就把 Thread.nativePeer ldr 给目标寄存器,根本不是方法调用了:

ldr x5, [x19, #PeerOffset]

结论

当 8.0 以上时,我们使用 ArtMethod 入口替换即可基本满足 Hook 需求。但如果 8.0 以下,如果不开启 debug 或者 deoptimize 的话,则必须使用 inline hook,否则会漏掉很多调用。


基本实现

  • 确定 ArtMethod 内存布局
  • Hooker 项解析
  • resolve 静态方法
  • resolve cache dex
  • 禁用某方法JIT & 手动 JIT 编译
  • Hook 线程安全
  • 原方法备份
  • 选择寄存器
  • 开始 hook

ArtMethod 内存布局

由于版本众多,以及 Android 平台的碎片化,Method 的内存布局往往是千变万化的。简单的根据版本写死 Offset 风险还是比较高的。


ArtMethod 的大小

首先最重要的的一点是确定 ArtMethod 的大小,前面我们知道,ArtMethod 被存放在线性内存区域,并且不会 Moving GC,那么,相邻的两个方法他们的 ArtMethod 也是相邻的,所以 size = ArtMethod2 - ArtMethod1


内部元素偏移

  • 我们可以在 Java 层反射得到一些值,或者说我们可以根据指定方法的属性确定预测值(accessFlag),然后我们根据预测值在 ArtMethod 中搜索偏移
  • 根据元素在 ArtMethod 中的相对位置确定(code_entry 在最后)

Hooker 项解析

  • 首先 Hook 项承载了目标方法的信息,我们根据这些信息找到目标方法。
  • 因为被 Hook 的方法会直接调到我们的 Hook 入口,Hook 入口本身也是一个 java 方法,所以参数需要和原方法匹配。

resolve 静态方法

  • 静态方法是懒加载的。
  • 如果一个类没有被初始化,那么其中的静态方法的入口统一为 art_quick_proxy_invoke_handler
  • 第一次调用时,art_quick_proxy_invoke_handler 会走到类初始化流程

resolve 静态方法

那么很简单,只要手动调用就行了,但是要注意保证调用失败,这里使用不匹配的参数。

    public static void resolveStaticMethod(Member method) {
        //ignore result, just call to trigger resolve
        if (method == null)
            return;
        try {
            if (method instanceof Method && Modifier.isStatic(method.getModifiers())) {
                ((Method) method).setAccessible(true);
                ((Method) method).invoke(new Object(), getFakeArgs((Method) method));
            }
        } catch (Throwable throwable) {
        }
    }

    private static Object[] getFakeArgs(Method method) {
        Class[] pars = method.getParameterTypes();
        if (pars == null || pars.length == 0) {
            return new Object[]{new Object()};
        } else {
            return null;
        }
    }

resolve dex cache

为了节省资源并且加快调用速度,和 ELF 的 got.plt 表类似,Caller 去搜索 Callee 的位置时,Callee 带着 index 去 DexCache 中找到对应位置的 Callee 的 ArtMethod 结构体。

但是,DexCache 是懒加载的,我们从 Hook 入口方法调用原方法这一行为 ART 是不知道的,所以无法自动完成这一动作,这里就需要我们手动完成这一操作。

当然后面我们我们也可以使用反射调用原方法来解决这一问题。


resolve 实现

  • 6.0 以其以下 DexCache 相关的字段并没有被 ART 隐藏,所以可以直接通过反射在 Java 层完成
  • 6.0 以上则需要在 Native 实现
  • 8.1 以上 DexCache 最大为 1024,index 实际为真实 index % 1024 再去取,则有可能在运行期间被覆盖
  • 所以 8.1 以后建议通过反射 invoke 调用原方法

手动 JIT

编译策略

  • 6.0 以其以下,默认在安装 apk 过程中会将 Dex 整体 OAT。
  • 而 6.0 以上,默认策略是 quick_profile。即根据 profile 文件编译已知的热点方法,则大部分方法都不会被编译
  • 则我们如果想使用 inline hook 的话,则必须手动将目标方法编译
  • 除此之外,将 hook 入口方法编译可以避免一些意想不到的问题

如何编译

  • ART 的主体是 libart.so,但是 Compiler 后端被单独编译到了 libart-compiler.so
  • 我们只需要 dlsym libart-compiler.so 的导出方法 jit_compile_method 调用即可
  • 需要注意 Android N 以上对 dlsym 的限制

extern "C" void* jit_load(bool* generate_debug_info) {
  VLOG(jit) << "loading jit compiler";
  auto* const jit_compiler = JitCompiler::Create();
  CHECK(jit_compiler != nullptr);
  *generate_debug_info = jit_compiler->GetCompilerOptions()->GetGenerateDebugInfo();
  VLOG(jit) << "Done loading jit compiler";
  return jit_compiler;
}

extern "C" bool jit_compile_method(
    void* handle, ArtMethod* method, Thread* self, bool osr)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  auto* jit_compiler = reinterpret_cast<JitCompiler*>(handle);
  DCHECK(jit_compiler != nullptr);
  return jit_compiler->CompileMethod(self, method, osr);
}

其他需要注意的

  • 手动编译 JNI 方法将发生未知错误
  • 在某些系统进程(zygote, system_server)里面,Compiler 是不需要初始化的,所以手动编译将会报错,很好理解,默认这些进程早就被 OAT 了

禁用某个方法的 JIT

  • 如果我们通过替换入口替换了原方法的 code_entry 来 hook,自然不希望当方法热度高的时候触发 JIT,那么,入口就会被替换掉了,Hook 失效。
  • 除此之外,当 backup ArtMethod 被我们魔改后,profile 触发 hook 入口方法的 JIT 时,在编译 invoke backup 方法的字节码,将会遇到错误
ResolveCompilingMethodsClass -> ClassLinker::ResolveMethod -> CheckIncompatibleClassChange -> ThrowIncompatibleClassChangeError

如何禁用?

ART 的判断逻辑

  bool IsCompilable() {
    if (IsIntrinsic()) {
      // kAccCompileDontBother overlaps with kAccIntrinsicBits.
      return true;
    }
    return (GetAccessFlags() & kAccCompileDontBother) == 0;
  }

那么加上 kAccCompileDontBother 即可。


Hook 线程安全

由于在 Hook 时需要修改 ArtMethod 中多个字段,ART 在运行时,众多线程会依赖 ArtMethod,则因此可能导致错误状态。

  • 正在 JIT 你修改的方法时
  • GC 时,GC 将会搜索栈,栈中有修改的 ArtMethod
  • 正好其他线程调到了正在被修改的方法
  • 其他线程发生栈回溯(异常),回溯到了正在修改的 ArtMethod

StopTheWorld

那么我们需要暂停所有线程,并且等待 GC 完成
幸运的是,ART 等待调试器也需要这一操作,不仅仅是暂停所有线程,还需要等待 GC。
至于是否会影响性能这点不用担心,实测是 nm 级的

void Dbg::SuspendVM() {
  // Avoid a deadlock between GC and debugger where GC gets suspended during GC. b/25800335.
  gc::ScopedGCCriticalSection gcs(Thread::Current(),
                                  gc::kGcCauseDebugger,
                                  gc::kCollectorTypeDebugger);
  Runtime::Current()->GetThreadList()->SuspendAllForDebugger();
}
void Dbg::ResumeVM() {
  Runtime::Current()->GetThreadList()->ResumeAllForDebugger();
}

备份原方法

我们需要一个 “容器” 来备份原 ArtMethod。这里有两种方法:

  • New 出来
  • 写一堆空方法作为 stub

这里我选择写 Stub,因为 New 有致命缺陷


New 的缺点

  • New 出来的 ArtMethod 不在 Linear 区,也就是说这个 “ArtMethod” 会被 Moving GC,那么每次调用原方法的时候,得去跳板中重新设置 ArtMethod 的地址。
  • 虽然可以通过 dlsym 使用 ART 内部的函数 “art::LinearAlloc::Alloc” 在 Linear 区分配 “ArtMethod”
  • 但是 ArtMethod 中的 declaring_class 是 GCRoot,是回 Moving GC 的,ART 并不知道他的存在,显然 GC 不会帮你更新假 “ArtMethod” 中的 declaring_class。
  • 那么还是一样,只要不使用 stub 都需要频繁手动更新地址

选择寄存器

  • 为了不破环栈结构,我们在 hook 时,需要使用纯汇编作为跳板,同时使用尽量少的寄存器完成工作
  • 如果通过保存恢复现场来保护寄存器和栈,在 ART 中也是不可行的(或者说仅仅在解释器模式下有希望)
  • 因为无论是 GC 还是栈回溯,以及其他的一些 ART 的动态行为,都依赖于栈和一些约定寄存器

ART 寄存器 的使用

  • X0 保存着 Callee 的 ArtMethod
  • X1 保存 this
  • X2 - X7 保存前 6 个非浮点参数
  • D0 - D7 前 8 个浮点参数
  • X19 当前 Thread
  • X20 GC 标记
  • X16/X17 IP0 IP1

最终选择 X17,X16 在跳板中有用到。


开始 Hook

Inline 与否

SandHook 支持两种 Hook “截获” 方案,inline 以及入口替换

  • 当 OS >= Android O 时,仅仅需要入口替换
  • 当 OS < Android O 时,考虑到大量存在的直接跳转情况,我们选择优先使用 Inline
  • 当条件不符合时,例如代码太短,放不下跳板等(后面指令检查细说),只能使用入口替换
  • 当然也可以自己选择

hook 流程

  • 首先需要获取 origin/hook/backup(可能没有) 三个 ArtMethod
  • 选择 hook 模式
  • 备份原方法
  • 安装跳板
  • 禁止 origin JIT
  • 当 OS >= O 并且 debug 模式下,将 origin 设为 native

备份原方法

  • 整体 mmcopy origin 到 backup 即可
  • 如果是 inline,由于原入口已经被塞入跳板,所以我们需要另外一块 call origin 的跳板
  • 禁止 backup JIT
  • 如果原方法非静态方法,要保证其是 private
  • 调用原方法,只需要反射调用 backup 方法即可

为何?

if (!m->IsStatic()) {
    // Replace calls to String. with equivalent StringFactory call.
    if (declaring_class->IsStringClass() && m->IsConstructor()) {
      m = WellKnownClasses::StringInitToStringFactory(m);
      CHECK(javaReceiver == nullptr);
    } else {
      // Check that the receiver is non-null and an instance of the field's declaring class.
      receiver = soa.Decode<mirror::Object>(javaReceiver);
      if (!VerifyObjectIsClass(receiver, declaring_class)) {
        return nullptr;
      }

      // Find the actual implementation of the virtual method.
      m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m, kRuntimePointerSize);
    }
  }

inline ArtMethod* Class::FindVirtualMethodForVirtualOrInterface(ArtMethod* method,
                                                                PointerSize pointer_size) {
  if (method->IsDirect()) {
    return method;
  }
  if (method->GetDeclaringClass()->IsInterface() && !method->IsCopied()) {
    return FindVirtualMethodForInterface(method, pointer_size);
  }
  return FindVirtualMethodForVirtual(method, pointer_size);
}

所以不设置 private 就不会直接调用原方法的 CodeEntry,自然入口替换 Hook 就会无效


跳板安装

入口替换

入口替换的跳板比较简单,主要就是安装在 origin 的 CodeEntry 上,完成两个任务

  • 将 X0 替换成 hook 的 ArtMethod,因为原来是 origin 的 ArtMethod
  • 跳转到 hook 的 CodeEntry

inline

inline 稍显复杂

  • 首先一段跳板替换了 origin CodeEntry 的前几行指令,直接跳转到二段跳板
  • 将原前几行指令备份到二段跳板中
  • 二段跳板首先判断 Callee 是否是需要 Hook 的方法
  • 是则设置 X0 跳转到 Hook 入口,不是则跳到备份的指令继续执行原方法

跳转图


入口相同的情况

  • 未编译的 JNI 方法
  • 逻辑相同的代码
  • 入口相同的方法依然可以重复 inline,因为其组成了责任链模式

跳板

跳板是一个个模版代码


入口替换跳板

FUNCTION_START(REPLACEMENT_HOOK_TRAMPOLINE)
    ldr RegMethod, addr_art_method
    ldr Reg0, addr_code_entry
    ldr Reg0, [Reg0]
    br Reg0
addr_art_method:
    .long 0
    .long 0
addr_code_entry:
    .long 0
    .long 0
FUNCTION_END(REPLACEMENT_HOOK_TRAMPOLINE)

inline 跳板

一段
FUNCTION_START(REPLACEMENT_HOOK_TRAMPOLINE)
    ldr RegMethod, addr_art_method
    ldr Reg0, addr_code_entry
    ldr Reg0, [Reg0]
    br Reg0
addr_art_method:
    .long 0
    .long 0
addr_code_entry:
    .long 0
    .long 0
FUNCTION_END(REPLACEMENT_HOOK_TRAMPOLINE)

二段
FUNCTION_START(INLINE_HOOK_TRAMPOLINE)
    ldr Reg0, origin_art_method
    cmp RegMethod, Reg0
    bne origin_code
    ldr RegMethod, hook_art_method
    ldr Reg0, addr_hook_code_entry
    ldr Reg0, [Reg0]
    br Reg0
origin_code:
    .long 0
    .long 0
    .long 0
    .long 0
    ldr Reg0, addr_origin_code_entry
    ldr Reg0, [Reg0]
    add Reg0, Reg0, SIZE_JUMP
    br Reg0
origin_art_method:
    .long 0
    .long 0
addr_origin_code_entry:
    .long 0
    .long 0
hook_art_method:
    .long 0
    .long 0
addr_hook_code_entry:
    .long 0
    .long 0
FUNCTION_END(INLINE_HOOK_TRAMPOLINE)

call origin
FUNCTION_START(CALL_ORIGIN_TRAMPOLINE)
    ldr RegMethod, call_origin_art_method
    ldr Reg0, addr_call_origin_code
    br Reg0
call_origin_art_method:
    .long 0
    .long 0
addr_call_origin_code:
    .long 0
    .long 0
FUNCTION_END(CALL_ORIGIN_TRAMPOLINE)

O & debug

  • Android O 及以上的 debug 模式会强制走解释器模式
  • 当 ART 发现你的方法已经被编译的时候,就不会走 CodeEntry
  • ArtInterpreterToInterpreterBridge 直接解释 CodeItem

解释器 Switch -> DoInvoke -> DoCall -> DoCallCommon -> PerformCall

bool ClassLinker::ShouldUseInterpreterEntrypoint(ArtMethod* method, const void* quick_code) {
  if (UNLIKELY(method->IsNative() || method->IsProxyMethod())) {
    return false;
  }

  if (quick_code == nullptr) {
    return true;
  }

  Runtime* runtime = Runtime::Current();
  instrumentation::Instrumentation* instr = runtime->GetInstrumentation();
  if (instr->InterpretOnly()) {
    return true;
  }

  if (runtime->GetClassLinker()->IsQuickToInterpreterBridge(quick_code)) {
    // Doing this check avoids doing compiled/interpreter transitions.
    return true;
  }

  if (Dbg::IsForcedInterpreterNeededForCalling(Thread::Current(), method)) {
    // Force the use of interpreter when it is required by the debugger.
    return true;
  }

  if (Thread::Current()->IsAsyncExceptionPending()) {
    // Force use of interpreter to handle async-exceptions
    return true;
  }

  if (runtime->IsJavaDebuggable()) {
    // For simplicity, we ignore precompiled code and go to the interpreter
    // assuming we don't already have jitted code.
    // We could look at the oat file where `quick_code` is being defined,
    // and check whether it's been compiled debuggable, but we decided to
    // only rely on the JIT for debuggable apps.
    jit::Jit* jit = Runtime::Current()->GetJit();
    return (jit == nullptr) || !jit->GetCodeCache()->ContainsPc(quick_code);
  }

  if (runtime->IsNativeDebuggable()) {
    DCHECK(runtime->UseJitCompilation() && runtime->GetJit()->JitAtFirstUse());
    // If we are doing native debugging, ignore application's AOT code,
    // since we want to JIT it (at first use) with extra stackmaps for native
    // debugging. We keep however all AOT code from the boot image,
    // since the JIT-at-first-use is blocking and would result in non-negligible
    // startup performance impact.
    return !runtime->GetHeap()->IsInBootImageOatFile(quick_code);
  }

  return false;
}

inline void PerformCall(Thread* self,
                        const CodeItemDataAccessor& accessor,
                        ArtMethod* caller_method,
                        const size_t first_dest_reg,
                        ShadowFrame* callee_frame,
                        JValue* result,
                        bool use_interpreter_entrypoint)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  if (LIKELY(Runtime::Current()->IsStarted())) {
    if (use_interpreter_entrypoint) {
      interpreter::ArtInterpreterToInterpreterBridge(self, accessor, callee_frame, result);
    } else {
      interpreter::ArtInterpreterToCompiledCodeBridge(
          self, caller_method, callee_frame, first_dest_reg, result);
    }
  } else {
    interpreter::UnstartedRuntime::Invoke(self, accessor, callee_frame, result, first_dest_reg);
  }
}

指令检查

  • Code 长度检查
  • Thumb 指令
  • 指令对齐
  • PC 寄存器相关指令

指令长度

如果我们需要 inline 一个已经编译的方法,我们就必须知道该方法 Code 的长度能否放下我们的跳转指令,否则就会破坏其他 Code。


获取指令长度

某个方法的 Code 在 Code Cache 中的布局为 CodeHeader + Code, 其中 CodeHeader 中存有 Code 的长度。

  uint32_t vmap_table_offset_ = 0u;
  uint32_t method_info_offset_ = 0u;
  QuickMethodFrameInfo frame_info_;
  // The code size in bytes. The highest bit is used to signify if the compiled
  // code with the method header has should_deoptimize flag.
  uint32_t code_size_ = 0u;
  // The actual code.
  uint8_t code_[0];

获取指令长度

JitCompile->CommitCode->CommitCodeInternal

  size_t alignment = GetInstructionSetAlignment(kRuntimeISA);
  size_t header_size = RoundUp(sizeof(OatQuickMethodHeader), alignment);
  size_t total_size = header_size + code_size;

  OatQuickMethodHeader* method_header = nullptr;
  uint8_t* code_ptr = nullptr;
  uint8_t* memory = nullptr;
  {
    ...
    {
      ....
      code_ptr = memory + header_size;

      std::copy(code, code + code_size, code_ptr);
      method_header = OatQuickMethodHeader::FromCodePointer(code_ptr);
      new (method_header) OatQuickMethodHeader(
          (stack_map != nullptr) ? code_ptr - stack_map : 0u,
          (method_info != nullptr) ? code_ptr - method_info : 0u,
          frame_size_in_bytes,
          core_spill_mask,
          fp_spill_mask,
          code_size);
      FlushInstructionCache(reinterpret_cast<char*>(code_ptr),
                            reinterpret_cast<char*>(code_ptr + code_size));

获取指令长度

那么可得出 CodeEntry - 4 就是存放 Code Size 的地址


Thumb 指令

  • ARM32 模式根据 PC 来区别是 ARM 指令还是 Thumb 指令。
  • 故 inline 的跳板也需要根据这两种模式进行分别实现
  • 并且在跳转的时候要注意入口地址符合要求
bool isThumbCode(Size codeAddr) {
            return (codeAddr & 0x1) == 0x1;
}

指令对齐

  • ART 使用 Thumb2 作为 Thumb 编译实现,Thumb2 指令又分为 Thumb16 和 Thumb32 指令
  • 例如 mov 是 16 位指令,ldr 是 32 位指令
  • 那么问题来了,我们一级跳板是 4 字节对齐的,原始指令是 2 字节对齐
  • 所以在备份原指令的时候一定要注意指令的完整性

PC 寄存器相关指令

先看一个 Java 函数:

public void setCloseOnTouchOutsideIfNotSet(boolean close) {
  if (!mSetCloseOnTouchOutside) {
        mCloseOnTouchOutside = close;
        mSetCloseOnTouchOutside = true;
    }
}  

汇编代码

CBNZ 依赖于当前 PC 值。
虽然这种情况不多,但是检查是必要的,如果我们发现这种指令,直接转用入口替换即可。


Xposed 兼容

  • 为何需要兼容
  • 可选方案
  • 实现
  • 性能优化

为何要兼容 Xposed?

  • Xposed 已经拥有众多优秀的开源模块
  • Xposed 特殊的(Callback)分发模式可支持多个模块同时 Hook 同一个函数
  • 原版 Xposed 需要 Root 并且替换定制 ART,并且 8.0 已经停止更新

可选方案

我们目前的方案需要手写一个签名与原方法类似的 Hook 方法,而 Xposed API 则使用 Callback,所以我们需要运行期间动态生成方法。

  • libffi 动态生成 Native 方法,将 origin 方法注册为 JNI 方法(Frida/Whale),相当于入口替换
  • DexMaker 生成 Java 方法,第一次生成较慢,但不存在兼容性
  • 或者自己根据调用约定从栈捞参数(epic),Hook 时较快,但运行时较慢(一次 Hook 调入需要调用很多方法解析参数),兼容性存疑

DexMaker

最终我选择了 DexMaker

  • 需要兼容前面的注解 API
  • 性能可接受,使用缓存只需要生成一次
  • 只需要完成参数打包的工作,生成的代码及其有限
  • 稳定不会有兼容问题
  • 运行时是最快的

性能优化

为了优化第一次生成 Hook 方法的性能缺陷,采取了一种折中的方法,既可以不需要从栈中解析参数,也可以不用动态生成方法。

  • 依然是写一堆 Stub,不过这次 Stub 是 Hook 方法
  • 为了兼容更多的参数情况(Object 类型以及基本类型(浮点除外)),将所有参数设为 long(32位为 int),返回值类似
  • 这样如果是基本类型参数,则可以简单转换得到值,Object 参数收到的则是内存地址
  • 参数多的 stub 可以兼容较少参数的情况

写了一个 python 脚本以自动生成

//stub of arg size 3, index 13
    public static long stub_hook_13(long a0, long a1, long a2) throws Throwable {
        return hookBridge(getMethodId(3, 13), null , a0, a1, a2);
    } 

参数转换

public static Object addressToObject64(Class objectType, long address) {
        if (objectType == null)
            return null;
        if (objectType.isPrimitive()) {
            if (objectType == int.class) {
                return (int)address;
            } else if (objectType == long.class) {
                return address;
            } else if (objectType == short.class) {
                return (short)address;
            } else if (objectType == byte.class) {
                return (byte)address;
            } else if (objectType == char.class) {
                return (char)address;
            } else if (objectType == boolean.class) {
                return address != 0;
            } else {
                throw new RuntimeException("unknown type: " + objectType.toString());
            }
        } else {
            return SandHook.getObject(address);
        }
    }

对象与地址互转

  • 一切的基础在于,当一个对象在栈上时,是不会 Moving GC 的,保证了对象地址在传递中的有效性
  • 地址 -> 对象, 使用 ART 内部的 AddWeakGlobalRef
  • 对象 -> 地址, 直接使用 Java 层的 Unsafe 类

结果

  • 如此,几乎 9 成以上的函数 Hook 都走内部 Stub 的方式,Hook 耗时在 1ms 以内
  • DexMaker 方式第一次大约需要 80ms 左右,后面直接加载约为 3 - 5ms,其实也能接受

Inline 处理

  • ART Inline 优化
  • 阻止 JIT Inline
  • 阻止 dex2oat Inline(Profile)
  • 系统类中 Inline 的如何处理

ART Inline 优化

ART 的 inline 类似其他语言的编译器优化,在 Runtime(JIT) 或者 dex2oat 期间, ART 将 “invoke 字节码指令” 替换成 callee 的方法体。
往往被 inline 的都是较为简单的方法。


阻止 JIT 期间的 Inline

观察 JIT Inline 的条件:


const CompilerOptions& compiler_options = compiler_driver_->GetCompilerOptions();
  if ((compiler_options.GetInlineDepthLimit() == 0)
      || (compiler_options.GetInlineMaxCodeUnits() == 0)) {
    return;
  }

 bool should_inline = (compiler_options.GetInlineDepthLimit() > 0)
      && (compiler_options.GetInlineMaxCodeUnits() > 0);
  if (!should_inline) {
    return;
  }

  size_t inline_max_code_units = compiler_driver_->GetCompilerOptions().GetInlineMaxCodeUnits();
  if (code_item->insns_size_in_code_units_ > inline_max_code_units) {
    VLOG(compiler) << "Method " << PrettyMethod(method)
                   << " is too big to inline: "
                   << code_item->insns_size_in_code_units_
                   << " > "
                   << inline_max_code_units;
    return false;
  }

当被 inline 方法的 code units 大于设置的阈值的时候,方法 Inline 失败。
这个阈值是 CompilerOptions -> inline_max_code_units_


经过搜索,CompilerOptions 一般与 JitCompiler 绑定:

class JitCompiler {
 public:
  static JitCompiler* Create();
  virtual ~JitCompiler();

..............
 private:
  std::unique_ptr<CompilerOptions> compiler_options_;

	............
};

}  // namespace jit
}  // namespace art

ART 的 JitCompiler 为全局单例:

  // JIT compiler
  static void* jit_compiler_handle_;

  jit->dump_info_on_shutdown_ = options->DumpJitInfoOnShutdown();
  if (jit_compiler_handle_ == nullptr && !LoadCompiler(error_msg)) {
    return nullptr;
  }

  jit_compiler_handle_ = (jit_load_)(&will_generate_debug_symbols);

extern "C" void* jit_load(bool* generate_debug_info) {
  VLOG(jit) << "loading jit compiler";
  auto* const jit_compiler = JitCompiler::Create();
  CHECK(jit_compiler != nullptr);
  *generate_debug_info = jit_compiler->GetCompilerOptions()->GetGenerateDebugInfo();
  VLOG(jit) << "Done loading jit compiler";
  return jit_compiler;
}

结论

ok,那么我们就得到了 “static void* jit_compiler_handle_” 的 C++ 符号 “_ZN3art3jit3Jit20jit_compiler_handle_E“

最后修改里面的值就可以了。

SandHook.disableVMInline()


阻止 dex2oat Inline

  • Android N 以上默认的 ART 编译策略为 speed-profile
  • 除了 JIT 期间的内联,系统在空闲时会根据这个所谓的 profile 进行 speed 模式的 dex2oat
  • speed 模式包含 Inline 优化
  • 现象是 App 多打开几次发现 Hook 不到了

如何阻止

  • 提前主动调用 dex2oat,附带 --inline-max-code-units=0 参数,要求 Dex 是动态加载的 (ArtDexOptimizer)
  • 如果上述条件无法满足,则需要干掉 profile 文件以阻止空闲时 dex2oat,当然会对性能造成一定影响 (SandHook.tryDisableProfile)

profile

  • 首先明确一点是 profile 是安装时创建的,并非由 ART 创建,ART 只负责读取以及写入
  • profile 存储了方法的热度,以指导 dex2oat 进程编译高热度方法
  • 如果你从 Google Play 下载某个热门 App,是有可能带有一份已经有内容的 profile 的
  • 综上,我们只需要删除这个文件,如果不保险,则可以修改权限使之不可写

profile path

/data/misc/profiles/cur/" + userId + “/” + selfPackageName + “/primary.prof”

bool ProfileCompilationInfo::Load(const std::string& filename, bool clear_if_invalid) {
  ScopedTrace trace(__PRETTY_FUNCTION__);
  std::string error;

  if (!IsEmpty()) {
    return kProfileLoadWouldOverwiteData;
  }

  int flags = O_RDWR | O_NOFOLLOW | O_CLOEXEC;
  ScopedFlock profile_file = LockedFile::Open(filename.c_str(), flags,
                                              /*block*/false, &error);

  if (profile_file.get() == nullptr) {
    LOG(WARNING) << "Couldn't lock the profile file " << filename << ": " << error;
    return false;
  }

系统类中被 Inline 的

  • 理论上非 root 的环境是没什么好的办法的
  • 只能特例一个个解决
  • dexOptimize Caller

dexOptimize

  • 我们 dexOptimize Caller 使 Caller 重返解释执行,那么就可以顺利的调入我们的 Hook 方法
  • dexOptimize 其实就是将 ArtMethod 的 CodeEntry 重新设置成解释 bridge
  • 依然是上面提到的 art_quick_to_interpreter_bridge/art_quick_generic_jni_trampoline

跳板获取

  • 这两个跳板不在动态链接表中,(fake)dlsym 无法搜索到
  • 但是这个符号是被保留的,在 SHT_SYMTAB 中,这个表存储了所有的符号,所以我重新实现了符号搜索
  • 除此之外也可以从从未调用的方法中获取,但是可能遇到被强制 dex2oat 的情况

https://github.com/ganyao114/AndroidELF


Android Q

  • AccessFlag
  • Hidden API

AccessFlag

这个比较好解决,Android Q 上也是因为 Hidden Api 机制为每个方法增加了一个 Flag,导致我使用预测 Flag 值在 ArtMethod 中搜索 Offset 未能搜到。

kAccPublicApi = 0x10000000 代表此方法/Field 为公开 API


uint32_t accessFlag = getIntFromJava(jniEnv, "com/swift/sandhook/SandHook",
                                                 "testAccessFlag");
            if (accessFlag == 0) {
                accessFlag = 524313;
                //kAccPublicApi
                if (SDK_INT >= ANDROID_Q) {
                    accessFlag |= 0x10000000;
                }
            }
            int offset = findOffset(p, getParentSize(), 2, accessFlag);

hidden api

这个是从 Android P 就开始引入的反射限制机制。
目前来说有几种方案:

  • Hook 法,Hook 某些判断函数,修改 ART 对限制 API 的判断流程
  • Hidden API 在内部抽象为 Policy,修改全局的 Policy 策略,一般是 Runtime 中的某个 Flag
  • 系统函数调 Hidden API 是 OK 的,想办法让 ART 误以为是系统函数调用 API,一般是修改 ClassLoader

P 与 Q 的区别

P 上,判断函数较为集中,Policy 的 Flag 也较为好搜索,然而到了 Q 上就多了,至于在 Runtime 中搜索 Flag,由于 Runtime 是个巨大的结构体,这并不是一个健壮的方法。。。


另外一种解决办法

  • 最后是想办法让 ART 误以为是系统函数调用 API,还有一种办法是双重反射,即用反射调用 Class.getDeclareMethod 之类的 API 去使用反射,也能达到目的。(这个方法还是在贴吧偶然看到的,OpenJDK 也有这个问题)
  • 后面就简单了,依据此法找到 Hidden API 的开关方法,调用即可。

实现

static {
        try {
            getMethodMethod = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
            forNameMethod = Class.class.getDeclaredMethod("forName", String.class);
            vmRuntimeClass = (Class) forNameMethod.invoke(null, "dalvik.system.VMRuntime");
            addWhiteListMethod = (Method) getMethodMethod.invoke(vmRuntimeClass, "setHiddenApiExemptions", new Class[]{String[].class});
            Method getVMRuntimeMethod = (Method) getMethodMethod.invoke(vmRuntimeClass, "getRuntime", null);
            vmRuntime = getVMRuntimeMethod.invoke(null);
        } catch (Exception e) {
            Log.e("ReflectionUtils", "error get methods", e);
        }
    }

    public static boolean passApiCheck() {
        try {
            addReflectionWhiteList("Landroid/", "Lcom/android/");
            return true;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            return false;
        }
    }


进程注入

到上面为止,Hook 的大部分细节已经介绍完了,但是本进程的 Hook 不是我们想要的。我们想要将 Hook 作用于其他进程则必须将 Hook 逻辑注入到目标进程。

  • Root 注入
  • 非 Root 注入,沙箱环境
  • 非 Root 插件加载
  • 代码植入

Root 注入

Root 注入基本分为:

  • 全局注入,一般选择注入 Zygote 进程
  • 单进程注入,ptrace

Zygote 注入

  • 原版 Xposed 修改了 app_process 的源码,在其中加入加载 Xposed 框架的逻辑
  • Edxp 依赖 “Riru”,Riru 伪造了 libmemtrack.so,在其中加入了加载 “模块” 逻辑,libmemtrack 是 Zygote 的必备库,这个库比较简单,所以成为了目标
  • 使用 Zygote 的注入,所有 android 进程都将附带 Xposed 的 lib,所以为 “全局注入”

单进程注入

  • 使用 ptrace,核心在于找到 mmap/dlopen/dlsym 等函数在目标进程的地址
  • 依据本进程计算偏移即可
  • 要注意 N 以上对 dlopen 的限制
  • 问题在于无法绕过目标进程的反调试保护,另外容易错过 Hook 时机
  • 另外这种方法也可以注入 Zygote

非 Root 注入

  • 需要沙箱环境,核心在于利用同 UID 免 Root 使用 ptrace,步骤和上面相同
  • 类似 GG 助手

https://github.com/ganyao114/SandBoxHookPlugin


非 Root 插件加载

  • 同样需要沙箱环境,不同的是需要沙箱在加载内部 app 进程时主动加载 Xposed 插件
  • 这样不会错过 Hook 时机,也不存在反调试问题
  • 然而稳定性取决于沙箱本身的稳定性
  • 类似 VirtualXposed

https://github.com/ganyao114/SandVXposed


代码植入

  • 解包目标 apk,植入 Xposed 代码,重打包
  • 需要 Hook 处理 apk 完整性检查,签名验证
  • 处理加壳
  • 容易被封号
  • 类似太极

你可能感兴趣的:(Android,jvm,Android技术)