首页
社区
课程
招聘
【游戏安全】2021年腾讯游戏安全大赛(安卓)
发表于: 2天前 493

【游戏安全】2021年腾讯游戏安全大赛(安卓)

P.Z 活跃值
2
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] = key2
3. length = key1 ^ key2
4. 对于每个字节 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 idaapi
import idautils
import 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 & 0xFF
 
 
def 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 results
 
 
def 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 decrypted
 
 
def 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 calls
 
 
def 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] = key2
3. length = key1 ^ key2
4. 对于每个字节 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 idaapi
import idautils
import 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 & 0xFF
 
 
def 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 results
 
 
def 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 decrypted
 
 
def 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:

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2天前 被P.Z编辑 ,原因:
上传的附件:
收藏
免费 10
支持
分享
最新回复 (2)
雪    币: 7758
活跃值: (8150)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
2
太细了
2天前
1
雪    币: 104
活跃值: (7531)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
tql
1天前
0
游客
登录 | 注册 方可回帖
返回