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.dllif (_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 000xFF,0xD0, // call rax0x48,0x83,0xC4,0x50, //add rsp,500x90, //nop0xE9,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 000xFF,0xD0, // call rax0x48,0x83,0xC4,0x50, //add rsp,500x90, //nop0xE9,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 000xFF,0xD0, // call rax0x48,0x83,0xC4,0x60, //add rsp,600x90, //nop0xE9,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);}
}
成功:
那么三问就都解决了。