Android Art Hook
Swift Gan
SandHook 是作用在 Android ART 虚拟机上的 Java 层 Hook 框架,作用于进程内是不需要 Root 的
https://github.com/ganyao114/SandHook
implementation 'com.swift.sandhook:hooklib:3.4.0'
// 不使用 Xposed API 则不需要引入
implementation 'com.swift.sandhook:xposedcompat:3.4.0'
@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);
}
}
//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());
}
});
非官方 Xposed 框架,支持 8.0 - 9.0
https://github.com/ElderDrivers/EdXposed/tree/sandhook
免 Root Xposed 环境
https://github.com/ganyao114/SandVXposed
在正式聊 Hook 方案之前,我们需要先了解一下 ART 对 invoke 字节码的实现,因为这会决定 Hook 的部分实现。
这里的实现理论上分为:解释器实现和编译实现(JIT/AOT)
实际上解释器的实现比较稳定和单一,我们仅仅需要关注编译器实现即可。
在了解 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_;
}
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 的透明引用。
// 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());
}
ART 中的 Compiler 有两种 Backend:
Quick 在 4.4 就引入,直到 6.0 一直作为默认 Compiler, 直到 7.0 被移除。
Optimizing 5.0 引入,7.0 - 9.0 作为唯一 Compiler。
还有个叫 portable,基本没用过。。。
下面以 Optimizing Compiler 为例分析 ART 方法调用的生成。
Optimizing比Quick生成速度慢,但是会附带各种优化:
其中包括 Invoke 代码生成:
invoke-static/invoke-direct 代码生成默认使用 Sharpening 优化。
我们重点关注 CodePtrLocation,但是 CodePtrLocation 在 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;
}
}
_functionxxx:
...
...
bl _functionxxx
blr lr
ldr lr [RegMethod(X0), #CodeEntryOffset]
blr lr
或许考虑到真正优化的地方在于如何更快的加载 ArtMethod 结构体,所以 8.0 之后编译后的代码都不会再省略:
ldr lr [RegMethod(X0), #CodeEntryOffset]
这一步。
// 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;
}
调用虚方法并不会使用虚方法的 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);
}
修改 VTable 是否可行?
// 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)
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();
}
一些特殊方法,主要服务于需要在 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
ART 额外维护了一批系统函数的高效实现,这些高效实现利用了CPU的指令,直接跳过了方法调用。
// System.arraycopy.
case kIntrinsicSystemArrayCopyCharArray:
return Intrinsics::kSystemArrayCopyChar;
case kIntrinsicSystemArrayCopy:
return Intrinsics::kSystemArrayCopy;
// Thread.currentThread.
case kIntrinsicCurrentThread:
return Intrinsics::kThreadCurrentThread;
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,否则会漏掉很多调用。
由于版本众多,以及 Android 平台的碎片化,Method 的内存布局往往是千变万化的。简单的根据版本写死 Offset 风险还是比较高的。
首先最重要的的一点是确定 ArtMethod 的大小,前面我们知道,ArtMethod 被存放在线性内存区域,并且不会 Moving GC,那么,相邻的两个方法他们的 ArtMethod 也是相邻的,所以 size = ArtMethod2 - ArtMethod1
那么很简单,只要手动调用就行了,但是要注意保证调用失败,这里使用不匹配的参数。
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;
}
}
为了节省资源并且加快调用速度,和 ELF 的 got.plt 表类似,Caller 去搜索 Callee 的位置时,Callee 带着 index 去 DexCache 中找到对应位置的 Callee 的 ArtMethod 结构体。
但是,DexCache 是懒加载的,我们从 Hook 入口方法调用原方法这一行为 ART 是不知道的,所以无法自动完成这一动作,这里就需要我们手动完成这一操作。
当然后面我们我们也可以使用反射调用原方法来解决这一问题。
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);
}
ResolveCompilingMethodsClass -> ClassLinker::ResolveMethod -> CheckIncompatibleClassChange -> ThrowIncompatibleClassChangeError
ART 的判断逻辑
bool IsCompilable() {
if (IsIntrinsic()) {
// kAccCompileDontBother overlaps with kAccIntrinsicBits.
return true;
}
return (GetAccessFlags() & kAccCompileDontBother) == 0;
}
那么加上 kAccCompileDontBother 即可。
由于在 Hook 时需要修改 ArtMethod 中多个字段,ART 在运行时,众多线程会依赖 ArtMethod,则因此可能导致错误状态。
那么我们需要暂停所有线程,并且等待 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。这里有两种方法:
这里我选择写 Stub,因为 New 有致命缺陷
最终选择 X17,X16 在跳板中有用到。
SandHook 支持两种 Hook “截获” 方案,inline 以及入口替换
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 上,完成两个任务
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(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)
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)
解释器 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);
}
}
如果我们需要 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 的地址
bool isThumbCode(Size codeAddr) {
return (codeAddr & 0x1) == 0x1;
}
先看一个 Java 函数:
public void setCloseOnTouchOutsideIfNotSet(boolean close) {
if (!mSetCloseOnTouchOutside) {
mCloseOnTouchOutside = close;
mSetCloseOnTouchOutside = true;
}
}
CBNZ 依赖于当前 PC 值。
虽然这种情况不多,但是检查是必要的,如果我们发现这种指令,直接转用入口替换即可。
我们目前的方案需要手写一个签名与原方法类似的 Hook 方法,而 Xposed API 则使用 Callback,所以我们需要运行期间动态生成方法。
最终我选择了 DexMaker
为了优化第一次生成 Hook 方法的性能缺陷,采取了一种折中的方法,既可以不需要从栈中解析参数,也可以不用动态生成方法。
写了一个 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);
}
}
ART 的 inline 类似其他语言的编译器优化,在 Runtime(JIT) 或者 dex2oat 期间, ART 将 “invoke 字节码指令” 替换成 callee 的方法体。
往往被 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()
/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;
}
https://github.com/ganyao114/AndroidELF
这个比较好解决,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);
这个是从 Android P 就开始引入的反射限制机制。
目前来说有几种方案:
P 上,判断函数较为集中,Policy 的 Flag 也较为好搜索,然而到了 Q 上就多了,至于在 Runtime 中搜索 Flag,由于 Runtime 是个巨大的结构体,这并不是一个健壮的方法。。。
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 注入基本分为:
https://github.com/ganyao114/SandBoxHookPlugin
https://github.com/ganyao114/SandVXposed