Frida学习-Android9下的Java层函数Hook原理
从ArtMethod变化观测 众所周知,Frida在native层的hook是通过inline hook实现的,就是在函数开头修改字节码,跳转到自己的trampoline函数,这个不难理解,也并非Frida所独有的。/但Frida真正强大的,在于其Java层函数Hook的功能。
在此次学习中,我选用Android9 ARM64作为测试环境。
接下来写一段最简单的代码,通过对add_test函数分析,来学习Frida的Java Hook原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package twogoat.opsu3.artmethod2;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.TextView;import twogoat.opsu3.artmethod2.databinding.ActivityMainBinding;public class MainActivity extends AppCompatActivity implements View .OnClickListener { static { System.loadLibrary("artmethod2" ); } public int add_test (int a, int b) { int res = a + b; return res; } private ActivityMainBinding binding; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); TextView tv = binding.sampleText; tv.setText(stringFromJNI()); Button button = findViewById(R.id.button); button.setOnClickListener(this ); } @Override public void onClick (View v) { add_test(1 , 2 ); stringFromJNI(); } public native String stringFromJNI () ; }
现代的安卓Art虚拟机中,Java层的一个个函数,本质上就是一个个ArtMethod对象,接下来查看一波源码中的关键结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class ArtMethod FINAL { ArtMethod () : access_flags_ (0 ), dex_code_item_offset_ (0 ), dex_method_index_ (0 ), method_index_ (0 ), hotness_count_ (0 ) { } protected : GcRoot<mirror::Class> declaring_class_; std::atomic<std::uint32_t > access_flags_; uint32_t dex_code_item_offset_; uint32_t dex_method_index_; uint16_t method_index_; uint16_t hotness_count_; struct PtrSizedFields { void * data_; void * entry_point_from_quick_compiled_code_; } ptr_sized_fields_; }
接下来简单介绍一下各字段的含义
重点在access_flags,dex_code_item_offset和ptr_sized_fields_.entry_point_from_quick_compiled_code
字段
含义
declaring_class_
该方法所属的类,也就是声明这个方法的 Class 对象。例如 A.foo() 中就是 A.class。
access_flags_
方法访问标志位,如 public、private、static、final、native、abstract、constructor 等。是按位组合的标志。
dex_code_item_offset_
方法在 dex 文件中对应 code_item 的偏移。普通 Java 方法靠它找到字节码;抽象方法、native 方法通常没有有效 code item。
dex_method_index_
该方法在 dex 文件 method_ids 表中的索引,用来定位方法声明信息:类、方法名、参数、返回值。
method_index_
ART 内部方法索引。对虚方法通常是 vtable index;对接口方法可能和 imtable/接口分发表相关;对 direct method 则是类方法表中的索引。
hotness_count_
热度计数。ART 用它统计方法执行频率,辅助 JIT 编译、热点优化等。
ptr_sized_fields_.data_
指针大小相关字段之一,含义随方法类型变化。对 native 方法常保存 JNI 函数地址或 native 相关数据;对普通方法可能和解释执行/Profiling/Runtime 数据有关。
ptr_sized_fields_.entry_point_from_quick_compiled_code_
quick compiled code 的入口地址。方法被调用时,如果有已编译机器码,就跳到这里执行;否则可能指向解释器桥、JNI bridge、resolution trampoline 等。
接下来仿照上面的结构体,尝试读出真实运行时,add_test函数的个字段含义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 void DumpArtMethod (jmethodID method_id) { if (method_id == nullptr ) { LOGE ("add_test jmethodID is null" ); return ; } const void *art_method = reinterpret_cast <const void *>(method_id); const size_t ptr_sized_fields_offset = PtrSizedFieldsOffset (); const size_t data_offset = ptr_sized_fields_offset; const size_t quick_code_offset = ptr_sized_fields_offset + sizeof (void *); const size_t art_method_size = ptr_sized_fields_offset + 2 * sizeof (void *); const uint32_t declaring_class = ReadField <uint32_t >(art_method, kDeclaringClassOffset); const uint32_t access_flags = ReadField <uint32_t >(art_method, kAccessFlagsOffset); const uint32_t dex_code_item_offset = ReadField <uint32_t >( art_method, kDexCodeItemOffsetOffset); const uint32_t dex_method_index = ReadField <uint32_t >( art_method, kDexMethodIndexOffset); const uint16_t method_index = ReadField <uint16_t >(art_method, kMethodIndexOffset); const uint16_t hotness_count = ReadField <uint16_t >(art_method, kHotnessCountOffset); const uintptr_t data = ReadPtrSizedField (art_method, data_offset); const uintptr_t quick_code = ReadPtrSizedField (art_method, quick_code_offset); LOGI ("add_test ArtMethod=%p pointer_size=%zu art_method_size=0x%zx" , art_method, sizeof (void *), art_method_size); LOGI (" offsets: declaring_class_=0x%zx access_flags_=0x%zx" " dex_code_item_offset_=0x%zx dex_method_index_=0x%zx" " method_index_=0x%zx hotness_count_=0x%zx" , kDeclaringClassOffset, kAccessFlagsOffset, kDexCodeItemOffsetOffset, kDexMethodIndexOffset, kMethodIndexOffset, kHotnessCountOffset); LOGI (" offsets: ptr_sized_fields_=0x%zx data_=0x%zx quick_code=0x%zx" , ptr_sized_fields_offset, data_offset, quick_code_offset); LOGI (" declaring_class_=0x%08" PRIx32, declaring_class); LOGI (" access_flags_=0x%08" PRIx32 " java_flags=0x%04" PRIx32 " runtime_flags=0x%08" PRIx32 " hidden_api=%s" , access_flags, access_flags & kAccJavaFlagsMask, access_flags & ~kAccJavaFlagsMask, HiddenApiListName (access_flags)); LOGI (" access_flags_: %s" , AccessFlagNames (access_flags).c_str ()); LOGI (" dex_code_item_offset_=0x%08" PRIx32 " (%" PRIu32 ")" , dex_code_item_offset, dex_code_item_offset); LOGI (" dex_method_index_=0x%08" PRIx32 " (%" PRIu32 ")" , dex_method_index, dex_method_index); LOGI (" method_index_=0x%04x (%u)" , static_cast <unsigned >(method_index), static_cast <unsigned >(method_index)); LOGI (" hotness_count_=0x%04x (%u)" , static_cast <unsigned >(hotness_count), static_cast <unsigned >(hotness_count)); LOGI (" ptr_sized_fields_.data_=%s" , DescribeAddress (reinterpret_cast <const void *>(data)).c_str ()); LOGI (" ptr_sized_fields_.entry_point_from_quick_compiled_code_=%s" , DescribeAddress (reinterpret_cast <const void *>(quick_code)).c_str ()); DumpBytes (" ArtMethod raw bytes" , art_method, art_method_size); DumpDexCodeItem (dex_method_index, dex_code_item_offset); DumpBytes (" quick_code bytes" , reinterpret_cast <const void *>(quick_code), 32 ); }
运行程序后,ArtMethod结构体解析如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 , add_test ArtMethod=0x787e628400 pointer_size=8 art_method_size=0x28 , offsets: declaring_class_=0x0 access_flags_=0x4 dex_code_item_offset_=0x8 dex_method_index_=0xc method_index_=0x10 hotness_count_=0x12 , offsets: ptr_sized_fields_=0x18 data_=0x18 quick_code=0x20 , declaring_class_=0x170c7598 , access_flags_=0x08080001 java_flags=0x0001 runtime_flags=0x08080000 hidden_api=whitelist , access_flags_: kAccPublic|kAccSkipAccessChecks/kAccFastNative|kAccSingleImplementation , dex_code_item_offset_=0x000002b4 (692 ) , dex_method_index_=0x00000007 (7 ) , method_index_=0x023e (574 ) , hotness_count_=0x0000 (0 ) , ptr_sized_fields_.data_=0x0 , ptr_sized_fields_.entry_point_from_quick_compiled_code_=0x7884116aa0 /system/lib64/libart.so+0x536aa0 , ArtMethod raw bytes address=0x787e628400 size=0x28 , +0x0000 : 98 75 0 c 17 01 00 0 8 0 8 b4 02 00 00 07 00 00 00 , +0x0010 : 3 e 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 , +0x0020 : a0 6 a 11 84 78 00 00 00 , quick_code bytes address=0x7884116aa0 size=0x20 , +0x0000 : 90 06 00 90 10 b6 46 f9 10 02 40 f9 10 0 a 40 f9 , +0x0010 : ff 83 03 d1 e0 07 01 6 d e2 0 f 02 6 d e4 17 03 6 d
可以看到在一开始,access_flags属性为kAccPublic | kAccSkipAccessChecks / kAccFastNative | kAccSingleImplementation
各标志位含义如下
标志
含义
kAccPublic
方法是 public,可公开访问。
kAccSkipAccessChecks
ART 运行时调用该方法时跳过访问权限检查。常见于已验证、可信或运行时生成/特殊处理的方法。
kAccFastNative
表示这是一个 fast JNI native 方法。JNI 调用开销更低,但 GC/线程状态处理限制更多。
kAccSingleImplementation
表示该方法在当前类层次/接口分派中只有一个实现,ART 可用它做优化,比如内联、去虚化调用。
虽然还看不太出什么,但可以看到entry_point_from_quick_compiled_code_执行的是/system/lib64/libart.so+0x536aa0
但在实际动态过程中,发现其实其应该是偏移0x55DAA0的地方,不过问题不大,我们继续来分析
在/system/lib64/libart.so+0x55DAA0,函数名为art_quick_to_interpreter_bridge ,它的核心功能为
1 2 3 4 5 1. 接收 quick 调用约定传来的参数 2. 构造解释器需要的 ShadowFrame 3. 找到 dex code item 4. 调用 ART interpreter 5. 把解释器执行结果按 quick 调用约定返回
它本质上是一个调用约定转换器
这表示add_test方法还没被编译为可用的字节码,需要跳转到解释器,从dex文件中找出方法定义的字节码
在内层的artQuickToInterpreterBridge中,也可以看到包含”dex”名的函数调用
接下来编写一个最简单的脚本去hook add_test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function hook_add ( ){ Java .perform (function ( ){ var MainActivity = Java .use ("twogoat.opsu3.artmethod2.MainActivity" ) var add_test = MainActivity .add_test .overload ('int' , 'int' ) add_test.implementation = function (a, b ){ console .log (`add_test => ${a} ${b} ` ) var result = add_test.call (this , a, b) console .log (`add_test res => ${result} ` ) return result } console .log ('[Frida] add_test hook installed' ) }) }
在hook成功后再去查看add_test的ArtMethod结构,发现果然被修改了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 I add_test ArtMethod=0x78690c7400 pointer_size=8 art_method_size=0x28 I offsets: declaring_class_=0x0 access_flags_=0x4 dex_code_item_offset_=0x8 dex_method_index_=0xc method_index_=0x10 hotness_count_=0x12 I offsets: ptr_sized_fields_=0x18 data_=0x18 quick_code=0x20 I declaring_class_=0x16f46158 I access_flags_=0x02000001 java_flags=0x0001 runtime_flags=0x02000000 hidden_api=whitelist I access_flags_: kAccPublic|kAccCompileDontBother I dex_code_item_offset_=0x000002b4 (692 ) I dex_method_index_=0x00000007 (7 ) I method_index_=0x023e (574 ) I hotness_count_=0x0000 (0 ) I ptr_sized_fields_.data_=0x0 I ptr_sized_fields_.entry_point_from_quick_compiled_code_=0x7884116aa0 /system/lib64/libart.so+0x536aa0 I ArtMethod raw bytes address=0x78690c7400 size=0x28 I +0x0000 : 58 61 f4 16 01 00 00 02 b4 02 00 00 07 00 00 00 I +0x0010 : 3 e 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 I +0x0020 : a0 6 a 11 84 78 00 00 00 I quick_code bytes address=0x7884116aa0 size=0x20 I +0x0000 : 50 00 00 58 00 02 1f d6 00 5f db 04 79 00 00 00 I +0x0010 : ff 83 03 d1 e0 07 01 6 d e2 0f 02 6 d e4 17 03 6 d
access_flags_现在除了kAccPublic只剩下了kAccCompileDontBother,他的含义为不要再尝试编译这个方法
但问题又来了entry_point_from_quick_compiled_code_仍然指向art_quick_to_interpreter_bridge(0x7884116AA0),似乎没有发生任何变化。不过可以看到的是,art_quick_to_interpreter_bridge的函数头明显发生了变化,接下来尝试动调可以看到,进行了一个inline hook的操作,这里跳转到的sub_7904db5f00应该就是Frida的自己的trampoline了
可以看到在调用sub_79058FB3B0后,根据结果是走它分配的对象函数或者走原来的art_quick_to_interpreter_bridge逻辑
这里sub_79058FB3B0没太看懂,有点复杂。
在之前的理论学习中,我了解到的是Frida会把art_quick_to_interpreter_bridge改为art_quick_generic_jni_trampoline,并把entry_point_from_jni_改为对应的implementation的hook方法。
但在android9下看起来并非如此,首先ArtMethod中既没有entry_point_from_jni_这个字段,其次在art_quick_generic_jni_trampoline下断点后,最终也并没有断住。
那么接下来需要结合实际的源码去分析一下了
Frida源码分析 查看frida-java-bridge-6.2.7/lib/android.js后,可以确认0x7904db5f00为writeArtQuickCodeReplacementTrampolineArm64,而0x79058FB3B0为的find_replacement_method_from_quick_code。
先来看看writeArtQuickCodeReplacementTrampolineArm64,它在trampoline中通过写入纯机器码的形式,实现了保存寄存器等操作,并调用了findReplacementFromQuickCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 function writeArtQuickCodeReplacementTrampolineArm64 (trampoline, target, redirectSize, { availableScratchRegs }, vm ) { const artMethodOffsets = getArtMethodSpec (vm).offset ; let offset; Memory .patchCode (trampoline, 256 , code => { const writer = new Arm64Writer (code, { pc : trampoline }); const relocator = new Arm64Relocator (target, writer); writer.putPushRegReg ('d0' , 'd1' ); writer.putPushRegReg ('d2' , 'd3' ); writer.putPushRegReg ('d4' , 'd5' ); writer.putPushRegReg ('d6' , 'd7' ); writer.putPushRegReg ('x1' , 'x2' ); writer.putPushRegReg ('x3' , 'x4' ); writer.putPushRegReg ('x5' , 'x6' ); writer.putPushRegReg ('x7' , 'x20' ); writer.putPushRegReg ('x21' , 'x22' ); writer.putPushRegReg ('x23' , 'x24' ); writer.putPushRegReg ('x25' , 'x26' ); writer.putPushRegReg ('x27' , 'x28' ); writer.putPushRegReg ('x29' , 'lr' ); writer.putSubRegRegImm ('sp' , 'sp' , 16 ); writer.putStrRegRegOffset ('x0' , 'sp' , 0 ); writer.putCallAddressWithArguments (artController.replacedMethods .findReplacementFromQuickCode , ['x0' , 'x19' ]); writer.putCmpRegReg ('x0' , 'xzr' ); writer.putBCondLabel ('eq' , 'restore_registers' ); writer.putStrRegRegOffset ('x0' , 'sp' , 0 ); writer.putLabel ('restore_registers' ); writer.putLdrRegRegOffset ('x0' , 'sp' , 0 ); writer.putAddRegRegImm ('sp' , 'sp' , 16 ); writer.putPopRegReg ('x29' , 'lr' ); writer.putPopRegReg ('x27' , 'x28' ); writer.putPopRegReg ('x25' , 'x26' ); writer.putPopRegReg ('x23' , 'x24' ); writer.putPopRegReg ('x21' , 'x22' ); writer.putPopRegReg ('x7' , 'x20' ); writer.putPopRegReg ('x5' , 'x6' ); writer.putPopRegReg ('x3' , 'x4' ); writer.putPopRegReg ('x1' , 'x2' ); writer.putPopRegReg ('d6' , 'd7' ); writer.putPopRegReg ('d4' , 'd5' ); writer.putPopRegReg ('d2' , 'd3' ); writer.putPopRegReg ('d0' , 'd1' ); writer.putBCondLabel ('ne' , 'invoke_replacement' ); do { offset = relocator.readOne (); } while (offset < redirectSize && !relocator.eoi ); relocator.writeAll (); if (!relocator.eoi ) { const scratchReg = Array .from (availableScratchRegs)[0 ]; writer.putLdrRegAddress (scratchReg, target.add (offset)); writer.putBrReg (scratchReg); } writer.putLabel ('invoke_replacement' ); writer.putLdrRegRegOffset ('x16' , 'x0' , artMethodOffsets.quickCode ); writer.putBrReg ('x16' ); writer.flush (); }); return offset; }
大概表示为它先保存参数寄存器,调用一个 Frida 查表函数,返回非零就把 x0 改成 replacement ArtMethod,再跳 replacement->quick_code;返回零才继续走原始 art_quick_to_interpreter_bridge。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 保存 x1-x7 / d0-d7 / x20-x30 保存 x0,也就是 ArtMethod* A mov x1, x19 ; x19 是 art::Thread* bl 0x79058FB3B0 ; findReplacementFromQuickCode(A, thread) cmp x0, xzr 如果 x0 == 0 : 恢复 A 执行被搬走的 art_quick_to_interpreter_bridge 原始指令 跳回 0x7884116AB0 如果 x0 != 0 : x0 = B ldr x16, [x0, br x16
接下来再看看replace的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 replace (impl, isInstanceMethod, argTypes, vm, api) { const { kAccCompileDontBother, artNterpEntryPoint } = api; this .originalMethod = fetchArtMethod (this .methodId , vm); const originalFlags = this .originalMethod .accessFlags ; if ((originalFlags & kAccXposedHookedMethod) !== 0 && xposedIsSupported ()) { const hookInfo = this .originalMethod .jniCode ; this .hookedMethodId = hookInfo.add (2 * pointerSize).readPointer (); this .originalMethod = fetchArtMethod (this .hookedMethodId , vm); } const { hookedMethodId } = this ; const replacementMethodId = cloneArtMethod (hookedMethodId, vm); this .replacementMethodId = replacementMethodId; patchArtMethod (replacementMethodId, { jniCode : impl, accessFlags : ((originalFlags & ~(kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag)) | kAccNative | kAccCompileDontBother) >>> 0 , quickCode : api.artClassLinker .quickGenericJniTrampoline , interpreterCode : api.artInterpreterToCompiledCodeBridge }, vm); let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag; if ((originalFlags & kAccNative) === 0 ) { hookedMethodRemovedFlags |= kAccSkipAccessChecks; } patchArtMethod (hookedMethodId, { accessFlags : ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0 }, vm); const quickCode = this .originalMethod .quickCode ; if (artNterpEntryPoint !== undefined && quickCode.equals (artNterpEntryPoint)) { patchArtMethod (hookedMethodId, { quickCode : api.artQuickToInterpreterBridge }, vm); } if (!isArtQuickEntrypoint (quickCode)) { const interceptor = new ArtQuickCodeInterceptor (quickCode); interceptor.activate (vm); this .interceptor = interceptor; } artController.replacedMethods .set (hookedMethodId, replacementMethodId); notifyArtMethodHooked (hookedMethodId, vm); }
重点关注这一行,我们之前的猜想其实是正确的,在android9下jniCode其实就对应ptr_sized_fields_.data_,而quickCode对应entry_point_from_quick_compiled_code_
1 2 3 4 5 6 7 8 9 const replacementMethodId = cloneArtMethod (hookedMethodId, vm);this .replacementMethodId = replacementMethodId;patchArtMethod (replacementMethodId, { jniCode : impl, accessFlags : ((originalFlags & ~(kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag)) | kAccNative | kAccCompileDontBother) >>> 0 , quickCode : api.artClassLinker .quickGenericJniTrampoline , interpreterCode : api.artInterpreterToCompiledCodeBridge }, vm);
至于interpreterCode,则会在patchArtMethod中由getArtMethodSpec去具体探测当前版本是否存在这个偏移字段,最后给出真正需要修改的字段
1 2 3 4 5 6 7 8 9 10 11 12 13 function patchArtMethod (methodId, patches, vm ) { const artMethodSpec = getArtMethodSpec (vm); const artMethodOffset = artMethodSpec.offset ; Object .keys (patches).forEach (name => { const offset = artMethodOffset[name]; if (offset === undefined ) { return ; } const address = methodId.add (offset); const write = (name === 'accessFlags' ) ? writeU32 : writePointer; write.call (address, patches[name]); }); }
也就是说,add_test的ArtMethod对象记为A。真正的执行点是Frida clone了一份A,记为B,然后把B的entry_point_from_quick_compiled_code_字段修改为quickGenericJniTrampoline,而data修改为真正的implentation函数
关键的流程总结如下
1 2 3 4 5 6 7 A.quick_code = art_quick_to_interpreter_bridge art_quick_to_interpreter_bridge 被 Frida inline patch patched bridge -> Frida dispatcher dispatcher 查 A -> B 映射 命中则 x0 = B,然后 br [B + quickCodeOffset] B.quickCode = ClassLinker.quickGenericJniTrampoline B.data_ = Frida NativeCallback implementation
通过对Frida的implementiaion对象解析,我们能进一步确认结论,0x79058FB3B0也就是find_replacement_method_from_quick_code返回的ArtMethod对象,才是真正的执行对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 I [Mapping] Frida replacement mapping observed from JS: I [Mapping] methods[0x78690c7400 ] = 0x787adbb570 I [Mapping] original(A) ArtMethod=0x78690c7400 declaring_class_=0x15ff1c38 access_flags_=0x02000001 dex_code_item_offset_=0x000002b4 dex_method_index_=7 method_index_=574 hotness_count_=0 data_=0x0 quick_code=0x7884116aa0 I [Mapping] original(A) data_: 0x0 I [Mapping] original(A) quick_code: 0x7884116aa0 /system/lib64/libart.so+0x536aa0 I [Mapping] replacement(B) ArtMethod=0x787adbb570 declaring_class_=0x15ff1c38 access_flags_=0x0a000101 dex_code_item_offset_=0x000002b4 dex_method_index_=7 method_index_=574 hotness_count_=0 data_=0x790573e048 quick_code=0x7138d010 I [Mapping] replacement(B) data_: 0x790573e048 <anonymous>+0x48 perms=rwxp I [Mapping] replacement(B) quick_code: 0x7138d010 /system/framework/arm64/boot.oat+0x10d010 I [Mapping] A access_flags=0x02000001 I access_flags_: raw=0x02000001 java_flags=0x0001 runtime_flags=0x02000000 hidden_api=whitelist I access_flags_: kAccPublic|kAccCompileDontBother I [Mapping] B access_flags=0x0a000101 I access_flags_: raw=0x0a000101 java_flags=0x0101 runtime_flags=0x0a000000 hidden_api=whitelist I access_flags_: kAccPublic|kAccNative|kAccCompileDontBother|kAccSingleImplementation I [Mapping] B data_/jniCode=0x790573e048 <anonymous>+0x48 perms=rwxp I [Mapping] B quickCode=0x7138d010 /system/framework/arm64/boot.oat+0x10d010 I [Mapping] scanning references to A(original)=0x78690c7400
可以看到,真正的ArtMethod对象,access_flags为kAccPublic|kAccNative|kAccCompileDontBother|kAccSingleImplementation
而且jniCode不为0,同时quickCode应该是quickGenericJniTrampoline
但非常搞的是,data/jniCode指向的0x790573e048,并不可以查看, Frida 自己的 range 枚举会故意隐藏这块内存。但是直接 hexdump() 成功
1 2 3 4 0x79057fa048: ldr x16, #0x79057fa058 0x79057fa04c: adr x17, #0x79057fa048 0x79057fa050: br x16 0x79057fa054: udf #0
检测思路 这里就不再往下追了,总而言之,这给了我进行Frida检测了新思路,虽然依赖于特定版本的Java层Hook,但它对自吐脚本,ssl pining bypass有着显著作用。
检测方法的ArtMethod结构体,查看AccessFlags是否被篡改为kAccCompileDontBother等属性
检测art_quick_to_interpreter_bridge是否被inline hook