版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/
前言
逆向目标是一个第三方 SDK,核心代码在 so 层,已知 so 有加壳。
调用入口是 *******************************************,会触发 libhexymsb.so 中的 token 和 time 检查方法,目标是绕过检查并成功调用 SDK。
Java 层代码逆向分析
通过 Frida hook 一下 目标类
javascript
/**
* Hook 指定类的所有方法(每个方法所有重载)
* @param {string} className - Java 类的完整名
*/
function hook_all_methods(className) {
Java.perform(function () {
var clazz = Java.use(className);
var methods = clazz.class.getDeclaredMethods(); // 反射获取所有声明的方法
var hooked = new Set(); // 用于避免重复 hook 相同方法名(因为多重载)
methods.forEach(function (m) {
var methodName = m.getName();
// 如果这个方法已经 Hook 过,就跳过
if (hooked.has(methodName)) return;
hooked.add(methodName);
try {
hook_method(className, methodName);
} catch (e) {
console.error("❌ Failed to hook " + methodName + ": " + e);
}
});
});
}
setImmediate(function () {
hook_all_methods("*******************************************");
});
// frida -H 127.0.0.1:1234 -F -l hook_class_methods.js
得到日志如下:
markdown
================= HOOK START =================
🎯 Class: *******************************************
🔧 Method: hdic
📥 Arguments:
[0]: com.superbock.App@48a2d8d
[1]: com.superbock.ui.StartupActivity
[2]: 1
📤 Return value: undefined
================== HOOK END ==================
调用的是一个 native 方法:public final native void hdic(Context context, String mainActivityClassName, int type);

JNI 函数地址追踪
so 中并没有找到对应的 native 方法,说明是动态注册的。

