破解 VMP+OLLVM 混淆:通过 Hook jstring 快速定位加密算法入口

版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/

VMP 壳 + OLLVM 的加密算法

某电商APP的加密算法经过dex脱壳分析,找到参数加密的方法在 DuHelper.doWork 中

package com.shizhuang.duapp.common.helper.ee;

import com.meituan.robust.ChangeQuickRedirect;
import lte.NCall;

/* loaded from: base.apk_classes9.jar:com/shizhuang/duapp/common/helper/ee/DuHelper.class */
public class DuHelper {
    public static ChangeQuickRedirect changeQuickRedirect;

    static {
        NCall.IV(new Object[]{282});
    }

    public static native int checkSignature(Object obj);

    public static String doWork(Object obj, String str) {
        return (String) NCall.IL(new Object[]{283, obj, str});
    }

    public static native String encodeByte(byte[] bArr, String str);

    public static native String getByteValues();

    public static native String getLeanCloudAppID();

    public static native String getLeanCloudAppKey();

    public static native String getWxAppId(Object obj);

    public static native String getWxAppKey();
}

DuHelper.doWork 是调用 lte.NCall.IL 进行加密,看起来是加了 VMP 壳,index 是 283,具体实现在 so 中。

return (String) NCall.IL(new Object[]{283, obj, str});

NCall.IL 实际调用的是 so 中的 sub_17EB8 函数,而且函数内部大量引用了x y 开头的全局变量。

word/media/image1.png
这个其实是做了 OLLVM 虚假控制流(bcf)混淆,通过伪条件隐藏真实的代码执行流。

关于 OLLVM 具体参考:

如何快速过 VMP壳 和 OLLVM 混淆还原加密算法?

jstring 相关的 JNI 函数

由于 NCall.IL 返回的是 Java 的 String 对象,所以在 native 层必然用到 jstring 相关的 JNI 函数。

    jstring     (*NewString)(JNIEnv*, const jchar*, jsize);
    jsize       (*GetStringLength)(JNIEnv*, jstring);
    const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);
    void        (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
    jstring     (*NewStringUTF)(JNIEnv*, const char*);
    jsize       (*GetStringUTFLength)(JNIEnv*, jstring);
    /* JNI spec says this returns const jbyte*, but that's inconsistent */
    const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);

https://cs.android.com/android/platform/superproject/+/android10-release:libnativehelper/include_jni/jni.h;l=371

使用 frida Hook jstring 相关 api

如果 hook jstring 相关 api 过滤出目标字符串并打印调用堆栈,是不是就可以快速定位到加密算法的位置了。

代码实现如下:

// ========== 工具函数 ==========

// 安全获取模块信息,失败返回 null
function safeGetModuleByAddress(address) {
    try {
        let module = Process.getModuleByAddress(address);
        if (module) {
            return module;
        }
    } catch (e) {
        // 获取失败,返回 null
    }
    return null;
}

// 安全读取 UTF-16 字符串,失败返回 null
function safeReadUtf16String(ptr, len) {
    try {
        return Memory.readUtf16String(ptr, len);
    } catch (e) {
        console.warn(`❌ Failed to read UTF-16 string at ${ptr}: ${e.message}`);
        return null;
    }
}

// 获取当前线程的调用栈(Backtrace),带符号信息
function getBacktrace(context) {
    const trace = Thread.backtrace(context, Backtracer.ACCURATE)
        .map(address => {
            const symbol = DebugSymbol.fromAddress(address);
            if (symbol && symbol.name) {
                return `${address} ${symbol.moduleName}!${symbol.name}!+0x${symbol.address.sub(Module.findBaseAddress(symbol.moduleName)).toString(16)}`;
            } else {
                const module = safeGetModuleByAddress(address);
                if (module) {
                    const offset = ptr(address).sub(module.base);
                    return `${address} ${module.name} + 0x${offset.toString(16)}`;
                } else {
                    return `${address} [Unknown]`;
                }
            }
        }).join("\n");
    return `🔍 Backtrace:\n${trace}\n`;
}

// ========== Hook JNI 方法 ==========

// Hook GetStringUTFChars
function hookGetStringUTFChars(targetStr = null, backtrace = false) {
    const symbols = Module.enumerateSymbolsSync("libart.so");
    for (let sym of symbols) {
        if (!sym.name.includes("CheckJNI") && sym.name.includes("GetStringUTFChars")) {
            console.log("[*] Found GetStringUTFChars at: " + sym.address + " (" + sym.name + ")");
            Interceptor.attach(sym.address, {
                onEnter: function (args) {
                    this.jstr = args[1];     // jstring 对象
                    this.isCopy = args[2];   // 是否是拷贝
                },
                onLeave: function (retval) {
                    if (retval.isNull()) return;
                    const cstr = Memory.readUtf8String(retval);
                    const shouldLog = targetStr === null || cstr.includes(targetStr);
                    if (!shouldLog) return;

                    let log = "\n====== 🧪 GetStringUTFChars Hook ======\n";
                    log += `📥 jstring: ${this.jstr}\n`;
                    log += `📥 isCopy: ${this.isCopy}\n`;
                    log += `📤 C String: ${cstr}\n`;
                    if (backtrace) log += getBacktrace(this.context);
                    log += "====== ✅ Hook End ======\n";
                    console.log(log);
                }
            });
            break;
        }
    }
}

// Hook NewStringUTF
function hookNewStringUTF(targetStr = null, backtrace = false) {
    const symbols = Module.enumerateSymbolsSync("libart.so");
    for (let sym of symbols) {
        if (!sym.name.includes("CheckJNI") && sym.name.includes("NewStringUTF")) {
            console.log("[*] Found NewStringUTF at: " + sym.address + " (" + sym.name + ")");
            Interceptor.attach(sym.address, {
                onEnter: function (args) {
                    this.cstr = args[1]; // 传入的 C 字符串指针
                    let log = "\n====== 🧪 NewStringUTF Hook ======\n";
                    try {
                        const inputStr = Memory.readUtf8String(this.cstr);
                        this.shouldLog = (inputStr !== null) && (targetStr === null || inputStr.includes(targetStr));
                        if (!this.shouldLog) return;
                        log += `📥 Input C String: ${inputStr}\n`;
                        if (backtrace) log += getBacktrace(this.context);
                        this._log = log;
                    } catch (e) {
                        console.error("Error reading string or generating log:", e);
                    }
                },
                onLeave: function (retval) {
                    if (this.shouldLog) {
                        this._log += `📤 Returned Java String: ${retval}\n`;
                        this._log += "====== ✅ Hook End ======\n";
                        console.log(this._log);
                    }
                }
            });
            break;
        }
    }
}

// Hook NewString(UTF-16)
function hookNewString(targetStr = null, backtrace = false) {
    const symbols = Module.enumerateSymbolsSync("libart.so");
    for (let sym of symbols) {
        if (!sym.name.includes("CheckJNI") && sym.name.includes("NewString")) {
            console.log("[*] Found NewString at: " + sym.address + " (" + sym.name + ")");
            Interceptor.attach(sym.address, {
                onEnter: function (args) {
                    this.len = args[2].toInt32(); // 字符串长度
                    const str = safeReadUtf16String(args[1], this.len); // 读取 UTF-16 内容
                    this.shouldLog = targetStr === null || (str != null && str.includes(targetStr));
                
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值