逆向某物 App 登录接口:还原 newSign 算法全流程
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
newSign 参数分析
通过 Hook Java 层加密算法得到 newSign 参数相关信息如下:
具体参考:逆向某物 App 登录接口:抓包分析 + Frida Hook 还原加密算法
入参:
MD5 update data Utf8: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/A+KLRtWKw+AfFWotfKtx+5J+ONciO*********************************************************************************************kK+Xiqtb6FajKK3aJ2vwB5l5lAKIhnvpOWXFWqYSQJy5g7oQ61Vwo+6MVB3U/wBT2CpM7AKDFH2Xj9Krb/0jNsPgNnA==
MD5 加密后的结果:
MD5 digest result Hex: 8f03e2117c**********d9b9b18c58
调用堆栈:
MessageDigest.digest() is called!
java.lang.Throwableat java.security.MessageDigest.digest(Native Method)at ff.l0.h(RequestUtils.java:3)at ff.l0.c(RequestUtils.java:12)at lte.NCall.IL(Native Method)at com.shizhuang.duapp.common.helper.net.interceptor.HttpRequestInterceptor.intercept(HttpRequestInterceptor.java)at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:10)at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:1)at kb.b.intercept(MergeHostAfterInterceptor.java:11)at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:10)at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:1)at kb.d.intercept(MergeHostInterceptor.java:8)at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:10)at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:1)at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:13)at okhttp3.RealCall.execute(RealCall.java:8)at retrofit2.OkHttpCall.execute(OkHttpCall.java:18)at retrofit2.adapter.rxjava2.CallExecuteObservable.subscribeActual(CallExecuteObservable.java:5)at ac2.m.subscribe(Observable.java:7)at retrofit2.adapter.rxjava2.BodyObservable.subscribeActual(BodyObservable.java:1)at ac2.m.subscribe(Observable.java:7)at pc2.j1.subscribeActual(ObservableMap.java:1)at ac2.m.subscribe(Observable.java:7)at io.reactivex.internal.operators.observable.ObservableRetryWhen$RepeatWhenObserver.subscribeNext(ObservableRetryWhen.java:5)at io.reactivex.internal.operators.observable.ObservableRetryWhen.subscribeActual(ObservableRetryWhen.java:7)at ac2.m.subscribe(Observable.java:7)at io.reactivex.internal.operators.observable.ObservableRetryBiPredicate$RetryBiObserver.subscribeNext(ObservableRetryBiPredicate.java:3) at io.reactivex.internal.operators.observable.ObservableRetryBiPredicate.subscribeActual(ObservableRetryBiPredicate.java:4)at ac2.m.subscribe(Observable.java:7)at io.reactivex.internal.operators.observable.ObservableSubscribeOn$a.run(ObservableSubscribeOn.java:1)at io.reactivex.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:3)at io.reactivex.internal.schedulers.ScheduledDirectTask.call(ScheduledDirectTask.java:1)at java.util.concurrent.FutureTask.run(FutureTask.java:237)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)at java.lang.Thread.run(Thread.java:761)
ff.l0.h
根据调用堆栈 hook 一下 ff.l0.h 方法,打印的传参和返回值
/*** Hook 指定类的指定方法(包括所有重载)* @param {string} className - Java 类的完整名* @param {string} methodName - 方法名*/
function hook_method(className, methodName) {Java.perform(function () {const Map = Java.use("java.util.Map");const MapEntry = Java.use("java.util.Map$Entry"); // 👈 必须显式声明 Map.Entry 类型const targetClass = Java.use(className);const overloads = targetClass[methodName].overloads;for (let i = 0; i < overloads.length; i++) {overloads[i].implementation = function () {let log = "\n================= HOOK START =================\n";log += "🎯 Class: " + className + "\n";log += "🔧 Method: " + methodName + "\n";log += "📥 Arguments:\n";for (let j = 0; j < arguments.length; j++) {const arg = arguments[j];try {// 如果是 Map 类型打印 Map 中的内容if (Map.class.isInstance(arg)) {log += ` [${j}] Map content:\n`;const entrySet = Java.cast(arg, Map).entrySet();const iterator = entrySet.iterator();while (iterator.hasNext()) {const rawEntry = iterator.next();const entry = Java.cast(rawEntry, MapEntry); // 👈 强制转换const k = entry.getKey();const v = entry.getValue();log += ` ${k} => ${v}\n`;}} else {log += ` [${j}]: ${arg.toString()}\n`;}} catch (e) {log += ` [${j}]: ${arg}\n`;}}const retval = this[methodName].apply(this, arguments);log += `📤 Return value: ${retval}\n`;log += "================== HOOK END ==================\n";console.log(log);return retval;};}});
}/*** Hook 指定类的所有方法(每个方法所有重载)* @param {string} className - Java 类的完整名*/
function hook_all_methods(className) {Java.perform(function () {var clazz = Java.use(className);var methods = clazz.class.getDeclaredMethods(); // 反射获取所有声明的方法var hooked = new Set(); // 用于避免重复 hook 相同方法名(因为多重载)methods.forEach(function (m) {var methodName = m.getName();// 如果这个方法已经 Hook 过,就跳过if (hooked.has(methodName)) return;hooked.add(methodName);try {hook_method(className, methodName);} catch (e) {console.error("❌ Failed to hook " + methodName + ": " + e);}});});
}setImmediate(function () {// hook_method('ff.l0', 'c')hook_all_methods("ff.l0");
});// frida -H 127.0.0.1:1234 -F -l hook_class_methods.js
输出如下:
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: h
📥 Arguments:[0]: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/A+KLRtWKw+AfFWotfKtx+5J+ONciO*********************************************************************************************iuI9AfGYr9R817W8CfUGlVASAn1T6bq4D7DF1sHPqUITT76LLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
📤 Return value: 997202002b**********37e2534113
================== HOOK END ==================
可以看出 ff.l0.h 的返回值和 md5加密后的值是一样的,所以 ff.l0.h 其实就是一个md5 加密方法
ff.l0.c
再往上层Hook ff.l0.c,输出如下:
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: c
📥 Arguments:[0] Map content:cipherParam => userNamecountryCode => 86password => 61f209b789**********6ad80b3a00type => pwduserName => 5f67625e05**********d138c2eb14_1[1]: 1750303548243[2]:
📤 Return value: 997202002b**********37e2534113
================== HOOK END ==================
第一个参数是一个 Map,存放的就是需要加密的请求参数。
dex 脱壳
使用 jadx 反编译 apk 并没有找到 ff.l0,应该是加了抽取壳,把 ff.l0 抽取到其他地方,运行时才恢复。
使用 frida_dex_dump 脱壳 dex
function getProcessName() {var openPtr = Module.getExportByName('libc.so', 'open');var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);var readPtr = Module.getExportByName("libc.so", "read");var read = new NativeFunction(readPtr, "int", ["int", "pointer", "int"]);var closePtr = Module.getExportByName('libc.so', 'close');var close = new NativeFunction(closePtr, 'int', ['int']);var path = Memory.allocUtf8String("/proc/self/cmdline");var fd = open(path, 0);if (fd != -1) {var buffer = Memory.alloc(0x1000);var result = read(fd, buffer, 0x1000);close(fd);result = ptr(buffer).readCString();return result;}return "-1";
}function mkdir(path) {var mkdirPtr = Module.getExportByName('libc.so', 'mkdir');var mkdir = new NativeFunction(mkdirPtr, 'int', ['pointer', 'int']);var opendirPtr = Module.getExportByName('libc.so', 'opendir');var opendir = new NativeFunction(opendirPtr, 'pointer', ['pointer']);var closedirPtr = Module.getExportByName('libc.so', 'closedir');var closedir = new NativeFunction(closedirPtr, 'int', ['pointer']);var cPath = Memory.allocUtf8String(path);var dir = opendir(cPath);if (dir != 0) {closedir(dir);return 0;}mkdir(cPath, 755);chmod(path);
}function chmod(path) {var chmodPtr = Module.getExportByName('libc.so', 'chmod');var chmod = new NativeFunction(chmodPtr, 'int', ['pointer', 'int']);var cPath = Memory.allocUtf8String(path);chmod(cPath, 755);
}function readStdString(str) {const isTiny = (str.readU8() & 1) === 0;if (isTiny) {return str.add(1).readUtf8String();}return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}function findSymbolInLib(libname, keywordList) {const libBase = Module.findBaseAddress(libname);if (!libBase) {console.error("[-] Library not loaded:", libname);return null;}const matches = [];const symbols = Module.enumerateSymbolsSync(libname);for (const sym of symbols) {if (keywordList.every(k => sym.name.includes(k))) {matches.push(sym);}}if (matches.length === 0) {console.error("[-] No matching symbol found for keywords:", keywordList);return null;}const target = matches[0]; // 取第一个匹配的console.log("[+] Found symbol:", target.name, " @ ", target.address);return target.address;
}function dumpDexToFile(filename, base, size) {// packageNamevar processName = getProcessName();if (processName != "-1") {// 判断是否以 .dex 结尾if (!filename.endsWith(".dex")) {filename += ".dex";}const dir = "/sdcard/Android/data/" + processName + "/dump_dex";const fullPath = dir + "/" + filename.replace(/\//g, "_").replace(/!/g, "_");// 创建目录mkdir(dir);// dump dexvar fd = new File(fullPath, "wb");if (fd && fd != null) {var dex_buffer = ptr(base).readByteArray(size);fd.write(dex_buffer);fd.flush();fd.close();console.log("[+] Dex dumped to", fullPath);}}
}function hookDexFileLoaderOpenCommon() {const addr = findSymbolInLib("libdexfile.so", ["DexFileLoader", "OpenCommon"]);if (!addr) return;Interceptor.attach(addr, {onEnter(args) {const base = args[0]; // const uint8_t* baseconst size = args[1].toInt32(); // size_t sizeconst location_ptr = args[4]; // const std::string& locationconst location = readStdString(location_ptr);console.log("\n[*] DexFileLoader::OpenCommon called");console.log(" base :", base);console.log(" size :", size);console.log(" location :", location);// 文件名const filename = location.split("/").pop();// 魔数var magic = ptr(base).readCString();console.log(" magic :", magic)// dex 格式校验if (magic.indexOf("dex") !== -1) {dumpDexToFile(filename, base, size)}},onLeave(retval) {}});
}setImmediate(hookDexFileLoaderOpenCommon);// frida -H 127.0.0.1:1234 -l dump_dex_from_open_common.js -f com.cyrus.example
参考:
-
ART 下 Dex 加载流程源码分析 和 通用脱壳点
-
https://github.com/CYRUS-STUDIO/frida_dex_dump
日志输出如下:
[+] Found symbol: _ZN3art13DexFileLoader10OpenCommonEPKhmS2_mRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS0_12VerifyResultE @ 0x7be3891c28
Spawned `com.shizhuang.duapp`. Use %resume to let the main thread start executing!
[Remote::com.shizhuang.duapp]-> %resume
[Remote::com.shizhuang.duapp]->
[*] DexFileLoader::OpenCommon calledbase : 0x7bd87de02csize : 450032location : /system/framework/org.apache.http.legacy.jarmagic : dex
039
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/org.apache.http.legacy.jar.dex[*] DexFileLoader::OpenCommon calledbase : 0x7bd87de02csize : 450032location : /system/framework/org.apache.http.legacy.jarmagic : dex
039
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/org.apache.http.legacy.jar.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b75c69000size : 8681372location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apkmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b7471e000size : 12888744location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes2.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes2.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b73b1b000size : 12592256location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes3.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes3.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b72f75000size : 12213596location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes4.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes4.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b7254f000size : 10637856location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes5.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes5.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b71d5e000size : 8324572location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes6.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes6.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b71a3e000size : 3273924location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes7.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes7.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b71270000size : 8183732location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes8.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes8.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b706ff000size : 11994176location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes9.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes9.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b6fc62000size : 11125808location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes10.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes10.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b6f0a2000size : 12319700location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes11.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes11.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b6e4e6000size : 12300396location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes12.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes12.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b6d8e4000size : 12587972location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes13.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes13.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b6cd5e000size : 12081268location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes14.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes14.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b6c15c000size : 12590752location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes15.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes15.dex[*] DexFileLoader::OpenCommon calledbase : 0x7bd2eb7000size : 1260244location : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/base.apk!classes16.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/base.apk_classes16.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b5e4497fcsize : 3782924location : /system/product/app/webview/webview.apkmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/webview.apk.dex[*] DexFileLoader::OpenCommon calledbase : 0x7bcd38e138size : 77880location : /system/product/app/webview/webview.apk!classes2.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/webview.apk_classes2.dex[*] DexFileLoader::OpenCommon calledbase : 0x7b5e4497fcsize : 3782924location : /system/product/app/webview/webview.apkmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/webview.apk.dex[*] DexFileLoader::OpenCommon calledbase : 0x7bcd38e138size : 77880location : /system/product/app/webview/webview.apk!classes2.dexmagic : dex
035
[+] Dex dumped to /sdcard/Android/data/com.shizhuang.duapp/dump_dex/webview.apk_classes2.dex
把所有 dex 拉取到本地
adb pull /sdcard/Android/data/com.shizhuang.duapp/dump_dex
反编译 dex
通过 grep 命令查找 “ff/l0” 在哪些 dex 有引用
wayne:/sdcard/Android/data/com.shizhuang.duapp/dump_dex # grep -rl "ff/l0" *.dex
base.apk_classes10.dex
base.apk_classes11.dex
base.apk_classes12.dex
base.apk_classes13.dex
base.apk_classes14.dex
base.apk_classes15.dex
base.apk_classes2.dex
base.apk_classes4.dex
base.apk_classes5.dex
base.apk_classes6.dex
base.apk_classes7.dex
base.apk_classes9.dex
使用 dex2jar 批量转换 dex
参考:FART 脱壳某大厂 App + CodeItem 修复 dex + 反编译还原源码
使用 jadx 反编译 ff.l0.c 方法得到源码如下:
public static String c(Map<String, Object> map, long j, String str) {synchronized (l0.class) {try {if (map == null) {return "";}map.put("uuid", he.a.i.t());map.put("platform", "android");map.put(NotifyType.VIBRATE, he.a.i.b());if (str == null) {str = "";}map.put("loginToken", str);map.put("timestamp", String.valueOf(j));String i = i(map);he.a.m.d(TAG, "StringToSign-body use Gson " + i);String doWork = DuHelper.doWork(he.a.h, i);map.remove("uuid");return h(doWork);} finally {}}
}
ff.l0.c 中调用 i 方法把参数拼接成字符串。
/*** 将传入的 Map<String, Object> 转换成字符串形式(key + value 拼接),* 处理数组、集合、JsonArray 等特殊类型;其中特殊类型会转成以 "," 分隔的字符串。*/
public static String i(Map<String, Object> map) {// 最终用于拼接所有 key-value 的结果字符串StringBuilder sb3 = new StringBuilder();// 遍历 map 中的每一项for (Map.Entry<String, Object> entry : map.entrySet()) {sb3.append(entry.getKey()); // 拼接 keyObject value = entry.getValue(); // 获取 value// 如果是 org.json.JSONArray 类型,打印警告(不建议使用此类型)if (value instanceof org.json.JSONArray) {a.l lVar = he.a.m; // 获取日志工具(推测)StringBuilder d = a.d.d("Please Not use this params type: ");d.append(value.getClass());// 打印错误日志 + 堆栈信息lVar.c(d.toString(), new Throwable());}// 如果是 Java 原生数组类型if (value != null && value.getClass().isArray()) {int length = Array.getLength(value);ArrayList arrayList = new ArrayList();for (int i = 0; i < length; i++) {// 使用 id.e.o() 方法处理数组每个元素后加入列表arrayList.add(id.e.o(Array.get(value, i)));}// 将所有元素用 "," 拼接后追加到结果中sb3.append(TextUtils.join(",", arrayList));}// 如果是集合类(List、Set)或 JsonArrayelse if ((value instanceof Collection) || (value instanceof JsonArray)) {Iterable iterable = (Iterable) value;ArrayList arrayList2 = new ArrayList();Iterator it = iterable.iterator();while (it.hasNext()) {// 使用 id.e.o() 方法处理每个元素后加入列表arrayList2.add(id.e.o(it.next()));}// 将所有元素用 "," 拼接后追加到结果中sb3.append(TextUtils.join(",", arrayList2));}// 其他类型,直接处理后拼接else {sb3.append(id.e.o(value));}}// 返回拼接好的字符串return sb3.toString();
}
id.e.o() 方法,将任意对象 obj 转换成 String 类型
@NonNull
public static String o(@Nullable Object obj) {String n = n(obj);String str = n;if (n == null) {str = "";}return str;
}@Nullable
public static String n(@Nullable Object obj) {if (obj == null) {return null;}// 判断对象是否需要用 Gson 转 JSONClass<?> cls = obj.getClass();if (!(obj instanceof CharSequence) && !f.a(cls)) {try {return k().toJson(obj);} catch (Exception e2) {HashMap hashMap = new HashMap();hashMap.put("class", cls.toString());hashMap.put("json", obj.toString());he.a.j.c(e2, "app_error_GsonHelper_toJson", hashMap);return null;}}// 如果是 Float 或 Double,并且数值是整数(如 3.0),就转为整数形式字符串("3" 而不是 "3.0")if ((obj instanceof Float) || (obj instanceof Double)) {Number number = (Number) obj;if (number.doubleValue() == number.longValue()) {valueOf = String.valueOf(number.longValue());}}// 其余类型直接用 String.valueOf 转换valueOf = String.valueOf(obj);return valueOf;
}
参数最终是调用了 DuHelper.doWork 进行加密。
String doWork = DuHelper.doWork(he.a.h, i);
he.a.h 是 Context,i 是拼接后的参数
doWork 返回的字符串再调用 ff.l0.h(md5) 方法加密。
public static String h(String str) {try {MessageDigest messageDigest = MessageDigest.getInstance("MD5");messageDigest.update(str.getBytes());byte[] digest = messageDigest.digest();StringBuilder sb3 = new StringBuilder();for (byte b : digest) {String hexString = Integer.toHexString(b & 255);while (hexString.length() < 2) {hexString = PushConstants.PUSH_TYPE_NOTIFY + hexString;}sb3.append(hexString);}return sb3.toString();} catch (NoSuchAlgorithmException e2) {e2.printStackTrace();return "";}
}
DuHelper.doWork
查找 DuHelper 所在的 dex
wayne:/sdcard/Android/data/com.shizhuang.duapp/dump_dex # grep -rl "DuHelper" .
./base.apk_classes9.dex
./base.apk.dex
使用 jadx 反编译
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 进行加密
return (String) NCall.IL(new Object[]{283, obj, str});
lte.NCall.IL
查找 NCall 所在的 dex
1|wayne:/sdcard/Android/data/com.shizhuang.duapp/dump_dex # grep -rl "NCall" .
./base.apk_classes5.dex
./base.apk_classes9.dex
./base.apk_classes10.dex
./base.apk_classes11.dex
使用 jadx 反编译
package lte;/* loaded from: base.apk_classes11.jar:lte/NCall.class */
public class NCall {static {System.loadLibrary("GameVMP");}public static native byte IB(Object[] objArr);public static native char IC(Object[] objArr);public static native double ID(Object[] objArr);public static native float IF(Object[] objArr);public static native int II(Object[] objArr);public static native long IJ(Object[] objArr);public static native Object IL(Object[] objArr);public static native short IS(Object[] objArr);public static native void IV(Object[] objArr);public static native boolean IZ(Object[] objArr);public static native int dI(int i);public static native long dL(long j);public static native String dS(String str);
}
hook lte.NCall.IL 方法并打印参数和结果
function hook_NCall_IL() {// 获取类 lte/NCallvar NCall = Java.use("lte.NCall");// Hook 静态方法 IL([Ljava/lang/Object;)Ljava/lang/Object;NCall.IL.overload('[Ljava.lang.Object;').implementation = function (args) {// 合并日志var logMessage = "Hooked lte/NCall->IL() method\n";// 打印传入的参数logMessage += "Arguments: [";for (var i = 0; i < args.length; i++) {logMessage += args[i].toString();if (i < args.length - 1) {logMessage += ", ";}}logMessage += "]\n";// 调用原始方法并获取返回值var result = this.IL(args);// 打印返回值logMessage += "Result: " + result;// 输出合并的信息console.log(logMessage);// 返回结果return result;};
}setImmediate(function () {Java.perform(hook_NCall_IL);
})
执行脚本
frida -H 127.0.0.1:1234 -F -l hook_NCall.js
输出如下:
Hooked lte/NCall->IL() method
Arguments: [283, com.shizhuang.duapp.modules.app.DuApplication@e7edb59, loginTokenplatformandroidtimestamp1728414660226****************fb63v5.43.0]
Result: knQQXR0br7Lqn4eabvJsdZ4D96wrRcYi2zPW************************************uZgrvFlZJ0mCmQBrhQQOR1PtwTx8iu3Yfc4=
有点像 VMP 壳,283 是 index。
libGameVMP.so 脱壳
lte.NCall.IL 是一个 native 方法,具体实现在 libGameVMP.so
用 IDA 打开 so 会报错,说明 so 应该是加了壳。
通过 frida_dump 脱掉 so 的壳,并用 SoFixer 修复 so。
具体参考:一文搞懂 SO 脱壳全流程:识别加壳、Frida Dump、原理深入解析
找到 lte.NCall.IL 的 JNI 方法入口
脱壳后的 so 中找不到 NCall.IL 方法,说明是动态注册的。
通过 frida 打印 lte.NCall 类中所有 JNI 方法信息如下:
[+] Found native method: _Z32android_os_Process_getUidForNameP7_JNIEnvP8_jobjectP8_jstring @ 0x7c65c0d648
========== [ JNI Method Info Dump ] ==========
[*] Target class: lte.NCall
[*] entry_point_from_jni_ offset = 24 bytes------------ [ #1 Native Method ] ------------
Method Name : public static native byte lte.NCall.IB(java.lang.Object[])
ArtMethod Ptr : 0x9f63a590
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #2 Native Method ] ------------
Method Name : public static native char lte.NCall.IC(java.lang.Object[])
ArtMethod Ptr : 0x9f63a5b8
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #3 Native Method ] ------------
Method Name : public static native double lte.NCall.ID(java.lang.Object[])
ArtMethod Ptr : 0x9f63a5e0
Native Addr : 0x7b6b455028
Module Name : libGameVMP.so
Module Offset : 0xe028
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #4 Native Method ] ------------
Method Name : public static native float lte.NCall.IF(java.lang.Object[])
ArtMethod Ptr : 0x9f63a608
Native Addr : 0x7b6b454fe8
Module Name : libGameVMP.so
Module Offset : 0xdfe8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #5 Native Method ] ------------
Method Name : public static native int lte.NCall.II(java.lang.Object[])
ArtMethod Ptr : 0x9f63a630
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #6 Native Method ] ------------
Method Name : public static native long lte.NCall.IJ(java.lang.Object[])
ArtMethod Ptr : 0x9f63a658
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #7 Native Method ] ------------
Method Name : public static native java.lang.Object lte.NCall.IL(java.lang.Object[])
ArtMethod Ptr : 0x9f63a680
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #8 Native Method ] ------------
Method Name : public static native short lte.NCall.IS(java.lang.Object[])
ArtMethod Ptr : 0x9f63a6a8
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #9 Native Method ] ------------
Method Name : public static native void lte.NCall.IV(java.lang.Object[])
ArtMethod Ptr : 0x9f63a6d0
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #10 Native Method ] ------------
Method Name : public static native boolean lte.NCall.IZ(java.lang.Object[])
ArtMethod Ptr : 0x9f63a6f8
Native Addr : 0x7b6b454fa8
Module Name : libGameVMP.so
Module Offset : 0xdfa8
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #11 Native Method ] ------------
Method Name : public static native int lte.NCall.dI(int)
ArtMethod Ptr : 0x9f63a720
Native Addr : 0x7b6b45293c
Module Name : libGameVMP.so
Module Offset : 0xb93c
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #12 Native Method ] ------------
Method Name : public static native long lte.NCall.dL(long)
ArtMethod Ptr : 0x9f63a748
Native Addr : 0x7b6b452ad0
Module Name : libGameVMP.so
Module Offset : 0xbad0
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------------------ [ #13 Native Method ] ------------
Method Name : public static native java.lang.String lte.NCall.dS(java.lang.String)
ArtMethod Ptr : 0x9f63a770
Native Addr : 0x7b6b452aec
Module Name : libGameVMP.so
Module Offset : 0xbaec
Module Base : 0x7b6b447000
Module Size : 462848 bytes
Module Path : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------[*] Total native methods found: 13
===============================================
参考:逆向 JNI 函数找不到入口?动态注册定位技巧全解析
找到 lte.NCall.IL(java.lang.Object[]) 方法在 libGameVMP.so 偏移 0xdfa8 的位置
__int64 __fastcall sub_DFA8(JNIEnv *env, jclass clazz, jobjectArray arr)
{return sub_17EB8((__int64)env, (__int64)arr);
}
OLLVM bcf(虚假控制流)
NCall.IL 实际调用的是 sub_17EB8 函数,而且函数内部大量引用了x y 开头的全局变量。
这个其实是做了 OLLVM 虚假控制流(bcf)混淆,通过伪条件隐藏真实的代码执行流。
关于 OLLVM 具体参考:
-
移植 OLLVM 到 LLVM 18,C&C++代码混淆
-
移植 OLLVM 到 Android NDK,Android Studio 中使用 OLLVM
-
OLLVM 增加 C&C++ 字符串加密功能
使用 Frida 反 OLLVM
1. 固定参数
为了方便分析,先固定一下 lte.NCall.IL 方法的调用参数
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");// 1. 创建 Integer 对象(包装 int)const arg0 = Integer.valueOf(283);// 2. 获取静态字段 instanceconst arg1 = DuApplication.instance.value;// 3. 构造字符串参数const arg2 = String.$new("cipherParamuserNamecountryCode86loginTokenpassword6716c*******************************************************42195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0");// 构造 Object[] 参数数组const argsArray = Java.array("java.lang.Object", [arg0, arg1, arg2]);// 5. 调用 NCall.IL(Object[])const result = NCall.IL(argsArray);// 6. 打印结果console.log("NCall.IL 返回值:", result);});
}
调用返回结果如下:
[Remote::**]-> NCall_IL()
NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/VILfEPi7ImlGxmmwj6+taHk6jQ4T********************************************************************************************rp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
2. 获取加密串调用堆栈
通过 Hook jstring 相关 JNI 接口,快速定位加密算法的具体位置,越过 OLLVM 混淆 + VMP 壳
参考:破解 VMP+OLLVM 混淆:通过 Hook jstring 快速定位加密算法入口
得到调用堆栈如下:
[Remote::cyrus]-> NCall_IL()====== 🧪 NewStringUTF Hook ======
📥 Input C String: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+**********************************************************************************************************************************************xrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+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: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ*********************************fEPi7ImlGxmmwj6+taHk6jQ4Tog7XzBbL
====== ✅ Hook End ======NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/VILfEPi7ImlGxmmwj6+taHk6jQ4T********************************************************************************************rp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
从日志输出可以知道:NewStringUTF 在 libdewuhelper.so 的 encode 函数中被调用,在 so 偏移 0x185c 处。
libdewuhelper.so
使用 frida dump 脱壳 libdewuhelper.so
python dump_so.py libdewuhelper.so
使用 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: "0101101000100010100100100000110001110010111010101010001011101110****************************************************************1111001011100010101000100100110010110010100010101011110010111100"
[<] encode 返回值: "dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQX******************************************k6jQ4Tog7XzBbLATwfAwFewviaX1/8WS4J271k/SPo
cXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA=="
NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC*******************************************6jQ4Tog7XzBbLATwfAwFewviaX1/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("cipherParamuserNamecountryCode86loginTokenpassword6716c*******************************************************42195743typepwduserNamef37bfa14057cf018011db67c963cd733_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: "0101101000100010100100100000110001110010111010101010001011101110****************************************************************1111001011100010101000100100110010110010100010101011110010111100"
[+] getValue returned: 0x7bd7646280 -> "****************"
NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/VILfEPi7ImlGxmmwj6+taHk6jQ4T********************************************************************************************rp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+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("cipherParamuserNamecountryCode86loginTokenpassword6716c*******************************************************42195743typepwduserNamef37bfa14057cf018011db67c963cd733_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+atsiQXC+*************************************************g7XzBbLATwfAwFewviaX1/8WS4J271k/SPocXykU4wnASDm+kFk63OxOynX9B1wA42cTOy3rHZ3W/ll1gBxtH5hmdGpnYqxrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==// app 返回的加密串
dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+*************************************************g7XzBbLATwfAwFewviaX1/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("cipherParamuserNamecountryCode86loginTokenpassword6716c*******************************************************42195743typepwduserNamef37bfa14057cf018011db67c963cd733_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+atsiQXC+**********************************************************************************************************************************************xrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
======= b64_encode END =======NCall.IL 返回值: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+**********************************************************************************************************************************************xrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
所以加密数据的实际长度是 208,并不是 223。
把 hexdump 复制到 CyberChef 使用标准 base64 编码结果 和 NCall.IL 返回值是一样的,也就是说 b64_encode 就是一个标准的 base64 编码方法。
使用 CyberChef 验证算法
所以 encode 方法的算法逻辑是:AES ECB 加密 + 标准 Base64 编码
对比 NCall.IL 方法的返回值是一致的。
使用 Python 还原 newSign 算法
下面是使用 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 = "cipherParamuserNamecountryCode86loginTokenpassword6716c*******************************************************42195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0"key = "****************" # 16字节 AES 密钥result = newSign(text, key)print("newSign 结果:", result)
运行输出如下:
======= newSign 开始 =======
[AES] 原文: cipherParamuserNamecountryCode86loginTokenpassword6716c*******************************************************42195743typepwduserNamef37bfa14057cf018011db67c963cd733_1********9b381828fb63v5.43.0
[AES] 密钥: ****************
[AES] 加密结果(Hex): 7565a85e56d1dcaf3b8f6376ec392fe2e38f5273ac874c6b279b7e6adb224170befdd20df0aa1ff469b6c7592297b4bf5482df10f8bb226946c669b08fafad68793a8d0e13******************************************************************************************************************************************8ab1ae9d83595ccc7c65e9db8d3cdafac89d3e06674a276d92fce01f3c58d0d2a85dec8fe4cb36849d9f7d5699218ff207552f40ae00a0c51f65e3f4aadbff48cdb0f80d9c
[Base64] 编码结果: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+**********************************************************************************************************************************************xrp2DWVzMfGXp24082vrInT4GZ0onbZL84B88WNDSqF3sj+TLNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
[MD5] Hash 结果: 92d2d46c07**********c281ccaa4c
======= newSign 结束 =======newSign 结果: 92d2d46c07**********c281ccaa4c
相关文章:
逆向某物 App 登录接口:还原 newSign 算法全流程
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/ newSign 参数分析 通过 Hook Java 层加密算法得到 newSign 参数相关信息如下: 具体参考:逆向某物 App 登录接口:抓包分析…...
2140、解决智力问题
题目 解答 正向不好做,反向遍历。 定义:dp[i] [i,n)的分数 初始化:dp[n]0 递推:dp[i]max(dp[i1],questions[i][0]dp[iquestions[i][1]1]) 如果越界了,就截断到dp[n] 最后return dp[0]即可 class Solution { publ…...
肖臻《区块链技术与应用》第六讲:比特币网络
一、分层架构:应用层之下的P2P网络 比特币并非凭空运作,它的协议运行在互联网的应用层之上。而在其底层,支撑整个系统的是一个对等网络(Peer-to-Peer, P2P)。可以这样理解: 应用层 (Application Layer): …...
(C++)素数的判断(C++教学)(C语言)
源代码: #include <iostream> using namespace std;int fun(int num){if(num<1){return 1;}if(num%20){return 0;}else{return 2;} }int main(){while (1){int y0;int num0;cout<<"请输入一个整数:\n";cin>>num;yfun(nu…...
openai-agents实现input_guardrails
目录 版本模块引入自定义LLM模型input_guardrail设置main函数 代码: input_guardrails.ipynb 版本 import agents print(agents.__version__)0.0.19模块引入 from __future__ import annotationsfrom pydantic import BaseModelfrom agents import (Agent,Guardr…...
在高数中 导数 微分 不定积分 定积分 的意义以及联系
在高等数学中,导数、微分、不定积分、定积分是微积分的核心概念,它们既有明确的定义和几何/物理意义,又相互关联。下面分别说明它们的意义,并总结它们之间的联系。 导数的意义 定义: 函数 y f(x) 在点 x 处的导数定义…...
Linux系统基本操作指令
Linux系统基本操作指令 文章目录 Linux系统基本操作指令一、介绍二、基础设置2.1 设置ubuntu与window的共享目录2.2 ubuntu系统简单介绍 三、Linux命令及工具介绍3.1 目录管理命令(功能,格式,参数,系统参数)3.2 文件操作命令 四、网络命令4.1…...
「Linux文件及目录管理」vi、vim编辑器
知识点解析 vi/vim编辑器简介 vi:Linux默认的文本编辑器,基于命令行操作,功能强大。vim:vi的增强版,支持语法高亮、多窗口编辑、插件扩展等功能。vi/vim基本模式 命令模式:默认模式,用于移动光标、复制、粘贴、删除等操作。插入模式:按i进入,用于输入文本。末行模式:…...
等等等等等等
欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和技术。 …...
JAVA集合篇--深入理解ConcurrentHashMap图解版
一、前言 在Java并发编程中,线程安全的Map实现一直是一个重要话题。虽然我们可以使用Collections.synchronizedMap()或者HashTable来获得线程安全的Map,但它们的性能在高并发场景下往往不尽人意。ConcurrentHashMap作为Java并发包中的重要组件࿰…...
Python嵌套循环
一、前言 在 Python 编程中,嵌套循环(Nested Loops) 是指在一个循环的内部再嵌套另一个循环。这种结构常用于处理多维数据结构(如二维数组、矩阵)、遍历组合数据、图形绘制等场景。 虽然嵌套循环在逻辑上更复杂&…...
linux编译安装nginx
1.到官网(nginx)下载nginx压缩包: 2.以(nginx-1.24.0.tar.gz)为例: 1.上传压缩包至linux服务器: rz 2.解压压缩包nginx-1.24.0.tar.gz: tar -zxvf nginx-1.24.0.tar.gz 3.在安装Nginx之前,需…...
算法-动态规划-钢条切割问题
钢条切割问题是一个经典的动态规划问题,旨在通过切割钢条获得最大收益。以下是详细解释和解决方案: 问题描述 给定长度为 n 的钢条和价格表 p,其中 p[i] 表示长度为 i 的钢条的价格(i 1, 2, ..., n)。目标ÿ…...
Java八股文——系统场景设计
如何设计一个秒杀场景? 面试官您好,设计一个秒杀系统,是对一个工程师综合技术能力的巨大考验。它的核心挑战在于,如何在极短的时间内,应对超高的并发请求,同时保证数据(尤其是库存)…...
如何在FastAPI中玩转GitHub认证,让用户一键登录?
title: 如何在FastAPI中玩转GitHub认证,让用户一键登录? date: 2025/06/22 09:11:47 updated: 2025/06/22 09:11:47 author: cmdragon excerpt: GitHub第三方认证集成通过OAuth2.0授权码流程实现,包含用户跳转GitHub认证、获取授权码、交换访问令牌及调用API获取用户信息四…...
[RPA] 影刀RPA实用技巧
1.给数字添加千分位分隔符 将变量variable的数值(2025.437)添加千分位分隔符,使其变为2,025.437 流程搭建: 关键指令: 2.删除网页元素 将bilibili官网的"动态"图标进行删除 流程搭建: 关键指令: 呈现效果…...
RA4M2开发IOT(7)----RA4M2驱动涂鸦CBU模组
RA4M2开发IOT.7--RA4M2驱动涂鸦CBU模组 概述视频教学样品申请硬件准备参考程序初始化 LSM6DSV16X 传感器初始化单双击识别主程序接口RA4M2接口生成UARTUART属性配置R_SCI_UART_Open()函数原型回调函数user_uart_callback0 ()变量定义更新敲击状态DP同步长按进入配网涂鸦协议解析…...
华为公布《鸿蒙编程语言白皮书》V1.0 版:解读适用场景
6 月 22 日消息,华为现已在其开发者网站上架《鸿蒙编程语言白皮书》V1.0 版本,主要围绕鸿蒙 HarmonyOS 整体框架、适用场景、演进策略、未来愿景四大角度进行阐述,文档访问地址(https://developer.huawei.com/consumer/cn/doc/gui…...
多源异构数据接入与实时分析:衡石科技的技术突破
在数字化转型的浪潮中,企业每天产生的数据量呈指数级增长。这些数据来自CRM系统、IoT设备、日志文件、社交媒体、交易平台等众多源头,格式各异、结构混乱、流速不一。传统的数据处理方式如同在无数孤立的岛屿间划着小船传递信息,效率低下且无…...
多设备Obsidian笔记同步:WebDAV与内网穿透技术高效实现教程
文章目录 前言1. Windows开启Webdav服务2. 客户端测试3. 安装Cpolar内网穿透实现公网访问Webdav4. 同步PC端笔记至WebDav4.1 首先需要在IIS中添加md的格式4.2 在Obsidian中安装第三方插件 5. 同步手机端笔记至WebDav 前言 各位好!在数字化浪潮席卷的当下࿰…...
Linux->进程概念(精讲)
引入:本文会讲到的东西有哪些? 注:要讲就讲清楚,所以从0到懂,目录在右侧 一:冯诺依曼体系结构 1:人物介绍 冯诺依曼是一个伟大的人,他提出了一个体系结构,被命名冯诺依…...
【舞蹈】PC-Dance:姿势可控的音乐驱动舞蹈合成
PC-Dance:姿势可控的音乐驱动舞蹈合成 自监督节奏对齐学习音乐到舞蹈的对齐嵌入-PC-Syn 中,依然怒了一种用于 自适应运动图构建(AMGC)的高效方案,可以基于图的优化效率并保持动作的多样性。 舞蹈合成 整体情况 我们的系统主要由音乐到舞蹈对齐嵌 入网络(M2D-Align)和姿势…...
uni-app项目实战笔记22--图片预览和切换
需求描述: 1、图片预览时,通常需要知道,当前预览的是第几张,总共有多少张图片; 2、当用户左右滑动切换预览图片时,当前预览索引需要随着进行切换。 下面简单介绍下实现过程: 1、在图片列表页…...
[特殊字符] AIGC工具深度实战:GPT与通义灵码如何彻底重构企业开发流程
🔍 第一模块:理念颠覆——为什么AIGC不是“玩具”而是“效能倍增器”? ▍企业开发的核心痛点图谱(2025版) 研发效能瓶颈:需求膨胀与交付时限矛盾持续尖锐,传统敏捷方法论已触天花板…...
华为OD机考-用户调度问题-DP(JAVA 2025B卷)
import java.util.Scanner;public class UserScheduling {public static void main(String[] args) {Scanner scanner new Scanner(System.in);int n scanner.nextInt(); // 用户个数int[][] costs new int[n][3]; // 存储每个用户使用A/B/C策略的系统消耗for (int i 0; i …...
【论文阅读 | CVPR 2024 |Fusion-Mamba :用于跨模态目标检测】
论文阅读 | CVPR 2024 |Fusion-Mamba :用于跨模态目标检测 1.摘要&&引言2.方法2.1 预备知识2.2 Fusion-Mamba2.2.1 架构特征提取与多模态融合(FMB模块)FMB的应用与输出2.2.2 关键组件3.2.2.1 SSCS 模块:浅层跨模态特征交互…...
Python 数据分析与可视化 Day 4 - Pandas 数据筛选与排序操作
🎯 今日目标 掌握 Pandas 中 groupby() 的使用方式学会使用 agg() 方法进行多个聚合掌握 pivot_table() 构建透视表结合分组与排序进行更深入的分析 🧮 一、基本分组统计(groupby) ✅ 分组 单列聚合 df.groupby("性别&qu…...
基于Vue.js的图书管理系统前端界面设计
一、系统前端界面设计要求与效果 (一)系统功能结构图 设计一个基于Vue.js的图书管理系统前端界面。要充分体现Vue的核心特性和应用场景,同时结合信息管理专业的知识。要求系统分为仪表盘、图书管理、借阅管理和用户管理四个主要模块&#x…...
FPGA故障注入测试软件使用指南
有数字芯片之母别称的FPGA,是国内在半导体行业率先取得重大突破的细分赛道,正迎来技术和市场形成共振的黄金发展期。 国内拥有最多的应用设计工程师与新兴从业人员,但到目前为止,还没有一款位流级别的专用EDA软件,服务用户日常应用开发所需的调试验证工作。 第一大厂商赛…...
Oracle 数据库查询:单表查询
作者:IvanCodes 日期:2025年6月22日 专栏:Oracle教程 在 Oracle 数据库操作中,查询数据是最频繁、最核心的操作之一。单表查询,即仅从一个表中检索信息,是所有复杂查询的基础。本笔记将系统梳理单表查询的关…...
【DDD】——带你领略领域驱动设计的独特魅力
🎼个人主页:【Y小夜】 😎作者简介:一位双非学校的大三学生,编程爱好者, 专注于基础和实战分享,欢迎私信咨询! 🎆入门专栏:🎇【MySQL࿰…...
阿里云CentOS系统搭建全攻略:开启云端技术之旅
前期准备:开启云端征程前的必备事项 在当今数字化时代,云计算已成为企业和开发者构建应用和服务的重要基础设施。阿里云作为全球领先的云计算服务提供商,提供了丰富的云计算产品和服务,其中 CentOS 系统在阿里云上的应用非常广泛…...
Flink图之间流转解析:从逻辑构建到物理执行的深度剖析
在Flink强大的数据处理体系中,Table Connectors实现了与外部结构化数据的高效交互,而Flink作业从代码到实际执行的背后,是各类图结构之间的流转与转换。这些图结构承载着作业的逻辑定义、任务划分与资源调度等关键信息,其流转过程…...
详解Redis数据库和缓存不一致的情况及解决方案
数据库与缓存不一致是分布式系统中常见问题,本质是数据在缓存层和存储层出现版本差异。 一、并发写操作导致不一致(最常见) 场景描述 线程A更新数据库 → 线程B更新数据库 → 线程B更新缓存 → 线程A更新缓存 结果:缓存中存储的…...
【CSS】CSS3媒体查询全攻略
媒体查询教程 媒体查询(Media Queries)是CSS3中引入的强大功能,允许内容根据设备特性(如屏幕尺寸、分辨率、方向等)进行自适应调整。以下是媒体查询的详细教程: 基本语法 media mediatype and (media feature) {/* CSS规则 */ }常用媒体类型 all - 所…...
深入理解Spring的ResponseBodyAdvice接口
什么是ResponseBodyAdvice? ResponseBodyAdvice是Spring框架4.2版本引入的一个非常有用的接口,它允许我们在控制器方法执行后、响应体写入前对响应进行统一处理。这个接口为开发者提供了对返回数据进行统一拦截和修改的能力,是Spring MVC响应处理流程中…...
C++法则5: 在函数调用过程中,具有非引用类型的参数要进行拷贝初始化。
C法则5: 在函数调用过程中,具有非引用类型的参数要进行拷贝初始化。 在 C 中,法则5指的是:当函数参数是非引用类型(即按值传递)时,传递给函数的实参会进行拷贝初始化(copy initializ…...
Python 使用 Requests 模块进行爬虫
目录 一、请求数据二、获取并解析数据四、保存数据1. 保存为 CSV 文件2. 保存为 Excel 文件打开网页图片并将其插入到 Excel 文件中 五、加密参数逆向分析1. 定位加密位置2. 断点调试分析3. 复制相关 js 加密代码,在本地进行调试(难)4. 获取 …...
day039-nginx配置补充
文章目录 0. 老男孩思想-如何提升能力?1. nginx登录认证功能1.1 创建密码文件1.2 修改子配置文件1.3 重启服务 2. nginx处理请求流程3. 配置默认站点4. location 命令5. 案例1-搭建大型直播购物网站5.1 配置本地hosts解析5.2 编写子配置文件5.3 创建相关目录/文件并…...
K8s入门指南:架构解析浓缩版与服务间调用实战演示
目录 前言一、k8s概念理解1、k8s整体架构(1) Master 主节点(2) Node 工作节点(3) Etcd 键值存储数据库 2、Pod被视为最小的部署单元3、k8s的五种控制器类型(1)…...
如何用AI开发完整的小程序<10>—总结
通过之前9节的学习。 如何用Ai制作一款简单小程序的内容就已经都介绍完了。 总结起来就以下几点: 1、搭建开发制作环境 2、创建页面(需要手动) 3、在页面上制作UI效果(让Ai搞,自己懂了后可以自己调) 4…...
Javaweb - 3 CSS
CSS 层叠样式表(Cascading Style Sheets),能够对网页中元素位置的排版进行像素级精确控制,支持几乎所有的字体字号样式,拥有对网页对象和模型样式编辑的能力。 简单来说,HTML 搭建一个毛坯房,C…...
【算法】【优选算法】优先级队列
目录 一、1046.最后一块石头的重量二、703. 数据流中的第 K 大元素三、692. 前 K 个⾼频单词四、295. 数据流的中位数 一、1046.最后一块石头的重量 题目链接:1046.最后一块石头的重量 题目描述: 题目解析: 题意就是让我们拿出提供的数组…...
PaddleOCR + Flask 构建 Web OCR 服务实战
1、前言 随着图像识别技术的发展,OCR(光学字符识别)已经成为很多应用场景中的基础能力。PaddleOCR 是百度开源的一个高性能 OCR 工具库,支持中英文、多语言、轻量级部署等特性。 而 Flask 是一个轻量级的 Python Web 框架,非常适合快速构建 RESTful API 或小型 Web 应用…...
openapi-generator-maven-plugin自动生成HTTP远程调用客户端
Java开发中调用http接口的时候,有很多可选的技术方案,比如:HttpURLConnection、RestTemplate、WebClient、Feign、Retrofit、Okhttp等,今天我们来看一个更优的技术方案OpenAPI Generator(http://openapi-generator.tech/) OpenAP…...
ms-swift 部分命令行参数说明
参考链接 命令行参数 — swift 3.6.0.dev0 文档 Qwen Chat num_train_epochs 训练的epoch数,默认为3 假设你有 1000 条训练样本,并且设置了: num_train_epochs 3 这意味着: 模型会完整地遍历这 1000 条数据 3 次。每一次…...
【学习笔记】深入理解Java虚拟机学习笔记——第10章 前端编译与优化
第10章 前端编译与优化 10.1 概述 1>前端编译器:Javac命令。 【.java文件->.class文件】 2>即时编译器:Hotspot.C1.C2 【.class文件->机器码】 3>提前编译器:JDK的Jaotc等【.java->机器码】 10.2 Javac 编译器 10.2.1 …...
删除node并且重装然后重装vue
参考第一篇文章 node.js卸载与安装超详细教程_node卸载重装-CSDN博客 第二篇文章安装vue Vue安装与配置教程(非常详细)_安装vue-CSDN博客...
Flink源码阅读环境准备全攻略:搭建高效探索的基石
想要深入探索Flink的底层原理,搭建一套完整且适配的源码阅读环境是必经之路。这不仅能让我们更清晰地剖析代码逻辑,还能在调试过程中精准定位关键环节。接下来,结合有道云笔记内容,从开发工具安装、源码获取导入到调试配置&#x…...
【破局痛点,赋能未来】领码 SPARK:铸就企业业务永续进化的智慧引擎—— 深度剖析持续演进之道,引领数字化新范式
摘要 在瞬息万变的数字时代,企业对业务连续性、敏捷创新及高效运营的需求日益迫切。领码 SPARK 融合平台,秉持“持续演进”这一核心理念,以 iPaaS 与 aPaaS 为双擎驱动,深度融合元数据驱动、智能端口调度、自动化灰度切换、AI 智…...