版權歸作者所有,如有轉發,請注明文章出處: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 具體參考:
-
移植 OLLVM 到 LLVM 18,C&C++代碼混淆
-
移植 OLLVM 到 Android NDK,Android Studio 中使用 OLLVM
-
OLLVM 增加 C&C++ 字符串加密功能
如何快速過 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));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 函數得到加密串
// 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);});
}
截取加密串的一部分用于過濾目標
let targetStr = "dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/";
得到 jstring api 調用堆棧
執行腳本并輸出日志到 jstring.txt
frida -H 127.0.0.1:1234 -F -l jstring.js -o jstring.txt
主動調用 NCall_IL(),日志輸出如下:
[*] 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 方法如下:
jstring __fastcall encode(JNIEnv *a1, __int64 a2, jbyteArray a3, jstring a4)
{const char *v7; // x23void *Value; // x20unsigned int v9; // w25jbyte *v10; // x24jbyte *v11; // x0jbyte *v12; // x26__int64 v13; // x9jbyte *v14; // x10jbyte *v15; // x11__int64 v16; // x8jbyte v17; // t1char *v18; // x25jstring v19; // x19__int128 *v21; // x10_OWORD *v22; // x11__int64 v23; // x12__int128 v24; // q0__int128 v25; // q1v7 = (*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 方法中的參數類型。
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*);
https://cs.android.com/android/platform/superproject/+/android10-release:libnativehelper/include_jni/jni.h;l=378
返回值 v19 來自于 v18,是 j_AES_128_ECB_PKCS5Padding_Encrypt 方法的返回值
v18 = (char *)j_AES_128_ECB_PKCS5Padding_Encrypt(v11, Value);
v11 通過與 v10 的相關計算得到,而 v10 的值來自于 a3。
Value 的值是一個通用類型指針
Value = (void *)j_getValue();
來自于 getValue_ptr() 的調用
// attributes: thunk
__int64 j_getValue(void)
{return getValue_ptr();
}
getValue_ptr 是一個函數指針,指向 getValue(),偏移為 0x5FB8,類型為:__int64 (*getValue_ptr)(void)
.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 的參數和返回值看看
[+] encode 函數地址: 0x7b62808724
[Remote::**]-> NCall_IL()
[>] a2 pointer: 0x7b625c5ea40 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 H.z.@20.p10.....
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 函數原型如下:
__int64 __fastcall getValue(const char *a1)
getValue 函數最后調用的是 j_b64_decode 函數
按 X 查找 j_b64_decode 函數的交叉引用,找到 j_b64_decode 的返回值類型其實是 char *
所以 getValue 的真實函數原型應該如下:
char* getValue(const char *a1)
hook getValue 函數并打印傳參和返回值
/*** 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
輸出如下:
[+] 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 函數
__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 加密數據
__int64 __fastcall AES128_ECB_encrypt(unsigned __int8 *a1, __int64 a2, int8x16_t *a3)
并使用 j_b64_encode 編碼
void *__fastcall b64_encode(char *a1, __int64 a2)
通過分析 AES_128_ECB_PKCS5Padding_Encrypt 匯編代碼得知:
-
a1 是需要加密的參數,類型是 char*
-
a2 是一個固定的數字,而且在加密方法里面沒有用到
-
a3 加密輸出的 buffer
-
返回值是加密串的長度
所以 AES128_ECB_encrypt 方法原型實際上應該是這樣:
__int64 AES128_ECB_encrypt(char *a1, __int64 a2, char *a3)
hook AES128_ECB_encrypt 方法并打印參數和返回值看看:
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
輸出如下:
[+] 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 中返回的加密串結尾部分有點不一樣
// 通過標準 Base64 編碼得到加密串
dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==// app 返回的加密串
dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX********************************************************bLATwfAwFewviaX1/8WS4J271k/SPocXwnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
b64_encode 函數分析
b64_encode 函數原型如下:
char *b64_encode(char *a1, __int64 a2)
使用 frida hook 一下 b64_encode 函數 并打印參數和返回值:
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 numberthis.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
輸出如下:
[+] 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
代碼實現如下:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import hashlibdef 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 paddingcipher = 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 encrypteddef base64_encode(data: bytes) -> str:encoded = base64.b64encode(data).decode('utf-8')print(f"[Base64] 編碼結果: {encoded}")return encodeddef md5_hash(data: str) -> str:md5_result = hashlib.md5(data.encode('utf-8')).hexdigest()print(f"[MD5] Hash 結果: {md5_result}")return md5_resultdef 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)
運行輸出如下:
======= 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