2022腾讯游戏安全技术竞赛初赛-题解

初赛题

这里有一个画了flag的小程序,可好像出了点问题,flag丢失了,需要把它找回来。

题目:

image-20220416172923803

找回flag样例:

image-20220416172931385

要求:

1、不得直接patch系统组件实现绘制(如:直接编写D3D代码绘制flag),只能对题目自身代码进行修改或调用。

2、找回的flag需要和预期图案(包括颜色)一致,如果绘制结果存在偏差会扣除一定分数。

3、赛后需要提交找回flag的截图解题代码或文档进行评分。

题解:

首要思路:找到绘画的部分,看下黄色为什么不显示

拿到题目ida打开搜索WinMain,可以找到关键函数Drawing

img

然后可以看到

img

做了一个时间检测,应该是作图四秒后清空内存,这与直接打开exe得到的情况是一样的。这里可以直接把0XFA0 patch成 0xFFFFA0

然后看到关键函数Shell_Code

img

可以看到大段的D3D的code

下断点动调之后不难发现 在Drewing_main这个函数进行了绘制

image-20220416180743558

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
__int64 __fastcall Drewing_main(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7)
{
__int64 v7; // rsi
__int64 result; // rax
__int64 v11; // rcx
int v12; // edx
int v13; // er9
int v14; // eax
int v15[10]; // [rsp+58h] [rbp-28h] BYREF

v7 = 0i64;
memset(v15, 0, 32);
v15[8] = 50;
v15[9] = 50;
while ( 2 )
{
result = (__int64)dword_23A30421301;
switch ( dword_23A30421301[v7] ) //OPCODE
{
case 0:
result = (unsigned int)v15[1];
v15[0] += v15[1];
goto LABEL_10;
case 1:
result = (unsigned int)v15[1];
v15[0] -= v15[1];
goto LABEL_10;
case 2:
v11 = dword_23A30421301[v7 + 1];
v7 += 2i64;
result = (unsigned int)v15[v11];
v15[dword_23A30421301[v7]] = result;
goto LABEL_10;
case 3:
v12 = dword_23A30421301[v7 + 1];
v7 += 2i64;
result = (__int64)dword_23A30421301;
v15[dword_23A30421301[v7]] = v12;
goto LABEL_10;
case 4:
++v7;
v13 = v15[0];
v14 = v15[0] * (v15[1] + 1);
v15[0] = dword_23A30421301[v7] ^ 0x414345;
result = (unsigned int)((v15[0] ^ (v15[1] + v13)) % 256
+ (((v15[0] ^ (v13 * v15[1])) % 256 + (((v15[0] ^ (v15[1] + v14)) % 256) << 8)) << 8));
v15[1] = result;
goto LABEL_10;
case 5:
result = ((__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, int, __int64, __int64, __int64, __int64, __int64))unk_23A30420000)(
(unsigned int)v15[4],
(unsigned int)v15[5],
(unsigned int)v15[6],
(unsigned int)v15[7],
-256,
a3,
a4,
a5,
a6,
a7);
goto LABEL_10;
case 6:
result = ((__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, int, __int64, __int64, __int64, __int64, __int64))unk_23A30420000)(
(unsigned int)v15[4],
(unsigned int)v15[5],
(unsigned int)v15[6],
(unsigned int)v15[7],
-13771801,
a3,
a4,
a5,
a6,
a7);
goto LABEL_10;
case 7:
return result;
default:
LABEL_10:
if ( (unsigned __int64)++v7 < 0x1301 )
continue;
return result;
}
}
}

可以发现这个里面只要是一个Switch在对一个opcode进行选择利用,类似于一个虚拟机,而v15是这个虚拟机的几个寄存器

看到dword_23A30421301里面的内容

image-20220416173018721

十分标准的opcode,所以我们可以用python对dword_23A30421301进行简单的转换,变成可以运行的python脚本

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
while len(opcode):
if opcode[0]==0:
print("regs[0] += regs[1]")
elif opcode[0]==1:
print("regs[0] -= regs[1]")
print("regs[0] = regs[0] if regs[0]>=0 else regs[0]+0x100000000")
elif opcode[0]==2:
next=opcode[1]
nextt=opcode[2]
print("regs[%d] = regs[%d]" % (nextt,next))
opcode=opcode[2:]
elif opcode[0]==3:
next=opcode[1]
nextt=opcode[2]
print("regs[%d] = %d" % (nextt,next))
opcode=opcode[2:]
elif opcode[0]==4:
next=opcode[1]
print("regs[0], regs[1] = encode(regs[0], regs[1], %d)" % (next))
opcode=opcode[1:]
elif opcode[0]==5:
print("render(regs[4], regs[5], regs[6], regs[7], 0xFFFFFF00)")
elif opcode[0]==6:
print("render(regs[4], regs[5], regs[6], regs[7], 0xFF2DDBE7)")
elif opcode[0]==7:
print("exit()")

opcode=opcode[1:]

然后看到case 4 的时候对regs进行了一个加密(这个也需要复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
regs=[0]*10
def encode(a,b,key):
result1=key^0x414345
print(hex(key))
result2=((result1^(b+a))%256)+\
(((result1^(a*b))%256)<<8)+\
(((result1^(b+a+a*b))%256)<<16)
return result1,result2
def render(a,b,c,d,color):

# if a>0x80000000:
# a=a-0x100000000
# if b>0x80000000:
# b=b-0x100000000
print(hex(a),hex(b),hex(c),hex(d))
regs[8] = 50
regs[9] = 50

拼接完后的结果可以运行出每次要用的 a,b,c,d

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
0x524895
-0x3b6 0x32 0x20a4ac 0x130bd0
0x5a8e2c
0x32 -0x186 0x1bcd69 0xe9bdc5
0x985ad2
-0x3b6 0xaa 0x8fb363 0xd91997
0xa9685d
0x32 0xe6 0x1cf400 0xe82b18
0x785cef
-0x3b6 -0xd2 0x391faa 0x6ee6d2
0x963ea7
0x32 0x15e 0xebe72 0xd77de2
0x465215
-0x37a -0x10e 0x71150 0x74fc28
0x856dce
0xaa -0x10e 0xdb3f17 0xc42e8b
0x758c6e
-0x302 0xe6 0x34cf2b 0x331fcf
0x98a6b4
0x6e -0x186 0xa59d19 0xd9e5f1
0x856ece
-0x33e 0xaa 0xcb5fe7 0xc42d8b
0xabfc52
0x28a 0x32 0xeabf17 0xa7e3ab
0x856ece
0x24e 0x6e 0xc42d8b 0xcb0f37
0x9654ea
0x212 0xaa 0xd717af 0x1f5b13
0x8523ac
0x1d6 0xe6 0xc460e9 0xe9ad55
0x86eacc
0x19a 0x122 0xc7a989 0xb9fd35
0xea3245
0x1d6 0x122 0xab7100 0x646cf8
0x854aec
0x212 0x122 0xc409a9 0x31cd9d
0x963dce
0x2c6 0x32 0xd77e8b 0x2f2773
0x98ee44
0x302 0x32 0xd9ad01 0x996535
0x78a213
0x33e 0x32 0x39e156 0xda4a26
0x526339
0x2c6 0x6e 0x13207c 0x346848
0x88574e
0x302 0xaa 0xc9140b 0xb5fa7
0x12445a
0x33e 0xe6 0x53071f 0xc7ab3b
0x965243
0x37a 0x122 0xd71106 0xd6329a
0xaa23e4
0x3b6 0x122 0xeb60a1 0xa58d79
0xaa2488
0x3f2 0x122 0xeb67cd 0xf5e9d9
0x965224
0x42e 0x122 0xd71161 0xd7d31
0x263554
0x3b6 0x32 0x677611 0x659df9
0x15478
0x3f2 0x32 0x40173d 0x557919
0x963524
0x42e 0x32 0xd77661 0x3d9d01
0xaebcdf
0x46a 0x32 0xefff9a 0xca2e06
0x8547ae
0x3f2 0x6e 0xc404eb 0xb7178b
0x9685aa
0x42e 0xaa 0xd7c6ef 0x8b6337
0x96335a
0x46a 0xaa 0xd7701f 0x677b0b
0x965234
0x4a6 0xaa 0xd71171 0xfd4d21
0x7845ee
0x4e2 0xaa 0x3906ab 0xbbf27
0x482526
0x46a 0xe6 0x96663 0xef5f33
0x326212
0x4a6 0x122 0x732157 0x835b9f
0x747475
0x4e2 0x122 0x353730 0x383434
0x2314ec
0x51e 0x122 0x6257a9 0x9555e9
0x9634ea
0x55a 0x122 0xd777af 0xdf5bd3

然后就是可以对着所给的预料的答案,可以猜到a是x坐标,b是y坐标

不难发现,前11个坐标有一些负数,而后面没有。所以猜测前11个坐标是没有显示的黄色的点(也就是要画的FLAG),后31个坐标是画出来的‘ACE’

那么刚好42个点,和预料一样。

根据黄色的旗帜有六个点是相同的x坐标,不难猜出我们的第一个点应该是(0x32,0x32),所以直接在绘画的时候patch掉这个点的坐标

image-20220416175119829

发现并没有出现点,所以后两个数其实对前两个数有一个check的功能,不能简单的改坐标

后面两个数在经过多次尝试,并没有理解是什么,在偶然的一次测试中,将后两个数交换

image-20220416175335866

image-20220416175410008

发现已经出现了黄块,所以我们推测后两个数在负数的时候被交换了,同时修改前面的坐标就可以了

那么思路就出来了:直接重写需要的Shellcode就可以了

给出shellcode的exp

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

def encode(a,b,key):
result1=key^0x414345
result2=((result1^(b+a))%256)+\
(((result1^(a*b))%256)<<8)+\
(((result1^(b+a+a*b))%256)<<16)
return result1,result2

shellcode=[]

def pos(x,y,key,type):
shellcode.append(3)
shellcode.append(x)
shellcode.append(4)

shellcode.append(3)
shellcode.append(y)
shellcode.append(5)

k1,k2=encode(x,y,key)
shellcode.append(3)
shellcode.append(k1)
shellcode.append(6)

shellcode.append(3)
shellcode.append(k2)
shellcode.append(7)

shellcode.append(type+5)

pos(0x32 , 0x32, 0x524895,0)
pos(0x32 , 0x6e, 0x5a8e2c,0)
pos(0x32 , 0xaa, 0x985ad2,0)
pos(0x32 , 0xe6, 0xa9685d,0)
pos(0x32 , 0x122, 0x785cef,0)
pos(0x32 , 0x15e, 0x963ea7,0)

pos(0x6e , 0x6e, 0x465215,0)
pos(0xaa , 0xaa, 0x856dce,0)
pos(0xe6 , 0xe6, 0x758c6e,0)

pos(0x6e , 0xe6, 0x98a6b4,0)
pos(0xaa , 0xe6, 0x856ece,0)

pos(0x28a , 0x32, 0xabfc52,1)
pos(0x24e , 0x6e, 0x856ece,1)
pos(0x212 , 0xaa, 0x9654ea,1)
pos(0x1d6 , 0xe6, 0x8523ac,1)
pos(0x19a , 0x122, 0x86eacc,1)
pos(0x1d6 , 0x122, 0xea3245,1)
pos(0x212 , 0x122, 0x854aec,1)
pos(0x2c6 , 0x32, 0x963dce,1)
pos(0x302 , 0x32, 0x98ee44,1)
pos(0x33e , 0x32, 0x78a213,1)
pos(0x2c6 , 0x6e, 0x526339,1)
pos(0x302 , 0xaa, 0x88574e,1)
pos(0x33e , 0xe6, 0x12445a,1)
pos(0x37a , 0x122, 0x965243,1)
pos(0x3b6 , 0x122, 0xaa23e4,1)
pos(0x3f2 , 0x122, 0xaa2488,1)
pos(0x42e , 0x122, 0x965224,1)
pos(0x3b6 , 0x32, 0x263554,1)
pos(0x3f2 , 0x32, 0x15478,1)
pos(0x42e , 0x32, 0x963524,1)
pos(0x46a , 0x32, 0xaebcdf,1)
pos(0x3f2 , 0x6e, 0x8547ae,1)
pos(0x42e , 0xaa, 0x9685aa,1)
pos(0x46a , 0xaa, 0x96335a,1)
pos(0x4a6 , 0xaa, 0x965234,1)
pos(0x4e2 , 0xaa, 0x7845ee,1)
pos(0x46a , 0xe6, 0x482526,1)
pos(0x4a6 , 0x122, 0x326212,1)
pos(0x4e2 , 0x122, 0x747475,1)
pos(0x51e , 0x122, 0x2314ec,1)
pos(0x55a , 0x122, 0x9634ea,1)

shellcode.append(7)

import struct

with open('shellcode.bin','wb') as f:
for i in shellcode:
f.write(struct.pack("<i",i))





前面的几个坐标有误的点,手动修改一下就可以了。

我们直接重写shellcode,并且写回到shellcode相应的地址处,通过ida的python执行之后

1
2
3
4
with open("D:\\XXXX\\shellcode.bin", 'rb') as f:
shellcode=f.read()
base=0x000140006350
[patch_byte(base+i,shellcode[i]) for i in range(len(shellcode))]

image-20220416181632398

在apply一下patch掉的内容,就可以得到修正后的程序了

结果:

image-20220416175908653

小记

这次还是顺利地晋级了决赛,但是决赛确实没办法进入前五拿到一个很好的名次,这次比赛还是比较有趣地。image-20220426124924767

题目附件地址:https://gslab.qq.com/html/competition/2022/race-pre.htm

希望能在鹅厂和你们相遇(虽然想去网易