type
status
date
slug
summary
tags
category
icon
password
0x00 引言0x01 游戏逆向1. 反调试检测2.IL2CPP逆向1) dump2) 硬币数量修和生命修改3) Mod Menu分析3.libil2cpp.so 加密分析1) sub_465AB4 分析2) VM分析3) 魔改XTEA分析4. libsec2023.so加密分析1) CRC校验反调试2) 去混淆3) 加密1分析4) 加密2分析0x02 注册机
0x00 引言
之前一直想仔细做一做初赛的题,但是奈何当时心太浮躁,也没有什么时间。趁着放假时间耐心的重新回头看这道题目,花费了很大的精力,但是迈出了很多“第一步”,做了很多新尝试,最终完整解出这道初赛赛题。
0x01 游戏逆向
1. 反调试检测
一开始发现压根进不去游戏,首先环境是Google Pixel 6的原系统,内核为自己编译的GKI内核,KernelSU提供ROOT支持,在其他软件压根检测不到的情况下游戏检测到了调试。
通过
mv /data/local/tmp /data/local/tmp1
测试发现居然通过了,原来是底下放了gdbserver
、lldb-server
以及android_server64
,给这三个放到单独的文件夹就探测不到了。实际上通过eBPF探查能够发现其在线程内一直通过faccessat
系统调用循环检查android_server64
和android_server
,甚至连frida-server
等都不做检测
2.IL2CPP逆向
使用 IL2CPP 开始构建时,Unity 会自动执行以下步骤:
- 将 Unity Scripting API 代码编译为常规 .NET DLL(托管程序集)。
- Applies managed bytecode stripping. This step significantly reduces the size of a built game.
- 将所有托管程序集转换为标准 C++ 代码。
- 使用本机平台编译器编译生成的 C++ 代码和 IL2CPP 的运行时部分。
- 将代码链接到可执行文件或 DLL,具体取决于目标平台。

1) dump
可以使用
Il2CppDumper
获取dump.cs
,也可以使用Zygisk-Il2CppDumper
直接静态dump
,但是前一种方法能得到更多的产物,方便后续的分析,缺点是在某些游戏有混淆等保护的情况下无法正常工作,而后一种因为是在运行过程中动态dump函数名和函数偏移,可绕过保护、加密以及混淆。因为绕不过对libil2cpp.so的分析,所以还是选择使用Il2CppDumper
静态dump,以获取更多的产物辅助分析。而这道题目的
libil2cpp.so
进行了加固,直接静态分析无法拿到真实的代码逻辑,需要在运行时进行解密。因此利用Frida dump运行过程中的so,这里把内存页面的属性修改为可读,不然就会出现无法访问内存的异常,但是这样就会导致程序crash掉,不过能够成功dump即可。对修复之前的so进行静态dump,能够成功解析。在解析之后利用对so进行修复,完成后通过
elf-dump-fix
maiyao1988 • Updated Jan 4, 2025
il2cppdumper
提供的IDA脚本恢复符号。
2) 硬币数量修和生命修改
在Dump出的内容中搜索coin,能搜到一个
MouseController
,显然是游戏控制相关。其中 private TssSdtInt Coins;
就是硬币数量的变量。同时还关注到CollectCoin
这个方法,控制硬币数量增加的代码应该就在这里,通过offset
定位到具体代码。确定通过
TssSdtInt__op_Increment
增加硬币数量。
而在该函数内部通过
GetValue
获取值并增1后SetValue
实现硬币收集时的数量增加。只需要Hook到ADD指令之后,修改x1寄存器的值即可。同理找到触碰到射线时生命减少的逻辑一样Hook即可实现硬币和生命值修改。
已经能够成功拿到
flag
sec2023_89e761

3) Mod Menu分析

在Dumper出的内容中能找到一个
SmallKeyboard
类,并且其方法名和部分成员是混淆的,很容易就可以和Mod Menu的输入键盘建立起联系。利用IDA分析时发现其中有一个方法
SmallKeyboard__iI1Ii
有三个分支,结合其中的逻辑可以大致判断出三个分支分别是KeyType < 2
键入数字,直接与之前的输入拼接
KeyType == 2
按下OK提交最终输入的数字串
KeyType == 3
按下Del删除前一个输入
因此需要着重分析第二个OK的分支。

主要关注后面的代码,首先将string转为uint64类型,然后进行check,最后生成下一轮的Token并存储类成员变量里,该token是随机生成的。因此主要分析
SmallKeyboard__iI1Ii_4610736
中的check逻辑。
在调用这个check函数的过程中,第一个参数是
SmallKeyboard_o *this
存储在x0
寄存器。第二个参数是用户输入值的uint64
,存储在X1
寄存器中
3.libil2cpp.so
加密分析
在
check
内部会有一个间接跳转,跳转位置是g_sec2023_p_array + 0x48
位置存储的地址。
而这个符号是从
libsec2023.so
导入的,直接分析能够发现0x48
偏移位置刚好是sub_31164

内部会调用
sub_3B8CC
,传入的参数就是转换后的uint64
类型输入
通过Frida hook能够计算出
g_sec2023_o_array
指向的其实是libil2cpp.so
中的0x465AB4
位置,但是由于存在检测,只能hook
一瞬间就会退出。如果IDA无法反汇编得到伪码的情况下(提示
START OF FUNCTION CHUNK FOR .init_proc
,当然如果使用IDA9.0就不会遇到这个问题),需要先undefine
掉影响其反汇编的.init_proc
函数(边界识别错误导致的),然后在0x465AB4
创建函数。1) sub_
465AB4
分析
在调用该函数的时候会传入两个参数,其中
a2
就是经过libsec2023.so
加密完后的结果,又传回libil2cpp.so
进行后续的加密与校验,这里会将其从uint64
拆成高低两个uint32
值,然后传入OO0OoOOo_Oo0___ctor
赋值到,OO0OoOOo_Oo0__oOOoO0o0
进行加密,然后重新取出加密后的结果。
对加密后的结果进行魔改
XTEA
加密,最终高位的uint32需要是0,低位的uint32将结果与token
的uint32
值进行比较。
通过frida Hook尝试直接开启作弊,能够成功实现永生和加速。但是题目要求是注册机,需要继续分析加密算法。由上面的分析可知,在
libil2cpp
中有两次加密,libsec2023
中有其他加密,先分析libil2cpp
中的加密。2) VM分析
首先需要分析
OO0OoOOo_Oo0__oOOoO0o0
的加密,主要逻辑在下面的大循环中,从private Dictionary<int, Action> oOOO0O00;
这个成员变量中一直取Item
并执行。
通过Frida Hook可以确定在不同输入情况下的索引是不会改变的,就是单纯的固定虚拟机。

那么可以尝试通过Frida拿到这些op对应的
handler
从输出的结果中筛出用到的所有Op以及其
handler
,定位到具体的函数发现其实就是在这个类中的一些函数名经过混淆的方法。
以
0x46ae50
为例,利用IDA插件简化MBA表达式后,能够看明白其主要的逻辑就是从数组中取出东西进行运算后再放到数组arr[index] = arr[index] + arr[index + 1]
,但是并非所有Handler都是这样的逻辑,而且暂时未知这个数组和index
的具体含义,除此之外还有一些其他的数组和类成员变量,因此需要分别分析,然后使用Frida进行VM的trace。
利用Frida Trace虚拟机,在进行分析后能够大致推测出
array32
应该是一个类似于栈的存储空间,array16
则是存储op的缓存区,除此之外还有一个存储输入数据的uint32
类型数组。另外在结构体中对每个缓存区都有一个index
用于从数组中索引数据。
能够从中逆向出VM加密算法并据此写出解密算法。
3) 魔改XTEA分析

编写加解密脚本,到此为止在libil2cpp.so中的加密部分已经分析完成,就是先通过vm加密,然后进行魔改xtea加密,最后进行校验。因此下一步的目标就是逆向libsec2023.so中的加密逻辑。
4. libsec2023.so加密分析
1) CRC校验反调试
只要使用Frida对
libsec2023
进行hook分析就会在一两秒后杀死App,怀疑是针对Frida InlineHook
进行的CRC校验,通过stackplz
进行分析发现每隔几秒就会读取一次libsec2023.so
文件,因此可以更加确定这一猜想。同时利用stackplz
的堆栈追踪功能定位进行文件读取的操作的位置。大部分关键代码都被间接跳转混淆,导致很难进行分析。通过分析汇编结合Frida的Hook能够发现将动态链接库文件每次读取
0x1000
,通过间接跳转到前面的位置进行CRC
校验,在这个过程中每次都会将上一次的值从w22寄存器存储到w3作为参数传入,然后将更新后的结果从w0存储到w22,直到计算完成。
在往调用栈的前面定位能够发现是
0x37108
位置调用函数完成上述的文件打开和读取以及CRC计算等工作,值得注意的是传入了一个参数,即x19
的值,而返回值与0比较。在后续BR X8
的时候会跳转到0x37134
,这里从x19 + 0x20
的位置取出两个值(位置1)并在位置2进行比较,因此可以推测传入了一个指针用于存储可能表征比较结果的内容,在位置2进行最终的比较,尝试使用Frida在CMP的位置修改这两个值,发现这两个值是两个地址,因此可能不是真正的CRC校验代码 
再往前追发现在
0x36498
位置进行了跳转,结束后通过CMP比较返回值和0,那么可以猜测只要hook这里修改w0
寄存器的值即可。
结果每次会出现五次比较,其中第一次返回值为-1,只要修改为0就不会退出。

2) 去混淆
首先想法是采用
frida-stalker
去trace指令执行流,对trace结果的br跳转情况进行分析,并且由于加密过程中存在混淆操作,所以br的跳转情况一般最多只会出现两种,大部分的跳转集中在循环过程中,跳转目标是一致的,另外可能会发生在循环边界位置,因此只需要看trace结果就能直接修复间接跳转,手动修复都会很快。但是需要注意Patch的过程中不要把正常指令给Patch掉,秉持着下面的原则
- 尽量少patch指令,一般情况下只patch最后两条即可。
- 只patch与间接跳转寄存器相关的指令
- 保证修复后的跳转在其他正常指令的后面。
本题目中间接跳转不是很多,手动patch就行,但是对于其他大规模使用间接跳转的情况就需要考虑自动化了。这里放两位大佬的复现文章,他们用自动化脚本实现去混淆。

3) 加密1分析
恢复完成后分析
sub_3B8CC
函数,内部先调用enc_1对输入变形后的的uint64
进行第一次整体加密,然后分别对高低32bit进行enc_2
加密,最后拼接。
首先看
enc_1

直接拷贝加密算法,然后写出解密算法,验证后确认没问题。
4) 加密2分析
加密2的算法主要调用了
sec2023.Encrypt
的encrypt
方法
脱壳后直接使用JEB 5打开就已经是去混淆的结果,直接写加解密算法即可

0x02 注册机
直接将各个算法加解密穿起来即可分别是
libsec2023
中的加密1 -> 加密2
,然后是libil2cpp
中的vm -> 魔改xtea
,输入按顺序经过这四个加密,然后解密倒过来即可。- 作者:LLeaves
- 链接:https://lleavesg.top//article/Tencent-2023-SecAndroid
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章