首页
社区
课程
招聘
[原创]2023腾讯游戏安全竞赛决赛题解(安卓)
2023-4-22 20:42 36365

[原创]2023腾讯游戏安全竞赛决赛题解(安卓)

2023-4-22 20:42
36365

前言

决赛在 il2cpp 上的防护以及 anti-debug 上并没有增加难度,而是把重心放在了注册机算法上,所以决赛篇就不重复写 dump 和 anti-debug 部分了

分析

getflag

在有 dump 的情况下拿 flag 就是洒洒水了,分析我都懒得贴了,上个代码意思一下

1
2
3
4
5
6
7
8
function CheatFlag()
{
    var g_libil2cpp_addr = Module.findBaseAddress("libil2cpp.so");
    var coin_cmp = g_libil2cpp_addr.add(0x4652ac);
    Memory.protect(coin_cmp, 4, "rwx");
    Memory.writeInt(coin_cmp, 0x7100001f);
    Memory.protect(coin_cmp, 4, "rx");
}CheatFlag();

注册机

il2cpp 中分析过程还是一样的,很容易可以找到验证点,稍微分析就可以知道这里的验证方法是低位与rand比较,高位为0

 

 

sec 的 so 里也同样有个加密函数

 

 

这次调试出现了一个奇怪的问题(上次打的时候没这个问题,不知道是怎么触发的),跳转 libsec 会到一个只读的段,里面代码完全相同,所以断点下载这里面根本不会断下来

 

 

把这个段删除就好了

 

 

分析加密函数 0x94368,像这种函数还是好计算的,不过为了方便也可以直接调试

 

 

可以看到 0x9E93C 函数的参数是输入& 0xFFFF00FFFFFF 的值,也就是拿了输入的这些位

 

同时下面那个函数 ida 也标出来了是 strcmp,观察参数可以理解 v8 是 0x9E93C 的输出

 

 

而函数内第一个用到输入的函数是 0x9f4c4,这个函数很简单就是赋值,那么接下来就是需要针对 v10,也就是需要观察下一个函数 0x9f0a8

 

这个函数里面有一些混淆但不多,还是调试起来看好一点

 

 

先看传进来的参数

 

 

第一个参数是输入&0xFFFF00FFFFFF,第二个 0x11 也就是 17,在 0x9E93C 中可以看到赋值 17,第三个参数是一串不知道干什么用的二进制,第四个是 0

 

第一个函数调用完也就是用了 a4 的那个函数,把 1 写到了 a4

 

第二个函数的参数 v15, ida 分析它是一个 char 数组,原本里面的东西应该是没有清空,执行完第二个函数之后就清空了并且 memcpy 了输入,第三个函数和第二个函数是一样的,所以结果就是把 17 存储到了 v14 的空间处

 

可以简单的把这俩个函数都归为 copy 的功能

 

至于后面的,就慢慢 F8,看它执行哪个分支

 

第一句是比较 (0x11 & 1) == 0,很显然结果是假

 

 

而后要执行的是这一块,把它暂且叫分支 1

 

 

这里的参数 a4 前面被赋值为1,v15 是 input 的复制,v13 前面还没有用到过

 

执行完以后 a4 和 v15 没有改变,而 v13 变成了 0x2b67,也就是 v15 的当前值

 

下一句用到了更改后的 v13,以及不知道用来做什么的二进制入参和没变过的 a4

 

执行完以后 v13 和 a3 都没有变化, a4 则变成了 0x2b67

 

似乎这就是复制来复制去,暂时猜不出有效信息

 

然后程序继续执行,跳到了这个分支,把它暂且叫分支 2

 

 

第一个函数执行完之后 v13 变成了8,v14 没有变化还是 0x11

 

第二个函数跟最开始的 copy 是一个函数,也就是把 v13 的 8 赋值给了 v14

 

第三个函数啥事没干,v14 进去什么样出来就什么样,不过出来的时候 x0 = 0,应该是用于判断走向的

 

