2023腾讯游戏安全竞赛-PC方向初赛复现
2023腾讯游戏安全竞赛-PC方向初赛复现
第一问
问题描述:在64位Windows10系统上运行contest.exe, 找到明文的信息,作为答案提交(1分)
直接运行程序,在contest.txt中拿到密文ImVkImx9JG12OGtlImV+,很像base64后的结果,但是直接解码得到的不是自然语言,整个exe程序也完全被VM了,动调看看。
一直动调到contest.txt被创建,在内存中搜索字符串可以找到一张表QRSTUVWXYZabcdefABCDEFGHIJKLMNOPwxyz0123456789+/ghijklmnopqrstuv,用这张表解码拿到明文catchmeifyoucan。在内存中也可以直接找到明文
第二问
问题描述:编写程序,运行时修改尽量少的contest.exe内存,让contest.exe 由写入密文信息变为写入明文信息成功。(满分2分)
这题在调试的时候费了一番功夫,既然exe一直在创建文件,写入内容,必然调用了CreateFileA以及WriteFile这些API,但是用x64dbg下断发现断不下来,自己写Hook也没有任何输出。没招了想着用驱动探探路(虽然题目不允许),CreateFile一系列的API最终是陷入NtCreateFile这个内核函数,我尝试Hook后直接蓝屏ATTEMPT_TO_WRITE_READONLY,恍然大悟原来之前断点和Hook都不成功是程序把自己的内存属性设成了只读。那么理论上来说,用VirtualProtect更改程序内存属性就可以了,但是还是无效。 用火绒剑扫一下钩子果然是赛题在作怪。

看了下这个钩子,直接跳转到一片unuse的内存,所以VirtualProtect失效。另外还有一个DbgUiRemoteBreakin的钩子,影响不是很大。
得想办法UnHook这个钩子,然后再用VirtualProtect重设页属性。正常的NtProtectVirtualMemory长这样:

被挂钩后其实只有第一句汇编变了,不过这个系统调用也不长,干脆全部还原一下。注意这个地址的属性是PAGE_EXECUTE_READ,在UnHook前要改成PAGE_EXECUTE_READWRITE,你可能会想NtProtectVirtualMemory不是被挂钩了吗,怎么改?注意这里挂钩的是三环下陷入内核的一个系统调用函数,对于每个进程都是独立的,挂钩自己的NtProtectVirtualMemory不影响其他进程。所以我们先自己写一个UnHook程序。
#include <Windows.h>
#include <psapi.h>
#include <TlHelp32.h>
#include <stdio.h>
#include <tchar.h>
#pragma comment(lib, "psapi.lib")
//ULONG64 g_UnHookAddr = 0x00007FFC23963D50;
//ULONG64 g_ntdllBase = 0x7FFC238C0000;
ULONG64 g_UnHookAddr = 0;
ULONG64 g_NtProtectVirtualMemoryOff = 0xA3D50;
const wchar_t* targetExeName = L"contest.exe";
const wchar_t* dllPath = L"C:\\Users\\Administrator\\source\\repos\\Tencent-Dll1\\x64\\Release\\Tencent-Dll1.dll";
BYTE UnHookShellCode[] = {
0x4C, 0x8B, 0xD1, 0xB8, 0x50, 0x00, 0x00, 0x00, 0xF6, 0x04, 0x25, 0x08, 0x03, 0xFE, 0x7F, 0x01,
0x75, 0x03, 0x0F, 0x05, 0xC3, 0xCD, 0x2E, 0xC3
};//正常的字节码
HMODULE GetRemoteNtdllBase(HANDLE hProcess) {
HMODULE hModules[1024];
DWORD cbNeeded;
// 枚举目标进程的所有模块
if (EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) {
WCHAR szModuleName[MAX_PATH];
// 遍历所有模块
for (DWORD i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) {
// 获取模块名称
if (GetModuleFileNameEx(hProcess, hModules[i], szModuleName, sizeof(szModuleName) / sizeof(WCHAR))) {
// 检查是否为 ntdll.dll
if (_wcsicmp(wcsrchr(szModuleName, L'\\') + 1, L"ntdll.dll") == 0) {
return hModules[i];
}
}
}
}
return NULL;
}
DWORD ProcessFind(const wchar_t* Exename)
{
HANDLE hProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (!hProcess)
{
return FALSE;
}
PROCESSENTRY32 info;
info.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(hProcess, &info))
{
return FALSE;
}
while (true)
{
if (memcmp(info.szExeFile, Exename, _tcslen(Exename)) == 0)
{
return info.th32ProcessID;
}
if (!Process32Next(hProcess, &info))
{
return FALSE;
}
}
return FALSE;
}
int main() {
DWORD tarPid = ProcessFind(targetExeName);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, tarPid);
if (!hProcess) {
printf("Failed when open process!\n");
return 0;
}
ULONG64 ntdllBase = (ULONG64)GetRemoteNtdllBase(hProcess);
if (!ntdllBase) {
printf("Failed when get ntdll base!\n");
return 0;
}
g_UnHookAddr = ntdllBase + g_NtProtectVirtualMemoryOff;
//改ntdll!NtProtectVirtualMemory的内存属性
DWORD oldProtect = 0;
if (!VirtualProtectEx(hProcess, (LPVOID)g_UnHookAddr, sizeof(UnHookShellCode), PAGE_EXECUTE_READWRITE, &oldProtect)){
printf("Failed when VirtualProtectEx!\n");
return 0;
}
//现在ntdll!NtProtectVirtualMemory可写了,取消钩子
if (!WriteProcessMemory(hProcess, (LPVOID)g_UnHookAddr, UnHookShellCode, sizeof(UnHookShellCode), 0))
{
printf("UnHook failed!!! [%d]\n", GetLastError());
return 0;
}
printf("UnHooked!\n");
system("pause");
}
取消成功:

