2023 第八届腾讯游戏安全技术竞赛初赛解题思路

警告
本文最后更新于 2023-04-19,文中内容可能已过时。

好久没有更新博客了,这次来分享一下 “2023 第八届腾讯游戏安全技术竞赛初赛” 比赛的解题思路。比较可惜的是,这次因为网鼎杯比赛时间与决赛冲突,没有来得及看决赛赛题。

解题过程

寻找关键代码

打开程序后发现程序特征为在当前目录下的 contest.txt 目录下不断写入密文信息,于是打开火绒剑监控系统事件

image-20230407182622967

查找其中一个写入动作的调用栈

image-20230407182657785

调试程序并 Dump 内存

使用 x64dbg 的 ScyllaHide 工具,找到上述的 contest.exe + 0xcf1b 的代码位置,在返回地址上面下一个断点,再通过栈回溯定位到内存中的代码位置

image-20230407182901147

另一方面,使用内存布局处的功能来 dump 内存,使用 IDA 打开分析汇编代码

关键代码分析

在 IDA 里面利用搜索字节码来找到对应的内存位置,然后通过 Rebase Shift delta 功能来同步内存地址与 x64dbg 一致。在 IDA 里面找到 contest.exe + 0x4646E0 位置的函数为关键函数,手动建立函数并查看伪代码。

在循环开始处,放一个日志断点,打印 x 和 y,方便后续的程序流程分析

image-20230407233147892

日志数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
x = 2AE1EAC8, y = 903C2B50
x = 9A9772B2, y = 2744F72B

循环 15 次
	x = B06537E4, y = 66E55E9D
	x = 90CC8B70, y = 84169EB2
x = B06537E4, y = 84169EB2

x = F0ED0BDF, y = E31B4F23

循环 11 次
	x = D735F6FE, y = C9C68E0A
	x = F5F0FF94, y = 182880A4
x = D735F6FE, y = 182880A4

x = 3D7394D7, y = 3782CBE2
x = C71A1587, y = C4587FB8
x = EC7C1889, y = 12C1A3E5
x = 24345B2, y = 4D97F947

整理汇编代码

 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
//x = 0x2AE1EAC8, y = 0x903C2B50;
检查 heap_ptr 是否初始化成功


//x = 0x9A9772B2, y = 0x2744F72B
v30 ^= y;
v2 = heap_ptr;
v2[0] = 0i64;
v2[1] = 0i64;
v2[2] = 0i64;
v2[3] = 0i64;
v2[4] = 0i64;
*((_BYTE *)v2 + 0x40) = 0;
v23 = 0i64;

//x = 0xB06537E4, y = 0x66E55E9D
v25 = v23;
if ( v23 < 0xF )
  继续循环
退出循环

//x = 0x90CC8B70, y = 0x84169EB2
byte_7FF74D0A72E8[v25 + 1] = byte_7FF74D0A72E8[v25 + 1] ^ 0x45;
v23 = v25 + 1;


//x = 0xF0ED0BDF, y = 0xE31B4F23
aCatchmeifyouca[0xF] = 0;
v6 = (void (__stdcall *)(HANDLE))funcs_table[0xADCF6EF4 - v27];
v6("catchmeifyoucan");
v19 = 0i64;

v6: 变表 base64 
QRSTUVWXYZabcdefABCDEFGHIJKLMNOPwxyz0123456789+/ghijklmnopqrstuv@

//x = D735F6FE, y = C9C68E0A
v22 = v19;
if ( v19 < 0xB )
	继续循环
退出循环

//x = F5F0FF94, y = 182880A4
aContestTxt[v22 + 1] ^= aContestTxt[0];
v19 = v22 + 1;


//x = 3D7394D7, y = 3782CBE2
aContestTxt[0xC] = 0;
v9 = alloca(0x10i64);
v21 = (HANDLE *)&v17;
0x7FFDE66A5310i64 -> CreateFileA()
((void (__stdcall *)(HANDLE))funcs_table[0xADCF6EF5 - v27])((HANDLE)0x7FFDE66A5310i64);
v20 = *v21;
v18 = v20;