后面又走到这了,把它暂且叫分支 3

 

 

实际上这个分支的函数就是第一次分支执行的,不过参数不一样,区别就在于 a4 换成了 v15, v15 目前还是 input 的复制

 

执行完以后 v13 从 8 变成了 0x75bc371,并且 v15 也被这个值覆盖了,看起来 v13 是一个 temp 值,只用于储存中间值

 

这之后又到 (8 & 1) == 0,这回的结果是真,会执行不同的分支

 

果然这一次直接执行到了分支 2,之前为假是执行到分支 1 的

 

 

这个函数之前把 0x11 变成了 0x8,这回变成了 0x4,可以猜测是除 2,判断结果还是 0

 

不出意外又执行到了分支 3,这一次 0x75bc371 变成了 0x362594b58b57e1, 好像执行一次,长度就会涨一倍,符合这种运算的应该是平方

1
2
3
4
>>> hex(0x75bc371*0x75bc371)
'0x362594b58b57e1'
>>> hex(0x2b67*0x2b67)
'0x75bc371'

那这个意思应该明白了

 

先判断是不是单数次方,是单数就执行分支 1,分支 1 是用最开始赋值的 1 来乘

 

然后把次方数除 2,如果不为零就执行分支 3 继续乘,乘法的两边是自己,也就是平方

 

那么 off_12EC00 是乘,off_12EC10 是除,off_12EC18 是判断

 

还剩下一个用到了入参的 off_12EC08,猜测是取余,只是因为输入的数太小而没有显示出作用

 

尝试输入一个大的 input 比如 0xFFFF00FFFFFF

 

本来这次运算的结果应该是左边 hex 中的内容,执行后右边变小了

 

 

按照它的读法,a3 是0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375,尝试一下

1
2
hex(0xfff8081bc7dca7d4e8c54ebc420e2cb73b9dbc3fdc7d4455b2f5b4ef598916bd20fc37f15870001bc8140007f8000001%0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375)
'0x228c023ea3485a0244b4820c12cc034e874a8afe26cfc87b997c23889da89ed6d7b038236b04f34dc353f'

发现结果是对应的

 

那么最开始所比较的应该是次方后的结果的字符串

 

果不其然

 

 

所以只要知道哪个数的17次方取余这个大数等于另一个大数就可以了

 

这个问题实际上是 rsa 问题,可以用 yafu 把模数分解乘两个大质数相乘,当时我吃了不会用 yafu 的亏

 

yafu.ini 配置如下,特别注意 dir 相关配置,我这配置有问题执行不下去,后面快结束了我才回头整的 yafu

1
2
3
4
5
6
7
8
9
10
11
B1pm1=100000
B1pp1=20000
B1ecm=11000
rhomax=1000
threads=16
pretest_ratio=0.25
%ggnfs_dir=.\ggnfs-bin\Win32\
ggnfs_dir=.\ggnfs-bin\
%ecm_path=.\gmp-ecm\bin\x64\Release\ecm.exe
%ecm_path=.\ecm\current\ecm
tune_info=       Intel(R) Xeon(R) CPU E5-4650 0 @ 2.70GHz,LINUX64,1.73786e-05,0.200412,0.400046,0.0987873,98.8355,2699.98

最后运算结果是

1
2
3
4
5
6
7
8
9
10
NFS elapsed time = 925.8149 seconds.
Total factoring time = 925.8163 seconds
 
 
***factors found***
 
P51 = 555183147936225271626794036740589959032732535469347
P51 = 640704384372038524783151782406101498608483916642951
 
ans = 1

顺手上传到 factordb 上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import gmpy2
 
q = 640704384372038524783151782406101498608483916642951
p = 555183147936225271626794036740589959032732535469347
n = 0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375
#n = p * q
#yafu-Win32 factor(0x28a831a5bf4b902e95318e50c2075259f91094d08d84409e1b76eadfa0865d1278acc90fa7c6cf6acb375)
 