接下来我的想法是直接在DLL中Hook WriteFile来更改写入的字符串,但是发现无论是Hook还是在调试器中给WriteFile下断都断不下来这个函数,这块没太懂,感觉是通过函数指针间接调用导致的吧。看来我之前分析的还有点问题,0环Hook NtCreateFile蓝屏的原因确实是因为内存属性的问题,但是3环Hook CreateFileA没反应应该是函数指针的问题,不过之前做的UnHook肯定也不是无用功。
那这下不得不看汇编了。把程序运行起来,右下角的堆栈窗口一直在变,其中就有WriteFile的符号,下个硬断追一下看看能不能发现什么。
神秘打野点,这里拿了WriteFile的函数指针。给他dump下来丢进IDA看看这块到底干了啥。发现IDA伪代码是一坨巨大的东西看了都要晕倒了,应该是VM的问题,没办法耐着性子继续追。

跑了一段,追踪到一个call,在这个call之后字符串就被写入了:

此时的参数窗口:

[rsp+30],也就是第7个参数填入的是写入的字符串,IDA中能看到这是一个函数指针表的其中一个,实际上call的是这个函数:

这是一个跳板函数,目标是:

然后其中的a7,也就是第七个参数,就是字符串缓冲区的指针,Hook这两个函数的其中一个应该就可以实现修改字符串了,Hook后面那个试试看。这个地方我调了很久,一直崩溃没法解决。后面仔细调了一下发现一个问题,这个函数的外层call长这样:
00007FF7FA85CEDE | 48:C74424 48 00000000 | mov qword ptr ss:[rsp+48],0 |
00007FF7FA85CEE7 | C74424 20 05000000 | mov dword ptr ss:[rsp+20],5 |
00007FF7FA85CEEF | 4C:89E9 | mov rcx,r13 |
00007FF7FA85CEF2 | BA FFFFFFFF | mov edx,FFFFFFFF |
00007FF7FA85CEF7 | 45:31C9 | xor r9d,r9d |
00007FF7FA85CEFA | FFD3 | call rbx | Call Write
00007FF7FA85CEFC | 48:83C4 50 | add rsp,50 |
00007FF7FA85CF00 | 48:8B45 A0 | mov rax,qword ptr ss:[rbp-60] |
在call rbx处跳转到跳板函数:
.text:00007FF7FA85D6A0 lea rax, sub_7FF7FA868750
.text:00007FF7FA85D6A7 jmp rax
从跳板函数跳到Write函数sub_7FF7FA868750,也就是刚刚说的目标Hook函数,这里值得注意的是,sub_7FF7FA868750不止写入密文时被调用,一定要是从call rbx处跳入这个函数才是写入密文,其他的调用不会经过call rbx,我还没有搞清楚为什么还会有其他的调用,总之除了写入密文之外的调用都是无效的,这种无效调用经过Hook函数的话可能会造成崩溃,因此只能从call rbx入手,只有当确定是写入密文的操作时,我们才Hook,也就是通过shellcode直接把call rbx改成跳入我们的Hook函数。
这一步没什么框架可用,手搓吧。
算出来call rbx这条汇编的Offset是0xCEFA
Original:
00007FF7FA85CEF7 | 45:31C9 | xor r9d,r9d |
00007FF7FA85CEFA | FFD3 | call rbx |
00007FF7FA85CEFC | 48:83C4 50 | add rsp,50 |
这边六个字节,可以用个相对跳转,还剩一个字节填90
Patched:
00007FF7FA85CEF7 | 45:31C9 | xor r9d,r9d
00007FF7FA85CEFA | E9 000000 | jmp myHookShellCodeAddr
00007FF7FA85CEFC | 90 | nop
这边先填00 00 00 00,还没想好跳哪,myHookShellCodeAddr = Base + Offset_0xCEFA + 5 + relativeAddr;
只要决定好myHookShellCodeAddr就能算出relativeAddr了,我们知道在exe程序的末尾往往会有一小段不使用的内存(更准确来说,是内存对齐后拉伸出来的一片内存区域,这片区域在节数据之外,是不使用的),可以把shellcode往这里写而不影响整体程序的执行。

