首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
20
19
[原创]最新版某气骑士分析记录-绕过某讯的防御dump内存并解密存档
发表于: 2026-5-5 17:01
11796
[原创]最新版某气骑士分析记录-绕过某讯的防御dump内存并解密存档
RytterMohn
2026-5-5 17:01
11796
# 最新版某气骑士分析记录-绕过某讯的防御dump内存并解密存档 前段时间想找一个能简单打发时间的小游戏,于是就想到了元气骑士,但是元气骑士的旧账号在另一个手机上,那个手机不在身边,但是我又不想重头开始玩,作为网安学生,于是有了自己逆向的想法。 ## 逆向分析 首先逆向一下元气骑士的apk包,一眼看到这是一个unity开发的游戏,然后同时看到的还有libijiami的so文件,说明这是一个使用爱加密的壳,这个il2cpp也必要逆向了,必然是加了密的:  如果想要拿到未加密的libil2cpp,一个可行的方法就是去内存找,因为代码加载的时候,必然是从内存中加载出去的: 通过万能的github,我找到可以扒内存的apk,并且还能自动fix ELF:  然后通过这个方法可以扒出来libil2cpp.so,but,等俺使用il2cpp dump的时候,发现解析不出来  完蛋,发现global-metadata.dat是加密的,所以需要用点其他手段,想办法解出来,使用frida对应用先attach一下看看:  MD,炸了,说明有frida检测,还能怎么办,接着搞呗,先看看加载了哪些so库: 因为我用的frida17,所以我的脚本如下,但是如果你用的frida其他版本,需要自己更改调用api: ```javascript console.log("[-] 脚本开始运行,正在搜索 dlopen 函数..."); // 1. 尝试查找 android_dlopen_ext (新版安卓常用) var dlopen = Module.getGlobalExportByName( "android_dlopen_ext"); // 2. 如果找不到,尝试查找 dlopen (旧版通用) if (!dlopen) { console.log("[-] 未找到 android_dlopen_ext,尝试查找标准 dlopen..."); dlopen = Module.findgetGlobalExportByName(null, "open"); } if (dlopen) { console.log("[*] 成功定位 dlopen 函数地址: " + dlopen); Interceptor.attach(dlopen, { onEnter: function(args) { // args[0] 通常是加载库的路径字符串 var pathPtr = args[0]; if (pathPtr !== undefined && pathPtr != null) { var path = pathPtr.readCString(); // 过滤一下,只显示包含 .so 的加载信息,看起来更清晰 if (path.indexOf(".so") >= 0) { console.log("[+] LOAD: " + path); } } } }); } else { console.log("[!] 严重错误: 无法在内存中找到任何 dlopen 函数!"); } ``` 然后使用spawn方式运行: ```cmd frida -U -f com.knight.union.mi -l hookopen.js ``` 打开一看,*了狗了:  腾讯的壳,爱加密的壳,还有个libmsaoaidsec.so,只能说NB 用代码开一下线程,看看到底是哪个狗东西把俺滴frida给杀了 ```javascript function hook_pthread_create() { Interceptor.attach(Module.getGlobalExportByName("pthread_create"), { onEnter: function (args) { var module = Process.findModuleByAddress(ptr(this.returnAddress)) if (module != null) { console.log("[pthread_create] called from", module.name) } else { console.log("[pthread_create] called from", ptr(this.returnAddress)) } }, }) } hook_pthread_create() ``` 查看线程,发现是调用了libtprt之后frida线程炸了,看来就是企鹅的锅。  腾讯的壳感觉大概率是不好搞 BUT!在我准备放弃的时候,我突然想到我手机上LSposed一直开着,说不定这个libtprt不会检查俺滴LSposed? 写个插件试试:(关于插件的编写见这个教程:<mark class="encrypted">a2fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3f1#2x3Y4m8G2K9X3W2W2i4K6u0W2j5$3&6Q4x3V1k6@1K9s2u0W2j5h3c8Q4x3X3b7J5x3o6t1&6x3o6x3^5i4K6u0V1x3g2)9J5k6o6q4Q4x3X3g2Z5N6r3#2D9i4@1g2r3i4@1u0o6i4K6R3&6</mark> (突然发现上了研只学会了写论文与专利,代码能力一落千丈,这就是修为散尽的感觉的吗?) ```kotlin package com.example.hookknight import android.app.Activity import android.os.Bundle import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import de.robv.android.xposed.callbacks.XC_LoadPackage class HookEntry : IXposedHookLoadPackage { override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { // 只对目标包生效 if (lpparam.packageName != "com.knight.union.mi") return XposedBridge.log("[$TAG] ========== 注入成功 ==========") try { // 只 hook Activity.onCreate!最简单的测试 XposedHelpers.findAndHookMethod( Activity::class.java, "onCreate", Bundle::class.java, object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam) { val activity = param.thisObject as Activity XposedBridge.log("[$TAG] ✅ onCreate -> ${activity.javaClass.simpleName}") } } ) XposedBridge.log("[$TAG] Hook Activity.onCreate 成功") } catch (e: Throwable) { XposedBridge.log("[$TAG] Hook 失败: ${e.message}") } } companion object { private const val TAG = "HookTest" } } ``` 然后测试一下看看,嘿!您还真别说,注入了  很好,下一步明确了,用LSposed大概率能够绕过某讯的检测,先写个脚本试一下: 下面这个是LSposed hook native的c++函数,之后我会写一个如何使用LSposed hook native的教程,这个相对来说还有点麻烦: ```cpp #include <android/log.h> #include "native_api.h" #define LOG_TAG "LSP-NativeTest" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) static void on_library_loaded(const char *name, void *handle) { LOGI("on_library_loaded: %s, handle=%p", name ? name : "(null)", handle); } // 关键入口:LSPosed 会调用这个函数 extern "C" [[gnu::visibility("default")]] [[gnu::used]] NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) { LOGI("native_init called, version=%u", entries ? entries->version : 0); // 直接返回回调函数,让 LSPosed 之后继续调 on_library_loaded return on_library_loaded; } ``` 然后使用adb看下日志,结果:  咋回事?怎么又被检测到了,又闪退了,不应该啊 讲道理,既然lsposed后台一直运行着,开启的时候不会被检测,但是问题为什么一写插件就会被检测到? 我有了一个大胆的想法,会不会是因为使用adb的原因?会不会是因为被检测到与电脑通信? 然后我又拔掉数据线试了一下,您猜怎么着,没闪退,说明找到问题了,如果不连接数据线到电脑的话,就不会被检测到,那么另一个问题来了,如何看到输出的日志?LSposed可以在app内看到日志,但是那是需要调用的一个java api,所以一个解决办法是在 Kotlin 里写个 LogUtil,然后使用 JNI 交互: ```cpp #include <jni.h> #include <android/log.h> #include <dlfcn.h> #include <pthread.h> #include <string> #include <cstring> #include <cstdio> #include "native_api.h" #define LOG_TAG "LSP-NativeTest" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) static JavaVM *g_JavaVm = nullptr; static jclass g_LogUtilClass = nullptr; static jmethodID g_LogFromNativeMethod = nullptr; static void XLog(JNIEnv *env, const char *msg) { if (!env || !g_LogUtilClass || !g_LogFromNativeMethod) { LOGI("XLog not ready: %s", msg); return; } jstring jmsg = env->NewStringUTF(msg); env->CallStaticVoidMethod(g_LogUtilClass, g_LogFromNativeMethod, jmsg); env->DeleteLocalRef(jmsg); } // LSPosed 加载任意 so 时的回调 static void on_library_loaded(const char *name, void *handle) { LOGI("on_library_loaded: %s, handle=%p", name ? name : "(null)", handle); if (g_JavaVm && g_LogUtilClass && g_LogFromNativeMethod) { JNIEnv *env = nullptr; if (g_JavaVm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) { char buf[256]; snprintf(buf, sizeof(buf), "on_library_loaded: %s", name ? name : "(null)"); XLog(env, buf); } } } // 关键入口:LSPosed 会调用这个函数 extern "C" [[gnu::visibility("default")]] [[gnu::used]] NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) { LOGI("native_init called, version=%u", entries ? entries->version : 0); if (g_JavaVm && g_LogUtilClass && g_LogFromNativeMethod) { JNIEnv *env = nullptr; if (g_JavaVm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) { XLog(env, "native_init called from LSPosed"); } } return on_library_loaded; } extern "C" [[gnu::visibility("default")]] [[gnu::used]] jint JNI_OnLoad(JavaVM *vm, void *) { g_JavaVm = vm; JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // 找到 com.example.hookknight.LogUtil jclass clazz = env->FindClass("com/example/hooknative/LogUtil"); if (!clazz) { LOGI("FindClass LogUtil failed"); return JNI_VERSION_1_6; } g_LogUtilClass = (jclass) env->NewGlobalRef(clazz); env->DeleteLocalRef(clazz); // 找到静态方法 logFromNative(Ljava/lang/String;)V g_LogFromNativeMethod = env->GetStaticMethodID( g_LogUtilClass, "logFromNative", "(Ljava/lang/String;)V" ); if (!g_LogFromNativeMethod) { LOGI("GetStaticMethodID logFromNative failed"); } else { jstring jmsg = env->NewStringUTF("JNI_OnLoad OK, Xposed log ready"); env->CallStaticVoidMethod(g_LogUtilClass, g_LogFromNativeMethod, jmsg); env->DeleteLocalRef(jmsg); } return JNI_VERSION_1_6; } ```  ## 内存il2cpp与global-metadata.dat获取 可以看到load成功了,说明il2cpp,能够从app前端获取到日志信息,下一步,薅内存。 这里其实我写了一个xpsoed的插件,但是一直打不开mem,就是如果打开就会出现无法读取mem的结果,但是如果使用frida进行ps,又会进行闪退,使用adb调试的时候元气骑士也会闪退。 于是我直接上杀招:Termux!Termux相当与linux中的shell,而且有了su权限之后简直就是把手机当linux来用(我直接给手机装了个docker哈哈!)  在Termux中使用 ```shell ps -ef | grep com.knight.union.mi ``` 可以拿到pid 有了pid可以在maps中查询到内存中加载的文件(建议了解一下linux中maps的原理,其实就是内存映射) 通过 ```shell cat /proc/<pid>/maps | grep il2cpp ``` 可以得到关于il2cpp的信息,可以看到有很多so文件,所以可以写个sh脚本把这些从内存中dump出来: 这里参考这位大佬的帖子:<mark class="encrypted">84fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3f1#2x3Y4m8G2K9X3W2W2i4K6u0W2j5$3&6Q4x3V1k6@1K9s2u0W2j5h3c8Q4x3X3b7I4z5o6b7@1y4e0R3%4i4K6u0V1x3g2)9J5k6o6q4Q4x3X3g2Z5N6r3#2D9i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1^5i4@1u0r3i4K6V1&6i4@1f1@1i4@1u0p5i4K6S2p5i4@1f1#2i4@1p5@1i4@1p5%4i4@1f1@1i4@1u0p5i4@1q4o6i4@1f1#2i4K6R3$3i4K6V1&6i4@1f1@1i4@1u0m8i4K6R3$3i4@1f1@1i4@1t1^5i4K6R3H3i4@1f1@1i4@1t1^5i4@1q4m8i4@1f1^5i4K6R3@1i4K6W2m8i4@1f1$3i4K6W2o6i4@1q4o6i4@1f1#2i4K6S2q4i4@1u0n7k6s2g2E0M7q4!0q4y4g2)9^5y4g2)9^5x3#2!0q4y4W2!0n7x3q4)9&6y4q4!0q4z5g2!0m8b7g2)9&6x3g2!0q4y4g2!0m8x3#2!0m8b7W2!0q4c8W2!0n7b7#2)9^5b7#2!0q4y4q4!0n7z5q4)9^5c8q4!0q4z5q4!0n7c8W2)9^5y4#2!0q4y4W2)9&6b7#2)9^5x3q4!0q4y4W2)9&6y4W2!0n7x3q4!0q4y4#2)9^5z5g2)9^5z5q4!0q4y4g2)9&6x3W2)9^5b7#2!0q4y4g2!0n7z5q4)9&6y4W2!0q4y4g2!0m8c8q4)9&6x3q4!0q4y4q4!0n7z5q4!0m8c8q4!0q4y4#2)9&6b7g2)9^5y4q4!0q4y4#2)9^5z5g2)9^5z5q4!0q4y4W2)9&6b7#2!0m8b7#2!0q4y4W2)9&6b7#2)9^5z5g2!0q4y4g2)9^5b7#2!0n7b7g2!0q4y4g2)9^5z5q4!0m8b7W2!0q4c8W2!0n7b7#2)9^5b7#2!0q4y4W2)9^5z5g2)9^5x3q4!0q4y4q4!0n7b7W2!0m8y4g2!0q4y4g2)9&6b7#2!0m8z5q4!0q4z5q4!0n7c8W2)9&6z5g2!0q4y4q4!0n7z5q4!0m8b7g2!0q4y4g2!0m8y4q4!0m8y4#2!0q4y4q4!0n7c8q4!0m8b7#2!0q4y4#2)9&6b7g2)9^5y4q4!0q4y4g2)9&6c8W2!0n7b7g2!0q4y4#2!0m8x3g2)9^5x3q4!0q4y4q4!0n7z5q4)9^5b7g2!0q4y4g2)9^5y4W2)9&6z5g2!0q4y4q4!0n7b7g2)9^5y4W2!0q4y4q4!0n7z5q4)9^5x3q4!0q4y4q4!0n7z5q4!0m8b7g2!0q4z5q4)9^5y4q4)9&6b7g2!0q4y4W2)9&6b7#2!0m8b7#2!0q4y4g2)9^5c8g2!0n7b7X3c8#2L8i4m8Q4c8e0k6Q4z5f1y4Q4z5o6m8Q4c8e0k6Q4z5e0k6Q4b7U0m8Q4c8e0N6Q4z5o6W2Q4z5o6S2Q4c8e0k6Q4z5f1y4Q4b7f1y4Q4c8e0N6Q4z5f1q4Q4z5o6c8A6L8o6u0U0M7s2m8Q4c8e0N6Q4z5f1q4Q4z5o6c8K6L8#2!0q4y4W2)9&6y4W2)9^5y4#2!0q4y4q4!0n7b7W2!0n7y4W2!0q4x3#2)9^5x3q4)9^5x3R3`.`.</mark> ```shell #!/system/bin/sh echo "##################################" echo "# Il2Cpp Memory Dumper (Enhanced) #" echo "# Based on NekoYuzu's script #" echo "##################################" if [[ $1 == "" ]]; then echo "* Usage: $0 <package> [output]" echo "* Example: $0 com.knight.union.mi /sdcard/dump" exit fi package=$1 if [[ $2 == "" ]]; then out=/sdcard/dump else out=$2 fi echo "- Target package: $package" echo "- Output directory: $out" mkdir -p "$out" # 获取用户ID和进程PID user=$(am get-current-user) pid=$(ps -ef | grep $package | grep u$user | awk '{print $2}') if [[ $pid == "" ]]; then echo "! Target package of current user ($user) not found, is process running?" exit fi echo "- Found target process: $pid" # 保存完整的maps文件 cp /proc/$pid/maps "$out/${package}_maps.txt" # 获取系统页大小 SYS_PAGESIZE=$(getconf PAGESIZE) HEX_PAGESIZE=$(printf "%x" $SYS_PAGESIZE) echo "- System page size: $SYS_PAGESIZE bytes (0x$HEX_PAGESIZE)" # 检查libil2cpp.so是否存在 if ! grep -q "libil2cpp.so" /proc/$pid/maps; then echo "! libil2cpp.so not found in memory" exit fi echo "- Starting dump process..." echo "" # 收集所有libil2cpp.so的内存段 mem_list=$(grep "libil2cpp.so" /proc/$pid/maps | awk -v OFS='|' '{for (i = 1; i <= NF; i+=6) {print $i,$(i+1),$(i+2),$(i+3),$(i+4),$(i+5)}}') # 用于合并的变量 lastFile= lastEnd= lastOffset= for memory in $mem_list; do local range=$(echo $memory | awk -F'|' '{print $1}') if [[ $range == "(deleted)" ]]; then continue fi local offset=$(echo $range | awk -F'-' '{print toupper($1)}') local end=$(echo $range | awk -F'-' '{print toupper($2)}') local perms=$(echo $memory | awk -F'|' '{print $2}') local fileOffset=$(echo $memory | awk -F'|' '{print $3}') local memIndicator=$(echo $memory | awk -v OFS=',' -F'|' '{print $4,$5}') # 检查ELF magic number dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;$offset" | bc) count=4 of="${out}/tmp" 2>/dev/null local fileExt="bin" local isELF=false if [[ $(cat "${out}/tmp") == $(echo -ne "ELF") ]]; then isELF=true fileExt="so" # 获取ELF架构信息 local EI_CLASS="Unknown" dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;${offset}+4" | bc) count=1 of="${out}/tmp" 2>/dev/null tmp=$(cat "${out}/tmp") if [[ $tmp == $(echo -ne "*") ]]; then EI_CLASS="32" elif [[ $tmp == $(echo -ne "*") ]]; then EI_CLASS="64" fi local EI_DATA="Unknown" dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;${offset}+5" | bc) count=1 of="${out}/tmp" 2>/dev/null tmp=$(cat "${out}/tmp") if [[ $tmp == $(echo -ne "*") ]]; then EI_DATA="LSB" elif [[ $tmp == $(echo -ne "*") ]]; then EI_DATA="MSB" fi local E_TYPE="Unknown" dd if="/proc/$pid/mem" bs=1 skip=$(echo "ibase=16;${offset}+10" | bc) count=2 of="${out}/tmp" 2>/dev/null tmp=$(cat "${out}/tmp" | xxd -p) case "$tmp" in "0100") E_TYPE="Relocatable" ;; "0200") E_TYPE="Executable" ;; "0300") E_TYPE="Shared object" ;; "0400") E_TYPE="Core" ;; *) E_TYPE="Processor-specific" ;; esac echo "═══════════════════════════════════════" echo " ELF ${EI_CLASS}-bit $E_TYPE (${EI_DATA})" echo " Address: 0x$offset - 0x$end" echo " Permissions: $perms" echo " File offset: 0x$fileOffset" else echo " Segment at 0x$offset [$perms]" fi # 确定输出文件名 if [[ $isELF == true ]]; then local fileOut="${out}/${offset}_${package}_libil2cpp.so" else local fileOut="${out}/${offset}_${package}_segment.bin" fi # Dump当前段 echo " Dumping: $range..." dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${offset}/$HEX_PAGESIZE" | bc) count=$(echo "ibase=16;(${end}-${offset})/$HEX_PAGESIZE" | bc) of="$fileOut" 2>/dev/null if [[ $? -ne 0 ]]; then echo " Failed to dump, skipping..." rm -f "$fileOut" continue fi # 检查.bss段 local bss_memory=$(grep -i "^${end}-" "/proc/$pid/maps" | grep "\[anon:.bss\]" | head -1) if [[ $bss_memory != "" ]]; then local bss_range=$(echo $bss_memory | awk '{print $1}') local bss_end=$(echo $bss_range | awk -F'-' '{print toupper($2)}') local bss_blocks=$(echo "ibase=16;(${bss_end}-${end})/${HEX_PAGESIZE}" | bc) echo " Adding .bss segment ($bss_blocks blocks)..." dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${end}/$HEX_PAGESIZE" | bc) count=$bss_blocks of="$out/tmp" 2>/dev/null cat "$out/tmp" >> "$fileOut" end=$bss_end fi # 合并逻辑 if [[ $isELF == true ]]; then # 这是一个新的ELF文件,不合并 lastFile=$fileOut lastOffset=$offset else # 非ELF段,尝试合并到上一个文件 if [[ $lastFile != "" ]]; then echo " Checking merge possibility..." local skipMerge=false if [[ $lastEnd != $offset ]]; then # 存在间隙 local gap_blocks=$(echo "ibase=16;(${offset}-${lastEnd})/${HEX_PAGESIZE}" | bc) if [[ $gap_blocks -gt $SYS_PAGESIZE ]]; then # 间隙太大,不合并 echo " Gap too large ($gap_blocks blocks), treating as separate file" skipMerge=true lastFile=$fileOut else # 间隙较小,填充并合并 echo " Filling gap ($gap_blocks blocks)..." dd if="/proc/$pid/mem" bs=$SYS_PAGESIZE skip=$(echo "ibase=16;${lastEnd}/$HEX_PAGESIZE" | bc) count=$gap_blocks of="$out/tmp" 2>/dev/null cat "$out/tmp" >> "$lastFile" fi fi if [[ $skipMerge == false ]]; then # 合并当前段到上一个文件 echo " Merging into previous file..." cat "$fileOut" >> "$lastFile" rm -f "$fileOut" fi else # 第一个非ELF段,无法合并 echo " No previous ELF file to merge" lastFile=$fileOut fi fi lastEnd=$end rm -f "${out}/tmp" echo "" done echo "═══════════════════════════════════════" echo " Dump completed!" echo "" echo "Output files in: $out" ls -lh "$out"/*.so 2>/dev/null # 找出最大的.so文件(通常是完整的libil2cpp.so) largest=$(ls -S "$out"/*.so 2>/dev/null | head -1) if [[ $largest != "" ]]; then size=$(stat -c%s "$largest" 2>/dev/null) size_mb=$(echo "scale=2; $size/1024/1024" | bc) echo "" echo "Largest file (likely the complete libil2cpp.so):" echo "→ $(basename $largest) (${size_mb} MB)" # 验证ELF文件 dd if="$largest" bs=1 count=4 of="${out}/tmp" 2>/dev/null if [[ $(cat "${out}/tmp") == $(echo -ne "ELF") ]]; then echo "ELF header verified" else echo "Warning: ELF header not found in largest file" fi rm -f "${out}/tmp" fi echo "" echo "Next steps:" echo "1. Check the largest .so file" echo "2. Use 'file' or 'readelf' to verify" echo "3. Extract global-metadata.dat from APK" echo "4. Use Il2CppDumper for analysis" echo "" echo "- Done!" ``` 运行代码,可以得到一个libil2cpp的文件,但是这个文件还不能直接用,需要用soFixer去修复一下 <mark class="encrypted">ed5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6r3z5p5I4q4c8W2c8Q4x3V1k6e0L8@1k6A6P5r3g2J5</mark> 这样就可以修复so文件,下一步是拿到global-metadata.dat文件,然后神奇的地方是,使用我的lsposed文件,可以看到global-metadata.dat已经被加载了,但是内存中却没有。 所以有可能是被加载到内存后关闭了文件,metadata在/dev/zero段中,查看内存确实存在/dev/zero中的文件  而且一共有三个,所以可以直接把这三个给dump出来: 因为这个量比较少吗,所以可以直接命令行给dump出来 ```shell cat /proc/<pid>/map_files/701b536000-701dc19000 > /sdcard/meta_seg_test1.dat && ls -lh /sdcard/meta_seg_test1.dat ``` 其中701b536000-701dc19000就是对应你上面的地址(需要更改为实际值,我图片是后来截的,所以对不上) 我发现一共有三个/dev/zero文件,所以一共dump出来三个: 然后使用010Editor分析这三个文件: 第一个文件:  全是0,pass掉 第二个文件:  明显是java有关的,pass 第三个文件:  非常明显是unity有关的,确定是这个了,但是这个文件的头不太对,因为正常应该是 AF 1B B1 FA开头的,不过没关系,直接手动修改 手动修改之后,用UnityMetadata的tamplates插件跑一下试试(这个插件在往上可以找到,这是010editor的插件) 看到结果都被识别出来了:  然后使用il2cppDumper解析一下,发现出问题了:  根据这篇博客的分析,对文件进行修改:<mark class="encrypted">38cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6J5k6h3W2E0N6g2)9J5k6h3#2G2k6g2)9J5c8U0t1H3x3U0c8Q4x3V1j5H3y4W2)9J5c8U0l9&6i4K6u0r3d9f1H3J5b7%4m8H3i4K6u0V1f1%4c8J5K9h3&6Y4i4K6u0V1e0r3W2@1k6i4u0S2L8s2y4Q4x3V1j5`.</mark>  然后放入il2cppdumper进行分析,但是结果还是GG了,这次的分析就到此位置。 事实上我还使用GameGuardian去从内存中拿去信息,结果也都是全部无法分析,感觉很有可能是il2cppdumper的版本问题,因为il2cppdumper毕竟现在也停止更新了,所以就到这里吧,希望能有大佬给出指导建议,不胜感激。  但是!在我发布上一个帖子的12小时后,看到了一个大哥的评论  然后我突发奇想,是不是版本不对,虽然里面写的是18,但是会不会应该是1F,于是开始修改,没想到真的通了 ## 修复后分析 修改版本号:  然后放到il2cppdumper中,得到最终数据,哈!真出来了  首先需要用sofix修复so文件:<mark class="encrypted">5d7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6o6K9r3g2F1P5h3q4F1k6$3#2A6L8X3M7&6i4K6u0r3f1$3!0r3K9i4S2W2M7W2)9J5x3H3`.`.</mark> 然后使用IDA打开修复后的so文件(新版某气骑士文件很大,分析起来要花很长时间),使用File -Script File,选择ida_py3.py(il2cppdumper安装包中自带的那个),选择分析出的stringliteral.json 之后选择 ida_with_struct_py3.py,分析script.json和il2cpp.h  分析完成了!!!  既然已经分析完成了,那就搞点事情干,直接分析解密的存档的方式: 可以通过查看包内文件的方式找到实际的存档文件,这次就以game.data为例进行分析 ## 分析存档 首先找到game.data  然后再IDA中按**G**,输入地址后跳跃到对应的位置,之后在对应位置右键查询 **List cross references to…**  看名字就知道这是个存档的地方,然后分析这个函数:  看看是谁调用的这个函数:  可以跳转到了LoadGameData,然后分析可以找到加密函数:   进去看看:  接着跟,这个代码就是最新版元气骑士的解密算法:  使用python简单复现一下: ```python def xor_crypt(data: bytes, password: str) -> bytes: result = bytearray(data) pwd_len = len(password) for i in range(len(result)): pc = ord(password[i % pwd_len]) t = ((i % 15) * (i % 5)) % 92 # 修正括号 result[i] = result[i] ^ pc ^ t return bytes(result) ``` 然后为了解密这个data,有一个简单的密码学方法,就是爆破,因为我们已经知道了加密算法,其次json格式的前两位是**{**和**“**。 通过这个我们可以爆破,爆破也是有技巧的,首先这是一个简单的xor加密,也就是说,如果尝试的密码和真实密码很相近,那么json格式中出现下列字符的数量会越多 ```python patterns=[b'{"', b'":', b',"', b'"}', b':[', b']}'] ``` 根据这个特性可以辅助我们选定密钥的长度,然后根据长度进行爆破 使用AI写一个爆破的脚本: ```python # -*- coding: utf-8 -*- import json from collections import defaultdict def xor_crypt(data: bytes, password: str) -> bytes: result = bytearray(data) pwd_len = len(password) for i in range(len(result)): pc = ord(password[i % pwd_len]) t = ((i % 15) * (i % 5)) % 92 # 修正括号 result[i] = result[i] ^ pc ^ t return bytes(result) def crack_by_your_idea(ciphertext: bytes, max_pwd_len: int = 50): """破解XOR密码""" cipher_bytes = bytearray(ciphertext) # JSON 常见的重复模式 patterns = [b'{"', b'":', b',"', b'"}', b':[', b']}'] candidates = {} for pattern in patterns: pattern_bytes = list(pattern) # 对每个密码长度 for pwd_len in range(1, max_pwd_len + 1): votes = [defaultdict(int) for _ in range(pwd_len)] # 扫描密文的每个位置 for start_pos in range(len(cipher_bytes) - len(pattern_bytes) + 1): temp_pwd = {} conflict = False for i, plain_byte in enumerate(pattern_bytes): pos = start_pos + i t = ((pos % 15) * (pos % 5)) % 92 # 修正括号 recovered = cipher_bytes[pos] ^ t ^ plain_byte # 必须是可打印字符 if not (32 <= recovered < 127): conflict = True break pwd_idx = pos % pwd_len if pwd_idx in temp_pwd: if temp_pwd[pwd_idx] != recovered: conflict = True break else: temp_pwd[pwd_idx] = recovered if not conflict: for pwd_idx, byte_val in temp_pwd.items(): votes[pwd_idx][byte_val] += 1 # 构建密码 password = bytearray(pwd_len) min_votes = float('inf') for i in range(pwd_len): if not votes[i]: password = None break most_common = max(votes[i].items(), key=lambda x: x[1]) password[i] = most_common[0] min_votes = min(min_votes, most_common[1]) if password and min_votes >= 2: try: pwd_str = password.decode('utf-8') if pwd_str not in candidates or min_votes > candidates[pwd_str][1]: candidates[pwd_str] = (pattern.decode('utf-8'), min_votes) except: pass return sorted(candidates.items(), key=lambda x: -x[1][1]) # 主破解流程 if __name__ == "__main__": # 二进制模式读取 with open("game.data", "rb") as f: ciphertext = f.read() print(f"密文长度: {len(ciphertext)} 字节") print("="*60) results = crack_by_your_idea(ciphertext, max_pwd_len=50) print(f"找到 {len(results)} 个候选密码\n") for pwd, (pattern, votes) in results[:20]: print(f"密码: '{pwd}' (长度={len(pwd)}, 票数={votes}, 模式='{pattern}')") try: result = xor_crypt(ciphertext, pwd) result_str = result.decode('utf-8', errors='replace') json.loads(result_str) print(f" ✓✓ 成功!这是有效的 JSON!") print(f" 预览: {result_str[:200]}...") # 二进制模式保存 with open("output.txt", "wb") as f: f.write(result) print(f" 已保存到 output.txt\n") break except json.JSONDecodeError as e2: print(f" ✗ 不是有效 JSON: {e2}") except Exception as e: print(f" ✗ 错误: {e}") ``` 然后就可以得到输出的结果,成果破解出来了某气骑士的密码:  可以看到里面有很多宠物解锁和技能解锁相关的数据: 
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
20
点赞
・
19
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
秋小梦
非常支持你的观点!
2026-8-2 17:53
mb_hzzndkns
谢谢你的细致分析,受益匪浅!
2026-7-29 14:16
wx_晨梦
期待更多优质内容的分享,论坛有你更精彩!
2026-7-16 05:33
孤独的街
非常支持你的观点!
2026-7-15 10:48
Lrh0822
感谢你分享这么好的资源!
2026-6-27 11:44
carrys
你的帖子非常有用,感谢分享!
2026-6-12 09:51
wx_Jhfff
你的帖子非常有用,感谢分享!
2026-6-10 15:03
FlyingDonkey
为你点赞!
2026-6-6 21:14
零_721409
这个讨论对我很有帮助,谢谢!
2026-5-25 10:30
trackL
感谢你的积极参与,期待更多精彩内容!
2026-5-22 19:42
086Love
谢谢你的细致分析,受益匪浅!
2026-5-22 15:17
Dy1an
为你点赞!
2026-5-21 10:05
心在o
非常支持你的观点!
2026-5-14 09:19
a0yark
非常支持你的观点!
2026-5-8 22:01
我的小拇指啊
这个讨论对我很有帮助,谢谢!
2026-5-8 20:23
冷管长
+1
谢谢你的细致分析,受益匪浅!
2026-5-7 16:02
rettozero
感谢你的积极参与,期待更多精彩内容!
2026-5-6 17:28
mb_aoooaosd
你的帖子非常有用,感谢分享!
2026-5-6 17:21
樂夜无殇
你的分享对大家帮助很大,非常感谢!
2026-5-6 13:55
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
4
)
rettozero
雪 币:
1180
活跃值:
(2376)
能力值:
( LV3,RANK:36 )
在线值:
发帖
3
回帖
5
粉丝
26
关注
私信
rettozero
2
楼
2026-5-6 17:28
0
Imxz
雪 币:
112
活跃值:
(9420)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
752
粉丝
9
关注
私信
Imxz
3
楼
tql
2026-5-8 11:12
0
meiko
雪 币:
210
活跃值:
(540)
能力值:
( LV2,RANK:10 )
在线值:
发帖
3
回帖
10
粉丝
4
关注
私信
meiko
4
楼
tql
2026-5-20 15:01
0
MENTAL
雪 币:
60
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
4
粉丝
0
关注
私信
MENTAL
5
楼
tql
2026-8-7 17:00
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
RytterMohn
2
发帖
2
回帖
20
RANK
关注
私信
他的文章
[原创]某气骑士 libtprt.so 反 Frida 机制分析与绕过
10955
[原创]最新版某气骑士分析记录-绕过某讯的防御dump内存并解密存档
11796
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部