c = 0x25f6b048b4f32e3ce9175bb64930f65101a706ae74988a4ec87b4d5ec7feb9223ab782bcf1ec9d7fee750
e = 17
 
phi = (p - 1) * (q - 1)
d = gmpy2.invert(e, phi)
#1 = (e * d) % phi
 
m = pow(c, d, n)
print(hex(m))
#m = (c ** d) % n
#c = (m ** e) % n

最终结果是 0x7be300df8b2c

 

就是说,0x7be300df8b2c 是 input % 0xFFFF00FFFFFF 的结果,还剩下 3 个 byte 在余下的运算之中

 

回到最初的解密 0x94368, strcmp 下面的是混淆看不懂结构了

 

我的做法是再每一处跳转做记号,并对被跳转地也做记号,这样就可以清晰看到路径

 

然后发现对照图来还比较方便

 

 

可以看到是先比较 strcmp 的结果,再下一次运行就往 3 走了

 

4 这里我做了标记,参数是排列好的输入,并且执行完之后改变了输入

 

 

5 就是正常的程序自检然后退出

 

那么重点就在 4 调用的函数 0x99ef4 之中

 

0x99ef4 虽然后面做了混淆,不过经过调试可以知道到 0x98e50 这个函数就计算完了,后面没有再改变

 

 

不过这个函数里面混淆的很厉害,什么也看不见,经过调试,可以发现有一处用到了输入,并且更改了输入

 

 

而这个函数内部不用说也是混淆的厉害,而且通过多次这个函数会发现这个函数不是一个固定的函数,就是说这里跳转的函数是从函数列表中拿出来的,与此同时 X8 似乎是个记录 index 的寄存器,观察汇编可以看出 X0 也是从内存列表中与 X8 相关取出的,于是乎可以拿到两个列表

 

另注意,ARM64架构处理器采用48位物理寻址机制,就是说开头 1 字节不要

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0:
0x7042D23DB4,0x7042D2327C,0x7042D10004,0x7042D22148,0x7042D11A88,0x7042D10004,0x7042D13AD0,0x7042D22148,0x7042D13AD0,0x7042D17158,0x7042D22148,0x7042D17E00,0x7042D12CC0,0x7042CFB1BC,0x7042CFB1BC,0x7042CFB1BC
10:
0x7042CFB1BC,0x7042CFB1BC,0x7042D27BF4,0x7042D11A88,0x7042D22148,0x7042D10004,0x7042D10004,0x7042D24E18,0x7042D25C50,0x7042D2A564,0x7042D21BE8,0x7042D22148,0x7042D21BE8,0x7042D2075C,0x7042D19B64,0x7042D16A78
20:
0x7042D11B2C,0x7042D2A564,0x7042D13420,0x7042D10AC4,0x7042D29054,0x7042D25980,0x7042D27BF4,0x7042D27BF4,0x7042D12AE8,0x7042D22148,0x7042D19B64,0x7042D19B64,0x7042D2A564,0x7042D13420,0x7042D16A78,0x7042D25980
30:
0x7042D10004,0x7042D29054,0x7042D12AE8,0x7042D18F4C,0x7042D10004,0x7042D24E18,0x7042D18F4C,0x7042D16518,0x7042D24E18,0x7042D17E00,0x7042D21BE8,0x7042D10004,0x7042D24E18,0x7042D2A564,0x7042D2A564,0x7042D2A564
40:
0x7042D13420,0x7042D16518,0x7042D24E18,0x7042D2A564,0x7042D21BE8,0x7042D19B64,0x7042D29054,0x7042D2A564,0x7042D13420,0x7042D16A78,0x7042D25980,0x7042D11B2C,0x7042D20458,0x7042D27BF4,0x7042D10AC4,0x7042D27BF4
50:
0x7042D12AE8,0x7042D10004,0x7042D29054,0x7042D12AE8,0x7042D10004,0x7042D22468,0x7042CFB1BC
 