比方我们选取00指令的起始位置,偏移为0x26C00,这里就是常规的inlineHook思路。
mov rax有两种字节码,这一种48:B8后接一个8字节立即数,后续在代码中改成myHookAddr就行。最后的回跳是跳到00007FF7FA85CEFC | 90 nop这个位置,接着正常执行。
注:下图回跳应为E9 EA62FEFF,应该跳到nop后一句,像图中这样写会崩

确定好myHookShellCodeAddr后,就可以计算上面的relativeAddr了,可以用我给出的公式算,也可以直接让x64dbg帮我们算好:

另外以上的操作注意端序问题。接下来终于终于可以开始写代码了:
#include "pch.h"
#include <Windows.h>
#include <process.h>
#include <stdio.h>
void DebugLog(const char* format, ...)
{
char buffer[1024];
va_list args;
va_start(args, format);
vsprintf_s(buffer, format, args);
va_end(args);
OutputDebugStringA(buffer);
}
BYTE hookWriteFileShellCode[] = {0xE9,0x01,0x9D,0x01,0x00,0x90};
BYTE callMyWriteFile[] = {
0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //mov rax,00 00 00 00 00 00 00 00
0xFF,0xD0, // call rax
0x48,0x83,0xC4,0x50, //add rsp,50
0x90, //nop
0xE9,0xEA,0x62,0xFE,0xFF, //jmp
};
DWORD64 oriWriteFileFunc = (DWORD64)GetModuleHandleA("contest.exe") + 0xD6A0;//call rbx后的跳板函数
DWORD64 oriCallRBX = (DWORD64)GetModuleHandleA("contest.exe") + 0xCEFA;
DWORD64 myHookWriteFileShellCodeAddr = (DWORD64)GetModuleHandleA("contest.exe") + 0x26C00;
char plainText[] = "catchmeifyoucan";
typedef void (*fphookWriteFile)(
DWORD64 RCX, DWORD64 RDX, DWORD64 R8, DWORD64 R9,
DWORD64 Par5, DWORD64 Par6, DWORD64 Par7, DWORD64 Par8,
DWORD64 Par9);
void __fastcall myHookWriteFile(
DWORD64 RCX, DWORD64 RDX, DWORD64 R8, DWORD64 R9,
DWORD64 Par5, DWORD64 Par6, DWORD64 Par7, DWORD64 Par8,
DWORD64 Par9) {
DebugLog("[+]replaced!\n");
Par8 = (Par8 & 0xFFFFFFFFFFFFFF00) | (strlen(plainText));
memcpy((void*)Par7, plainText, sizeof(plainText));
fphookWriteFile ptr = (fphookWriteFile)oriWriteFileFunc;
return ptr(RCX, RDX, R8, R9, Par5, Par6, Par7, Par8, Par9);
}
UINT WINAPI MainThread(PVOID) {
/*Install Hook Write File*/
DWORD oldProtect = 0;
VirtualProtect((LPVOID)oriCallRBX, sizeof(hookWriteFileShellCode), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((void*)oriCallRBX, hookWriteFileShellCode, sizeof(hookWriteFileShellCode));
VirtualProtect((LPVOID)myHookWriteFileShellCodeAddr, sizeof(callMyWriteFile), PAGE_EXECUTE_READWRITE, &oldProtect);
*(DWORD64*)&callMyWriteFile[2] = (DWORD64)myHookWriteFile;
memcpy((void*)myHookWriteFileShellCodeAddr, callMyWriteFile, sizeof(callMyWriteFile));
return 1;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
DebugLog("Begin Hooked!");
HANDLE hThread = (HANDLE)_beginthreadex(nullptr, NULL, MainThread, nullptr, 0, nullptr);
}
}
代码比较简单。看看效果:

实现了不崩溃的持续写入明文,第二问solved。
第三问
问题描述:编写程序,运行时修改尽量少的contest.exe内存,让contest.exe 往入自行指定的不同的文件里写入明文信息成功。(满分3分)
第二问分析的很完善了,第三问是一个道理,不过是把WriteFile变成了CreateFileA,先逆出Call CreateFileA


值得注意的是这里call rax后的跳板函数跟call WriteFile是同一个函数,应该是改变了参数。
同时观察传参窗口,第六个参数是文件名:

开始写代码,这里把myHookCreateFileAShellCodeAddr写在偏移0x26C20处
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <Windows.h>
#include <process.h>
#include <stdio.h>
void DebugLog(const char* format, ...)
{
char buffer[1024];
va_list args;
va_start(args, format);
vsprintf_s(buffer, format, args);
va_end(args);
OutputDebugStringA(buffer);
}
BYTE hookWriteFileShellCode[] = {0xE9,0x01,0x9D,0x01,0x00,0x90};
BYTE callMyWriteFile[] = {
0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //mov rax,00 00 00 00 00 00 00 00
0xFF,0xD0, // call rax
0x48,0x83,0xC4,0x50, //add rsp,50
0x90, //nop
0xE9,0xEA,0x62,0xFE,0xFF, //jmp
};
BYTE hookCreateFileAShellCode[] = { 0xE9,0x8B,0xA0,0x01,0x00,0x90 };
BYTE callMyCreateFileA[] = {
0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //mov rax,00 00 00 00 00 00 00 00
0xFF,0xD0, // call rax
0x48,0x83,0xC4,0x60, //add rsp,60
0x90, //nop
0xE9,0x60,0x5F,0xFE,0xFF, //jmp
};
DWORD64 oriWriteFileFunc = (DWORD64)GetModuleHandleA("contest.exe") + 0xD6A0;//call rbx后的跳板函数
DWORD64 oriCallRBX = (DWORD64)GetModuleHandleA("contest.exe") + 0xCEFA;
DWORD64 myHookWriteFileShellCodeAddr = (DWORD64)GetModuleHandleA("contest.exe") + 0x26C00;
DWORD64 oriCreateFileFunc = (DWORD64)GetModuleHandleA("contest.exe") + 0xD6A0;//call rbx后的跳板函数
DWORD64 oriCallRAX = (DWORD64)GetModuleHandleA("contest.exe") + 0xCB90;
DWORD64 myHookCreateFileAShellCodeAddr = (DWORD64)GetModuleHandleA("contest.exe") + 0x26C20;
char plainText[] = "catchmeifyoucan";
char fileName[] = "flag.txt";
typedef void (*fphookWriteFile)(
DWORD64 RCX, DWORD64 RDX, DWORD64 R8, DWORD64 R9,
DWORD64 Par5, DWORD64 Par6, DWORD64 Par7, DWORD64 Par8,
DWORD64 Par9);
typedef void (*fphookCreateFileA)(
DWORD64 RCX, DWORD64 RDX, DWORD64 R8, DWORD64 R9,
DWORD64 Par5, DWORD64 Par6, DWORD64 Par7, DWORD64 Par8,
DWORD64 Par9);
void __fastcall myHookWriteFile(
DWORD64 RCX, DWORD64 RDX, DWORD64 R8, DWORD64 R9,
DWORD64 Par5, DWORD64 Par6, DWORD64 Par7, DWORD64 Par8,
DWORD64 Par9) {
DebugLog("[+]plaintext replaced!\n");
Par8 = (Par8 & 0xFFFFFFFFFFFFFF00) | (strlen(plainText));
memcpy((void*)Par7, plainText, sizeof(plainText));
fphookWriteFile ptr = (fphookWriteFile)oriWriteFileFunc;
return ptr(RCX, RDX, R8, R9, Par5, Par6, Par7, Par8, Par9);
}
void __fastcall myHookCreateFileA(
DWORD64 RCX, DWORD64 RDX, DWORD64 R8, DWORD64 R9,
DWORD64 Par5, DWORD64 Par6, DWORD64 Par7, DWORD64 Par8,
DWORD64 Par9) {
DebugLog("[+]file name replaced!\n");
memcpy((void*)Par6, fileName, sizeof(fileName));
fphookCreateFileA ptr = (fphookCreateFileA)oriCreateFileFunc;
return ptr(RCX, RDX, R8, R9, Par5, Par6, Par7, Par8, Par9);
}
UINT WINAPI MainThread(PVOID) {
/*Install Hook WriteFile*/
DWORD oldProtect = 0;
VirtualProtect((LPVOID)oriCallRBX, sizeof(hookWriteFileShellCode), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((void*)oriCallRBX, hookWriteFileShellCode, sizeof(hookWriteFileShellCode));
VirtualProtect((LPVOID)myHookWriteFileShellCodeAddr, sizeof(callMyWriteFile), PAGE_EXECUTE_READWRITE, &oldProtect);
*(DWORD64*)&callMyWriteFile[2] = (DWORD64)myHookWriteFile;
memcpy((void*)myHookWriteFileShellCodeAddr, callMyWriteFile, sizeof(callMyWriteFile));
/*Install Hook CreateFileA*/
VirtualProtect((LPVOID)oriCallRAX, sizeof(hookCreateFileAShellCode), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((void*)oriCallRAX, hookCreateFileAShellCode, sizeof(hookCreateFileAShellCode));
VirtualProtect((LPVOID)myHookCreateFileAShellCodeAddr, sizeof(callMyCreateFileA), PAGE_EXECUTE_READWRITE, &oldProtect);
*(DWORD64*)&callMyCreateFileA[2] = (DWORD64)myHookCreateFileA;
memcpy((void*)myHookCreateFileAShellCodeAddr, callMyCreateFileA, sizeof(callMyCreateFileA));
return 1;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
DebugLog("Begin Hooked!");
HANDLE hThread = (HANDLE)_beginthreadex(nullptr, NULL, MainThread, nullptr, 0, nullptr);
}
}
成功:

那么三问就都解决了。

浙公网安备 33010602011771号