本题难度适中,非常适合入门复现。
目标实现无敌版鼠鼠,效果图:
所有org、dump、fix以及分析文件都已放在附件(除了游戏题目由附件大小限制没放)。视频版教程 :967K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4f1s2N6r3M7s2A6F1c8h3&6y4Unity 逆向,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其实也不用在意,只需要找一个合适的时机 du
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-289974.htm