版权归作者所有,如有转发,请注明文章出处: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 开头的全局变量。

这个其实是做了 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*);
使用 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));

最低0.47元/天 解锁文章
1425

被折叠的 条评论
为什么被折叠?



