Frida学习-Android9下的Java层函数Hook原理

两只羊 Lv3

从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 {

// Used to load the 'artmethod2' library on application startup.
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());

// Example of a call to a native method
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();
}

/**
* A native method that is implemented by the 'artmethod2' native library,
* which is packaged with this application.
*/
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_ 方法访问标志位,如 publicprivatestaticfinalnativeabstractconstructor 等。是按位组合的标志。
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 0c 17 01 00 08 08 b4 02 00 00 07 00 00 00
, +0x0010: 3e 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00
, +0x0020: a0 6a 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 0a 40 f9
, +0x0010: ff 83 03 d1 e0 07 01 6d e2 0f 02 6d e4 17 03 6d

可以看到在一开始,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 调用约定返回

它本质上是一个调用约定转换器

image-20260504110708287

这表示add_test方法还没被编译为可用的字节码,需要跳转到解释器,从dex文件中找出方法定义的字节码

在内层的artQuickToInterpreterBridge中,也可以看到包含”dex”名的函数调用

image-20260504110906008

接下来编写一个最简单的脚本去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}` )
//dumpFridaMangler(add_test)
var result = add_test.call(this, a, b)
console.log(`add_test res => ${result}`)
return result
}
console.log('[Frida] add_test hook installed')
//dumpFridaMangler(add_test)
})
}

在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: 3e 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00
I +0x0020: a0 6a 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 6d e2 0f 02 6d e4 17 03 6d

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了

image-20260504105133983

可以看到在调用sub_79058FB3B0后,根据结果是走它分配的对象函数或者走原来的art_quick_to_interpreter_bridge逻辑

这里sub_79058FB3B0没太看懂,有点复杂。

image-20260504105216480

在之前的理论学习中,我了解到的是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);

// Save FPRs.
writer.putPushRegReg('d0', 'd1');
writer.putPushRegReg('d2', 'd3');
writer.putPushRegReg('d4', 'd5');
writer.putPushRegReg('d6', 'd7');

// Save core args, callee-saves & LR.
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');

// Save ArtMethod* + alignment padding.
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');

// Set value of x0 in the current frame.
writer.putStrRegRegOffset('x0', 'sp', 0);

writer.putLabel('restore_registers');

// Restore ArtMethod*
writer.putLdrRegRegOffset('x0', 'sp', 0);
writer.putAddRegRegImm('sp', 'sp', 16);

// Restore core args, callee-saves & LR.
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');

// Restore FPRs.
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, #0x20]
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);

// Remove kAccFastInterpreterToInterpreterInvoke and kAccSkipAccessChecks to disable use_fast_path
// in interpreter_common.h
let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag;
if ((originalFlags & kAccNative) === 0) {
hookedMethodRemovedFlags |= kAccSkipAccessChecks;
}

patchArtMethod(hookedMethodId, {
accessFlags: ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0
}, vm);

const quickCode = this.originalMethod.quickCode;

// Replace Nterp quick entrypoints with art_quick_to_interpreter_bridge to force stepping out
// of ART's next-generation interpreter and use the quick stub instead.
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有着显著作用。

  1. 检测方法的ArtMethod结构体,查看AccessFlags是否被篡改为kAccCompileDontBother等属性
  2. 检测art_quick_to_interpreter_bridge是否被inline hook
  • 标题: Frida学习-Android9下的Java层函数Hook原理
  • 作者: 两只羊
  • 创建于 : 2026-05-04 10:40:59
  • 更新于 : 2026-05-04 13:43:40
  • 链接: https://twogoat.github.io/2026/05/04/Frida学习-Java-Hook原理/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
Frida学习-Android9下的Java层函数Hook原理