OLLVM 混淆 + VMP 壳照样破!绕过加壳 SDK 的核心检测逻辑

版权归作者所有,如有转发,请注明文章出处: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 检查指令?

  1. 通过 IDA 去断点调试跟踪一下 vmInterpret 分析执行流程找到找到检查指令

  2. 通过 IDA 或者 Frida trace 一下 vmInterpret 分析 trace 下来的汇编代码和寄存器数据找到检查指令

  3. ...

但是,这些效率都太低了,有没有更好的方法可以快速定位到检查指令所在的位置呢?

快速定位核心方法位置

在 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 ==================
相关推荐
三少爷的鞋5 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)9 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码11 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程11 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓16 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer