-
-
【游戏安全】2021年腾讯游戏安全大赛(安卓)
-
发表于: 2天前 493
-
本题难度适中,非常适合入门复现。
目标实现无敌版鼠鼠,效果图:

所有org、dump、fix以及分析文件都已放在附件(除了游戏题目由附件大小限制没放)。
视频版教程 :dc5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4f1s2N6r3M7s2A6F1c8h3&6y4
Unity 逆向,Mono 打包的 apk,找到 Assembly-CSharp.dll,确定为 Mono 类的逆向,关键的业务代码也就在这个 dll 里,然而一看发现不对劲,拉一个正常的对比。

既然是加密了,动态肯定会解密,于是就是了解一下 Unity 其 Mono 打包成 apk 的加载流程。
于是去查看 libmono.so 哪里动了手脚,尝试通过 frida hook mono_image_open_from_data_with_name,因为该函数是加载 Assembly-Csharp.dll 的函数。
但是有 frida 检测,似乎无法直接到达加载该 dll 的时机,所以现在去解决 frida 检测。
先看了 init_array 看看反调试是不是写这了,然后发现 0x1F120 的函数,显然是动态解密一些数据的函数,不管是调试还是模拟执行都行,不过通过 LLM 辅助解析,写个 idapython 脚本,该函数就是传一个常量,返回解密后的数据到 bss 数据段。
同时写一段注释到每个字符串所在位置,完整脚本如下。
执行完所有字符串所在位置就确定了

再回到frida检测时候弹出的字符串是 hack detected, type:frida,然后就程序就退出了,那么通过该字符串定位到此处,不难就发现了

既然确定了退出的地方,那么其实可以跳过如何检测的逻辑,直接结束本题了。
但现在毕竟是在复现题目,是为了学到更多东西,我开始去理清整个加载流程,因为还有很多检测加载细节。
所以现在就是分岔口,由于已经知道检测后退出的地方,一种选择可以直接去 patch 重签名,直接跟着其他 wp 即可,另一种选择就是去认真审计一下加载的流程,检测的流程,更加仔细感受一下整个题目。
再者我在这题中不想调试,原因也是很多时候其实不好调试,一般都是通过模拟执行或者 Hook 框架来确定一些值,当然这题其实能调,过往我也喜欢调试来确定,但是这题我想锻炼通过审计、模拟执行以及 Hook 框架来处理问题。
我认为看一篇文章最核心的还是思考作者如何想到这一点,往往文章中缺失的一点是作者在写这篇文章的思考过程与试错点,这些过程写起来很麻烦而且因人而异,但我写 write up 会更多的写是怎么思考以及试错后得到信息与解决方法,如果只是这里 patch 一下,那里跑个脚本,其实学不到什么东西,我的整个分析也许做不到足够细,但我想我在这篇文章中尽量把大局整理明白,还有些细节与思考需要读者自行去尝试。
先开始理清整个加载流程
Zygote 进程:
应用进程启动 (Java 早期):
System.loadLibrary("sec2021") 触发 (Java 层):
进入 Native 层 (Linker 阶段):
JNI 握手 (Native 层):
回到 Java 层 (attachBaseContext):
现在给出每个细节依据,先从 AndroidManifest.xml 找到了
android:name="com.tencent.games.sec2021.Sec2021Application",确定了这是apk启动的入口点,在该类中。

先是 System.loadLibrary("sec2021"),那么运行了 init_array,去 ida 审计发现有两个函数,第一个 init 函数其实就是初始化 method(第二个 init 函数不重要)。

随后去看 JNIOnload,通过 java 层会调用 native 层的 initialize 函数,而且 init_array 会初始化了 methods,不难想象 JNIOnload 里是动态注册该函数的过程。

JNIOnload 执行完后,返回运行到 attachBaseContext 方法,其中执行 initialize naitive 函数。
至此,整个运行流程就明白了,不难猜测检测函数都在 initialize native 函数进行初始化的(毕竟 init_array 与 JNIOnload 无事发生),这里 frida 等 libsec2021.so 加载了 hook 上即可。
开始审计 initialize 函数,该函数地址在 init_array 初始化 methods 的时候就可以找到是在 5464 地址,这里 coreObj 初始化了一个对象,后面经常使用。

经过初始化后可以发现各种检测函数的注册是在 initialize_process,在函数刚开始的地方,将一些函数注册到了刚刚初始化的对象上,且这些注册函数还上了虚假控制流和控制流平坦化。

显然是里面有好康的但出题人不让康,所以上了不好康的混淆,这里直接猜测里面就是检测 frida ida 这些,因为在上面的小节我们已经把字符串混淆都处理差不多了,没有发现 frida、ida 这些字符串,说明大概率就在这里面了。
接下来也都是一些函数注册到了刚刚的结构体上,手动翻了这些函数,不过逆向的角度来说不太重要,我们关注点是哪里检测的问题,直到翻到 initialize_process 末尾,调用到了之前标记的函数。

这里我的标记链是 alreadyChecked_c -> alreadyChecked_b -> alreadyChecked_a,而 alreadyChecked_a 就是检测到 hack detected, type:%s 退出的地方。
刚刚的理清流程、审计函数主要是分析了

于是可以发现这几个调用点,基本上都可以回溯到 intialize 函数进行了注册,作为逆向可以不进行关注注册细节,我们只需要分析其中一个调用逻辑即可,就拿刚刚 initialize_process 的末尾来举例,我们只需要把 BEQ 直接 patch 成 B,恒跳转到不会失败的分支即可。

同样处理其他几个调用点,写一个 frida 脚本(frida版本 16.1.4,Android 10,Pixel 3XL)
成功 patch,也不会被检测到退出了!

既然检测已过,且该程序的加解密都是落地的,所以基本上接下来没有什么困难的了。
先去获得正确的 Assembly-CSharp.dll,同样开始文章刚开始的思路,去 hook libmono.so 的 mono_image_open_from_data_with_name 函数。

去看了眼该函数,发现该函数不能正常反编译(这属于系统函数所以不正常),但也不是花指令之类,所以估计是sec里对mono还进行了修改(从之前字符串解密的的text与libmono也有感觉)。
但具体如何修改mono其实也不用在意,只需要找一个合适的时机 dump 出 mono 即可看到 libsec 是修改后的 mono 了,也就是只要落地,那么不管咋加密都能 dump 出来,毕竟 frida 检测都过了。
dump 出来(可以拿SoFixer修一下,但只是看看函数也可以不修),如果对比正常的 libmono.so 会发现多个跳转,但之后的代码就一样了。

所以直接 hook sub_190C74,再根据函数原型。
正常的 hook dump Assembly-CSharp.dll 出来即可。
但先等等看游戏逻辑,上面分析有些细节没讲
真相是该流程下 libmono.so 在正常调用到 mono_image_open_from_data_with_name 时候,做了一个跳转到 sec2021(可以 frida 看参数或者调试确定),处理过 dll 再跳回正常执行。
其实也是收回伏笔了,一开始我们找到的 Assembly-CSharp 静态是给加密的,所以正常读取肯定会做过处理后再使用,具体就是跳到了 0001CEE0 地址位置。
该函数审计可得
至此,整理一下目前所得的信息
将 dll 拖进 dnSpy,游戏逻辑很简单,直接 nop 掉检测碰到激光的分支即可。

现在要想一下如何处理 libsec 了,首先是之前在 frida 修改的检测跳转,手动去把 BEQ 改成 B 就可以。
检测干掉了还差一处地方,也就是 mono_image_open_from_data_with_name 入口的跳转,是特别处理了开头不是 MZ 的 dll,由于我们将 patch 好的 Assembly-CSharp,且已经实现了解密,所以不需要走 libsec 中解密的流程。
这里直接将过了 MZ 检测的 dll 直接去加载 dll,不去做解密操作了,同样是把这里的 BEQ 改成 B 即可。

然而在重打包的地方有处小坑,猜测是由于 apktool b 重打包的时候会尝试编译所有的 XML、resoureces.arsc 重新生成资源索引表,导致 libsec 读取某些资源的偏移量出问题了。
其实具体的检测函数我没在文中细写(分析的 i64 看附件),实际上很多检测函数要读 assets 或是其他内容进行 crc 校验等,估计是在这个过程如果用 apktool 会导致无法对应内容从而用 apktool 重打包会导致运行失败。(当然这是猜测,如果有师傅有其他理解欢迎讨论)
于是我们就直接用压缩软件直接打开 apk,不去解压,将目标 Assembly-CSharp 和 libsec2021 换成我们 patch 好的,再重新签名。

5d8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2P5r3S2&6k6h3q4^5i4K6u0W2j5$3!0E0i4K6u0r3x3U0l9J5x3g2)9J5c8U0l9@1i4K6u0r3x3o6c8Q4x3V1k6Y4M7$3I4S2j5U0t1H3x3U0q4Q4x3X3c8H3M7X3g2Q4x3X3c8S2L8X3c8J5L8$3W2V1i4K6u0r3
723K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3f1#2x3Y4m8G2K9X3W2W2i4K6u0W2j5$3&6Q4x3V1k6X3L8%4u0#2L8g2)9J5k6i4m8Z5M7q4)9K6c8X3g2^5N6s2u0S2i4K6y4p5M7r3q4Y4k6g2)9J5y4e0y4V1x3g2)9J5y4e0t1$3k6X3W2D9N6r3g2J5i4K6t1#2x3$3c8V1K9h3N6W2M7%4c8Q4x3U0f1J5y4X3!0J5k6r3g2J5j5Y4W2Q4x3U0f1K6k6s2k6A6k6i4N6K6i4K6t1#2x3U0k6@1P5i4m8W2K9h3c8Q4x3U0f1K6k6o6x3@1x3W2)9J5y4X3q4E0M7q4)9K6b7X3#2G2k6q4)9K6c8s2k6A6k6i4N6@1K9s2u0W2j5h3c8Q4x3U0k6S2L8i4m8Q4x3@1u0@1K9h3c8Q4x3@1b7I4y4o6t1H3y4K6M7#2
024K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9K9h3&6C8L8r3g2&6M7r3W2F1k6#2)9J5k6i4c8G2M7q4)9J5c8X3N6K6L8r3q4T1x3U0l9J5x3g2)9J5k6s2m8J5k6g2)9J5c8W2)9J5x3$3q4H3M7q4)9J5y4f1f1#2i4K6t1#2z5o6S2Q4x3U0f1^5y4W2)9J5y4f1f1$3i4K6t1#2z5f1g2Q4x3U0f1&6x3l9`.`.
[ 安卓系统 (Android OS) ] ↓[ Unity 引擎层 (libunity.so) ] ↓[ Mono 虚拟机层 (libmono.so) ] <--- 你可以 Hook 这里 ↓[ 托管代码层 (Assembly-CSharp.dll) ] <--- 你可以直接反编译这里 ↓[ JIT 编译器 ] -> [ 生成内存中的 ARM 指令 ] -> [ CPU 执行 ][ 安卓系统 (Android OS) ] ↓[ Unity 引擎层 (libunity.so) ] ↓[ Mono 虚拟机层 (libmono.so) ] <--- 你可以 Hook 这里 ↓[ 托管代码层 (Assembly-CSharp.dll) ] <--- 你可以直接反编译这里 ↓[ JIT 编译器 ] -> [ 生成内存中的 ARM 指令 ] -> [ CPU 执行 ]var libbase = Module.findBaseAddress("libmono.so"); console.log("libbase", libbase); var addr = Module.findExportByName("libmono.so", "mono_image_open_from_data_with_name"); console.log("mono_image_open_from_data_with_name", addr); var libbase = Module.findBaseAddress("libmono.so"); console.log("libbase", libbase); var addr = Module.findExportByName("libmono.so", "mono_image_open_from_data_with_name"); console.log("mono_image_open_from_data_with_name", addr); ============================================================doSome 字符串解密器 v1.1============================================================[*] 批量解密模式...============================================================doSome 字符串解密结果============================================================[ 28] @ 0x372c4: "Author: saitexie walterjxli"[ 59] @ 0x372e3: "Do you know how unity mono works?"[ 96] @ 0x37308: "res/drawable-xhdpi-v4/ -> assets/bin/Data/Managed/"[ 150] @ 0x3733e: "initialize"[ 164] @ 0x3734c: "()I"[ 171] @ 0x37353: "com/tencent/games/sec2021/Sec2021Application"[ 219] @ 0x37383: "com/tencent/games/sec2021/Sec2021IPC"[ 259] @ 0x373ab: "hack detected, risk score:%d"[ 291] @ 0x373cb: "getApplicationInfo"[ 313] @ 0x373e1: "()Landroid/content/pm/ApplicationInfo;"[ 355] @ 0x3740b: "getFilesDir"[ 370] @ 0x3741a: "()Ljava/io/File;"[ 390] @ 0x3742e: "sourceDir"[ 403] @ 0x3743b: "packageName"[ 418] @ 0x3744a: "nativeLibraryDir"[ 438] @ 0x3745e: "getAbsolutePath"[ 457] @ 0x37471: "/proc/self/status"[ 478] @ 0x37486: "TracerPid:"[ 492] @ 0x37494: "diediedie"[ 505] @ 0x374a1: "/proc/self/maps"[ 524] @ 0x374b4: "rb"[ 530] @ 0x374ba: "delete"[ 540] @ 0x374c4: "%zx-%zx %c%c%c%c %x %x:%x %u %s"[ 575] @ 0x374e7: "android/os/Debug"[ 595] @ 0x374fb: "isDebuggerConnected"[ 618] @ 0x37512: "sec2021"[ 629] @ 0x3751d: "getClass"[ 641] @ 0x37529: "getName"[ 652] @ 0x37534: "getSuperclass"[ 669] @ 0x37545: "android/app/Application"[ 696] @ 0x37560: "java/lang/Class"[ 715] @ 0x37573: "()Ljava/lang/Class;"[ 738] @ 0x3758a: "()Landroid/content/pm/ApplicationInfo;"[ 780] @ 0x375b4: "()Ljava/io/File;"[ 800] @ 0x375c8: "()Ljava/lang/String;"[ 824] @ 0x375e0: "Assembly-CSharp.dll"[ 847] @ 0x375f7: "Mono.Security.dll"[ 868] @ 0x3760c: "mscorlib.dll"[ 884] @ 0x3761c: "System.Core.dll"[ 903] @ 0x3762f: "System.dll"[ 917] @ 0x3763d: "UnityEngine.dll"[ 936] @ 0x37650: "UnityEngine.Networking.dll"[ 966] @ 0x3766e: "UnityEngine.PlaymodeTestsRunner.dll"[1005] @ 0x37695: "UnityEngine.UI.dll"[1027] @ 0x376ab: "base.apk"[1039] @ 0x376b7: "android/content/Context"[1066] @ 0x376d2: "()Ljava/lang/ClassLoader;"[1095] @ 0x376ef: "()Ljava/lang/String;"[1119] @ 0x37707: "zip file"[1131] @ 0x37713: "libsec2021.so"[1148] @ 0x37724: "%s/%s"[1157] @ 0x3772d: "dalvik.system.PathClassLoader"[1190] @ 0x3774e: "toString"[1202] @ 0x3775a: "getClassLoader"[1220] @ 0x3776c: "Ljava/lang/String;"[1242] @ 0x37782: "%s%s"[1250] @ 0x3778a: "/app/data/libbugly/crash.info"[1283] @ 0x377ab: "can you crack me?"[1304] @ 0x377c0: "__optional__"[1320] @ 0x377d0: "cc/binmt/signature/PmsHookApplication"[1361] @ 0x377f9: "com/cloudinject/feature/App"[1392] @ 0x37818: "np/manager/FuckSign"[1415] @ 0x3782f: "java/lang/ClassLoader"[1440] @ 0x37848: "findClass"[1453] @ 0x37855: "(Ljava/lang/String;)Ljava/lang/Class;"[1494] @ 0x3787e: "getPackageManager"[1515] @ 0x37893: "()Landroid/content/pm/PackageManager;"[1556] @ 0x378bc: "getPackageName"[1574] @ 0x378ce: "getPackageInfo"[1592] @ 0x378e0: "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"[1649] @ 0x37919: "signatures"[1663] @ 0x37927: "[Landroid/content/pm/Signature;"[1698] @ 0x3794a: "toByteArray"[1713] @ 0x37959: "()[B"[1721] @ 0x37961: "java/io/ByteArrayInputStream"[1753] @ 0x37981: "<init>"[1763] @ 0x3798b: "([B)V"[1772] @ 0x37994: "java/security/cert/CertificateFactory"[1813] @ 0x379bd: "getInstance"[1828] @ 0x379cc: "(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;"[1891] @ 0x37a0b: "X.509"[1900] @ 0x37a14: "generateCertificate"[1923] @ 0x37a2b: "(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"[1982] @ 0x37a66: "getEncoded"[1996] @ 0x37a74: "java/security/MessageDigest"============================================================共解密 85 个字符串========================================================================================================================doSome 字符串解密器 v1.1============================================================[*] 批量解密模式...============================================================doSome 字符串解密结果============================================================[ 28] @ 0x372c4: "Author: saitexie walterjxli"[ 59] @ 0x372e3: "Do you know how unity mono works?"[ 96] @ 0x37308: "res/drawable-xhdpi-v4/ -> assets/bin/Data/Managed/"[ 150] @ 0x3733e: "initialize"[ 164] @ 0x3734c: "()I"[ 171] @ 0x37353: "com/tencent/games/sec2021/Sec2021Application"[ 219] @ 0x37383: "com/tencent/games/sec2021/Sec2021IPC"[ 259] @ 0x373ab: "hack detected, risk score:%d"[ 291] @ 0x373cb: "getApplicationInfo"[ 313] @ 0x373e1: "()Landroid/content/pm/ApplicationInfo;"[ 355] @ 0x3740b: "getFilesDir"[ 370] @ 0x3741a: "()Ljava/io/File;"[ 390] @ 0x3742e: "sourceDir"[ 403] @ 0x3743b: "packageName"[ 418] @ 0x3744a: "nativeLibraryDir"[ 438] @ 0x3745e: "getAbsolutePath"[ 457] @ 0x37471: "/proc/self/status"[ 478] @ 0x37486: "TracerPid:"[ 492] @ 0x37494: "diediedie"[ 505] @ 0x374a1: "/proc/self/maps"[ 524] @ 0x374b4: "rb"[ 530] @ 0x374ba: "delete"[ 540] @ 0x374c4: "%zx-%zx %c%c%c%c %x %x:%x %u %s"[ 575] @ 0x374e7: "android/os/Debug"[ 595] @ 0x374fb: "isDebuggerConnected"[ 618] @ 0x37512: "sec2021"[ 629] @ 0x3751d: "getClass"[ 641] @ 0x37529: "getName"[ 652] @ 0x37534: "getSuperclass"[ 669] @ 0x37545: "android/app/Application"[ 696] @ 0x37560: "java/lang/Class"[ 715] @ 0x37573: "()Ljava/lang/Class;"[ 738] @ 0x3758a: "()Landroid/content/pm/ApplicationInfo;"[ 780] @ 0x375b4: "()Ljava/io/File;"[ 800] @ 0x375c8: "()Ljava/lang/String;"[ 824] @ 0x375e0: "Assembly-CSharp.dll"[ 847] @ 0x375f7: "Mono.Security.dll"[ 868] @ 0x3760c: "mscorlib.dll"[ 884] @ 0x3761c: "System.Core.dll"[ 903] @ 0x3762f: "System.dll"[ 917] @ 0x3763d: "UnityEngine.dll"[ 936] @ 0x37650: "UnityEngine.Networking.dll"[ 966] @ 0x3766e: "UnityEngine.PlaymodeTestsRunner.dll"[1005] @ 0x37695: "UnityEngine.UI.dll"[1027] @ 0x376ab: "base.apk"[1039] @ 0x376b7: "android/content/Context"[1066] @ 0x376d2: "()Ljava/lang/ClassLoader;"[1095] @ 0x376ef: "()Ljava/lang/String;"[1119] @ 0x37707: "zip file"[1131] @ 0x37713: "libsec2021.so"[1148] @ 0x37724: "%s/%s"[1157] @ 0x3772d: "dalvik.system.PathClassLoader"[1190] @ 0x3774e: "toString"[1202] @ 0x3775a: "getClassLoader"[1220] @ 0x3776c: "Ljava/lang/String;"[1242] @ 0x37782: "%s%s"[1250] @ 0x3778a: "/app/data/libbugly/crash.info"[1283] @ 0x377ab: "can you crack me?"[1304] @ 0x377c0: "__optional__"[1320] @ 0x377d0: "cc/binmt/signature/PmsHookApplication"[1361] @ 0x377f9: "com/cloudinject/feature/App"[1392] @ 0x37818: "np/manager/FuckSign"[1415] @ 0x3782f: "java/lang/ClassLoader"[1440] @ 0x37848: "findClass"[1453] @ 0x37855: "(Ljava/lang/String;)Ljava/lang/Class;"[1494] @ 0x3787e: "getPackageManager"[1515] @ 0x37893: "()Landroid/content/pm/PackageManager;"[1556] @ 0x378bc: "getPackageName"[1574] @ 0x378ce: "getPackageInfo"[1592] @ 0x378e0: "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"[1649] @ 0x37919: "signatures"[1663] @ 0x37927: "[Landroid/content/pm/Signature;"[1698] @ 0x3794a: "toByteArray"[1713] @ 0x37959: "()[B"[1721] @ 0x37961: "java/io/ByteArrayInputStream"[1753] @ 0x37981: "<init>"[1763] @ 0x3798b: "([B)V"[1772] @ 0x37994: "java/security/cert/CertificateFactory"[1813] @ 0x379bd: "getInstance"[1828] @ 0x379cc: "(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;"[1891] @ 0x37a0b: "X.509"[1900] @ 0x37a14: "generateCertificate"[1923] @ 0x37a2b: "(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"[1982] @ 0x37a66: "getEncoded"[1996] @ 0x37a74: "java/security/MessageDigest"============================================================共解密 85 个字符串============================================================"""doSome 字符串解密脚本 - IDAPython分析结论:- data 数组: 0x372a8 (加密数据源)- bss_data: 0x39998 (解密结果存储) - key_table: unk_34EB7 (密钥指针), 使用负偏移访问解密算法 (从汇编分析):1. data[idx] = key1 (异或密钥, 存储在R8)2. data[idx+1] = key23. length = key1 ^ key24. 对于每个字节 i: - 计算 key_idx = (i / 17) * 17 (使用UMULL + LSR实现) - key_byte = unk_34EB7[-key_idx] (负偏移, 并且R1在循环中递增) - decrypted[i] = data[idx+2+i] ^ key1 ^ key_byte 关键汇编 (0x1F26C - 0x1F298): UMULL R4, R6, R0, R7 ; R7 = 0xF0F0F0F1, 计算 i / 17 ... MOV R6, R6, LSR#4 ; R6 = i / 17 ADD R6, R6, R6, LSL#4 ; R6 = (i/17) * 17 LDRB R6, [R1, -R6] ; R1 = unk_34EB7, 取 unk_34EB7[-R6] ADD R1, R1, #1 ; R1 递增 实际效果: key_table[i % 17], 从 unk_34EB7 开始, 顺序访问"""import idaapiimport idautilsimport idc# ============== 配置区 ==============DATA_ADDR = 0x372a8 # data 数组基址BSS_DATA_ADDR = 0x39998 # bss_data 数组基址 KEY_TABLE_REF = 0x34EB7 # unk_34EB7 - 密钥表引用点KEY_TABLE_LEN = 17 # 密钥表长度 (0x11)# ============== 辅助函数 ==============def get_bytes_at(addr, size): """从IDB读取字节""" return bytes([idc.get_wide_byte(addr + i) for i in range(size)])def decrypt_string(idx): """ 解密单个字符串 - 完全模拟汇编逻辑 @param idx: data数组中的起始索引 (传递给doSome的参数) @return: (解密后的字符串, 原始长度, 下一个索引) 汇编逻辑模拟: R1 初始化为 unk_34EB7 循环中: key_idx = (i / 17) * 17 key_byte = R1[-key_idx], 然后 R1++ 等价于: key_byte = unk_34EB7[i] - unk_34EB7[(i/17)*17] = unk_34EB7[i % 17] """ # 读取 key1 (data_171/R8) 和 key2 key1 = idc.get_wide_byte(DATA_ADDR + idx) # data[idx] key2 = idc.get_wide_byte(DATA_ADDR + idx + 1) # data[idx+1] # 计算字符串长度: v9 = v8 ^ data_171 length = key1 ^ key2 if length == 0: return ("", 0, idx + 4) # 空字符串,跳过header + checksum # 解密循环 - 精确模拟汇编 # R1 = unk_34EB7, 每次迭代 R1++ # key_idx = (i / 17) * 17 # key_byte = R1[-key_idx] = unk_34EB7[i - key_idx] = unk_34EB7[i % 17] decrypted = bytearray() for i in range(length): enc_byte = idc.get_wide_byte(DATA_ADDR + idx + 2 + i) # 模拟: UMULL R4, R6, R0, R7 (R7=0xF0F0F0F1) + LSR#4 + ADD # 这是 ARM 除以 17 的优化: i / 17 key_idx = (i // KEY_TABLE_LEN) * KEY_TABLE_LEN # LDRB R6, [R1, -R6] 其中 R1 = unk_34EB7 + i # 所以实际是: unk_34EB7[i - key_idx] = unk_34EB7[i % 17] key_offset = i - key_idx # = i % 17 key_byte = idc.get_wide_byte(KEY_TABLE_REF + key_offset) # decrypted = enc ^ key1 ^ key_byte dec_byte = enc_byte ^ key1 ^ key_byte decrypted.append(dec_byte) # 尝试解码为字符串 try: result = decrypted.decode('utf-8') except: try: result = decrypted.decode('latin-1') except: result = decrypted.hex() # 下一个索引 = 当前索引 + 2(header) + length + 2(checksum + terminator) next_idx = idx + 2 + length + 2 return (result, length, next_idx)def calc_checksum(data_bytes): """计算XOR校验和""" checksum = 0xFF for b in data_bytes: checksum ^= b return checksum & 0xFFdef decrypt_all_strings(max_count=100, max_offset=2000): """ 批量解密所有字符串 @param max_count: 最大解密数量 @param max_offset: 最大偏移量限制 """ results = [] idx = 0 count = 0 print("=" * 60) print("doSome 字符串解密结果") print("=" * 60) while count < max_count and idx < max_offset: try: decrypted, length, next_idx = decrypt_string(idx) if length > 0 and length < 256: # 合理的字符串长度 # 过滤掉不可打印的垃圾数据 if decrypted and all(c.isprintable() or c in '\r\n\t' for c in decrypted): results.append({ 'index': idx, 'data_addr': hex(DATA_ADDR + idx), 'length': length, 'decrypted': decrypted }) print(f"[{idx:4d}] @ {DATA_ADDR + idx:#x}: \"{decrypted}\"") count += 1 # 移动到下一个条目 if next_idx <= idx: idx += 1 # 防止死循环 else: idx = next_idx except Exception as e: print(f"Error at idx {idx}: {e}") idx += 1 print("=" * 60) print(f"共解密 {len(results)} 个字符串") print("=" * 60) return resultsdef decrypt_by_param(param_value): """ 根据 doSome 的参数值解密 这是最精确的方式,直接模拟 doSome(param) 的行为 @param param_value: 传递给 doSome 的参数值 """ decrypted, length, _ = decrypt_string(param_value) print(f"doSome({param_value}) = \"{decrypted}\" (len={length})") return decrypteddef find_doSome_calls(): """ 查找所有调用 doSome 的位置并提取参数 """ dosome_addr = idc.get_name_ea_simple("doSome") if dosome_addr == idc.BADADDR: print("未找到 doSome 函数") return [] print(f"doSome 函数地址: {dosome_addr:#x}") print("-" * 60) calls = [] for xref in idautils.XrefsTo(dosome_addr): caller_addr = xref.frm caller_func = idaapi.get_func(caller_addr) if caller_func: func_name = idc.get_func_name(caller_func.start_ea) else: func_name = "unknown" # 尝试获取参数值 (ARM/Thumb): 往前查找设置 R0 的指令 # 优化策略: 往前查找 10 条指令,根据指令长度自动回溯 param = None curr_search_addr = caller_addr for _ in range(10): curr_search_addr = idc.prev_head(curr_search_addr) if curr_search_addr == idc.BADADDR: break mnem = idc.print_insn_mnem(curr_search_addr) # 特征识别: # 1. MOV R0, #imm / MOVS R0, #imm # 2. MOVW R0, #imm # 3. LDR R0, [PC, #offset] (常量池加载) if mnem in ('MOV', 'MOVS', 'MOVW', 'LDR'): if idc.get_operand_type(curr_search_addr, 0) == idc.o_reg and idc.get_operand_value(curr_search_addr, 0) == 0: # 0 = R0 op_type = idc.get_operand_type(curr_search_addr, 1) if op_type in (idc.o_imm, idc.o_mem): op_value = idc.get_operand_value(curr_search_addr, 1) if op_value != idc.BADADDR: # 如果是 LDR R0, [PC, #off],get_operand_value 会直接返回常量池里的值 param = op_value break if param is not None: try: decrypted, length, _ = decrypt_string(param) calls.append({ 'call_addr': caller_addr, 'func_name': func_name, 'param': param, 'decrypted': decrypted }) print(f"{caller_addr:#x} in {func_name}: doSome({param}) = \"{decrypted}\"") except: print(f"{caller_addr:#x} in {func_name}: doSome({param}) = <解密失败>") else: calls.append({ 'call_addr': caller_addr, 'func_name': func_name, 'param': None, 'decrypted': None }) print(f"{caller_addr:#x} in {func_name}: doSome(?) = <参数未知>") return callsdef patch_comments(): """ 在所有 doSome 调用点添加解密后的注释 """ calls = find_doSome_calls() for call in calls: if call['decrypted']: # 添加注释 comment = f"doSome({call['param']}) = \"{call['decrypted']}\"" idc.set_cmt(call['call_addr'], comment, 0) print(f"已添加注释 @ {call['call_addr']:#x}: {comment}") print(f"\n共添加 {sum(1 for c in calls if c['decrypted'])} 个注释")# ============== 主入口 ==============if __name__ == "__main__": print("\n" + "=" * 60) print("doSome 字符串解密器 v1.1") print("=" * 60 + "\n") # 方式1: 批量解密所有字符串 # print("[*] 批量解密模式...") results = decrypt_all_strings(max_count=100) # 方式2: 查找调用点并解密 print("\n[*] 查找 doSome 调用点...") # find_doSome_calls() # 方式3: 添加注释到IDB patch_comments() # 方式4: 单个解密测试 # decrypt_by_param(0) # decrypt_by_param(100)"""doSome 字符串解密脚本 - IDAPython分析结论:- data 数组: 0x372a8 (加密数据源)- bss_data: 0x39998 (解密结果存储) - key_table: unk_34EB7 (密钥指针), 使用负偏移访问解密算法 (从汇编分析):1. data[idx] = key1 (异或密钥, 存储在R8)2. data[idx+1] = key23. length = key1 ^ key24. 对于每个字节 i: - 计算 key_idx = (i / 17) * 17 (使用UMULL + LSR实现) - key_byte = unk_34EB7[-key_idx] (负偏移, 并且R1在循环中递增) - decrypted[i] = data[idx+2+i] ^ key1 ^ key_byte 关键汇编 (0x1F26C - 0x1F298): UMULL R4, R6, R0, R7 ; R7 = 0xF0F0F0F1, 计算 i / 17 ... MOV R6, R6, LSR#4 ; R6 = i / 17 ADD R6, R6, R6, LSL#4 ; R6 = (i/17) * 17 LDRB R6, [R1, -R6] ; R1 = unk_34EB7, 取 unk_34EB7[-R6] ADD R1, R1, #1 ; R1 递增 实际效果: key_table[i % 17], 从 unk_34EB7 开始, 顺序访问"""import idaapiimport idautilsimport idc# ============== 配置区 ==============DATA_ADDR = 0x372a8 # data 数组基址BSS_DATA_ADDR = 0x39998 # bss_data 数组基址 KEY_TABLE_REF = 0x34EB7 # unk_34EB7 - 密钥表引用点KEY_TABLE_LEN = 17 # 密钥表长度 (0x11)# ============== 辅助函数 ==============def get_bytes_at(addr, size): """从IDB读取字节""" return bytes([idc.get_wide_byte(addr + i) for i in range(size)])def decrypt_string(idx): """ 解密单个字符串 - 完全模拟汇编逻辑 @param idx: data数组中的起始索引 (传递给doSome的参数) @return: (解密后的字符串, 原始长度, 下一个索引) 汇编逻辑模拟: R1 初始化为 unk_34EB7 循环中: key_idx = (i / 17) * 17 key_byte = R1[-key_idx], 然后 R1++ 等价于: key_byte = unk_34EB7[i] - unk_34EB7[(i/17)*17] = unk_34EB7[i % 17] """ # 读取 key1 (data_171/R8) 和 key2 key1 = idc.get_wide_byte(DATA_ADDR + idx) # data[idx] key2 = idc.get_wide_byte(DATA_ADDR + idx + 1) # data[idx+1] # 计算字符串长度: v9 = v8 ^ data_171 length = key1 ^ key2 if length == 0: return ("", 0, idx + 4) # 空字符串,跳过header + checksum # 解密循环 - 精确模拟汇编 # R1 = unk_34EB7, 每次迭代 R1++ # key_idx = (i / 17) * 17 # key_byte = R1[-key_idx] = unk_34EB7[i - key_idx] = unk_34EB7[i % 17] decrypted = bytearray() for i in range(length): enc_byte = idc.get_wide_byte(DATA_ADDR + idx + 2 + i) # 模拟: UMULL R4, R6, R0, R7 (R7=0xF0F0F0F1) + LSR#4 + ADD # 这是 ARM 除以 17 的优化: i / 17 key_idx = (i // KEY_TABLE_LEN) * KEY_TABLE_LEN # LDRB R6, [R1, -R6] 其中 R1 = unk_34EB7 + i # 所以实际是: unk_34EB7[i - key_idx] = unk_34EB7[i % 17] key_offset = i - key_idx # = i % 17 key_byte = idc.get_wide_byte(KEY_TABLE_REF + key_offset) # decrypted = enc ^ key1 ^ key_byte dec_byte = enc_byte ^ key1 ^ key_byte decrypted.append(dec_byte) # 尝试解码为字符串 try: result = decrypted.decode('utf-8') except: try: result = decrypted.decode('latin-1') except: result = decrypted.hex() # 下一个索引 = 当前索引 + 2(header) + length + 2(checksum + terminator) next_idx = idx + 2 + length + 2 return (result, length, next_idx)def calc_checksum(data_bytes): """计算XOR校验和""" checksum = 0xFF for b in data_bytes: checksum ^= b return checksum & 0xFFdef decrypt_all_strings(max_count=100, max_offset=2000): """ 批量解密所有字符串 @param max_count: 最大解密数量 @param max_offset: 最大偏移量限制 """ results = [] idx = 0 count = 0 print("=" * 60) print("doSome 字符串解密结果") print("=" * 60) while count < max_count and idx < max_offset: try: decrypted, length, next_idx = decrypt_string(idx) if length > 0 and length < 256: # 合理的字符串长度 # 过滤掉不可打印的垃圾数据 if decrypted and all(c.isprintable() or c in '\r\n\t' for c in decrypted): results.append({ 'index': idx, 'data_addr': hex(DATA_ADDR + idx), 'length': length, 'decrypted': decrypted }) print(f"[{idx:4d}] @ {DATA_ADDR + idx:#x}: \"{decrypted}\"") count += 1 # 移动到下一个条目 if next_idx <= idx: idx += 1 # 防止死循环 else: idx = next_idx except Exception as e: print(f"Error at idx {idx}: {e}") idx += 1 print("=" * 60) print(f"共解密 {len(results)} 个字符串") print("=" * 60) return resultsdef decrypt_by_param(param_value): """ 根据 doSome 的参数值解密 这是最精确的方式,直接模拟 doSome(param) 的行为 @param param_value: 传递给 doSome 的参数值 """ decrypted, length, _ = decrypt_string(param_value) print(f"doSome({param_value}) = \"{decrypted}\" (len={length})") return decrypteddef find_doSome_calls(): """ 查找所有调用 doSome 的位置并提取参数 """ dosome_addr = idc.get_name_ea_simple("doSome") if dosome_addr == idc.BADADDR: print("未找到 doSome 函数") return [] print(f"doSome 函数地址: {dosome_addr:#x}") print("-" * 60) calls = [] for xref in idautils.XrefsTo(dosome_addr): caller_addr = xref.frm caller_func = idaapi.get_func(caller_addr) if caller_func: func_name = idc.get_func_name(caller_func.start_ea) else: func_name = "unknown" # 尝试获取参数值 (ARM/Thumb): 往前查找设置 R0 的指令 # 优化策略: 往前查找 10 条指令,根据指令长度自动回溯 param = None curr_search_addr = caller_addr for _ in range(10): curr_search_addr = idc.prev_head(curr_search_addr) if curr_search_addr == idc.BADADDR: break mnem = idc.print_insn_mnem(curr_search_addr) # 特征识别: # 1. MOV R0, #imm / MOVS R0, #imm # 2. MOVW R0, #imm # 3. LDR R0, [PC, #offset] (常量池加载) if mnem in ('MOV', 'MOVS', 'MOVW', 'LDR'): if idc.get_operand_type(curr_search_addr, 0) == idc.o_reg and idc.get_operand_value(curr_search_addr, 0) == 0: # 0 = R0 op_type = idc.get_operand_type(curr_search_addr, 1) if op_type in (idc.o_imm, idc.o_mem): op_value = idc.get_operand_value(curr_search_addr, 1) if op_value != idc.BADADDR: # 如果是 LDR R0, [PC, #off],get_operand_value 会直接返回常量池里的值 param = op_value break if param is not None: try: decrypted, length, _ = decrypt_string(param) calls.append({ 'call_addr': caller_addr, 'func_name': func_name, 'param': param, 'decrypted': decrypted }) print(f"{caller_addr:#x} in {func_name}: doSome({param}) = \"{decrypted}\"") except:赞赏
- 【游戏安全】2021年腾讯游戏安全大赛(安卓) 494
- [原创]D3CTF2023 REVERSE 11882
- [原创]IL2CPP 逆向初探 33276
- 【ACTF2022】Inflated(C++异常处理控制流下的OLLVM混淆) 19155