通过 jni_addr.py 脚本追踪一下目标类的 jni 函数地址
css
def main():
class_name = "*******************************************"
device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")
pid = device.get_frontmost_application().pid
session: frida.core.Session = device.attach(pid)
script = session.create_script(read_frida_js_source("jni_addr.js"))
script.on('message', on_message)
script.load()
script.exports.getjnimethodaddr(class_name)
# 退出
session.detach()
具体参考:逆向 JNI 函数找不到入口?动态注册定位技巧全解析
得到日志如下,hdic 函数入口在 libhexymsb.so,偏移 0x37c3c 的位置
markdown
------------ [ #34 Native Method ] ------------
Method Name : public final native void *******************************************.hdic(android.content.Context,java.lang.String,int)
ArtMethod Ptr : 0x7d52b1bb38
Native Addr : 0x7c682c4c3c
Module Name : libhexymsb.so
Module Offset : 0x37c3c
Module Base : 0x7c6828d000
Module Size : 335872 bytes
Module Path : /data/app/com.demotestapp.test-9uO6vVLGXVJ71eZiJvIxYQ==/lib/arm64/libhexymsb.so
------------------------------------------------
so 反汇编代码分析
使用 IDA Pro 打开 libhexymsb.so 按 G 跳转到 0x37c3c ,再 F5 一下得到反汇编代码如下:

反汇编代码分析:
1、参数准备逻辑
ini
v15[2] = a3;
v15[3] = a4;
v15[0] = 0;
v15[1] = a2;
v15[4] = a5;
把 a2~a5 填入参数数组 v15,供后续解释器读取。a1 是 JNIEnv*,a2 是 jboject,a3 是 Context,a4 是 String,a5 是 int
2、结构体和 flag 设置
ini
v11 = 0;
v12 = 257;
v14 = 0;
v13 = 1;
这些字节构成一个紧凑的 flag 结构,代表不同标志配置,可能是传递给 vmInterpret 函数的执行参数(例如:执行类型、安全等级、是否调试模式等)。
3、准备参数指针
ini
v6 = &unk_24202;
v7 = 32;
v8 = v15;
v9 = &v11;
v10 = 0;
v6 是一个指针,指向 .rodata 段中的某个地址,由于 IDA 并不知道这块内存是什么类型,所以叫 unk_,即"未知类型"。
.rodata + 0x24202 是一个只读区域的地址,它的第一个字节是 0xC7。
css
.rodata:0000000000024202 C7 unk_24202 DCB 0xC7 ; DATA XREF: sub_37C3C+20↓o
这些变量构成一个参数结构体,最终会被传入:
kotlin
return vmInterpret(a1, &v6, &off_46C50);
-
a1: JNIEnv指针。
-
&v6: 应该是所有参数结构组合的起始指针。
-
off_46C50: 通常是某个回调表或解释器指令表。
off_46C50 保存的是 sub_438F4 函数地址
css
.data.rel.ro:0000000000046C50 F4 38 04 00 00 00 00 00 off_46C50 DCQ sub_438F4 ; DATA XREF: sub_33FF0+3C↑o
sub_438F4 函数地址反汇编代码如下:

VMP 壳分析
使用 Frida Hook 一下 sub_438F4 函数并打印堆栈、参数和返回值
ini
const libName = "libhexymsb.so";
const offset = 0x438F4;
function hookFunction() {
let baseAddr = Module.findBaseAddress(libName);
if (!baseAddr) {
console.error(`❌ 找不到模块 ${libName}`);
return;
}
let funcAddr = baseAddr.add(offset);
console.log(`✅ Hooking ${libName} at offset 0x${offset.toString(16)} => ${funcAddr}`);
let callCount = 0;
Interceptor.attach(funcAddr, {
onEnter(args) {
this.callId = ++callCount;
this.jniEnv = args[0];
this.arg = args[1].toUInt32();
this.log = [];
this.log.push(`\n================== 🚀 Call #${this.callId} Start ==================`);
this.log.push(`🧵 ThreadId: ${Process.getCurrentThreadId()}`);
this.log.push(`🔹 JNIEnv*: ${this.jniEnv}`)
this.log.push(`🔹 unsigned int a2: ${this.arg}`);
// 可选打印调用栈
let backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
.join("\n ");
this.log.push("🔙 Backtrace:\n " + backtrace);
},
onLeave(retval) {
const jstringObj = retval;
if (!jstringObj.isNull()) {
const jniEnv = Java.vm.getEnv(); // 或者用 this.env
const str = jniEnv.getStringUtfChars(jstringObj, null).readCString();
this.log.push(`🔸 jstring (retval): ${str}`);
} else {
this.log.push("🔸 返回值为空 (null jstring)");
}
this.log.push(`================== ✅ Call #${this.callId} End ==================\n`);
console.log(this.log.join("\n"));
}
});
}
if (Java.available) {
Java.perform(hookFunction);
} else {
hookFunction();
}
// frida -H 127.0.0.1:1234 -F -l sub_438F4.js -o sub_438F4.log
输出日志如下:
sql
================== 🚀 Call #155 Start ==================
🧵 ThreadId: 26195
🔹 JNIEnv*: 0x7d5498e180
🔹 unsigned int a2: 410
🔙 Backtrace:
0x7c66b60604 libhumruj.so!vmInterpret+0x868
0x7ccf8ffb7c libart.so!_ZN3art12StackVisitor9WalkStackILNS0_16CountTransitionsE0EEEvb+0x1ac
🔸 jstring (retval): ZW5jcnlwdE1ENVRvU3RyaW5nKGNvbnRleHQucGFja2FnZU5hbWUp
================== ✅ Call #155 End ==================
从日志可以看出 sub_438F4 函数 在 libhumruj.so!vmInterpret+0x868 处被调用。
使用 IDA 打开 libhumruj.so,找到 vmInterpret 实现如下:
makefile
.text:0000000000018D9C ; void __fastcall vmInterpret(__int64, __int64 *)
.text:0000000000018D9C EXPORT vmInterpret
.text:0000000000018D9C vmInterpret ; DATA XREF: LOAD:0000000000000DA8↑o
...
.text:0000000000018D9C
.text:0000000000018D9C ; __unwind { // __gxx_personality_v0
.text:0000000000018D9C FF C3 04 D1 SUB SP, SP, #0x130
.text:0000000000018DA0 E8 63 00 FD STR D8, [SP,#0x130+var_70]
.text:0000000000018DA4 FD 7B 0D A9 STP X29, X30, [SP,#0x130+var_60]
.text:0000000000018DA8 FC 6F 0E A9 STP X28, X27, [SP,#0x130+var_50]
.text:0000000000018DAC FA 67 0F A9 STP X26, X25, [SP,#0x130+var_40]
.text:0000000000018DB0 F8 5F 10 A9 STP X24, X23, [SP,#0x130+var_30]
.text:0000000000018DB4 F6 57 11 A9 STP X22, X21, [SP,#0x130+var_20]
.text:0000000000018DB8 F4 4F 12 A9 STP X20, X19, [SP,#0x130+var_10]
.text:0000000000018DBC FD 03 03 91 ADD X29, SP, #0x130+var_70
.text:0000000000018DC0 48 D0 3B D5 MRS X8, #3, c13, c0, #2
.text:0000000000018DC4 E8 0B 00 F9 STR X8, [SP,#0x130+var_120]
.text:0000000000018DC8 08 15 40 F9 LDR X8, [X8,#0x28]
.text:0000000000018DCC F6 03 02 AA MOV X22, X2
.text:0000000000018DD0 F3 03 00 AA MOV X19, X0
.text:0000000000018DD4 A8 03 1F F8 STUR X8, [X29,#-0x10]
.text:0000000000018DD8 34 20 41 A9 LDP X20, X8, [X1,#0x10]
.text:0000000000018DDC 35 00 40 F9 LDR X21, [X1]
.text:0000000000018DE0 E1 27 00 F9 STR X1, [SP,#0x130+var_E8]
.text:0000000000018DE4 E8 2F 00 F9 STR X8, [SP,#0x130+var_D8]
.text:0000000000018DE8 4A 90 00 94 BL .getJNIWrapper
.text:0000000000018DE8
.text:0000000000018DEC BC 02 40 79 LDRH W28, [X21]
.text:0000000000018DF0 29 01 00 D0 29 21 0A 91 ADRL X9, off_3E288
.text:0000000000018DF8 EC 03 14 AA MOV X12, X20
.text:0000000000018DFC 88 1F 40 92 AND X8, X28, #0xFF
.text:0000000000018E00 28 79 68 F8 LDR X8, [X9,X8,LSL#3]
.text:0000000000018E04 09 80 0B 91 ADD X9, X0, #0x2E0
.text:0000000000018E08 E9 83 03 A9 STP X9, X0, [SP,#0x130+var_F8]
.text:0000000000018E0C C9 22 00 91 ADD X9, X22, #8
.text:0000000000018E10 E9 DB 02 A9 STP X9, X22, [SP,#0x130+var_108]
.text:0000000000018E14 C9 42 00 91 ADD X9, X22, #0x10
.text:0000000000018E18 E9 13 00 F9 STR X9, [SP,#0x130+var_110]
.text:0000000000018E1C F4 2B 00 F9 STR X20, [SP,#0x130+var_E0]
.text:0000000000018E20 00 01 1F D6 BR X8
.text:0000000000018E20
.text:0000000000018E20 ; End of function vmInterpret
sub_438F4 函数是在 0x18D9C + 0x868 = 0x19604 处被调用,也就是在 loc_195F0 代码块中。

loc_195F0 是 .data.rel.ro:off_3E288 指令处理表中的其中一个
kotlin
.data.rel.ro:000000000003E288 24 8E 01 00 00 00 00 00 off_3E288 DCQ sub_18E24 ; DATA XREF: vmInterpret+54↑o
...
.data.rel.ro:000000000003E450 60 EB 01 00 00 00 00 00 DCQ loc_1EB60
.data.rel.ro:000000000003E458 00 DB 01 00 00 00 00 00 DCQ loc_1DB00
.data.rel.ro:000000000003E460 E4 08 02 00 00 00 00 00 DCQ loc_208E4
.data.rel.ro:000000000003E468 64 9D 01 00 00 00 00 00 DCQ loc_19D64
.data.rel.ro:000000000003E8C0 F0 95 01 00 00 00 00 00 DCQ loc_195F0
...
.data.rel.ro 是一种 初始化时可以写入,运行时变为只读的数据段,通常用于:
-
虚表(vtable)
-
函数指针数组
-
常量结构体中的函数成员指针
-
全局对象指针表(比如解释器或虚拟机中的指令表)
vmInterpret 的汇编代码是 ARM64 架构下的一段典型的 虚拟机指令分发逻辑,配合前面提到的 .data.rel.ro:off_3E288 指令处理表来看,它实际上是在实现一种 opcode 跳转表(dispatch table)调用机制。
汇编分析:
arduino
.text:0000000000018DEC BC 02 40 79 LDRH W28, [X21]
从 X21 寄存器指向的内存地址处加载一个 16 位无符号整数(opcode)到 W28。
arduino
.text:0000000000018DF0 29 01 00 D0 29 21 0A 91 ADRL X9, off_3E288
将 off_3E288(虚拟机的 opcode 函数指针表)地址加载到 X9。
arduino
.text:0000000000018DF8 EC 03 14 AA MOV X12, X20
把 X20 的值拷贝到 X12(通常保存某些状态、栈帧、寄存器上下文等)。
r
.text:0000000000018DFC 88 1F 40 92 AND X8, X28, #0xFF
对 opcode (X28) 做 AND #0xFF 操作,截取最低 8 位(即最多 256 条指令),作为索引。
arduino
.text:0000000000018E00 28 79 68 F8 LDR X8, [X9, X8, LSL #3]
从 off_3E288 + X8 * 8 中加载一个函数地址到 X8,这就是通过 opcode 查找函数地址的过程。
arduino
.text:0000000000018E08 E9 83 03 A9 STP X9, X0, [SP,#var_F8]
.text:0000000000018E0C C9 22 00 91 ADD X9, X22, #8
.text:0000000000018E10 E9 DB 02 A9 STP X9, X22, [SP,#var_108]
.text:0000000000018E14 C9 42 00 91 ADD X9, X22, #0x10
.text:0000000000018E18 E9 13 00 F9 STR X9, [SP,#var_110]
.text:0000000000018E1C F4 2B 00 F9 STR X20, [SP,#var_E0]
上面这几行是保存寄存器或参数到栈,为跳转后函数执行准备上下文。
arduino
.text:0000000000018E20 00 01 1F D6 BR X8
✅ 这是整个流程的核心:跳转到 X8 指向的函数执行。
vmInterpret 就是 VMP 解释器入口函数,这里用到的 VMP 加固技术来自:github.com/maoabc/nmmp
vmInterpret 参考源码:github.com/maoabc/nmmp...
所以,hdic 函数 最终会走到 vmInterpret 解释执行。
如何找到 hdic 函数中的 token 和 time 检查指令?
-
通过 IDA 去断点调试跟踪一下 vmInterpret 分析执行流程找到找到检查指令
-
通过 IDA 或者 Frida trace 一下 vmInterpret 分析 trace 下来的汇编代码和寄存器数据找到检查指令
-
...
但是,这些效率都太低了,有没有更好的方法可以快速定位到检查指令所在的位置呢?
快速定位核心方法位置
在 token 和 time 检查方法中必然会用到系统的一些 api,比如 jni 中的 jstring 相关接口、time、log 等函数。是否可以从这些 api 入手拿到 check 方法的调用堆栈从而快速定位 check 方法的位置。
比如,在调用 SDK 检测不通过时会打印如下日志:
ini
07-24 13:20:31.551 9354 9354 I ==Ksfndkd==: checkToken error
07-24 13:20:31.552 9354 9354 I ==Ksfndkd==: checkTime error
使用 Frida Hook 一下 Java 和 Native 层的日志 api ,过滤出包含指定 tag 的日志日志并打印调用堆栈。
ini
/**
* 根据 tag 和 log level 过滤出指定日志并打印调用堆栈
*/
// tag 和 log level(Java 和 Native 通用)
const targetTags = ["==Ksfndkd=="];
const targetLevels = [4]; // VERBOSE=2, DEBUG=3, INFO=4, WARN=5, ERROR=6
function hookJavaLog() {
Java.perform(function () {
const Log = Java.use("android.util.Log");
// 枚举 Log 类中定义的等级常量
const levels = {
VERBOSE: Log.VERBOSE.value,
DEBUG: Log.DEBUG.value,
INFO: Log.INFO.value,
WARN: Log.WARN.value,
ERROR: Log.ERROR.value,
};
Log.v.overload('java.lang.String', 'java.lang.String').implementation = function (tag, msg) {
const result = this.v(tag, msg);
if (targetTags.includes(tag) && targetLevels.includes(levels.VERBOSE)) {
console.log(`\n📢 [Java Log.v] ${tag}: ${msg}`);
console.log("🔍 Java Stacktrace:");
const stack = Java.use("java.lang.Exception").$new().getStackTrace();
for (let i = 0; i < stack.length; i++) {
console.log(" at " + stack[i].toString());
}
}
return result;
};
Log.d.overload('java.lang.String', 'java.lang.String').implementation = function (tag, msg) {
const result = this.d(tag, msg);
if (targetTags.includes(tag) && targetLevels.includes(levels.DEBUG)) {
console.log(`\n📢 [Java Log.d] ${tag}: ${msg}`);
console.log("🔍 Java Stacktrace:");
const stack = Java.use("java.lang.Exception").$new().getStackTrace();
for (let i = 0; i < stack.length; i++) {
console.log(" at " + stack[i].toString());
}
}
return result;
};
Log.i.overload('java.lang.String', 'java.lang.String').implementation = function (tag, msg) {
const result = this.i(tag, msg);
if (targetTags.includes(tag) && targetLevels.includes(levels.INFO)) {
console.log(`\n📢 [Java Log.i] ${tag}: ${msg}`);
console.log("🔍 Java Stacktrace:");
const stack = Java.use("java.lang.Exception").$new().getStackTrace();
for (let i = 0; i < stack.length; i++) {
console.log(" at " + stack[i].toString());
}
}
return result;
};
Log.w.overload('java.lang.String', 'java.lang.String').implementation = function (tag, msg) {
const result = this.w(tag, msg);
if (targetTags.includes(tag) && targetLevels.includes(levels.WARN)) {
console.log(`\n📢 [Java Log.w] ${tag}: ${msg}`);
console.log("🔍 Java Stacktrace:");
const stack = Java.use("java.lang.Exception").$new().getStackTrace();
for (let i = 0; i < stack.length; i++) {
console.log(" at " + stack[i].toString());
}
}
return result;
};
Log.e.overload('java.lang.String', 'java.lang.String').implementation = function (tag, msg) {
const result = this.e(tag, msg);
if (targetTags.includes(tag) && targetLevels.includes(levels.ERROR)) {
console.log(`\n📢 [Java Log.e] ${tag}: ${msg}`);
console.log("🔍 Java Stacktrace:");
const stack = Java.use("java.lang.Exception").$new().getStackTrace();
for (let i = 0; i < stack.length; i++) {
console.log(" at " + stack[i].toString());
}
}
return result;
};
console.log("[✔] Hooked Java android.util.Log.{v,d,i,w,e}");
});
}
function hookNativeLogPrint() {
const addr = Module.findExportByName(null, "__android_log_print");
if (!addr) {
console.error("❌ __android_log_print not found");
return;
}
Interceptor.attach(addr, {
onEnter(args) {
const prio = args[0].toInt32(); // 日志等级
const tag = args[1].readCString();
const fmt = args[2].readCString();
if (targetTags.includes(tag) && targetLevels.includes(prio)) {
console.log(`\n📢 [Native Log] [${prio}] ${tag}: ${fmt}`);
console.log("🔍 Native Backtrace:");
const trace = Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress)
.join("\n ↪ ");
console.log(" ↪ " + trace);
}
}
});
console.log("[✔] Hooked native __android_log_print");
}
function main() {
hookNativeLogPrint();
if (Java.available) {
Java.perform(hookJavaLog);
} else {
console.warn("⚠ Java is not available.");
}
}
setImmediate(main);
// frida -H 127.0.0.1:1234 -F -l log.js
日志输出如下:
ini
[✔] Hooked native __android_log_print
[✔] Hooked Java android.util.Log.{v,d,i,w,e}
[Remote::Demo]->
📢 [Native Log] [4] ==Ksfndkd==: checkToken error
🔍 Native Backtrace:
↪ 0x7c66afbe24 lib6700b7c.so!0x14e24
↪ 0x7ccf561354 libart.so!art_quick_generic_jni_trampoline+0x94
↪ 0x7ccf558338 libart.so!art_quick_invoke_stub+0x228
↪ 0x7ccf56785c libart.so!_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc+0xfc
↪ 0x7ccf78d814 libart.so!_ZN3art12_GLOBAL__N_111ScopedCheck13CheckInstanceERNS_18ScopedObjectAccessENS1_12InstanceKindEP8_jobjectb+0x84
↪ 0x7ccf8d544c libart.so!_ZN3art12_GLOBAL__N_118InvokeWithArgArrayERKNS_33ScopedObjectAccessAlreadyRunnableEPNS_9ArtMethodEPNS0_8ArgArrayEPNS_6JValueEPKc+0x6c
↪ 0x7ccf9232b4 libart.so!_ZNK3art6Thread13DecodeJObjectEP8_jobject+0x64
↪ 0x7ccf8d64fc libart.so!_ZN3art35InvokeVirtualOrInterfaceWithJValuesERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectP10_jmethodIDPK6jvalue+0x1a4
↪ 0x7ccf7bc7d0 libart.so!_ZN3art3JNI15CallVoidMethodAEP7_JNIEnvP8_jobjectP10_jmethodIDPK6jvalue+0x270
↪ 0x7ccf7917a8 libart.so!_ZN3art12_GLOBAL__N_111ScopedCheck17CheckMethodAndSigERNS_18ScopedObjectAccessEP8_jobjectP7_jclassP10_jmethodIDNS_9Primitive4TypeENS_10InvokeTypeE+0x498
↪ 0x7ccf792208 libart.so!_ZN3art12_GLOBAL__N_18CheckJNI11CallMethodAEPKcP7_JNIEnvP8_jobjectP7_jclassP10_jmethodIDPK6jvalueNS_9Primitive4TypeENS_10InvokeTypeE+0x7d0
↪ 0x7c66b668c4 libhumruj.so!vmInterpret+0x6b28
↪ 0x7ccf5883a0 libart.so!_ZN3art11ClassLinker14EnsureResolvedEPNS_6ThreadEPKcNS_6ObjPtrINS_6mirror5ClassEEE+0x698
↪ 0x7ccf589294 libart.so!_ZN3art11ClassLinker11LookupClassEPNS_6ThreadEPKcmNS_6ObjPtrINS_6mirror11ClassLoaderEEE+0xcc
↪ 0x7cb77dbef0 libhexymsb.so!0x3cef0
↪ 0x7ccf561354 libart.so!art_quick_generic_jni_trampoline+0x94
📢 [Native Log] [4] ==Ksfndkd==: checkTime error
🔍 Native Backtrace:
↪ 0x7c66afbe24 lib6700b7c.so!0x14e24
↪ 0x7ccf561354 libart.so!art_quick_generic_jni_trampoline+0x94
↪ 0x7ccf558338 libart.so!art_quick_invoke_stub+0x228
↪ 0x7ccf56785c libart.so!_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc+0xfc
↪ 0x7ccf78d814 libart.so!_ZN3art12_GLOBAL__N_111ScopedCheck13CheckInstanceERNS_18ScopedObjectAccessENS1_12InstanceKindEP8_jobjectb+0x84
↪ 0x7ccf8d544c libart.so!_ZN3art12_GLOBAL__N_118InvokeWithArgArrayERKNS_33ScopedObjectAccessAlreadyRunnableEPNS_9ArtMethodEPNS0_8ArgArrayEPNS_6JValueEPKc+0x6c
↪ 0x7ccf9232b4 libart.so!_ZNK3art6Thread13DecodeJObjectEP8_jobject+0x64
↪ 0x7ccf8d64fc libart.so!_ZN3art35InvokeVirtualOrInterfaceWithJValuesERKNS_33ScopedObjectAccessAlreadyRunnableEP8_jobjectP10_jmethodIDPK6jvalue+0x1a4
↪ 0x7ccf7bc7d0 libart.so!_ZN3art3JNI15CallVoidMethodAEP7_JNIEnvP8_jobjectP10_jmethodIDPK6jvalue+0x270
↪ 0x7ccf7917a8 libart.so!_ZN3art12_GLOBAL__N_111ScopedCheck17CheckMethodAndSigERNS_18ScopedObjectAccessEP8_jobjectP7_jclassP10_jmethodIDNS_9Primitive4TypeENS_10InvokeTypeE+0x498
↪ 0x7ccf792208 libart.so!_ZN3art12_GLOBAL__N_18CheckJNI11CallMethodAEPKcP7_JNIEnvP8_jobjectP7_jclassP10_jmethodIDPK6jvalueNS_9Primitive4TypeENS_10InvokeTypeE+0x7d0
↪ 0x7c66b668c4 libhumruj.so!vmInterpret+0x6b28
↪ 0x7ccf5883a0 libart.so!_ZN3art11ClassLinker14EnsureResolvedEPNS_6ThreadEPKcNS_6ObjPtrINS_6mirror5ClassEEE+0x698
↪ 0x7ccf589294 libart.so!_ZN3art11ClassLinker11LookupClassEPNS_6ThreadEPKcmNS_6ObjPtrINS_6mirror11ClassLoaderEEE+0xcc
↪ 0x7cb77dbef0 libhexymsb.so!0x3cef0
↪ 0x7ccf561354 libart.so!art_quick_generic_jni_trampoline+0x94
从日志可以看出 checkToken 和 checkTime 都在 lib6700b7c.so!0x14e24 中。
过 OLLVM 混淆
lib6700b7c.so!0x14e24 方法如下,代码中使用了 OLLVM fla 混淆,而且调用了很多 JNI 相关的接口。

通过 Frida Hook 一下 JNI 相关调用接口,分析一下 JNI 调用
ini
/**
* ✅ 封装调用 JNI 的 CallObjectMethodA 方法(用于调用 Java 实例方法,返回 jobject)
* 注意:此版本用于调用无参数的方法
*
* @param {NativePointer} env - JNIEnv 指针
* @param {NativePointer} obj - Java 对象 jobject(或 jclass)
* @param {NativePointer} methodId - 方法 ID(jmethodID)
* @returns {NativePointer} - 返回值 jobject(例如 jstring)
*/
function callObjectMethod(env, obj, methodId) {
// 枚举 libart.so 中的所有符号
const symbols = Module.enumerateSymbolsSync("libart.so");
// 查找 CallObjectMethodA 的地址
const addr_CallObjectMethodA = symbols.find(s => s.name.includes("CallObjectMethodA"))?.address;
if (!addr_CallObjectMethodA)
throw new Error("❌ 未找到 CallObjectMethodA");
// 定义 CallObjectMethodA 为 NativeFunction
const callV = new NativeFunction(addr_CallObjectMethodA, 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
// 调用 CallObjectMethodA,最后一个参数为 ptr(0) 表示无参数(const jvalue* args = NULL)
const result = callV(env, obj, methodId, ptr(0));
return result; // 返回 Java 方法的结果对象指针(例如 jstring)
}
/**
* ✅ 获取 jclass 的 Java 类名(通过调用 Class.getName() 实例方法)
*
* @param {NativePointer} envPtr - native 层传入的 JNIEnv 指针
* @param {NativePointer} jclassPtr - Java 类对象 jclass
* @param {boolean} fullpath - 是否返回完整路径(如 "java.lang.String"),默认 true
* @returns {string} - 返回类名字符串
*/
function getJClassName(envPtr, jclassPtr, fullpath = true) {
// 获取 Java 层的 JNIEnv 封装对象
const env = Java.vm.getEnv();
// 查找 java.lang.Class 类
const javaLangClass = env.findClass("java/lang/Class");
// 获取 getName() 方法的 jmethodID,签名为 ()Ljava/lang/String;
const getNameMethod = env.getMethodId(javaLangClass, "getName", "()Ljava/lang/String;");
// 调用 jclass.getName() 获取类名对应的 jstring 对象
const classNameJStr = callObjectMethod(env, jclassPtr, getNameMethod);
// 将 jstring 转换为 UTF-8 字符串指针
const classNameCStr = env.getStringUtfChars(classNameJStr);
// 从内存中读取 C 字符串内容
let className = classNameCStr.readCString();
// 如果不需要全路径,只保留类名部分
if (!fullpath) {
const idx = className.lastIndexOf('.');
if (idx !== -1) {
className = className.substring(idx + 1);
}
}
return className;
}
function hookJNI() {
const symbols = Module.enumerateSymbolsSync("libart.so");
const addr_CallStaticBooleanMethodV = symbols.find(s => s.name.includes("CallStaticBooleanMethodV"))?.address;
const addr_FindClass = symbols.find(s => s.name.includes("FindClass"))?.address;
const addr_GetStaticMethodID = symbols.find(s => s.name.includes("GetStaticMethodID"))?.address;
const addr_NewStringUTF = symbols.find(s => s.name.includes("NewStringUTF"))?.address;
if (addr_CallStaticBooleanMethodV) {
Interceptor.attach(addr_CallStaticBooleanMethodV, {
onEnter: function (args) {
this.data = {
env: args[0],
clazz: args[1],
methodID: args[2],
va_list: args[3],
className: null,
};
this.data.className = getJClassName(this.data.env, this.data.clazz);
},
onLeave: function (retval) {
console.log(`\n➡️ [CallStaticBooleanMethodV]`);
console.log(` ➤ ClassName : ${this.data.className}`);
console.log(` ➤ jmethodID : ${this.data.methodID}`);
console.log(` ➤ va_list* : ${this.data.va_list}`);
console.log(` 🔙 Return : ${retval}`);
}
});
}
if (addr_FindClass) {
Interceptor.attach(addr_FindClass, {
onEnter(args) {
this.nameStr = args[1].readCString();
},
onLeave(retval) {
console.log(`\n➡️ [JNI FindClass]`);
console.log(` ➤ ClassName : ${this.nameStr}`);
console.log(` 🔙 Ret jclass: ${retval}`);
}
});
}
if (addr_GetStaticMethodID) {
Interceptor.attach(addr_GetStaticMethodID, {
onEnter(args) {
this.data = {
env: args[0],
clazz: args[1],
methodName: args[2].readCString(),
sigStr: args[3].readCString(),
className: null,
};
this.data.className = getJClassName(this.data.env, this.data.clazz);
},
onLeave(retval) {
console.log(`\n➡️ [JNI GetStaticMethodID]`);
console.log(` ➤️ ClassName : ${this.data.className}`);
console.log(` ➤ Method : ${this.data.methodName}`);
console.log(` ➤ Signature : ${this.data.sigStr}`);
console.log(` 🔙 Ret : ${retval}`);
}
});
}
if (addr_NewStringUTF) {
Interceptor.attach(addr_NewStringUTF, {
onEnter(args) {
this.str = args[1].readCString();
},
onLeave(retval) {
console.log(`\n➡️ [JNI NewStringUTF]`);
console.log(` ➤ Input : ${this.str}`);
console.log(` 🔙 Ret jstring: ${retval}`);
}
});
}
}
setImmediate(hookJNI);
// frida -H 127.0.0.1:1234 -F -l jni.js
日志输出如下:
markdown
➡️ [JNI GetStaticMethodID]
➤️ ClassName : ****************************************
➤ Method : hasWorkFile
➤ Signature : (Landroid/content/Context;Ljava/lang/String;)Z
🔙 Ret : 0x7cc748a1a0
➡️ [CallStaticBooleanMethodV]
➤ ClassName : ****************************************
➤ jmethodID : 0x7cc748a1a0
➤ va_list* : 0x7fd44ee5f0
🔙 Return : 0x1
➡️ [JNI GetStaticMethodID]
➤️ ClassName : ****************************************
➤ Method : ca
➤ Signature : (Landroid/content/Context;)Z
🔙 Ret : 0x7cc748a128
➡️ [CallStaticBooleanMethodV]
➤ ClassName : ****************************************
➤ jmethodID : 0x7cc748a128
➤ va_list* : 0x7fd44ee550
🔙 Return : 0x301aa100
从日志中可以看出 SDK 调用了 **************************************** 的 hasWorkFile 和 ca 方法
方法原型如下:
java
public static final native boolean ca(Context paramContext);
public static final native boolean hasWorkFile(Context paramContext, String paramString);
hasWorkFile 方法应该是用于判断某个文件是否存在的。所以 check 逻辑大概率在 ca 方法中。
通过 Frida 绕过 SDK 检查
通过 Frida 强制修改 ca 的返回值为 true
javascript
Java.perform(function () {
var targetClass = "****************************************";
var targetMethod = "ca";
var clazz = Java.use(targetClass);
clazz[targetMethod].implementation = function () {
console.log("================= HOOK START =================");
console.log("🎯 Class:", targetClass);
console.log("🔧 Method:", targetMethod);
for (var i = 0; i < arguments.length; i++) {
console.log(`📥 Arguments:\n [${i}]:`, arguments[i]);
}
var returnValue = true;
console.log("📤 Return value:", returnValue);
console.log("================== HOOK END ==================");
return returnValue;
};
});
// frida -H 127.0.0.1:1234 -F -l Bhubscfh.js
没有再报 checkToken error 和 checkTime error,成功绕过 SDK 的检查
markdown
================= HOOK START =================
🎯 Class: ****************************************
🔧 Method: ca
📥 Arguments:
[0]: com.superbock.App@7896143
📤 Return value: true
================== HOOK END ==================