0:
0xF01E0FF3,0x95417BFD,0xD30043FD,0x92003F3,0x5000000,0xB200E000,0x64000000,0x761303E1,0xF8000000,0xBD417BFD,0x673303E0,0xC04207F3,0x14123456,0,0x6D5F6D76,0x206E6961
10:
0x41007825,0,0xC38043FF,0x91000009,0x541F03E8,0xCB856129,0xFD0033EA,0x75C03BFF,0xFD401BFF,0x18137C0C,0x1E1C03EB,0x6F00158C,0x7A9F03ED,0x31CB7D8E,0x424D6D2F,0x750B01CE
20:
0x3F001DD0,0x6F9E7610,0x63861DD0,0x601001EE,0x8F0001BF,0xFDAD694E,0xE88005AD,0x1D00216B,0xAEFFFEBC,0xD53F03EB,0x77EB6D4C,0x400B6D2D,0x75857D8E,0x3D1D1D8E,0x2E2D01CC,0xC8AB694C
30:
0xFA80056B,0xBF000D7F,0x1FFFFF11,0xDD003BEB,0x1E01D56B,0xBDC03BEB,0x558037EB,0x719F196B,0x418037EB,0xB9400FEC,0x7A9D03EB,0x3F03058D,0xFDC033ED,0x60907D8D,0x18105D8C,0x1E1F798C
40:
0x57871DAC,0x759F018C,0x55803BEC,0x71901D80,0x340003EC,0x16EC654D,0xBF00019F,0x6F877DAE,0x639F1DAE,0x610C01CD,0x2EC6D4D,0x3D001DAD,0x48EB35AD,0x9D00058C,0x572001A0,0x5E80216B
50:
0x3FFFFEAC,0x61800508,0x5804011F,0xBCFFF8C1,0xF58043FF,0xEAC73BD4,0xD06BA216

X1 中储存着输入

 

 

多执行几次可以发现这应该是一个整体,以 8 字节为一个整体

 

最后一个字节表示 index

 

 

然后就可以通过看这块内存猜运算了,不过这也局限于比较简单的,如果比较复杂还是猜不出来,这个时候可以用 ida trace

 

比方说第 1e 个指令赋值 0x6B(上次知道的),想知道 0x6B 是怎么来的就得看 trace 了

 

可以通过下条件断点精准降落

 

 

断下来之后先修改 trace 选项

 

 

取消跳过 debugger 段

 

 

再点选项上面的 Instruction tracing,然后对下一句语句按 F4,就可以了记录下中间所有的指令以及寄存器的变化,点开 trace window 右键 Export trace to text file,然后就可以面向 trace 逆向了

 

 

很容易可以找到 0x6b 是来源于 0x6ECDD62D00 + 0x15A

