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更改程序内存属性就可以了,但是还是无效。 用火绒剑扫一下钩子果然是赛题在作怪。

image-20250317123733472

看了下这个钩子,直接跳转到一片unuse的内存,所以VirtualProtect失效。另外还有一个DbgUiRemoteBreakin的钩子,影响不是很大。

得想办法UnHook这个钩子,然后再用VirtualProtect重设页属性。正常的NtProtectVirtualMemory长这样:

image-20250317170511808

被挂钩后其实只有第一句汇编变了,不过这个系统调用也不长,干脆全部还原一下。注意这个地址的属性是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");
}

取消成功:

image-20250317180331213

接下来我的想法是直接在DLL中Hook WriteFile来更改写入的字符串,但是发现无论是Hook还是在调试器中给WriteFile下断都断不下来这个函数,这块没太懂,感觉是通过函数指针间接调用导致的吧。看来我之前分析的还有点问题,0环Hook NtCreateFile蓝屏的原因确实是因为内存属性的问题,但是3环Hook CreateFileA没反应应该是函数指针的问题,不过之前做的UnHook肯定也不是无用功。

那这下不得不看汇编了。把程序运行起来,右下角的堆栈窗口一直在变,其中就有WriteFile的符号,下个硬断追一下看看能不能发现什么。

神秘打野点,这里拿了WriteFile的函数指针。给他dump下来丢进IDA看看这块到底干了啥。发现IDA伪代码是一坨巨大的东西看了都要晕倒了,应该是VM的问题,没办法耐着性子继续追。

image-20250317184623254

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

image-20250317200318523

此时的参数窗口:

image-20250317200346250

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

image-20250317201538811

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

image-20250317201553434

然后其中的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往这里写而不影响整体程序的执行。

image-20250318224457845

比方我们选取00指令的起始位置,偏移为0x26C00,这里就是常规的inlineHook思路。

mov rax有两种字节码,这一种48:B8后接一个8字节立即数,后续在代码中改成myHookAddr就行。最后的回跳是跳到00007FF7FA85CEFC | 90 nop这个位置,接着正常执行。

注:下图回跳应为E9 EA62FEFF,应该跳到nop后一句,像图中这样写会崩

image-20250318225532903

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

image-20250318225957329

另外以上的操作注意端序问题。接下来终于终于可以开始写代码了:

#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);
    }
}

代码比较简单。看看效果:

image-20250318233914840

实现了不崩溃的持续写入明文,第二问solved。

第三问

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

第二问分析的很完善了,第三问是一个道理,不过是把WriteFile变成了CreateFileA,先逆出Call CreateFileA

image-20250319081308718

image-20250319081351497

值得注意的是这里call rax后的跳板函数跟call WriteFile是同一个函数,应该是改变了参数。

同时观察传参窗口,第六个参数是文件名:

image-20250319082411284

开始写代码,这里把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);
    }
}


成功:

image-20250319085939734

那么三问就都解决了。

posted @ 2025-03-19 09:06  凉猹  阅读(81)  评论(0)    收藏  举报