破解 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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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;…