破解 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 具體參考:

  • 移植 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 函數

word/media/image2.png

按 X 查找 j_b64_decode 函數的交叉引用,找到 j_b64_decode 的返回值類型其實是 char *

word/media/image3.png

所以 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 就是要加密的參數,和輸出參數是一致的

word/media/image4.png

AES128_ECB_encrypt 函數返回值的 hex

word/media/image5.png

使用 AES ECB 加密得到一樣的結果

word/media/image6.png

再通過 base64 編碼加密串

word/media/image7.png

編碼后的結果與 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 編碼方法。

word/media/image8.png

使用 CyberChef 還原算法

所以 encode 方法的算法邏輯是:AES ECB 加密 + 標準 Base64 編碼

word/media/image9.png
對比 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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/85677.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/85677.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/85677.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Automatisch:開源的工作流自動化利器

在當今數字化的時代,企業和個人都在尋找高效的方式來自動化業務流程,減少手動操作帶來的時間和成本消耗。Automatisch 作為一款開源的 Zapier 替代方案,為我們提供了一個強大而靈活的工具,讓工作流自動化變得更加簡單和可控。 一、Automatisch 簡介 Automatisch 是一個商…

RAG應用效果評估框架與優化指南

1. 引言:為何RAG評估至關重要? 一個RAG系統通常包含多個可調參數和可替換組件(如不同的嵌入模型、向量數據庫、LLM、Prompt模板等)。沒有有效的評估機制,優化過程就像“盲人摸象”,難以判斷改動是否帶來了真正的提升。 RAG評估的核心目的: 量化系統性能:將RAG的“好壞…

豆包大模型應用場景

豆包作為通用大模型&#xff0c;應用場景其實覆蓋了個人和企業兩端。個人端要突出生活化功能——比如幫學生解題、幫上班族寫周報&#xff1b;企業端則要強調降本增效&#xff0c;比如客服自動化、代碼生成這些硬需求。用戶沒指定角度&#xff0c;那就都覆蓋吧。 注意到用戶用“…

OSITCP/IP

模型&協議 在互聯網發展的早期,不同的計算機廠商有不同的網絡傳輸協議,例如:IBM的SNA協議、蘋果的AppleTalk協議等,這些協議互不兼容,導致雖然不同的產商計算機在物理層面是鏈接的,但是在網絡上基本無法完成正常通信。這就導致一個用戶如果使用了某個廠商的某個網絡…

店匠科技閃耀“跨博會”,技術+生態打造靈活出海能力

2025年6月16日至18日&#xff0c;第八屆全球跨境電商節暨第十屆深圳國際跨境電商貿易博覽會&#xff08;簡稱“跨博會”&#xff09;在深圳會展中心舉行。作為全球跨境電商行業的年度盛會&#xff0c;本屆展會以“文化跨境、品牌出海、智量強國”為主題&#xff0c;匯聚近 1500…

selenium彈框元素定位-凍結界面

有些網站上面的元素&#xff0c;我們鼠標放在上面&#xff0c;會動態彈出一些內容。 但是當我們的鼠標從音樂圖標移開&#xff0c;這個欄目就整個消失了&#xff0c;就沒法查看其對應的HTML。 怎么辦&#xff1f;在開發者工具欄console里面執行如下js代碼 &#xff1a; setTi…

美學心得(第二百七十九集)羅國正

美學心得&#xff08;第二百七十九集&#xff09; 羅國正 &#xff08;2025年6月&#xff09; 3299、分清不同本體、主體及其之間的關系&#xff0c;是 正確的審美、判斷首先的關鍵 羅國正 &#xff08;2025年6月11日于廣州&#xff09; “人也按照美的規律來建造。”這句話…

云祺容災備份系統公有云備份與恢復實操-AWS

1、創建訪問密鑰 訪問并登錄AWS控制臺&#xff0c;點擊右上角用戶名、安全憑證&#xff0c;在我的安全憑證窗口中&#xff0c;下拉找到訪問密鑰&#xff0c;并點擊創建訪問密鑰&#xff0c;選擇其他&#xff0c;點擊下一步&#xff0c;即可獲得密鑰信息如圖1至圖6。 注意&…

windows內網穿透

內網穿透&#xff08;NAT穿透&#xff09;是一種通過技術手段將局域網&#xff08;內網&#xff09;中的服務暴露到公網&#xff08;外網&#xff09;的方法&#xff0c;使外部用戶能夠訪問內網資源。其核心是解決因NAT&#xff08;網絡地址轉換&#xff09;或防火墻限制導致的…

threejs 實現720°全景圖,;兩種方式:環境貼圖、CSS3DRenderer渲染

前提 有一個前提條件&#xff1a;六張大小一致的圖片&#xff0c;六個圖片分別對應的是720全景圖的六個面&#xff1a;上、下、左、右、前、后。 這個不是那種無人機拍攝的全景圖&#xff0c;是六個圖片拼起來的&#xff0c;這樣的取景方式要比無人機的要經濟一些。 ---…

老牌軟件 Ghost 備份還原操作基礎

一、Ghost 簡介 Symantec Ghost&#xff08;也稱為 Norton Ghost&#xff09; 是一款強大的磁盤克隆和備份還原工具&#xff0c;廣泛用于系統部署、數據恢復和災難恢復。其主要功能包括&#xff1a; 創建磁盤鏡像&#xff08;.GHO文件&#xff09;備份/還原分區或整個硬盤支持…

SSH連接服務器并同步本地文件

SSH連接服務器并同步本地文件 1. 復制本地公鑰 cat ~/.ssh/id_rsa.pub如果不確定本地是否有公鑰 ls ~/.ssh/id_rsa.pub# 如果出現如下&#xff0c;則說明你本地存在公鑰 # /Users/username/.ssh/id_rsa.pub若沒有公鑰&#xff0c;需生成 # 使用下面命令&#xff0c;然后一路回…

中英泰馬來語訂貨系統:助力東南亞批發貿易企業數字化轉型升級

隨著全球數字化轉型浪潮的推進&#xff0c;東南亞地區的批發貿易企業也正逐步邁向數字化發展道路。特別是在中英泰馬來語訂貨系統的推動下&#xff0c;東南亞的批發商和零售商能夠更高效、便捷地開展跨國貿易與供應鏈管理。這不僅幫助傳統企業提高了運營效率&#xff0c;還助力…

微信小程序獲取指定元素,滾動頁面到指定位置

微信小程序獲取指定元素&#xff0c;滾動頁面到指定位置 微信小程序獲取指定元素的寬高等信息,并滾動頁面到指定位置 微信小程序獲取指定元素的寬高等信息,并滾動頁面到指定位置 注&#xff1a;原生小程序開發&#xff1a; createSelectorQuery() 創建一個選擇器查詢實例。 sel…

LeetCode熱題100—— 118. 楊輝三角

https://leetcode.cn/problems/pascals-triangle/description/?envTypestudy-plan-v2&envIdtop-100-liked 題解 代碼 public List<List<Integer>> generate(int numRows) {List<List<Integer>> datatList new ArrayList<>();for(int i …

Python函數/Lambda/nested function/decorator/kwargs:全面教程

目錄 函數簡介基本函數語法函數參數返回值高級函數概念列表推導式與Lambda函數實用示例 函數簡介 函數是可重用的代碼塊&#xff0c;用于執行特定任務。它們有助于組織代碼&#xff0c;促進復用&#xff0c;并使程序更易于維護。可以將函數視為程序中的小型程序。 基本函數…

UG NX二次開發(C++)-創建草圖(基于平面、X軸和參考點)

文章目錄 1、前言2、在UG NX中的操作3、代碼實現3.1 添加頭文件3.2 在項目中聲明一個創建草圖的函數3.3 創建草圖函數的實現代碼3.4 函數調用3.5 實現效果1、前言 作為一款大型的CAD/CAM軟件,UG NX在建模中草圖的作用非常重要,功能也非常強大,所以在UG NX中學會草圖的二次開…

計算機視覺課程筆記-機器學習中典型的有監督與無監督學習方法的詳細分類、標簽空間性質、解釋說明,并以表格形式進行總結

? 一、有監督學習&#xff08;Supervised Learning&#xff09; 定義&#xff1a;有監督學習中&#xff0c;模型訓練依賴于已標注的樣本&#xff0c;即輸入和輸出&#xff08;標簽&#xff09;成對出現。 標簽空間可能是&#xff1a; 離散型&#xff08;Discrete&#xff09…

HTTPS加密原理

一、什么是HTTPS&#xff1f; 1.1 https是在http協議上加了一層加密解密層 如圖&#xff1a; https協議就是在http協議的基礎上經過一層加密解密層發送&#xff0c;然后接收端同樣需要經過加密解密層才能獲取到發送過來的數據&#xff0c;這樣就可以保證數據傳輸的安全性&…

無人機測量風速的思路

無人機測量風速主要依靠兩種思路&#xff1a;直接測量和間接測量&#xff08;估算&#xff09;。具體方法取決于無人機的類型、搭載的傳感器以及應用場景。 以下是主要的測量方法&#xff1a; 直接測量法&#xff08;使用氣象傳感器&#xff09;&#xff1a; 原理&#xff1a;…