前言:
BlueArchive是一款二次元游戏,最初的想法是试试看安卓逆向怎么玩,直到遇到上libtprt,艰难重重终于克服,在此前:对别人的修改做法也比较感兴趣,就花了点小钱入手一份样本来试试看。
作者还是有点实力的,感觉比较好笑随即打算写一篇样本分析。
分析:
打开第一眼,就是两个大大的问候,java层就没啥必要看了,也比较简陋,这种核心功能肯定是封装在native层里面去对其他进程做手脚。
打开IDA看一眼,存在着大概三种混淆:
花指令跳转混淆:
控制流平坦化混淆:
还有些比较奇怪的跳转混淆什么的。
太多了混淆了,注定没法一眼丁真,那就跟着特征去追吧:跟着关键字符串“BlueArchive”,奇迹的起点来。
很轻松地追到这里,通过最高权限执行其他动态链接库的加载,然后隐藏进程(hide map、hide solist),这个叫knm的其实就是一个加载器,用来把自己的外挂打入目标进程的内存中。
分别执行了两个命令:
1 | /data/data/com.ba.xxxx/zrq -pkg com.RoamingStar.BlueArchive -lib |
一个zrq(注入器)用来将动态链接库注入到目标进程中(名字就叫so),然后so自己再接受另外的命令去执行不同的行为。先看看注入器是什么情况,拖到IDA分析:
(事后补充,找了一下,发现注射器的项目地址:指路)
可以确认到就是native层注入的工具,通过篡改链接器linker64,实现的so注入:
结合我自己扒取的反作弊保护样本来看,这个思路是对的:tprt检测的是 进程、端口、还有adb调试状态、以及一些路径(/data/local/tmp)这类比较常见的特征,而作者采用链接器劫持的办法注入进去不是没道理。(游戏启动后会载入il2cpp这个核心逻辑,然后il2cpp.so的DT_NEEDED会要求载入更多动态链接库、相当于PE文件里面的IAT函数引入表一样要求加载必要的so,而执行这一步骤的核心组件就是linker链接器,通过hook篡改链接器这种系统组件可以把自定义的函数库加载到目标进程中)
继续分析:
这套更像“调用 linker 内部入口 + 篡改 linker 数据结构”,不是传统 inline hook。
- 内部接口调用链(本地/辅助侧)
- sub_3D2A0 -> sub_3EA40 -> sub_3E920
- sub_3E920会解析 linker64,按 Android 版本找内部符号:__loader_dlopen / __dl__Z8__dlopenPKciPKv / __dl__ZL10dlopen_ext… / g_dl_mutex
- sub_3EA40再走这个内部函数指针去加载(而非单纯 libc dlopen),并在部分版本上加 g_dl_mutex 锁
- 真正“劫持/篡改 linker”的核心(目标进程)
- 关键在 sub_2DF40 + sub_306C0(native)/sub_309E0(NativeBridge)
- sub_2DF40先定位 linker 内部全局:__dl__ZL6solist、__dl__ZL6sonext,再推断 soinfo->base 与 soinfo->next 偏移
- sub_306C0/sub_309E0在目标进程遍历 solist,找到当前注入 so 对应 soinfo 后,把它从链表摘掉;必要时也修 sonext
- 这就是“隐藏在 linker 视角下的已加载库”
- 隐藏手段
- -hide_solist:上面这套 solist/sonext 摘链(最关键)
- -hide_maps:sub_31870 对每个段做“读出内容 -> 重新匿名映射 -> 写回内容”,让 /proc/pid/maps 不再显示原文件映射路径
- -dl_memfd:sub_2ECA0/sub_2F4E0 走 memfd + android_dlopen_ext,减少磁盘路径痕迹,失败再 fallback legacy dlopen
- 一句话总结
- 它不是改 linker 代码流,而是:借 linker 内部入口完成加载,再改 linker 维护的 soinfo 链表 + maps 映射外观,实现“已注入但难被常规枚举看到”。
怎么远程调用linker呢?手工找内部符号,然后通过远程调用的办法去执行linker内部的函数(直接按函数指针调用),两个关键知识点:改 solist/sonext 链表、以及重映射段来影响 /proc/pid/maps 显示,白嫖隐藏技巧,真的笑了。到这里注入器已经分析完了,合理的。
接下来是常规的游戏修改器(so)文件,要找到篡改的点,一般也就是Hook的位置嘛,总不可能直接动态patch游戏内部的逻辑,搜搜就有了入手点,用的是DobbyHook的框架(指路)

知道用的是什么框架之后就去看看DobbyHook是怎么实现的:和frida差不多,这个就熟悉了,一样是inlinehook,那么整个动态链接库里面必然存在一些特征码,也就是dobbyhook里面的函数,可以直接定位到修改器hook了哪里,从而找到这个外挂实现了什么功能。
如下图,框架内部本身就有一些Hook的关键字符串定位到Hook代码的位置
不过翻了半天,看不懂(太多混淆了),换个思路:
并且发现一个特征字符串:libcnm那么一切又回到了最开始的地方,说明libcnm是应用与钩子程序沟通通讯的端点,来选择是否启用外挂之类的,那么这个so也没啥好看的了,也就是一个类似于frida-agent一样的内部钩子,负责修改游戏逻辑,而外部的so控制器才决定了hook哪里。
那么来到libcnm分析看看,由于存在一些混淆,直接看反汇编是不行滴,找找特征:
很容易就能想到肯定是hook il2cpp嘛。不过引用似乎被混淆了,没法直接定位关键的位置,主逻辑不行那就从DobbyHook下手,实在不行就用frida来hook你的hook,观察调用位置。
看看:

知道这个就非常好办了。
显然是没法静态分析的,但是由于小作坊,我们可以hook你的hook去获取真实它想要hook的地址。
到这里分析也就结束了,下面是一些小插曲:
可以看到,部分函数还是有控制流平坦化保护,导致看不太懂代码逻辑的。研究了下这部分其实是可以解的,通过分发器的变量,收集到达的真实块,从真实块尾部去patch就可以还原这部分的逻辑,如下图所示:
利用分发块的分发变量去模拟执行,跑通所有路径获取真实块的下一个跳转最后的结果:
示例的去混淆比较简单,真实的场景是这样的,不过由于控制流平坦化变种蛮多,一时半会我也懒得去解完这些混淆,能解部分关键的已经够用了,这些多出来的混淆块可能是dobby hook或者是其他通讯用的代码,看了意义也不是特别大。
总结:
以上!整个安卓外挂的流程就是:启动外挂应用app,载入注入器和外挂动态链接库,把动态链接库注入游戏进程并隐藏自身,通过DobbyHook框架、以及动态链接库自己实现的通讯协议来规避检测,然后在应用自身的so来与注入的动态链接库通讯,实现作弊功能的开关,并由应用自身的so来传递要hook的位置、参数什么的,最终实现作弊效果。
作者使用了极大量的混淆,但是实际上关键特征都没隐藏起来,导致解析还是相对比较轻松的,要抄的话直接摘几个框架、Hook外挂的Hook地址,扒取下来自己用就行,难度也不大。整个外挂并没有对libtprt做手脚,在下篇我们将来挑战看看反作弊界最高的山!