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 ==================
相关推荐
棒棒AIT22 分钟前
mac 苹果电脑 Intel 芯片(Mac X86) 安卓虚拟机 Android模拟器 的救命稻草(下载安装指南)
android·游戏·macos·安卓·mac
fishwheel36 分钟前
Android:Reverse 实战 part 2 番外 IDA python
android·python·安全
消失的旧时光-19433 小时前
Android网络框架封装 ---> Retrofit + OkHttp + 协程 + LiveData + 断点续传 + 多线程下载 + 进度框交互
android·网络·retrofit
zcychong4 小时前
Handler(二):Java层源码分析
android
Chef_Chen5 小时前
从0开始学习R语言--Day58--竞争风险模型
android·开发语言·kotlin
用户2018792831676 小时前
演员的智能衣橱系统之Selector选择器
android
Kapaseker6 小时前
憋了一周了,12000字深入浅出Android的Context机制
android
betazhou7 小时前
MySQL ROUTER安装部署
android·数据库·mysql·adb·mgr·mysql router