1
2
3
4
5
6
000013D3    libsec2023.so:0000007042D18DDC    ADD             X2, X8, X0          X2=000000000000015A
000013D3    libsec2023.so:0000007042D0D010    LDR             X16, [X1,#0x178]    X16=B400006ECDD62D00
000013D3    libsec2023.so:0000007042D0D030    ADD             X12, X16, X2        X12=B400006ECDD62E5A
000013D3    libsec2023.so:0000007042D0D074    STR             X12, [SP,#0x10]
000013D3    libsec2023.so:0000007042D0D2C4    LDR             X0, [SP,#0x10]      X0=B400006ECDD62E5A
000013D3    libsec2023.so:0000007042D0D2C8    LDRB            W0, [X0]            X0=000000000000006B

0x6ECDD62D00 是这一轮的 X0,再往上其实可以找到是来源于 input 那块空间的后面一个指针,不过知道是来源于这里的就行了

 

 

剩余的字节都以此类推,还包括一些跳转

 

最大的跳转是一个 256 轮次的,可以还原之后,运算一轮对比结果,如果运算十轮都没错,那基本上就是没错了

 

我最终扣出来的正向代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include<stdio.h>
 
int main()
{
 
    int input = 0xffffff;//这是输入的剩下的那几位
    for(int j = 0; j < 256; ++j)//这是0x53->0x19之间的256次循环
    {
        int magic[3] = {0x6b, 0xA2, 0x16};
        int t = ((input >> 16) >> 3);
        int temp[3] = {};
        for(int i = 0; i < 3; ++i)//这是0x28->0x1d之间的3次循环
        {
            long long s = (2 - i) * 8;
 
            long long tt = ((t | (input << 5)) >> s) ^ s;
            //printf("tt = 0x%llx\n", tt);
            long long ttt = (tt & 0xff) << 2;
 
            long long tttt = (((((tt >> 6) | (tt<< 0x1A)) & 0x3FC000003) | ttt) & 3) | ttt;
            //printf("tttt = 0x%llx\n", tttt);
            tttt += magic[i];
            temp[i] = tttt & 0xff;
            //printf("tttt = 0x%llx\n", tttt);
 
        }
 
        int temp2[3] = {};
        for(int i = 2;i >= 0; --i)//这是0x32->0x2a之间的3次循环
        {
            long long t = temp[i];
            long long tt = ((t >> 5) | (t << 3)) ^ magic[i];
            //printf("tt = 0x%llx\n", tt);
            temp2[i] = tt & 0xff;
        }
 
        temp2[0] += 0x75;
        temp2[1] ^= 0xfe;
        temp2[2] += 0xc1;
 
 
        for(int i = 0; i < 3; ++i)//本来这个循环只是0x500x45的两次循环,修改了一下逻辑把前面一部分加进去了
        {
            if(i == 0)
            {
                long long t = temp2[i] & 0xff;
                //printf("t = 0x%x\n", t);
                long long ttt = 0xff00 | t;
                long long tttt = (((t >> 0x1f) | (t << 1)) & 0xfffffffe);
                long long ttttt = ((((ttt >> 7) | (ttt << 0x19) & 0x1FE000001) | tttt) & 1);
                long long tt = (tttt | ttttt) ^ (2 - i);
                //printf("tt = 0x%x\n", tt);
                ((char*)&input)[2-i] = tt & 0xff;
            }
            else
            {
                long long t = temp2[i] & 0xff;
                //printf("t = 0x%x\n", t);
                long long tt = ((((t << 1) | (t >> 0x1f)) & 0x1fe) | ((t >> 7 | t << 0x19) & 0x1FFFFFF))^ (2 - i);
                //printf("tt = 0x%x\n", tt);
                ((char*)&input)[2-i] = tt & 0xff;
            }
        }
 
 
    }
    printf("result = 0x%08x\n", input);
 
}

不是很懂有些部分怎么逆,范围也不大,就直接爆破了,最终 solve 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include<stdio.h>
 
int vm(int inp)
{
    int input = inp;//这是输入的剩下的那几位
    for(int j = 0; j < 256; ++j)//这是0x53->0x19之间的256次循环
    {
        int magic[3] = {0x6b, 0xA2, 0x16};
        int t = ((input >> 16) >> 3);
        int temp[3] = {};
        for(int i = 0; i < 3; ++i)//这是0x28->0x1d之间的3次循环
        {
            long long s = (2 - i) * 8;
 
            long long tt = ((t | (input << 5)) >> s) ^ s;
            //printf("tt = 0x%llx\n", tt);
            long long ttt = (tt & 0xff) << 2;
 
            long long tttt = (((((tt >> 6) | (tt<< 0x1A)) & 0x3FC000003) | ttt) & 3) | ttt;
            //printf("tttt = 0x%llx\n", tttt);
            tttt += magic[i];
            temp[i] = tttt & 0xff;
            //printf("tttt = 0x%llx\n", tttt);
 
        }
 
        int temp2[3] = {};
        for(int i = 2;i >= 0; --i)//这是0x32->0x2a之间的3次循环
        {
            long long t = temp[i];
            long long tt = ((t >> 5) | (t << 3)) ^ magic[i];
            //printf("tt = 0x%llx\n", tt);
            temp2[i] = tt & 0xff;
        }
 
        temp2[0] += 0x75;
        temp2[1] ^= 0xfe;
        temp2[2] += 0xc1;
 
 
        for(int i = 0; i < 3; ++i)//本来这个循环只是0x500x45的两次循环,修改了一下逻辑把前面一部分加进去了
        {
            if(i == 0)
            {
                long long t = temp2[i] & 0xff;
                //printf("t = 0x%x\n", t);
                long long ttt = 0xff00 | t;
                long long tttt = (((t >> 0x1f) | (t << 1)) & 0xfffffffe);
                long long ttttt = ((((ttt >> 7) | (ttt << 0x19) & 0x1FE000001) | tttt) & 1);
                long long tt = (tttt | ttttt) ^ (2 - i);
                //printf("tt = 0x%x\n", tt);
                ((char*)&input)[2-i] = tt & 0xff;
            }
            else
            {
                long long t = temp2[i] & 0xff;
                //printf("t = 0x%x\n", t);
                long long tt = ((((t << 1) | (t >> 0x1f)) & 0x1fe) | ((t >> 7 | t << 0x19) & 0x1FFFFFF))^ (2 - i);
                //printf("tt = 0x%x\n", tt);
                ((char*)&input)[2-i] = tt & 0xff;
            }
        }
 
 
    }
    return input;
}
 
int main()
{
    int tofind = 0;
    printf("pls input token:");
    scanf("%d", &tofind);
    int i;
    for(i = 0; i < 0xffffff; ++i)
    {
        if( vm(i) == tofind )
        {
            break;
        }
    }
 
    unsigned long long aa = (unsigned long long)(((unsigned long long)(i & 0xffff00)) << 40) | (unsigned long long)((unsigned long long)(i & 0xff) << 24);
 
    aa |= 0x7be300df8b2c;
 
    printf("%llu", aa);
}

后记

vm 逆到最后一天晚上 9 点,那时候还没有解 RSA ,准备出去吃个饭,回来就直接交了,后来看到论坛一个 512 位的 RSA yafu 爆破,觉得还是有希望能出的,11点半解出来两个大质数,当时最终代码不是这样写的,打了一个 0xffffff 的表,直接找表,导致代码庞大,测试打包改wp也要时间,最后上传时间太长,逾期了 3 分钟

 

当然,最主要还是 rsa 问题以前上手解的少了,工具不熟悉,流程不熟悉,不然也不会最后 10 分钟才完整搞完,既然事实已成,后悔也是无用,今年也要毕业了,明年就没得打了,爷青结


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2023-4-22 20:49 被|_|sher编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (6)
雪    币: 19389
活跃值: (29037)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-4-23 09:33
2
1
感谢分享
雪    币: 0
活跃值: (827)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
冷瞳 2023-4-24 11:46
3
0
干货满满
雪    币: 18
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_赛特 2023-4-25 21:51
4
0
可惜了。时间再充分一点点的话,您应该是比赛的第一名了。
雪    币: 555
活跃值: (162)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Conney 2023-4-26 00:05
5
0
太强了,感谢分享
雪    币: 2093
活跃值: (1820)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
洪七公. 2023-5-2 20:16
6
3
感谢分享
雪    币: 20
活跃值: (70)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_vjvzkpxk 2024-4-24 02:41
7
0
大佬太强了,想重金请您帮逆一下一个FPS游戏的视角的加速度逻辑(手指拖动视角的时候会有加速度)。如果您感兴趣的话麻烦看看私信~
游客
登录 | 注册 方可回帖
返回