本文档是进行完相关分析之后的总结回忆, 所以可能有的地方的花指令是被去除了再截图的, 函数名和数据结构被重命名了. vm算法没有逆出来
根据题目提示要求, flag会在1000分的时候出现. 打开安装包发现是unity打包的游戏, 用010editor打开global-metadata.dat文件发现头文件没有被加密, 字符串也完好, 拖入il2cppdumper提示失败. ida打开libil2cpp.so查看init.array段, 第一个函数中看到了module_base: %p, g_unpacker: %p
字符串, off_13BAFF0
则是g_sec2023_p_array
, 猜测是动态加载的时候会被libsec2023.so解密出来.
粗略看了一下解密挺复杂的, 所以首先尝试从内存中dump出来libil2cpp.so, 然后再使用il2cppdumper生成类信息, 这次可以直接成功. 观察数据结构类名函数名等信息, 在相关排查后确定老鼠的相关控制逻辑在MouseController
类中, 其中有一个private void CollectCoin(Collider2D coinCollider)
函数, 参数是碰撞器, 使用ida打开这个函数并且导入结构体类名信息, 其伪代码中出现了一个特殊的数字1000, 题目提到了分数多于1000即会出现flag, 估计关键点在这里.
其关键位置原本的汇编指令是CMP W0, #0x3E8
, 这里使用gg修改器修改为CMP W0, #0
, 然后随便吃个金币后成功得到flag.
前面分析大概能够猜测到libsec2023.so中大概率会存在反调试的功能, 首先在手机端开启frida_server, 结果并没有附加到游戏上打开游戏便被检测到了. 首先对其字符串操作函数进行hook看看有没有关键信息, hook掉strcmp, strncmp, strlen函数能够发现存在字符串re.frida.server的对比, 以及sscanf函数对maps文件的处理.
因为我并没有注入游戏就被检测了, 那么关于这种检测常见的有:
检测/data/local/tmp文件夹下的re.frida.server
检测27042端口
检测dbus
查阅资料发现解决第一个问题需要重新编译frida, 这有点太麻烦了, 所以我直接打开frida_server的二进制文件patch掉re.frida.server字符串, 并且使用./fs -l 0.0.0.0:23948
命令切换监听端口, 这样一来就能够成功过掉检测.
在这之后我继续分析libil2cpp.so内的功能流程, 我发现frida hook它是没问题的, 但是只要hook libsec2023.so就会崩掉, 刚开始我是怀疑crc检测, 检测到了就调用kill
或者exit
等函数, 但是我hook掉退出函数发现并没有被调用. 对于这些检测函数, 其大概的逻辑是新开一个线程不间断扫描内存段, 并且计算出一个值, 如果值改变了那么就说明程序段被篡改了.
针对上面这个逻辑, 我首先想到了frida的异常hook, 对指定的地址修改内存属性为只能执行, 读取后进入异常打印堆栈信息. 结果发现要么崩溃要么没有效果, 所以只能另外想办法.
我这边正好有一个修改完内核之后的手机, 可以使用rwProcMem33开源项目中的硬件断点, 所以我使用这个工具尝试一下能否找到关键点. 结果成功定位到crc的关键函数, 下图中的libsec2023.so的基址是0x7d02bc8000
命中地址0x7d02bff704
减去基址得到偏移为0x37704
, 为了保险还对其他的函数下了相同的内存断点, 发现断下来的地址只有这一个, 并且如果游戏进程没有重开, 返回值都相同, 跟进ida中查看是一个很简单的计算函数, 他的调用者sub_370AC
很有可能就是进行相关操作的检测函数, 对其进行分析:
因为前面分析知道sub_376CC
正常情况的返回值都是相等的, 所以我们进行inlinehook之后会导致该返回值修改, 走到崩溃路线, 因此关键在cmp语句或者下面的cesl语句, 这句csel语句的意思是x8 = w0==w8 ? x8 : x9
, x8与x9相差0x10, 这就导致了控制流的改变. 值得指出的是371F0这里的语句是错误的, 在内存dump下来的libsec2023.so中这里的值是BR X8
. 将371DC处的值改为NOP
之后x8的值就不会被修改为x9了, 也就绕过了inlinehook检测. 至此frida就能够完全正常使用了
除了上面说到的反调试, 在libsec2023.so中还存在大量的花指令为csel...br reg形式的指令, 能够使得函数不被ida正常解析出来, 其br之后实际还是继续执行下一条指令. 在这些花指令中还藏着正常的选择语句块, 或者跳过几条语句的埋坑, 所以不方便用脚本批量还原掉, 我是踩完坑被误导之后才决定分析到哪里再配合frida-trace手动去除的.
例如下面两个截图是还原前后的效果
分析算法首先需要找到输入的值是从哪里传入的, 观察类名发现有一个SmallKeyboard
类, 其中有被混淆过的参数, 经过一个个查看发现iI1Ii
函数存在关键逻辑, 主要是有三种按键: 数字, 删除, 回车确定. 查看伪代码能够发现关键逻辑, 使用frida hook System_Convert__ToUInt64_8763844
得到其返回值确实就是我们的输入值. 为了方便, 我输入的值是666666, 对应的16进制值是0xa2c2a
跟进SmallKeyboard__iI1Ii_4610736
函数发现其最后jumpout到别的地方去了, 用frida hook后得到其跳转的位置是libsec2023.so + 0x31164处, 从这里开始有比较误导性的混淆, 我的截图都是去除混淆之后加了注释的.
sub_31164
函数调用了sub_3B8CC
之后拿到一个函数指针跳转出去了, 经过动态调试能够知道跳转到il2cpp中去了. 这里跟进看看sub_3B8CC
做了什么事情.
经过验证这个函数存在错误的b跳转, 伪代码因此是存在错误的, 按照截图中最后的返回值恒为0, 这显然是错误的. 但是可以看出来大概做了什么事情:
sub_3B9D4处理输入值
sub_3A924处理第一步输出值的一部分
sub_3A924处理第一步输出值的另一部分
返回
sub_3B9D4函数看起来很简单, 按照伪代码的逻辑, 对于输入值0, 我们应该返回(0x85-24)<<24, 也就是0x6d000000, 但最后的结果是0x6d94cae4. 那这个伪代码就没用了, 需要查看汇编代码是什么情况, 为了方便分析, 使用frida-trace对该函数进行一遍trace得到对应的log记录, 进行汇编分析, 汇编分析时以输入参数为发力点, 观察数据变化.
分析完以上的汇编代码后可以还原对应的算法
使用frida hook来验证结果是否正确, 确实无误
之后来编写对应的逆算法为
在这个函数中有很多复杂且奇怪的函数, 并且也出现了函数指针+1000多的数字, 因此可以猜想这里主要做的事情是准备java层环境, 之后调用java函数, 将伪代码中JNIEnv*参数类型标注出来. 刷新伪代码后能够很明显看到先new了一个java层的bytearray, 然后findclass, GetStaticMethodID得到函数, 最后在sub_3ABBC中调用这个函数.
这里我首先对findclass进行hook, 发现类名是空的, 静态函数名称能得到是encrypt, 基本可以确定无误. 用jadx打开dex发现并没有这个函数, 因此怀疑是走RegisterNatives动态注册的. 使用frida对RegisterNatives进行hook, 发现了一条记录
但是经过排查很明显不是上面的encrypt函数. 思考一下, 除了动态注册以外, 有可能是动态加载了dex, 于是使用frida-dexdump将内存中的dex dump下来. 果然在某个dex中发现了该函数.
但是其控制流也被混淆了, 好在函数本身并不复杂, 因此可以将伪代码直接复制新建一个java工程, 测试功能完好之后对其进行调试分析, 将函数逻辑梳理一下得到其逻辑代码如下
经过分析也能得出对应的逆算法为如下
使用frida hook得到的结果与上面算法计算得到的结果一致
梳理完这个函数那么算法在libsec2023.so的部分就已经结束了, 按照动态流程分析执行完sub_31164后会跳回到libil2cpp.so + 0x465AB4中, 从java层这个encrypt返回的结果与0x465AB4所拦截的结果是一致的. 所以libsec2023.so部分到此结束.
这个函数是从libsec2023.so返回所进入的函数, 首先根据伪代码观察其流程, 主要做的事情是初始化了一些数组和对象, 将上一个encrypt的结果传入sub_46AD44函数中进行加密, 然后在一个do_while循环中进行第二次加密. 最后如果v28等于0并且v27与token的值相等就开启mod成功, 这里手动patch判断语句可以实现作弊功能, 所以整体思路是对的.
首先来分析第一个sub_46AD44函数, 这个函数非常复杂, 主要是重要的字段名信息还有函数名信息都被混淆掉了.
整个类中做了什么事情, 首先会初始化一些参数, 最重要的是某个迭代值(类内偏移0x2c), action词典(类内偏移0x38), 码表(类内偏移0x10). 该类在初始化的时候会注册0x1-0x16的值与某个操作函数对应关系在action词典中, 然后在一个大循环中随着迭代值的变化在码表中选择不同的action, 类似于状态机的实现. 下面这个sub_46AD44函数内的循环即是主要逻辑.
最重要的事情是无论输入值是多少, 该状态机的码表和操作序列是一样的. 因此可以dump码表和操作序列, dump出来的码表和操作序列如下
其中修改data的函数总共执行了48次, 且看起来非常有规律性, 因此一种可行方案是对这里用到的总共13种操作函数进行重写, 实现一个和游戏分离离线的状态机, 然后再对该算法进行逆向. 时间和工作量原因我这里并没有对该算法实现相应的逆向.
虽然上一个算法并没有实现对应的逆向算法, 但是其输入输出是一一对应的, 所以这个部分也能够继续进行. 这个算法大部分数据的来源都是已知的, 除了其中的某个数组, 这个数组可以用frida获取, 并且其值是锁定的, 不会改变
v29本身的+8是跳过其他结构, 到数据区, 可以看到数组的下标一定是[0, 1, 2, 3]中的一个, 所以把数组的这四项dump下来即可实现该算法. 经过研究其等价的c代码如下:
那么它的逆算法也能给出是这样的, 经过验算无误
到此为止成功获取了flag, 过掉反调试使得frida能够正常工作, 之后静态分析加动态调试还原了除vm的算法以外的其余算法.
.text:
00000000000371C8
41
01
00
94
BL sub_376CC ;调用的计算函数
.text:
00000000000371C8
.text:
00000000000371CC
C8
1A
40
B9 LDR W8, [X22,
.text:
00000000000371D0
09
07
80
52
MOV W9,
.text:
00000000000371D4
1F
00
08
6B
CMP
W0, W8 ;对返回值进行判断
.text:
00000000000371D8
08
05
80
52
MOV W8,
.text:
00000000000371DC
08
01
89
9A
CSEL X8, X8, X9, EQ ;根据判断结果进行赋值
.text:
00000000000371E0
A8
6A
68
F8 LDR X8, [X21,X8]
.text:
00000000000371E4
09
3E
91
52
49
F8 A6
72
MOV W9,
.text:
00000000000371EC
08
01
09
8B
ADD X8, X8, X9
.text:
00000000000371F0
01
00
00
14
B loc_371F4
.text:
00000000000371C8
41
01
00
94
BL sub_376CC ;调用的计算函数
.text:
00000000000371C8
.text:
00000000000371CC
C8
1A
40
B9 LDR W8, [X22,
.text:
00000000000371D0
09
07
80
52
MOV W9,
.text:
00000000000371D4
1F
00
08
6B
CMP
W0, W8 ;对返回值进行判断
.text:
00000000000371D8
08
05
80
52
MOV W8,
.text:
00000000000371DC
08
01
89
9A
CSEL X8, X8, X9, EQ ;根据判断结果进行赋值
.text:
00000000000371E0
A8
6A
68
F8 LDR X8, [X21,X8]
.text:
00000000000371E4
09
3E
91
52
49
F8 A6
72
MOV W9,
.text:
00000000000371EC
08
01
09
8B
ADD X8, X8, X9
.text:
00000000000371F0
01
00
00
14
B loc_371F4
3b9e4
csel x10, xzr, x9, lo ; x10
=
0xa2c5a
-
-
>
0x0
(null)
3b9e8
adrp x9,
3b9ec
add x9, x9,
3b9f0
ldr x10, [x9, x10] ; x10
=
0x0
-
-
>
0x7c8f337108
3b9f4
mov w11,
3b9f8
movk w11,
3b9fc
add x10, x10, x11 ; x10
=
0x7c8f337108
-
-
>
0x7d0333ea04
(j)
3ba00
br x10 ;必定跳转
3ba04
mov w10,
3ba08
mov w11,
3ba0c
cmp
x10,
3ba10
mov w12,
3ba14
csel x11, x12, x11, ge ; x11
=
0x18
-
-
>
0x10
3ba18
ldr x11, [x9, x11] ; x11
=
0x10
-
-
>
0x7c8f337138
3ba1c
mov w12,
3ba20
movk w12,
3ba24
str
wzr, [sp,
3ba28
add x12, x11, x12 ; x12
=
0x740078fc
-
-
>
0x7d0333ea34
(xh��
3
)
3ba2c
mov w11,
3ba30
br x12 ;必定跳转
;我称前面是第一个代码块
3ba34
ldr w12, [x0, x8, lsl
3ba38
add x13, sp,
3ba3c
mov w14,
3ba40
lsr w12, w12, w11 ; 取字节
3ba44
eor w12, w12, w10 ; x12
=
0x0
-
-
>
0x3
和字节序号做异或
3ba48
strb w12, [x13, x10] ; 存到[sp,
...
;我称这为第二个代码块, 主要做的事情是取
666666
(
64
位)的高
32
位, 和字节序号异或存回去, 省略掉的部分是其他字节相同操作
3ba74
ldrb w11, [sp,
3ba78
ldrb w13, [sp,
3ba7c
mov w12,
3ba80
ldrb w14, [sp,
3ba84
eor w11, w11, w12 ; x11
=
0x3
-
-
>
0x85
byte
4
和
0x86
异或
3ba88
mov w12,
3ba8c
eor w12, w13, w12 ; x12
=
0xd3
-
-
>
0xd2
0xd3
和byte
2
异或
3ba90
ldrb w13, [sp,
3ba94
strb w11, [sp,
3ba98
sub w11, w14,
3ba9c
strb w11, [sp,
3baa0
mov w11,
3baa4
sub w13, w13,
3baa8
strb w12, [sp,
3baac
mov w12,
3bab0
strb w13, [sp,
3bab4
mov w13,
3bab8
cmp
x11,
3babc
str
wzr, [x0, x8, lsl
3bac0
csel x12, x13, x12, ge ; x12
=
0x8
-
-
>
0x20
3bac4
ldr x12, [x9, x12] ; x12
=
0x20
-
-
>
0x7c8f3371e4
3bac8
mov w13,
3bacc
movk w13,
3bad0
mov w10, wzr ; x10
=
0xffffffffffffffff
-
-
>
0x0
(null)
3bad4
add x13, x12, x13 ; x13
=
0x740078fc
-
-
>
0x7d0333eae0
(�
3
)
3bad8
mov w12,
3badc
br x13 ;
;我称这为第三个代码块, 主要做的事情是每个字节分别进行对应加减法
3bae0
add x13, sp,
3bae4
ldrb w14, [x13, x11] ; x14
=
0x2
-
-
>
0x85
取最高字节
3bae8
sub w14, w14, w12 ; x14
=
0x85
-
-
>
0x6d
3baec
strb w14, [x13, x11] ;最高字节
-
0x18
存回去 byte[
3
]
-
=
0x18
3baf0
and
w14, w14,
3baf4
lsl w14, w14, w12 ; x14
=
0x6d
-
-
>
0x6d000000
3baf8
sub x11, x11,
3bafc
mov w13,
3bb00
add w10, w14, w10 ; x10
=
0x0
-
-
>
0x6d000000
3bb04
mov w14,
3bb08
cmp
x11,
3bb0c
str
w10, [x0, x8, lsl
3bb10
csel x13, x14, x13, ge ; x13
=
0x8
-
-
>
0x20
3bb14
ldr x13, [x9, x13] ; x13
=
0x20
-
-
>
0x7c8f3371e4
3bb18
mov w14,
3bb1c
movk w14,
3bb20
sub w12, w12,
3bb24
add x13, x13, x14 ; x13
=
0x7c8f3371e4
-
-
>
0x7d0333eae0
(�
3
)
3bb28
br x13 ;循环节
...
;我称这为第四个代码块, 主要做的事情是对每个字节分别进行byte[i]
-
=
8
*
i;
;在这之后代码会取低
32
位进行相同的操作
3b9e4
csel x10, xzr, x9, lo ; x10
=
0xa2c5a
-
-
>
0x0
(null)
3b9e8
adrp x9,
3b9ec
add x9, x9,
3b9f0
ldr x10, [x9, x10] ; x10
=
0x0
-
-
>
0x7c8f337108
3b9f4
mov w11,
3b9f8
movk w11,
3b9fc
add x10, x10, x11 ; x10
=
0x7c8f337108
-
-
>
0x7d0333ea04
(j)
3ba00
br x10 ;必定跳转
3ba04
mov w10,
3ba08
mov w11,
3ba0c
cmp
x10,
3ba10
mov w12,
3ba14
csel x11, x12, x11, ge ; x11
=
0x18
-
-
>
0x10
3ba18
ldr x11, [x9, x11] ; x11
=
0x10
-
-
>
0x7c8f337138
3ba1c
mov w12,
3ba20
movk w12,
3ba24
str
wzr, [sp,
3ba28
add x12, x11, x12 ; x12
=
0x740078fc
-
-
>
0x7d0333ea34
(xh��
3
)
3ba2c
mov w11,
3ba30
br x12 ;必定跳转
;我称前面是第一个代码块
3ba34
ldr w12, [x0, x8, lsl
3ba38
add x13, sp,
3ba3c
mov w14,
3ba40
lsr w12, w12, w11 ; 取字节
3ba44
eor w12, w12, w10 ; x12
=
0x0
-
-
>
0x3
和字节序号做异或
3ba48
strb w12, [x13, x10] ; 存到[sp,
...
;我称这为第二个代码块, 主要做的事情是取
666666
(
64
位)的高
32
位, 和字节序号异或存回去, 省略掉的部分是其他字节相同操作
3ba74
ldrb w11, [sp,
3ba78
ldrb w13, [sp,
3ba7c
mov w12,
3ba80
ldrb w14, [sp,
3ba84
eor w11, w11, w12 ; x11
=
0x3
-
-
>
0x85
byte
4
和
0x86
异或
3ba88
mov w12,
3ba8c
eor w12, w13, w12 ; x12
=
0xd3
-
-
>
0xd2
0xd3
和byte
2
异或
3ba90
ldrb w13, [sp,
3ba94
strb w11, [sp,
3ba98
sub w11, w14,
3ba9c
strb w11, [sp,
3baa0
mov w11,
3baa4
sub w13, w13,
3baa8
strb w12, [sp,
3baac
mov w12,
3bab0
strb w13, [sp,
3bab4
mov w13,
3bab8
cmp
x11,
3babc
str
wzr, [x0, x8, lsl
3bac0
csel x12, x13, x12, ge ; x12
=
0x8
-
-
>
0x20
3bac4
ldr x12, [x9, x12] ; x12
=
0x20
-
-
>
0x7c8f3371e4
3bac8
mov w13,
3bacc
movk w13,
3bad0
mov w10, wzr ; x10
=
0xffffffffffffffff
-
-
>
0x0
(null)
3bad4
add x13, x12, x13 ; x13
=
0x740078fc
-
-
>
0x7d0333eae0
(�
3
)
3bad8
mov w12,
3badc
br x13 ;
;我称这为第三个代码块, 主要做的事情是每个字节分别进行对应加减法
3bae0
add x13, sp,
3bae4
ldrb w14, [x13, x11] ; x14
=
0x2
-
-
>
0x85
取最高字节
3bae8
sub w14, w14, w12 ; x14
=
0x85
-
-
>
0x6d
3baec
strb w14, [x13, x11] ;最高字节
-
0x18
存回去 byte[
3
]
-
=
0x18
3baf0
and
w14, w14,
3baf4
lsl w14, w14, w12 ; x14
=
0x6d
-
-
>
0x6d000000
3baf8
sub x11, x11,
3bafc
mov w13,
3bb00
add w10, w14, w10 ; x10
=
0x0
-
-
>
0x6d000000
3bb04
mov w14,
3bb08
cmp
x11,
3bb0c
str
w10, [x0, x8, lsl
3bb10
csel x13, x14, x13, ge ; x13
=
0x8
-
-
>
0x20
3bb14
ldr x13, [x9, x13] ; x13
=
0x20
-
-
>
0x7c8f3371e4
3bb18
mov w14,
3bb1c
movk w14,
3bb20
sub w12, w12,
3bb24
add x13, x13, x14 ; x13
=
0x7c8f3371e4
-
-
>
0x7d0333eae0
(�
3
)
3bb28
br x13 ;循环节
...
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2023-4-17 20:18
被juice4fun编辑
,原因: