2018腾讯游戏安全竞赛题目分析

今年的腾讯游戏安全竞赛就要开始了,但是我对游戏安全这个方向还不太熟悉,因此找了去年的题目来做赛前练习

资格赛题目

标准版

题目一打开长这样:

赛题说明:

1
2
1.username与regcode是一一对应的关系,填入正确的username和regcode点击Go按钮出现注册成功提示。
2.要求根据CrackMe写出对应注册机,注册机能够根据任意合法用户名生成正确的注册码。

可以看出这是一个CrackMe程序,开始分析。

上图为一神器,可直接通过可视化界面获取到MFC组件的响应函数,神器链接

如果不使用上面的神器的话,此程序是一个MFC程序,获取输入框的方式有两种GetDlgItemGetWindowText,查看这两个函数的调用,发现在sub_4026F0中有多处对GetDlgItem的调用,因此分析这个函数,IDA对此函数生成的主要伪C代码如下:

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
void __thiscall (HWND *this)
{
......

v21 = (CWnd *)this;
v1 = GetDlgItem(this[8], 1000);
memset(&lParam, 0, 0x400u);
SendMessageA(v1, 0xDu, 0x400u, (LPARAM)&lParam);
v2 = GetDlgItem(*((HWND *)v21 + 8), 1001);
memset(&v23, 0, 0x400u);
SendMessageA(v2, 0xDu, 0x400u, (LPARAM)&v23);
v3 = v21;
v4 = CWnd::GetDlgItem(v21, 1004);
v20 = SendMessageW(*((HWND *)v4 + 8), 0xF0u, 0, 0) == 1;
v21 = (CWnd *)&v14;
v19 = 15;
v18 = 0;
LOBYTE(v14) = 0;
if ( (_BYTE)v23 )
v5 = strlen((const char *)&v23);
else
v5 = 0;
sub_402A70(&v14, &v23, v5);
v49 = 0;
v13 = 15;
v12 = 0;
v8 = 0;
if ( (_BYTE)lParam )
v6 = strlen((const char *)&lParam);
else
v6 = 0;
sub_402A70(&v8, &lParam, v6);
v49 = -1;
if ( (unsigned __int8)sub_405510(*(LPVOID *)&v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) )
{
v24 = 'e R';
v40 = 0;
v7 = (wchar_t *)&v24;
v25 = 'i g';
v26 = 't s';
v27 = 'r e';
v28 = (int)&loc_53001D + 3;
v29 = 'c u';
v30 = 'e c';
v31 = 's s';
v32 = &loc_43002C;
v33 = 'n o';
v34 = 'r g';
v35 = 't a';
v36 = 'l u';
v37 = 't a';
v38 = 'o i';
v39 = '! n';
}
else
{
*(_DWORD *)v41 = 'e R';
v7 = v41;
v42 = 'i g';
v43 = 't s';
v44 = 'r e';
v45 = &loc_460020;
v46 = 'i a';
v47 = 'e l';
v48 = 'd';
}
CWnd::SetDlgItemTextW(v3, 1003, v7);
}

可以看出这是程序的主流程。下面就一步一步分析此函数:

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
void __thiscall (HWND *this)
{
......

v21 = (CWnd *)this;
UserName_Text_ID = GetDlgItem(this[8], 1000);
memset(&UserName, 0, 02000u);
SendMessageA(UserName_Text_ID, 'r', 02000u, (LPARAM)&UserName);
RegCode_Text_ID = GetDlgItem(*((HWND *)v21 + 8), 1001);
memset(&RegCode, 0, 02000u);
SendMessageA(RegCode_Text_ID, 'r', 02000u, (LPARAM)&RegCode);// 存储RegCode
v3 = v21;
User_Choice = CWnd::GetDlgItem(v21, 1004);
v20 = SendMessageW(*((HWND *)User_Choice + 8), 0xF0u, 0, 0) == 1;// 判断选择的是标准版还是进阶版
v21 = (CWnd *)&v14;
v19 = 15;
v18 = 0;
LOBYTE(v14) = 0;
if ( (_BYTE)RegCode )
v5 = strlen((const char *)&RegCode); // 判断RegCode长度
else
v5 = 0;
assign_sub_402A70(&v14, &RegCode, v5); // string assign
// 将RegCode的值赋给v14
v49 = 0;
v13 = 15;
v12 = 0;
v8 = 0;
if ( (_BYTE)UserName )
v6 = strlen((const char *)&UserName); // 判断UserName长度
else
v6 = 0;
assign_sub_402A70(&v8, &UserName, v6); // string assign
// 将Username的值赋给v8
v49 = -1;
if ( sub_405510(*(LPVOID *)&v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) )// 主检验函数
{
v24 = 'e R';
v40 = 0;
v7 = (wchar_t *)&v24;
v25 = 'i g';
v26 = 't s';
v27 = 'r e';
v28 = (int)&loc_53001D + 3;
v29 = 'c u';
v30 = 'e c'; // RegisterSuccess,Congratulation!
v31 = 's s';
v32 = &loc_43002C;
v33 = 'n o';
v34 = 'r g';
v35 = 't a';
v36 = 'l u';
v37 = 't a';
v38 = 'o i';
v39 = '! n';
}
else
{
*(_DWORD *)v41 = 'e R';
v7 = v41;
v42 = 'i g';
v43 = 't s';
v44 = 'r e'; // RegisterFailed
v45 = &loc_460020;
v46 = 'i a';
v47 = 'e l';
v48 = 'd';
}
CWnd::SetDlgItemTextW(v3, 1003, v7);
}

以上步骤完成了基本的数据获取、校验操作,其中有一个主要的校验函数sub_405510,下面来重点分析这个函数:

当返回值v13True时才能注册成功,因此我们需要关注使v13True的函数。

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
if ( sub_404F00((int *)&UserName) )
{
sub_405040((int)&UserName, &v26, &v27, &v28, (int *)&v29, &v30);
v31 = '