版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/
VMP 壳 + OLLVM 的加密算法
某电商APP的加密算法经过dex脱壳分析,找到参数加密的方法在 DuHelper.doWork 中
java
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 中。
javascript
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 函数。
scss
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 过滤出目标字符串并打印调用堆栈,是不是就可以快速定位到加密算法的位置了。
代码实现如下:
kotlin
// ========== 工具函数 ==========
// 安全获取模块信息,失败返回 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));
if (!this.shouldLog) return;
this._log = "\n====== 🧪 NewString Hook ======\n";
this._log += `📥 Length: ${this.len}\n`;
this._log += str !== null ?
`📥 UTF-16 Content: ${str}\n` :
`📥 UTF-16 Content: [invalid UTF-16, ptr=${args[1]}]\n`;
if (backtrace) this._log += getBacktrace(this.context);
},
onLeave: function (retval) {
if (this.shouldLog) {
this._log += `📤 Returned jstring: ${retval}\n`;
this._log += "====== ✅ Hook End ======\n";
console.log(this._log);
}
}
});
break;
}
}
}
// Hook GetStringChars(返回 UTF-16 内容)
function hookGetStringChars(targetStr = null, backtrace = false) {
const symbols = Module.enumerateSymbolsSync("libart.so");
for (let sym of symbols) {
if (!sym.name.includes("CheckJNI") && sym.name.includes("GetStringChars")) {
console.log("[*] Found GetStringChars at: " + sym.address + " (" + sym.name + ")");
Interceptor.attach(sym.address, {
onEnter: function (args) {
this.jstr = args[1];
this.isCopy = args[2];
},
onLeave: function (retval) {
if (retval.isNull()) return;
const str = safeReadUtf16String(retval, 100); // 读取最多 100 个字符
const shouldLog = targetStr === null || (str != null && str.includes(targetStr));
if (!shouldLog) return;
let log = "\n====== 🧪 GetStringChars Hook ======\n";
log += `📥 jstring: ${this.jstr}\n`;
log += `📥 isCopy: ${this.isCopy}\n`;
log += `📤 UTF-16 String: ${str}\n`;
if (backtrace) log += getBacktrace(this.context);
log += "====== ✅ Hook End ======\n";
console.log(log);
}
});
break;
}
}
}
// Hook ReleaseStringChars
function hookReleaseStringChars(backtrace = false) {
const symbols = Module.enumerateSymbolsSync("libart.so");
for (let sym of symbols) {
if (!sym.name.includes("CheckJNI") && sym.name.includes("ReleaseStringChars")) {
console.log("[*] Found ReleaseStringChars at: " + sym.address + " (" + sym.name + ")");
Interceptor.attach(sym.address, {
onEnter: function (args) {
let log = "\n====== 🧪 ReleaseStringChars Hook ======\n";
log += `📥 jstring: ${args[1]}\n`;
log += `📥 chars: ${args[2]}\n`;
if (backtrace) log += getBacktrace(this.context);
log += "====== ✅ Hook End ======\n";
console.log(log);
}
});
break;
}
}
}
// Hook GetStringLength(返回 UTF-16 字符长度)
function hookGetStringLength(backtrace = false) {
const symbols = Module.enumerateSymbolsSync("libart.so");
for (let sym of symbols) {
if (!sym.name.includes("CheckJNI") && sym.name.includes("GetStringLength")) {
console.log("[*] Found GetStringLength at: " + sym.address + " (" + sym.name + ")");
Interceptor.attach(sym.address, {
onEnter: function (args) {
this.jstr = args[1];
this._log = "\n====== 🧪 GetStringLength Hook ======\n";
this._log += `📥 jstring: ${this.jstr}\n`;
if (backtrace) this._log += getBacktrace(this.context);
},
onLeave: function (retval) {
this._log += `📤 Length: ${retval.toInt32()}\n`;
this._log += "====== ✅ Hook End ======\n";
console.log(this._log);
}
});
break;
}
}
}
// Hook GetStringUTFLength(返回 UTF-8 编码后的长度)
function hookGetStringUTFLength(backtrace = false) {
const symbols = Module.enumerateSymbolsSync("libart.so");
for (let sym of symbols) {
if (!sym.name.includes("CheckJNI") && sym.name.includes("GetStringUTFLength")) {
console.log("[*] Found GetStringUTFLength at: " + sym.address + " (" + sym.name + ")");
Interceptor.attach(sym.address, {
onEnter: function (args) {
this.jstr = args[1];
this._log = "\n====== 🧪 GetStringUTFLength Hook ======\n";
this._log += `📥 jstring: ${this.jstr}\n`;
if (backtrace) this._log += getBacktrace(this.context);
},
onLeave: function (retval) {
this._log += `📤 UTF-8 length: ${retval.toInt32()}\n`;
this._log += "====== ✅ Hook End ======\n";
console.log(this._log);
}
});
break;
}
}
}
// ========== 启动 Hook ==========
setImmediate(function () {
// 设置目标字符串和是否打印回溯
let targetStr = "dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/";
let backtrace = true;
// 启动 Hook,按需启用
hookNewStringUTF(targetStr, backtrace);
hookGetStringUTFChars(targetStr, backtrace);
hookNewString(targetStr, backtrace);
hookGetStringChars(targetStr, backtrace);
// hookGetStringUTFLength(true);
// hookGetStringLength(true);
// hookReleaseStringChars(true);
});
调用目标函数触发 jstring 相关 api
使用固定参数主动调用 NCall.IL 函数得到加密串
ini
// Java 调用 native 方法示例
function NCall_IL() {
Java.perform(() => {
const Integer = Java.use("java.lang.Integer");
const String = Java.use("java.lang.String");
const DuApplication = Java.use("com.shizhuang.duapp.modules.app.DuApplication");
const NCall = Java.use("lte.NCall");
const arg0 = Integer.valueOf(283);
const arg1 = DuApplication.instance.value;
const arg2 = String.$new("cipherParamuserNamecountryCode86loginTokenpassword6716c58dc32e96f889a035d0c17490beplatformandroidtimestamp1744042195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0");
const argsArray = Java.array("java.lang.Object", [arg0, arg1, arg2]);
const result = NCall.IL(argsArray);
console.log("NCall.IL 返回值:", result);
});
}
截取加密串的一部分用于过滤目标
ini
let targetStr = "dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/";
得到 jstring api 调用堆栈
执行脚本并输出日志到 jstring.txt
r
frida -H 127.0.0.1:1234 -F -l jstring.js -o jstring.txt
主动调用 NCall_IL(),日志输出如下:
ini
[*] Found NewStringUTF at: 0x7be2fe9bd8 (_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc)
[*] Found GetStringUTFChars at: 0x7be2feadc8 (_ZN3art3JNI17GetStringUTFCharsEP7_JNIEnvP8_jstringPh)
[*] Found NewString at: 0x7be2fe9bd8 (_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc)
[*] Found GetStringChars at: 0x7be2fe90e0 (_ZN3art3JNI14GetStringCharsEP7_JNIEnvP8_jstringPh)
[*] Found NewStringUTF at: 0x7be2fe9bd8 (_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc)
[*] Found GetStringUTFChars at: 0x7be2feadc8 (_ZN3art3JNI17GetStringUTFCharsEP7_JNIEnvP8_jstringPh)
[*] Found NewString at: 0x7be2fe9bd8 (_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc)
[*] Found GetStringChars at: 0x7be2fe90e0 (_ZN3art3JNI14GetStringCharsEP7_JNIEnvP8_jstringPh)
[Remote::**]-> NCall_IL()
====== 🧪 NewStringUTF Hook ======
📥 Input C String: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPoc
XykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
🔍 Backtrace:
0x7b627e185c libdewuhelper.so!encode+0x138!+0x185c
0x7b6ca0f388 base.odex!0x808388!+0x808388
📤 Returned Java String: 0x99
====== ✅ Hook End ======
====== 🧪 GetStringChars Hook ======
📥 jstring: 0x15
📥 isCopy: 0x0
📤 UTF-16 String: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bL
🔍 Backtrace:
0x7b595b7208 frida-agent-64.so!0x1da208!+0x1da208
0x7b595b6d38 frida-agent-64.so!0x1d9d38!+0x1d9d38
====== ✅ Hook End ======
NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocX
ykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
从日志输出可以知道:NewStringUTF 在 libdewuhelper.so 的 encode 函数中被调用,在 so 偏移 0x185c 处。
libdewuhelper.so
使用 frida dump 脱壳 libdewuhelper.so
python dump_so.py libdewuhelper.so
参考:一文搞懂 SO 脱壳全流程:识别加壳、Frida Dump、原理深入解析
使用 IDA 反汇编 libdewuhelper.so 的 encode 方法如下:
ini
jstring __fastcall encode(JNIEnv *a1, __int64 a2, jbyteArray a3, jstring a4)
{
const char *v7; // x23
void *Value; // x20
unsigned int v9; // w25
jbyte *v10; // x24
jbyte *v11; // x0
jbyte *v12; // x26
__int64 v13; // x9
jbyte *v14; // x10
jbyte *v15; // x11
__int64 v16; // x8
jbyte v17; // t1
char *v18; // x25
jstring v19; // x19
__int128 *v21; // x10
_OWORD *v22; // x11
__int64 v23; // x12
__int128 v24; // q0
__int128 v25; // q1
v7 = (*a1)->GetStringUTFChars(a1, a4, 0LL);
Value = (void *)j_getValue();
v9 = (*a1)->GetArrayLength(a1, a3);
v10 = (*a1)->GetByteArrayElements(a1, a3, 0LL);
v11 = (jbyte *)malloc(v9 + 1);
v12 = v11;
if ( (int)v9 >= 1 )
{
if ( v9 <= 0x1F || v11 < &v10[v9] && v10 < &v11[v9] )
{
v13 = 0LL;
LABEL_6:
v14 = &v11[v13];
v15 = &v10[v13];
v16 = v9 - v13;
do
{
v17 = *v15++;
--v16;
*v14++ = v17;
}
while ( v16 );
goto LABEL_8;
}
v13 = v9 & 0x7FFFFFE0;
v21 = (__int128 *)(v10 + 16);
v22 = v11 + 16;
v23 = v9 & 0xFFFFFFE0;
do
{
v24 = *(v21 - 1);
v25 = *v21;
v21 += 2;
v23 -= 32LL;
*(v22 - 1) = v24;
*v22 = v25;
v22 += 2;
}
while ( v23 );
if ( v13 != v9 )
goto LABEL_6;
}
LABEL_8:
v11[v9] = 0;
v18 = (char *)j_AES_128_ECB_PKCS5Padding_Encrypt(v11, Value);
free(v12);
(*a1)->ReleaseStringUTFChars(a1, a4, v7);
(*a1)->ReleaseByteArrayElements(a1, a3, v10, 0LL);
v19 = (*a1)->NewStringUTF(a1, v18);
if ( v18 )
free(v18);
if ( Value )
free(Value);
return v19;
}
encode 方法中用到的 JNI 函数如下,可以根据 JNI 函数原型去还原 encode 方法中的参数类型。
scss
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
jsize (*GetArrayLength)(JNIEnv*, jarray);
jbyte* (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);
void (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
void (*ReleaseByteArrayElements)(JNIEnv*, jbyteArray, jbyte*, jint);
jstring (*NewStringUTF)(JNIEnv*, const char*);
返回值 v19 来自于 v18,是 j_AES_128_ECB_PKCS5Padding_Encrypt 方法的返回值
ini
v18 = (char *)j_AES_128_ECB_PKCS5Padding_Encrypt(v11, Value);
v11 通过与 v10 的相关计算得到,而 v10 的值来自于 a3。
Value 的值是一个通用类型指针
ini
Value = (void *)j_getValue();
来自于 getValue_ptr() 的调用
javascript
// attributes: thunk
__int64 j_getValue(void)
{
return getValue_ptr();
}
getValue_ptr 是一个函数指针,指向 getValue(),偏移为 0x5FB8,类型为:__int64 (*getValue_ptr)(void)
css
.data:0000000000005FB8 ; __int64 (*getValue_ptr)(void)
.data:0000000000005FB8 0C 16 00 00 00 00 00 00 getValue_ptr DCQ getValue ; DATA XREF: j_getValue↑o
.data:0000000000005FB8 ; j_getValue+4↑r
.data:0000000000005FB8 ; j_getValue+8↑o
encode 函数分析
使用 frida 打印一下 encode 的参数和返回值看看
markdown
[+] encode 函数地址: 0x7b62808724
[Remote::**]-> NCall_IL()
[>] a2 pointer: 0x7b625c5ea4
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7b625c5ea4 48 f2 7a 9e 40 32 30 14 70 31 30 14 02 00 00 00 [email protected].....
7b625c5eb4 00 00 00 00 90 28 30 14 00 00 00 00 00 00 00 00 .....(0.........
7b625c5ec4 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7b625c5ed4 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
[>] jbyteArray (length=195):
00000000 63 69 70 68 65 72 50 61 72 61 6d 75 73 65 72 4e cipherParamuserN
00000010 61 6d 65 63 6f 75 6e 74 72 79 43 6f 64 65 38 36 amecountryCode86
00000020 6c 6f 67 69 6e 54 6f 6b 65 6e 70 61 73 73 77 6f loginTokenpasswo
00000030 72 64 36 37 31 36 63 35 38 64 63 33 32 65 39 36 rd6716c58dc32e96
00000040 66 38 38 39 61 30 33 35 64 30 63 31 37 34 39 30 f889a035d0c17490
00000050 62 65 70 6c 61 74 66 6f 72 6d 61 6e 64 72 6f 69 beplatformandroi
00000060 64 74 69 6d 65 73 74 61 6d 70 31 37 34 34 30 34 dtimestamp174404
00000070 32 31 39 35 37 34 33 74 79 70 65 70 77 64 75 73 2195743typepwdus
00000080 65 72 4e 61 6d 65 66 33 37 62 66 61 31 34 30 35 erNamef37bfa1405
00000090 37 63 66 30 31 38 30 31 31 64 62 36 37 63 39 36 7cf018011db67c96
000000a0 33 63 64 37 33 33 5f 31 75 75 69 64 34 63 33 61 3cd733_1********
000000b0 39 62 33 38 31 38 32 38 66 62 36 33 76 35 2e 34 9b381828fb63v5.4
000000c0 33 2e 30 3.0
[>] jstring a4: "010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100"
[<] encode 返回值: "dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPo
cXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA=="
NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocX
ykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
从日志可以知道
-
jbyteArray a3 就是原始的参数数据
-
encode 返回值 和 NCall.IL 返回值 是一样的
getValue 函数分析
IDA 反汇编代码中 getValue 函数原型如下:
arduino
__int64 __fastcall getValue(const char *a1)
getValue 函数最后调用的是 j_b64_decode 函数

按 X 查找 j_b64_decode 函数的交叉引用,找到 j_b64_decode 的返回值类型其实是 char *

所以 getValue 的真实函数原型应该如下:
arduino
char* getValue(const char *a1)
hook getValue 函数并打印传参和返回值
ini
/**
* hook getValue 函数并打印参数和返回值
*/
function hookGetValue() {
const moduleName = "libdewuhelper.so";
const funcOffset = 0x160C;
// 获取模块基址
const base = Module.findBaseAddress(moduleName);
if (!base) {
console.error("[!] 模块未加载:", moduleName);
return;
}
const funcAddr = base.add(funcOffset);
console.log("[+] getValue 函数地址:", funcAddr);
// Hook 函数
Interceptor.attach(funcAddr, {
onEnter(args) {
this.argStr = Memory.readCString(args[0]);
console.log(`[*] getValue called with arg: "${this.argStr}"`);
},
onLeave(retval) {
const retStr = Memory.readCString(retval);
console.log(`[+] getValue returned: ${retval} -> "${retStr}"`);
}
});
}
// Java 调用 native 方法示例
function NCall_IL() {
Java.perform(() => {
const Integer = Java.use("java.lang.Integer");
const String = Java.use("java.lang.String");
const DuApplication = Java.use("com.shizhuang.duapp.modules.app.DuApplication");
const NCall = Java.use("lte.NCall");
const arg0 = Integer.valueOf(283);
const arg1 = DuApplication.instance.value;
const arg2 = String.$new("cipherParamuserNamecountryCode86loginTokenpassword6716c58dc32e96f889a035d0c17490beplatformandroidtimestamp1744042195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0");
const argsArray = Java.array("java.lang.Object", [arg0, arg1, arg2]);
const result = NCall.IL(argsArray);
console.log("NCall.IL 返回值:", result);
});
}
setImmediate(getValue)
// frida -H 127.0.0.1:1234 -F -l getValue.js -o log.txt
输出如下:
ini
[+] getValue 函数地址: 0x7b6280860c
[Remote::**]-> NCall_IL()
[*] getValue called with arg: "010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100"
[+] getValue returned: 0x7bd7646280 -> "****************"
NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXwnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
得到 AES 加密密钥:****************
AES_128_ECB_PKCS5Padding_Encrypt 函数分析
j_AES_128_ECB_PKCS5Padding_Encrypt 实际调用的是 AES_128_ECB_PKCS5Padding_Encrypt 函数
scss
__int64 __fastcall AES_128_ECB_PKCS5Padding_Encrypt(__int64 a1, __int64 a2)
{
...
do
{
j_AES128_ECB_encrypt(&v8[v30], a2, &v29[v30]);
--v31;
v30 += 16LL;
}
while ( v31 );
LABEL_68:
j_b64_encode(v29, v28);
return init_proc(v8);
}
AES_128_ECB_PKCS5Padding_Encrypt 里面调用 j_AES128_ECB_encrypt 加密数据
arduino
__int64 __fastcall AES128_ECB_encrypt(unsigned __int8 *a1, __int64 a2, int8x16_t *a3)
并使用 j_b64_encode 编码
java
void *__fastcall b64_encode(char *a1, __int64 a2)
通过分析 AES_128_ECB_PKCS5Padding_Encrypt 汇编代码得知:
-
a1 是需要加密的参数,类型是 char*
-
a2 是一个固定的数字,而且在加密方法里面没有用到
-
a3 加密输出的 buffer
-
返回值是加密串的长度
所以 AES128_ECB_encrypt 方法原型实际上应该是这样:
java
__int64 AES128_ECB_encrypt(char *a1, __int64 a2, char *a3)
hook AES128_ECB_encrypt 方法并打印参数和返回值看看:
kotlin
function AES128_ECB_encrypt() {
const soName = "libdewuhelper.so";
const funcName = "AES128_ECB_encrypt";
const funcAddr = Module.getExportByName(soName, funcName);
console.log("[+] AES128_ECB_encrypt 地址:", funcAddr);
Interceptor.attach(funcAddr, {
onEnter(args) {
this.inputPtr = args[0];
this.a2 = args[1].toInt32();
this.outputPtr = args[2];
this.log = "";
this.log += "\n======= AES128_ECB_encrypt =======\n";
this.log += `[>] 明文地址 a1 = ${this.inputPtr}\n`;
this.log += `[>] a2 = ${this.a2}\n`;
this.log += `[>] 输出缓冲区地址 a3 = ${this.outputPtr}\n`;
this.log += "[>] 明文内容:\n";
this.log += hexdump(this.inputPtr, {
offset: 0,
length: 256,
header: true,
ansi: false
}) + "\n";
},
onLeave(retval) {
const encryptedLen = retval.toInt32();
this.log += `[<] 返回值:加密结果长度 = ${encryptedLen}\n`;
this.log += "[<] 密文内容:\n";
this.log += hexdump(this.outputPtr, {
offset: 0,
length: Math.min(encryptedLen, 256),
header: true,
ansi: false
}) + "\n";
this.log += "======= AES128_ECB_encrypt END =======\n";
console.log(this.log);
}
});
}
// Java 调用 native 方法示例
function NCall_IL() {
Java.perform(() => {
const Integer = Java.use("java.lang.Integer");
const String = Java.use("java.lang.String");
const DuApplication = Java.use("com.shizhuang.duapp.modules.app.DuApplication");
const NCall = Java.use("lte.NCall");
const arg0 = Integer.valueOf(283);
const arg1 = DuApplication.instance.value;
const arg2 = String.$new("cipherParamuserNamecountryCode86loginTokenpassword6716c58dc32e96f889a035d0c17490beplatformandroidtimestamp1744042195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0");
const argsArray = Java.array("java.lang.Object", [arg0, arg1, arg2]);
const result = NCall.IL(argsArray);
console.log("NCall.IL 返回值:", result);
});
}
setImmediate(function () {
Java.perform(function () {
AES128_ECB_encrypt()
});
})
// frida -H 127.0.0.1:1234 -F -l AES128_ECB_encrypt.js -o log.txt
输出如下:
ini
[+] AES128_ECB_encrypt 地址: 0x7b628093d0
[Remote::**]-> NCall_IL()
======= AES128_ECB_encrypt =======
[>] 明文地址 a1 = 0x7bd768cf00
[>] a2 = -681286304
[>] 输出缓冲区地址 a3 = 0x7bd768d0c0
[>] 明文内容:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7bd768cf00 63 69 70 68 65 72 50 61 72 61 6d 75 73 65 72 4e cipherParamuserN
7bd768cf10 61 6d 65 63 6f 75 6e 74 72 79 43 6f 64 65 38 36 amecountryCode86
7bd768cf20 6c 6f 67 69 6e 54 6f 6b 65 6e 70 61 73 73 77 6f loginTokenpasswo
7bd768cf30 72 64 36 37 31 36 63 35 38 64 63 33 32 65 39 36 rd6716c58dc32e96
7bd768cf40 66 38 38 39 61 30 33 35 64 30 63 31 37 34 39 30 f889a035d0c17490
7bd768cf50 62 65 70 6c 61 74 66 6f 72 6d 61 6e 64 72 6f 69 beplatformandroi
7bd768cf60 64 74 69 6d 65 73 74 61 6d 70 31 37 34 34 30 34 dtimestamp174404
7bd768cf70 32 31 39 35 37 34 33 74 79 70 65 70 77 64 75 73 2195743typepwdus
7bd768cf80 65 72 4e 61 6d 65 66 33 37 62 66 61 31 34 30 35 erNamef37bfa1405
7bd768cf90 37 63 66 30 31 38 30 31 31 64 62 36 37 63 39 36 7cf018011db67c96
7bd768cfa0 33 63 64 37 33 33 5f 31 75 75 69 64 34 63 33 61 3cd733_1********
7bd768cfb0 39 62 33 38 31 38 32 38 66 62 36 33 76 35 2e 34 9b381828fb63v5.4
7bd768cfc0 33 2e 30 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 3.0.............
7bd768cfd0 6e 54 34 47 5a 30 6f 6e 62 5a 4c 38 34 42 38 38 nT4GZ0onbZL84B88
7bd768cfe0 00 04 6b d7 7b 00 00 00 c0 2d 50 d8 7b 00 00 00 ..k.{....-P.{...
7bd768cff0 00 00 00 00 00 00 00 00 1a 61 70 70 53 74 61 74 .........appStat
[<] 返回值:加密结果长度 = 223
[<] 密文内容:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7bd768d0c0 75 65 a8 5e 56 d1 dc af 3b 8f 63 76 ec 39 2f e2 ue.^V...;.cv.9/.
7bd768d0d0 e3 8f 52 73 ac 87 4c 6b 27 9b 7e 6a db 22 41 70 ..Rs..Lk'.~j."Ap
7bd768d0e0 be fd d2 0d f0 aa 1f f4 69 b6 c7 59 22 97 b4 bf ........i..Y"...
7bd768d0f0 54 82 df 10 f8 bb 22 69 46 c6 69 b0 8f af ad 68 T....."iF.i....h
7bd768d100 79 3a 8d 0e 13 a2 0e d7 cc 16 cb 01 3c 1f 03 01 y:..........<...
7bd768d110 5e c2 f8 9a 5f 5f fc 59 2e 09 db bd 64 fd 23 e8 ^...__.Y....d.#.
7bd768d120 71 7c a4 53 8c 27 01 20 e6 fa 41 64 eb 73 b1 3b q|.S.'. ..Ad.s.;
7bd768d130 29 d7 f4 1d 70 03 8d 9c 4c ec b7 ac 76 77 5b f9 )...p...L...vw[.
7bd768d140 65 d6 00 71 b4 7e 61 99 d1 a9 9d 8a b1 ae 9d 83 e..q.~a.........
7bd768d150 59 5c cc 7c 65 e9 db 8d 3c da fa c8 9d 3e 06 67 Y\.|e...<....>.g
7bd768d160 4a 27 6d 92 fc e0 1f 3c 58 d0 d2 a8 5d ec 8f e4 J'm....<X...]...
7bd768d170 cb 36 84 9d 9f 7d 56 99 21 8f f2 07 55 2f 40 ae .6...}V.!...U/@.
7bd768d180 00 a0 c5 1f 65 e3 f4 aa db ff 48 cd b0 f8 0d 9c ....e.....H.....
7bd768d190 6c 00 61 00 6d 00 62 00 64 00 61 00 24 00 32 l.a.m.b.d.a.$.2
======= AES128_ECB_encrypt END =======
使用 CyberChef 验证参数和算法
a1 就是要加密的参数,和输出参数是一致的

AES128_ECB_encrypt 函数返回值的 hex

使用 AES ECB 加密得到一样的结果

再通过 base64 编码加密串

编码后的结果与 app 中返回的加密串结尾部分有点不一样
markdown
// 通过标准 Base64 编码得到加密串
dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
// app 返回的加密串
dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXwnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
b64_encode 函数分析
b64_encode 函数原型如下:
scss
char *b64_encode(char *a1, __int64 a2)
使用 frida hook 一下 b64_encode 函数 并打印参数和返回值:
javascript
function hook_b64_encode() {
const soName = "libdewuhelper.so";
const funcName = "b64_encode";
const funcAddr = Module.getExportByName(soName, funcName);
console.log("[+] b64_encode 地址:", funcAddr);
Interceptor.attach(funcAddr, {
onEnter(args) {
this.a1 = args[0];
this.a2 = args[1].toInt32(); // 转成 JS number
this.log = "";
this.log += "\n======= b64_encode =======\n";
this.log += `[>] 原始数据地址 a1 = ${this.a1}\n`;
this.log += `[>] 数据长度 a2 = ${this.a2}\n`;
this.log += "[>] 原始数据内容:\n";
this.log += hexdump(this.a1, {
offset: 0,
length: Math.min(this.a2, 256),
header: true,
ansi: false
}) + "\n";
},
onLeave(retval) {
this.log += `[<] 返回值(Base64字符串地址)= ${retval}\n`;
const b64Str = Memory.readCString(retval);
this.log += `[<] Base64 编码结果: ${b64Str}\n`;
this.log += "======= b64_encode END =======\n";
console.log(this.log);
}
});
}
// Java 调用 native 方法示例
function NCall_IL() {
Java.perform(() => {
const Integer = Java.use("java.lang.Integer");
const String = Java.use("java.lang.String");
const DuApplication = Java.use("com.shizhuang.duapp.modules.app.DuApplication");
const NCall = Java.use("lte.NCall");
const arg0 = Integer.valueOf(283);
const arg1 = DuApplication.instance.value;
const arg2 = String.$new("cipherParamuserNamecountryCode86loginTokenpassword6716c58dc32e96f889a035d0c17490beplatformandroidtimestamp1744042195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0");
const argsArray = Java.array("java.lang.Object", [arg0, arg1, arg2]);
const result = NCall.IL(argsArray);
console.log("NCall.IL 返回值:", result);
});
}
setImmediate(function () {
Java.perform(function () {
hook_b64_encode();
});
})
// frida -H 127.0.0.1:1234 -F -l b64_encode.js -o log.txt
输出如下:
ini
[+] b64_encode 地址: 0x7b6280a5c8
======= b64_encode =======
[>] 原始数据地址 a1 = 0x7bd768d440
[>] 数据长度 a2 = 208
[>] 原始数据内容:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7bd768d440 75 65 a8 5e 56 d1 dc af 3b 8f 63 76 ec 39 2f e2 ue.^V...;.cv.9/.
7bd768d450 e3 8f 52 73 ac 87 4c 6b 27 9b 7e 6a db 22 41 70 ..Rs..Lk'.~j."Ap
7bd768d460 be fd d2 0d f0 aa 1f f4 69 b6 c7 59 22 97 b4 bf ........i..Y"...
7bd768d470 54 82 df 10 f8 bb 22 69 46 c6 69 b0 8f af ad 68 T....."iF.i....h
7bd768d480 79 3a 8d 0e 13 a2 0e d7 cc 16 cb 01 3c 1f 03 01 y:..........<...
7bd768d490 5e c2 f8 9a 5f 5f fc 59 2e 09 db bd 64 fd 23 e8 ^...__.Y....d.#.
7bd768d4a0 71 7c a4 53 8c 27 01 20 e6 fa 41 64 eb 73 b1 3b q|.S.'. ..Ad.s.;
7bd768d4b0 29 d7 f4 1d 70 03 8d 9c 4c ec b7 ac 76 77 5b f9 )...p...L...vw[.
7bd768d4c0 65 d6 00 71 b4 7e 61 99 d1 a9 9d 8a b1 ae 9d 83 e..q.~a.........
7bd768d4d0 59 5c cc 7c 65 e9 db 8d 3c da fa c8 9d 3e 06 67 Y\.|e...<....>.g
7bd768d4e0 4a 27 6d 92 fc e0 1f 3c 58 d0 d2 a8 5d ec 8f e4 J'm....<X...]...
7bd768d4f0 cb 36 84 9d 9f 7d 56 99 21 8f f2 07 55 2f 40 ae .6...}V.!...U/@.
7bd768d500 00 a0 c5 1f 65 e3 f4 aa db ff 48 cd b0 f8 0d 9c ....e.....H.....
[<] 返回值(Base64字符串地址)= 0x7bd83d1840
[<] Base64 编码结果: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
======= b64_encode END =======
NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
所以加密数据的实际长度是 208,并不是 223。
把 hexdump 复制到 CyberChef 使用标准 base64 编码结果 和 NCall.IL 返回值是一样的,也就是说 b64_encode 就是一个标准的 base64 编码方法。

使用 CyberChef 还原算法
所以 encode 方法的算法逻辑是:AES ECB 加密 + 标准 Base64 编码
对比 NCall.IL 方法的返回值是一致的。
使用 python 还原算法
下面是使用 Python 实现的完整加密流程,包括:
-
aes_ecb_encrypt(plaintext, key):AES ECB 模式加密(PKCS7 padding)
-
base64_encode(data):标准 Base64 编码
-
md5_hash(data):MD5 哈希
-
newSign(text, key):整合上面函数:先 AES-ECB 加密,再 base64 编码,最后 md5 哈希
安装依赖(如未安装):
pip install pycryptodome
代码实现如下:
python
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import hashlib
def aes_ecb_encrypt(plaintext: str, key: str) -> bytes:
key_bytes = key.encode('utf-8')
data_bytes = pad(plaintext.encode('utf-8'), AES.block_size) # PKCS7 padding
cipher = AES.new(key_bytes, AES.MODE_ECB)
encrypted = cipher.encrypt(data_bytes)
print(f"[AES] 原文: {plaintext}")
print(f"[AES] 密钥: {key}")
print(f"[AES] 加密结果(Hex): {encrypted.hex()}")
return encrypted
def base64_encode(data: bytes) -> str:
encoded = base64.b64encode(data).decode('utf-8')
print(f"[Base64] 编码结果: {encoded}")
return encoded
def md5_hash(data: str) -> str:
md5_result = hashlib.md5(data.encode('utf-8')).hexdigest()
print(f"[MD5] Hash 结果: {md5_result}")
return md5_result
def newSign(text: str, key: str) -> str:
print("\n======= newSign 开始 =======")
encrypted = aes_ecb_encrypt(text, key)
b64 = base64_encode(encrypted)
md5_result = md5_hash(b64)
print("======= newSign 结束 =======\n")
return md5_result
# 示例调用
if __name__ == "__main__":
text = "cipherParamuserNamecountryCode86loginTokenpassword6716c58dc32e96f889a035d0c17490beplatformandroidtimestamp1744042195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0"
key = "****************" # 16字节 AES 密钥
result = newSign(text, key)
print("newSign 结果:", result)
运行输出如下:
markdown
======= newSign 开始 =======
[AES] 原文: cipherParamuserNamecountryCode86loginTokenpassword6716c58dc32e96f889a035d0c17490beplatformandroidtimestamp1744042195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0
[AES] 密钥: ****************
[AES] 加密结果(Hex): 7565a85e56d1dcaf3b8f6376ec392fe2e38f5273ac874c6b279b7e6adb224170befdd20df0aa1ff469b6c7592297b4bf5482df10f8bb226946c669b08fafad68793a8d0e13a20ed7cc16cb013c1f03015ec2f89a5f5ffc592e09dbbd64fd23e8717ca4538c270120e6fa4164eb73b13b29d7f41d70038d9c4cecb7ac76775bf965d60071b47e6199d1a99d8ab1ae9d83595ccc7c65e9db8d3cdafac89d3e06674a276d92fce01f3c58d0d2a85dec8fe4cb36849d9f7d5699218ff207552f40ae00a0c51f65e3f4aadbff48cdb0f80d9c
[Base64] 编码结果: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
[MD5] Hash 结果: 92d2d46c077c7517922898c281ccaa4c
======= newSign 结束 =======
newSign 结果: 92d2d46c077c7517922898c281ccaa4c