//x = C71A1587, y = C4587FB8
if (v18 + 1 < 2)
   x = EC7C1889, y = 12C1A3E5
else
   x = 54FCED91, y = D9D7BE2E
   

//x = 54FCED91, y = D9D7BE2E
to x = 810ECE3A, y = 775C01B0

//x = 810ECE3A, y = 775C01B0
v31 = 0;
v11 = (void (__stdcall *)(HANDLE))funcs_table[0xADCF6EF2 - v27];
v11(heap_ptr);
//00007FF74D048750 -> WriteFile()
((void (__stdcall *)(HANDLE))funcs_table[0xADCF6EF6 - v27])((HANDLE)0x7FFDE66A5790i64);
//<contest.JMP.&CloseHandle>
((void (__stdcall *)(HANDLE))funcs_table[0xADCF6EF3 - v27])(*v21);


//x = EC7C1889, y = 12C1A3E5
HeapFree(heap_ptr);

//x = 24345B2, y = 4D97F947
return 1i64;

伪代码

逆向分析后,整理程序伪代码,其中 msg 和 file 在初始阶段应该是加密的,省略了申请 Heap 语句

 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
#include <stdio.h>
#include <Windows.h>

char msg[] = "Ecatchmeifyoucan";
char file[] = "@contest.txt";
char msg_base64[0x41];

const char* custom_base64_table = "QRSTUVWXYZabcdefABCDEFGHIJKLMNOPwxyz0123456789+/ghijklmnopqrstuv@";
void custom_base64_encode(const unsigned char* src, size_t src_len, char* dst)
{
	size_t i, j;
	unsigned char buf[3];

	for (i = 0, j = 0; i < src_len; i += 3, j += 4)
	{
		buf[0] = src[i];
		buf[1] = (i + 1 < src_len) ? src[i + 1] : 0;
		buf[2] = (i + 2 < src_len) ? src[i + 2] : 0;

		dst[j] = custom_base64_table[buf[0] >> 2];
		dst[j + 1] = custom_base64_table[((buf[0] & 0x03) << 4) | (buf[1] >> 4)];
		dst[j + 2] = custom_base64_table[((buf[1] & 0x0F) << 2) | (buf[2] >> 6)];
		dst[j + 3] = custom_base64_table[buf[2] & 0x3F];
	}

	switch (src_len % 3)
	{
	case 1:
		dst[j - 2] = '@';
		dst[j - 1] = '@';
		break;
	case 2:
		dst[j - 1] = '@';
		break;
	}

	dst[j] = '\0';
}

int main() {
	while (true) {

		for (int i = 0; i < 16; i++) {
			msg[i + 1] ^= msg[0];
		}

		custom_base64_encode((const unsigned char*)msg + 1, 16, msg_base64);
		for (int i = 0; i < 12; i++) {
			file[i + 1] ^= file[0];
		}

		HANDLE hFile = CreateFileA(file + 1, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
		if (hFile == INVALID_HANDLE_VALUE)
		{
			continue;
		}

		DWORD dwBytesWritten;
		WriteFile(hFile, msg_base64, 16, &dwBytesWritten, NULL);
		CloseHandle(hFile);

		Sleep(1000);
	}

	return 0;
}

解题

问题 1

在 64 位 Windows10 系统上运行 contest.exe, 找到明文的信息,作为答案提交(1 分)

明文的信息为:catchmeifyoucan

问题 2

编写程序,运行时修改尽量少的 contest.exe 内存,让 contest.exe 由写入密文信息变为写入明文信息成功。(满分 2 分)

解题思路

把以下内存位置做一个内存修改

1
2
3
4
5
6
7
.vmp1:00007FF724CCC751                 lea     rcx, aEcatchmeifyouc+1 ; "catchmeifyoucan"
.vmp1:00007FF724CCC758                 mov     edx, 0Fh
.vmp1:00007FF724CCC75D                 lea     r9, [rbp+40h+var_5C]
.vmp1:00007FF724CCC761                 call    rbx
.vmp1:00007FF724CCC763                 mov     r8, 1D9542F1355E13BEh
.vmp1:00007FF724CCC76D                 mov     r11, 672B49509D2B1AE5h
.vmp1:00007FF724CCC777                 mov     r10, 82B1A3E4CC35D43Dh

把内存中的汇编代码修改为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.vmp1:00007FF724CCC751                 lea     rcx, aEcatchmeifyouc+1 ; "catchmeifyoucan"
.vmp1:00007FF724CCC758                 mov     edx, 0Fh
.vmp1:00007FF724CCC75D                 lea     r9, [rbp+40h+var_5C]
.vmp1:00007FF724CCC761                 mov     rdi, r8
.vmp1:00007FF724CCC764                 mov     rsi, rcx
.vmp1:00007FF724CCC767                 push    10h
.vmp1:00007FF724CCC769                 pop     rcx
.vmp1:00007FF724CCC76A                 cld
.vmp1:00007FF724CCC76B                 rep movsb
.vmp1:00007FF724CCC76D                 mov     r11, 672B49509D2B1AE5h
.vmp1:00007FF724CCC777                 mov     r10, 82B1A3E4CC35D43Dh

r8 寄存器存储要用于存储加密信息的堆地址,rcx 寄存器存储明文数据,修改后,将明文数据中的 16 字节数据复制到堆上,以便在写入文件时输出明文信息。

问题 3

编写程序,运行时修改尽量少的 contest.exe 内存,让 contest.exe 往入自行指定的不同的文件里写入明文信息成功。(满分 3 分)

解题思路

把以下程序中的加密文件名做一个修改,修改成我们的目标文件名,这里的代码中实现要求新的文件名长度不能大于原来的文件名,如果需要大于原来的文件名,可以在创建文件函数时的文件名指针做一个修改,移动到一处不使用的内存空间,在这段空间中放入新的文件名。

解释

问题 2,问题 3 的代码见 main.cpp 文件,编译后直接运行即可,如果要修改写出的文件名,修改 WRITE_FILENAME 宏即可,代码中有比较详细的注释,具体代码含义见注释。

解题代码

main.cpp

  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
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>


#define TARGET_PROCESS_NAME "contest.exe"
#define WRITE_FILENAME "wjh.txt"
#define INJECT_ADDRESS_OFFSET 0xC761
#define FILENAME_ADDRESS_OFFSET 0x772FA

BYTE shellcode[] = { 0x49, 0x8B, 0xF8, 0x48, 0x8B, 0xF1, 0x6A, 0x10, 0x59, 0xFC, 0xF3, 0xA4 };

// Helper function to get the base address of a module in a process
DWORD_PTR GetModuleBaseAddress(HANDLE hProcess, const char* moduleName)
{
	DWORD_PTR lpBaseAddress = 0;
	HMODULE hMods[1024];
	DWORD cbNeeded;

	if (!EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) {
		return 0;
	}

	for (int i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
		char szModName[MAX_PATH];
		if (GetModuleBaseNameA(hProcess, hMods[i], szModName, sizeof(szModName)) == 0) {
			continue;
		}
		if (_stricmp(szModName, moduleName) != 0) {
			continue;
		}
		MODULEINFO modInfo;
		if (!GetModuleInformation(hProcess, hMods[i], &modInfo, sizeof(modInfo))) {
			continue;
		}
		lpBaseAddress = (DWORD_PTR)modInfo.lpBaseOfDll;
		break;
	}

	return lpBaseAddress;
}

// Helper function to suspend/resume all threads in a process
int SuspendResumeThreads(DWORD pid, BOOL suspend)
{
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, pid);
	if (hSnapshot == INVALID_HANDLE_VALUE)
	{
		printf("Failed to create thread snapshot. Error code: %d\n", GetLastError());
		return 1;
	}

	THREADENTRY32 te;
	te.dwSize = sizeof(THREADENTRY32);

	while (Thread32Next(hSnapshot, &te))
	{
		if (te.th32OwnerProcessID != pid)
			continue;

		HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);
		if (hThread == NULL)
			continue;

		if (suspend)
			SuspendThread(hThread);
		else
			ResumeThread(hThread);

		CloseHandle(hThread);
	}

	CloseHandle(hSnapshot);
	return 0;
}

// Helper function to modify memory in a process
int ModifyMemory(HANDLE hProcess, LPVOID lpAddress, LPVOID lpBuffer, SIZE_T nSize)
{
	DWORD dwOldProtect;
	if (!VirtualProtectEx(hProcess, lpAddress, nSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)) {
		printf("Failed to modify process memory protection. Error code: %d\n", GetLastError());
		return 1;
	}

	SIZE_T bytesWritten;
	if (!WriteProcessMemory(hProcess, lpAddress, lpBuffer, nSize, &bytesWritten)) {
		printf("Failed to write to process memory. Error code: %d\n", GetLastError());
	}


  
	if (!VirtualProtectEx(hProcess, lpAddress, nSize, dwOldProtect, &dwOldProtect)) {
		printf("Failed to restore process memory protection. Error code: %d\n", GetLastError());
		return 1;
	}
  
	return 0;
}

// Helper function to find process by name
int FindProcessByName(const char* name, DWORD& pid)
{
    DWORD processes[1024];
    DWORD bytesReturned;

    if (!EnumProcesses(processes, sizeof(processes), &bytesReturned))
    {
        printf("Failed to enumerate processes. Error code: %lu\n", GetLastError());
        return 1;
    }

    DWORD count = bytesReturned / sizeof(DWORD);

    for (DWORD i = 0; i < count; i++)
    {
        HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processes[i]);

        if (hProcess)
        {
            char filename[MAX_PATH];
            if (GetModuleFileNameExA(hProcess, NULL, filename, MAX_PATH) > 0)
            {
                if (strstr(filename, name) != NULL)
                {
                    pid = processes[i];
                    CloseHandle(hProcess);
                    return 0;
                }
            }
        }

        CloseHandle(hProcess);
    }

    printf("Target process %s not found\n", name);
    return 1;
}

int main()
{
    DWORD pid = 0;
    HANDLE hProcess = NULL;
    char* pBaseAddress = NULL;

    if (FindProcessByName(TARGET_PROCESS_NAME, pid))
    {
        return 1;
    }

    // Get handle to the target process
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (hProcess == NULL)
    {
        printf("OpenProcess failed. Error code: %lu\n", GetLastError());
        return 1;
    }

    // Get the address to modify in the target process
    pBaseAddress = (char*)GetModuleBaseAddress(hProcess, TARGET_PROCESS_NAME);

    // Suspend all threads of the target process
    if (SuspendResumeThreads(pid, true))
    {
        printf("Failed to suspend process threads. Error code: %lu\n", GetLastError());
        goto cleanup;
    }

    // Modify the memory of the target process
    if (ModifyMemory(hProcess, pBaseAddress + INJECT_ADDRESS_OFFSET, shellcode, sizeof(shellcode)))
    {
        printf("Failed to modify memory. Error code: %lu\n", GetLastError());
        goto cleanup;
    }

    // Modify the memory of the target process
    if (ModifyMemory(hProcess, pBaseAddress + FILENAME_ADDRESS_OFFSET, (LPVOID)WRITE_FILENAME, sizeof(WRITE_FILENAME)))
    {
        printf("Failed to modify memory. Error code: %lu\n", GetLastError());
        goto cleanup;
    }


    // Resume all threads of the target process
    if (SuspendResumeThreads(pid, false))
    {
        printf("Failed to resume process threads. Error code: %lu\n", GetLastError());
        goto cleanup;
    }

    printf("Memory modification successful\n");

cleanup:
    CloseHandle(hProcess);
    return 0;
}
0%