2023腾讯游戏安全PC决赛复现

WriteUp 3个月前 admin
74 0 0

作者坛账号:Kvancy


前言

最近一直在做往年腾讯游戏安全PC端的复现,但是2023年决赛的题做到第三问就开始有点做不动了,想在网上找找题解但是并没有找到,于是便想分享一下自己的解题经验和第三问的思路。鉴于知识的局限性,文中可能存在疏漏或不足之处,如果发现任何错误或不准确之处,请不吝赐教。

题目

2023腾讯游戏安全PC决赛复现


(一)解题过程

第一问让我们杀死进程,可以尝试根据windowsAPI提供的TerminateProcess函数去停止进程,写份代码测试下

 复制代码 隐藏代码
bool KillProcessByName(const char* processName) {
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(PROCESSENTRY32);
    if (Process32First(hSnapshot, &pe)) {
        do {
            if (strcmp(pe.szExeFile, processName) == 0) {
                HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
                if (hProcess) {
                    TerminateProcess(hProcess, 0);
                    CloseHandle(hProcess);
                    Num++;
                    printf("Kill suc:[%d]n", Num);
                }
            }
        } while (Process32Next(hSnapshot, &pe));
    }

    CloseHandle(hSnapshot);
    return true;
}
int main()
{
    while(1)
    {
        KillProcessByName("WorkingService.exe");
    }
    return 0;
}

以管理员身份启动测试

启动前:

2023腾讯游戏安全PC决赛复现


启动后


2023腾讯游戏安全PC决赛复现


2023腾讯游戏安全PC决赛复现


(二)解题过程

先DIE查壳发现有VMP,只能动调入手,先观察程序功能。程序启动后会有两个进程启动,并且其中之一占用CPU性能较高;程序目录会创建十六个文件,并会做重复的删除和重新创建的操作;观察任务管理器发现进程会自动重启,手动杀死进程后就会重生一个新的进程。

拖进xdbg里观察符号,发现引入了ShellExcuteA这个函数,正好是启动进程函数,对这个函数下断点,发现确实断了下来,观察传参窗口。

2023腾讯游戏安全PC决赛复现


第六个参数不同,对应的是lpParameters(传入进程的参数),一个是working,一个是daemon restart

根据这个在控制台上运行加上参数working,发现只有一个工作进程,CPU占用100%,同理加上daemon restart,只有一个守护进程CPU占用不到1%。

调试参数为working的进程,观察线程发现有16个线程运行同一个函数,刚好对应16个txt日志文件的输出!!!

2023腾讯游戏安全PC决赛复现


于是进入线程入口开始分析,对所有可能的写入文件的windowsAPI下断,发现都没有断下来,于是向CreateFile等API下断,

所有线程在CreateFileA上成功断下,观察传参确定这是对日志文件的操作函数

2023腾讯游戏安全PC决赛复现


有了这个依据,能确认的就是16个线程会循环通过CreateFileA打开文件,这可能是占用CPU的一个主要因素。但是跟之前运行的结果不同,调试发现,写入文件的API一直没有被调用,而正常运行的结果是,十六个文件会循环的做一个过程,文件被创建->文件被写入->文件被删除->新文件创建…。我通过加working参数运行,只会有一个循环过程,即不会有新文件诞生,文件内容也不会被修改。所以,十六个线程循环CreateFileA打开文件过程是无用功,只会徒增CPU负担!那么我们是不是可以HOOK CreateFileA这个函数,然后每个线程调用它的第二次的时候我们进行线程终止,是不是可以减少CPU的压力了呢?写份代码跑一下看看

 复制代码 隐藏代码
#include "pch.h"
#include <windows.h>
#include <shellapi.h>
#include <detours.h>
#include <tlhelp32.h>
#pragma comment(lib,"detours.lib")
#define _DEBUG
#define DBGMGEBOX(fmt, ...)
    do {
         /* 假设最大长度为1024,根据需要调整大小 */
        wsprintfA(out, fmt, __VA_ARGS__);
        MessageBoxA(NULL, out, "提示", MB_OK);
    } while(0)

char out[100];
DWORD tlsIndex;//tls索引
typedef BOOL(WINAPI* ShellExecuteExA_t)(SHELLEXECUTEINFOA*);
typedef HANDLE (WINAPI* CreateFileA_t)(
    LPCSTR                lpFileName,
    DWORD                 dwDesiredAccess,
    DWORD                 dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD                 dwCreationDisposition,
    DWORD                 dwFlagsAndAttributes,
    HANDLE                hTemplateFile
);

ShellExecuteExA_t TrueShellExecuteExA = NULL;
CreateFileA_t TrueCreateFileA = NULL;

BOOL WINAPI HookedShellExecuteExA(SHELLEXECUTEINFOA* pExecInfo) {
#if 1
    //执行第一个ShellExecuteExA守护进程
    static int Num = 0;
    DBGMGEBOX("ShellExecuteExA 被调用:Num = %dnhProcess = %p", Num, pExecInfo->hProcess);
    if (Num == 0)
    {
        Num++;
        return TrueShellExecuteExA(pExecInfo);
    }
    else
    {
        return TrueShellExecuteExA(pExecInfo);
    }
#else
    //执行第二个ShellExecuteExA病毒进程
    static int Num = 0;
    DBGMGEBOX("ShellExecuteExA 被调用:Num = %d n调用者窗口句柄 = 0x%pn", Num, pExecInfo->hwnd);
    if (Num == 0)
    {
        Num++;
        DBGMGEBOX("[2]:当前线程ID:%d", GetCurrentThreadId());
        pExecInfo->lpFile = "C:\Users\Administrator\Desktop\自动F8直到call.txt";//修改参数导致重启失败;
        return TrueShellExecuteExA(pExecInfo);

    }
    else
    {

        DBGMGEBOX("[1]:当前线程ID:%d", GetCurrentThreadId());
        return TrueShellExecuteExA(pExecInfo);
    }
#endif
}
HANDLE WINAPI HookCreateFileA(
    LPCSTR                lpFileName,
    DWORD                 dwDesiredAccess,
    DWORD                 dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD                 dwCreationDisposition,
    DWORD                 dwFlagsAndAttributes,
    HANDLE                hTemplateFile
)
{

    //判断线程是否是第一次运行CreateFileA,是的话就放行,不是第一次运行就终止线程
    // 获取当前线程的TLS值
    LPVOID tlsValue = TlsGetValue(tlsIndex);

    if (tlsValue == NULL)
    {
        // 第一次运行,设置TLS值
#ifdef _DEBUG
        DBGMGEBOX("放行nlpFileName:%sn", lpFileName);
#endif
        TlsSetValue(tlsIndex, (LPVOID)1);
    }
    else
    {
        // 不是第一次运行,终止线程
#ifdef _DEBUG
        DBGMGEBOX("终止nlpFileName:%sn", lpFileName);
#endif
        ExitThread(0);
    }
    return CreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:

        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        /*TrueShellExecuteExA = (ShellExecuteExA_t)DetourFindFunction("shell32.dll", "ShellExecuteExA");
        DetourAttach(&(PVOID&)TrueShellExecuteExA, HookedShellExecuteExA);*/


        tlsIndex = TlsAlloc();//初始化TLS
        TrueCreateFileA = (CreateFileA_t)DetourFindFunction("kernelbase.dll", "CreateFileA");
        DetourAttach(&(PVOID&)TrueCreateFileA, HookCreateFileA);

        DetourTransactionCommit();
        break;
    case DLL_PROCESS_DETACH:
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(&(PVOID&)TrueShellExecuteExA, HookedShellExecuteExA);
        DetourDetach(&(PVOID&)TrueCreateFileA, HookCreateFileA);
        TlsFree(tlsIndex);//清理TLS
        DetourTransactionCommit();
        break;
    }
    return TRUE;
}

注入进去查看CPU,与不注入的CPU情况对比如图,进程CPU占比显著下降

2023腾讯游戏安全PC决赛复现


2023腾讯游戏安全PC决赛复现
现在我们能够确定,是这十六个线程循环打开文件(也可能做了其他的事情)占用CPU大量资源。但是直接线程退出的方式也会导致日志文件被删除,题目意思是说在保持WorkingService.exe正常运行不崩溃、主体功能无损的前提下,使之占用CPU下降到平均5%以下,WorkingService.exe的主体功能为写入信息。在没有源码的情况下提取这个部分,自行编写一个性能更佳的服务,现在直接退出线程的方式会导致日志文件删除过快,虽然写入了信息,但是根本没法看啊,所以我们为了日志文件更方便的浏览,应该在WriteFile之后保存文件。那我们得先hook一下删除文件的函数,看一下在哪调用的,然后选择过滤掉这个函数从而保存日志文件。


尝试hook一下DeleteFileA这个函数

2023腾讯游戏安全PC决赛复现


发现没有被调用。

翻阅了下CreateFileA的文档,找到dwFlagsAndAttributes这个参数,发现这个参数包含了FILE_FLAG_DELETE_ON_CLOSE这个属性,意思是文件将在关闭所有句柄后立即删除

 复制代码 隐藏代码
HANDLE CreateFileA(
    LPCSTR lpFileName,    // 文件名
    DWORD dwDesiredAccess, // 访问模式
    DWORD dwShareMode,     // 共享模式
    LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性
    DWORD dwCreationDisposition, // 创建或打开文件的方式
    DWORD dwFlagsAndAttributes, // 文件属性
    HANDLE hTemplateFile // 模板文件句柄
);
FILE_FLAG_DELETE_ON_CLOSE0x04000000 文件将在关闭所有句柄后立即删除,其中包括指定的句柄和任何其他打开或重复的句柄。

因此,只要我们改了这个属性,日志文件就不会再被删除,写代码测试下

 复制代码 隐藏代码
HANDLE WINAPI HookCreateFileA(
    LPCSTR                lpFileName,
    DWORD                 dwDesiredAccess,
    DWORD                 dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD                 dwCreationDisposition,
    DWORD                 dwFlagsAndAttributes,
    HANDLE                hTemplateFile
)
{
    //缩小每个线程执行CreateFileA的次数,CPU降低到%5以下
    // 获取当前线程的TLS值
    LPVOID tlsValue = TlsGetValue(tlsIndex);

    if ((DWORD_PTR)tlsValue <= 0x10)
    {
#ifdef _KDEBUG
        DBGMGEBOX("放行nlpFileName:%sn", lpFileName);
#endif
        dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL;
        TlsSetValue(tlsIndex, (LPVOID)((DWORD_PTR)tlsValue + 1));
    }
    else
    {
#ifdef _KDEBUG
        DBGMGEBOX("终止nlpFileName:%sn", lpFileName);
#endif
        ExitThread(0);
    }
    return TrueCreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}

2023腾讯游戏安全PC决赛复现


2023腾讯游戏安全PC决赛复现
测试得到文件确实没有被删除,并且密文也成功写入文件,CPU占用也少于5%(峰值3%,最后趋近于0)


到这工作进程已经差不多分析完了,但是别忘了还有一个守护进程,守护进程应该也算是正常功能的一部分,那我们现在要开始结合守护进程分析。根据之前分析得到工作进程启动是通过守护进程调用ShellExcuteExA来传入working参数实现的,而这个后面启动的进程才是我们应该去注入的。而且因为程序有循环自生自灭的一个功能,我们可能要考虑循环注入实现对所有工作进程的hook。首先考虑第一个问题,如何去注入ShellExcuteExA启动的工作进程,第一个方式是Hook ShellExcuteExA这个函数,在其启动前获取ShellExcuteExA返回值->进程句柄,然后通过进程句柄进行注入;第二个方式是编写一个辅助程序,循环对每一个WorkingService.exe进程进行注入,这个要考虑到hook是否会影响守护进程的正常功能。我想先试一下第二个方式,较为简单一点,先测试一下守护进程是否会调用CreateFileA函数,如果会的话,我们要进行一下过滤操作。Hook测试了下,守护进程先启动,然后是工作进程,但是迟迟没有调试弹窗出现,说明守护进程没有调用CreateFileA。

2023腾讯游戏安全PC决赛复现


因此,采用方式二,只要对所有WorkingService.exe进程进行注入,Hook CreateFileA函数即可在正常程序功能执行的情况下,降低CPU能耗,写一个程序测试一下

 复制代码 隐藏代码
int main() {
    while (1)
    {
        int Num = 0;
        ve  = GetPidByProcName("WorkingService.exe");
        if (!ve.empty())
        {
            printf("There are currently %d processesn", ve.size());
            //依次注入并用map标记是否被注入过
            for (auto i : ve)
            {
                auto it = mp.find(i);
                if (it == mp.end())//没有被注入过
                {
                    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, i);
                    if (InjectDll(hProcess, dllPath))
                    {
                        Num++;
                        mp.insert({ i,true });
                        printf("Inject  process Success!n", Num);
                    }
                }
                else //曾经注入过该进程
                {
                    printf("[%d] Has Been Injectedn",i);
                    continue;
                }
            }
        }
        else
        {
            printf("Wating WorkingService.exe ...n");
        }
        system("cls");
        //Sleep(200);
    }
    return 0;
}

这里使用Vector去接受所有WorkingService.exe进程的pid,然后用Map去映射一个bool值来判断是否这个进程被注入过,程序运行起来发现CPU能耗还是100%,但是程序告诉我注入成功。???疑惑住了

2023腾讯游戏安全PC决赛复现


因为之前注入的时候可以成功降低CPU能耗的,我很确信我的dll是没问题的,所以问题可能就是注入的方式了。考虑到之前注入使用工具注入,在进程启动的同时注入dll,而现在注入是在程序完全跑起来之后,所以怀疑是被下了钩子破坏了注入。在ARK工具里扫一下钩子看一眼。

2023腾讯游戏安全PC决赛复现


豁然开朗!第一个钩子勾住了NtProtectVirtualMemory函数,而我们注入模块的时候就会调用这个内核函数去修改内存权限,第二个钩子勾住了DbgUiRemoteBreakin函数,我也不知道这个函数是干什么的,问一下GPT得知

DbgUiRemoteBreakin函数是一个Windows API函数,它的作用是向指定的进程发送一个远程调试器的断点请求。当这个函数被调用时,目标进程会触发一个断点异常(通常是访问违规异常),如果目标进程已经有一个调试器附加,那么调试器就会捕获这个异常并进行处理。

这个函数通常用于以下情况:

  1. 附加调试器:如果你想要附加一个调试器到一个正在运行的进程,你可以调用DbgUiRemoteBreakin来使进程触发断点,然后使用调试器来附加到该进程。

  2. 检测调试:如果一个进程检测到自己被调试,它可以调用DbgUiRemoteBreakin来尝试断开任何远程调试器的连接。

看来是能附加调试的,勾住这个来反调试,哇哦,不错的方式。但是我们就先不管它了,先成功注入dll再说。所以现在看来还得先还原NtProtectVirtualMemory函数取消钩子,再调用注入函数,修改一下代码跑一下发现日志文件不能循环写入,应该是线程都退出之后,进程并未停止,猜测是线程函数里有计数的东西,到某个值之后会通知进程结束,从而让守护进程再生工作进程,于是我改了一下hook代码,让线程终止16次后终止进程,从而实现日志文件循环再生。

效果图:

2023腾讯游戏安全PC决赛复现


2023腾讯游戏安全PC决赛复现


题目二的结果应该就是这样了,能保持写入日志文件功能的同时不破坏工作进程循环启动的机制,大功告成!!!

附一份exe源码和一份dll源码

 复制代码 隐藏代码
//main.cpp
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <vector>
#include <map>
char dllPath[] = R"(C:Users15386DesktoptemphookDll.dll)";
std::vector<DWORD64>ve;
std::map<DWORD64, bool>mp;
BYTE jmpBytes[] = {
0xE9, 0xE0, 0x26, 0x16, 0x00
};
BYTE originBytes[] = {
0x4C, 0x8B, 0xD1, 0xB8, 0x50, 0x00, 0x00, 0x00, 0xF6, 0x04, 0x25, 0x08, 0x03, 0xFE, 0x7F, 0x01,
0x75, 0x03, 0x0F, 0x05, 0xC3, 0xCD, 0x2E, 0xC3
};
std::vector<DWORD64> GetPidByProcName(const char* processName) {
    HANDLE hProcessSnap = INVALID_HANDLE_VALUE;
    PROCESSENTRY32 pe32 = { 0 };

    std::vector<DWORD64> vec;
    vec.clear();
    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE) {
        vec;
    }

    pe32.dwSize = sizeof(PROCESSENTRY32);

    if (Process32First(hProcessSnap, &pe32)) {
        do {
            if (strcmp(pe32.szExeFile, processName) == 0) {
                vec.push_back(pe32.th32ProcessID);
            }
        } while (Process32Next(hProcessSnap, &pe32));
    }
    CloseHandle(hProcessSnap);
    return vec;
}

DWORD64 GetModuleBase(DWORD64 pid,const char* ModuleName)
{
    HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
    if (hModuleSnap != INVALID_HANDLE_VALUE) {
        MODULEENTRY32 me32;
        me32.dwSize = sizeof(MODULEENTRY32);
        if (Module32First(hModuleSnap, &me32)) {
            do {
                if (_stricmp(me32.szModule, ModuleName) == 0) {
                    return (DWORD64)me32.modBaseAddr;
                }
            } while (Module32Next(hModuleSnap, &me32));
        }
        CloseHandle(hModuleSnap);
    }
    return 0;
}

BOOL InjectDll(HANDLE hProcess, LPCSTR dllPath) {

    LPVOID pRemoteDllPath = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
    if (pRemoteDllPath == NULL) {
        printf("VirtualAllocEx Failed:[%d]n", GetLastError());
        return FALSE;
    }

    if (!WriteProcessMemory(hProcess, pRemoteDllPath, dllPath, strlen(dllPath) + 1, NULL)) {
        printf("WriteProcessMemory Failed:[%d]n", GetLastError());
        VirtualFreeEx(hProcess, pRemoteDllPath, 0, MEM_RELEASE);
        return FALSE;
    }

    LPTHREAD_START_ROUTINE lpLoadLibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
    if (lpLoadLibrary == NULL) {
        printf("GetProcAddress Failed:[%d]n", GetLastError());
        VirtualFreeEx(hProcess, pRemoteDllPath, 0, MEM_RELEASE);
        return FALSE;
    }

    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, lpLoadLibrary, pRemoteDllPath, 0, NULL);
    if (hThread == NULL) {
        printf("CreateRemoteThread Failed:[%d]n", GetLastError());
        VirtualFreeEx(hProcess, pRemoteDllPath, 0, MEM_RELEASE);
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE);

    DWORD dwExitCode;
    if (GetExitCodeThread(hThread, &dwExitCode) && dwExitCode == 0) {
        printf("LoadLibraryA Failed in remote processn");
        CloseHandle(hThread);
        VirtualFreeEx(hProcess, pRemoteDllPath, 0, MEM_RELEASE);
        return FALSE;
    }

    CloseHandle(hThread);
    VirtualFreeEx(hProcess, pRemoteDllPath, 0, MEM_RELEASE);

    return TRUE;
}

BOOL RemoveHook(HANDLE hProcess,PVOID unHookAddr, BYTE* originBytes)
{
   PDWORD oldProtect = 0;
   if (!WriteProcessMemory(hProcess, (PVOID)unHookAddr, originBytes, sizeof(originBytes), 0))
   {
       printf("RemoveHook Failed!!!: [%d]n", GetLastError());
       return FALSE;
   }
}
int main() {
    while (1)
    {
        int Num = 0;
        ve  = GetPidByProcName("WorkingService.exe");//获取所有进程pid
        if (!ve.empty())
        {
            printf("There are currently %d processesn", ve.size());
            //依次注入并用map标记是否被注入过
            for (auto i : ve)
            {
                auto it = mp.find(i);
                if (it == mp.end())//没有被注入过
                {
                    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, i);//要用管理员权限运行,要不然为返回空
                    if (!hProcess) printf("OpenProcess Error:[%d]", GetLastError());
                    DWORD64 unHookAddr = GetModuleBase(i,"ntdll.dll") + 0xA0990;//NtProtectVirtualMemory地址,ntdll + 0xA0990
                    if (RemoveHook(hProcess, (PVOID)unHookAddr, originBytes))//先取消NtProtectVirtualMemory钩子再注入
                    {
                        if (InjectDll(hProcess, dllPath))
                        {
                            Num++;
                            mp.insert({ i,true });//注入成功后进行标记
                            printf("Inject  process Success!n", Num);
                        }
                    }

                }
                else  continue;//曾经注入过该进程
            }
        }
        else printf("Wating WorkingService.exe ...n");
        Sleep(1000);
        system("cls");

    }
    return 0;
}
 复制代码 隐藏代码
//dllmain.cpp
#include "pch.h"
#include <windows.h>
#include <shellapi.h>
#include <detours.h>
#include <tlhelp32.h>
#include <stdlib.h>
#pragma comment(lib,"detours.lib")
#define _KDEBUG
#define DBGMGEBOX(fmt, ...)
    do {
         /* 假设最大长度为1024,根据需要调整大小 */
        wsprintfA(out, fmt, __VA_ARGS__);
        MessageBoxA(NULL, out, "提示", MB_OK);
    } while(0)

char out[100];
DWORD tlsIndex;//tls索引
typedef BOOL(WINAPI* ShellExecuteExA_t)(SHELLEXECUTEINFOA*);
typedef HANDLE (WINAPI* CreateFileA_t)(
    LPCSTR                lpFileName,
    DWORD                 dwDesiredAccess,
    DWORD                 dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD                 dwCreationDisposition,
    DWORD                 dwFlagsAndAttributes,
    HANDLE                hTemplateFile
);
ShellExecuteExA_t TrueShellExecuteExA = NULL;
CreateFileA_t TrueCreateFileA = NULL;
BOOL WINAPI HookedShellExecuteExA(SHELLEXECUTEINFOA* pExecInfo) {
#if 1
    //执行第一个ShellExecuteExA守护进程
    static int Num = 0;
    DBGMGEBOX("ShellExecuteExA 被调用:Num = %dnhProcess = %p", Num, pExecInfo->hProcess);
    if (Num == 0)
    {
        Num++;
        return TrueShellExecuteExA(pExecInfo);
    }
    else
    {
        return TrueShellExecuteExA(pExecInfo);
    }
#else
    //执行第二个ShellExecuteExA病毒进程
    static int Num = 0;
    DBGMGEBOX("ShellExecuteExA 被调用:Num = %d n调用者窗口句柄 = 0x%pn", Num, pExecInfo->hwnd);
    if (Num == 0)
    {
        Num++;
        DBGMGEBOX("[2]:当前线程ID:%d", GetCurrentThreadId());
        pExecInfo->lpFile = "C:\Users\Administrator\Desktop\自动F8直到call.txt";//修改参数导致重启失败;
        return TrueShellExecuteExA(pExecInfo);

    }
    else
    {

        DBGMGEBOX("[1]:当前线程ID:%d", GetCurrentThreadId());
        return TrueShellExecuteExA(pExecInfo);
    }
#endif
}
HANDLE WINAPI HookCreateFileA(
    LPCSTR                lpFileName,
    DWORD                 dwDesiredAccess,
    DWORD                 dwShareMode,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    DWORD                 dwCreationDisposition,
    DWORD                 dwFlagsAndAttributes,
    HANDLE                hTemplateFile
)
{

    //判断线程是否是第一次运行CreateFileA,是的话就放行,不是第一次运行就终止线程
    // 获取当前线程的TLS值
    LPVOID tlsValue = TlsGetValue(tlsIndex);
    if ((DWORD_PTR)tlsValue <= 0x10)
    {
        //
#ifdef _KDEBUG
        DBGMGEBOX("放行nlpFileName:%sn", lpFileName);
#endif
        dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL;
        TlsSetValue(tlsIndex, (LPVOID)((DWORD_PTR)tlsValue + 1));
    }
    else
    {
        static int Num = 0;//如果终止了十六个线程,再终止进程,使得守护线程再生工作进程
        // 不是第一次运行,终止线程
#ifdef _KDEBUG
        DBGMGEBOX("终止nlpFileName:%sn", lpFileName);
#endif
        Num++;
        ExitThread(0);
        if (Num == 16) exit(0);
    }
    return TrueCreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:

        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());

        //ShellExecuteExA hook
        /*TrueShellExecuteExA = (ShellExecuteExA_t)DetourFindFunction("shell32.dll", "ShellExecuteExA");
        DetourAttach(&(PVOID&)TrueShellExecuteExA, HookedShellExecuteExA);*/


        tlsIndex = TlsAlloc();//初始化TLS
        TrueCreateFileA = (CreateFileA_t)DetourFindFunction("kernelbase.dll", "CreateFileA");
        DetourAttach(&(PVOID&)TrueCreateFileA, HookCreateFileA);

        DetourTransactionCommit();
        break;
    case DLL_PROCESS_DETACH:
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(&(PVOID&)TrueShellExecuteExA, HookedShellExecuteExA);
        DetourDetach(&(PVOID&)TrueCreateFileA, HookCreateFileA);
        TlsFree(tlsIndex);//清理TLS

        DetourTransactionCommit();
        break;
    }
    return TRUE;
}

(三)思路分享

接着是第三题,要求是WorkingService.exe的主体功能为写入信息。在没有源码的情况下提取这个部分,自行编写一个性能更佳的服务,完全替代WorkingService.exe写入同样的密文信息,且运行时平均占用CPU时间低于5%。(2分)

根据之前分析,现在一直的程序功能主要体现在,创建守护进程启动->启动工作进程->创建十六个线程->每个线程创建一个日志文件->向文件写入密文->进程退出->守护进程循环创建工作进程,大致功能应该就是这些,而现在要解决的问题就是日志文件名和日志密文是怎么产生的。

在WriteFile下个断点看一眼密文的大致模样。

2023腾讯游戏安全PC决赛复现


猜测有可能是base64加密,因为之前查看字符串有看到base64的码表,还是一个魔改码表,直接丢cyberchef试一下解密

2023腾讯游戏安全PC决赛复现


2023腾讯游戏安全PC决赛复现
好熟悉的字符串,这不正是初赛题的明文嘛,这个明显就是base64加密明文了,但是还有一个疑问,就是不同的文件会打印出不同的密文,把十六个文件的密文提取出来对比一下


 复制代码 隐藏代码
1 F2FU4Wht52lm+2dV4WFu 6128
2 4WECF2ht52lm+2dV4WFu 20512
2 4WECF2ht52lm+2dV4WFu 22060
3 Fx9U4RY7ERcwDRkDFx84 22708
4 Fx8CFxY7ERcwDRkD4WFu 25296
5 Fx9U4RZt5xcwDRkDFx84 25376
6 Fx8CFxY7ERcwDRkDFx84 25620
3 Fx9U4RY7ERcwDRkDFx84 25756
2 4WECF2ht52lm+2dV4WFu 26136
3 Fx9U4RY7ERcwDRkDFx84 27704
7 4R9U4RY7ERcwDWdV4WE4 27976
8 4WECF2ht5xcwDRkDFx84 28380
3 Fx9U4RY7ERcwDRkDFx84 28624
9 Fx9U4RY7EWlm+xkDFx84 28764
10 4WECFxY7ERcwDWdV4WFu 29904
11 Fx9U4RY7ERcwDWdV4WE4 31648

对比发现十六个线程有十一个密文,16:11的不规则性我觉得甚至不止十一个,因为这个采样只是采取了一个进程创建的十六个线程,如果有更多进程可能就意味着更多种密文。

根据base64编码规则,如果码表和明文都一致的话,那密文就只能有一个,说明每个线程的码表和明文至少有一个不一致。那现在只能继续分析他的加密方式,在现有的码表处下断追踪到加密函数

2023腾讯游戏安全PC决赛复现


在内存窗口发现传入的第一个参数是一个字符串地址->c tc me an,貌似跟catchmeifyoucan明文很像,所以这是修改了明文,然后再通过正常的码表进行base64加密?提取出这个明文,试一下对着这个码表加密,对比运行后的密文就知道了,

 复制代码 隐藏代码
明文:0x15, 0x61, 0x02, 0x15, 0x68, 0x1B, 0x13, 0x69, 0x66, 0x79, 0x6F, 0x75, 0x63, 0x17, 0x18
实际密文:F2ECF2g7EWlm+2dV4R84
正常base64密文:F2ECF2g7EWlm+2dV4R84
明文:0x63, 0x17, 0x74, 0x63, 0x1E, 0x6D, 0x65, 0x1F, 0x10, 0x0F, 0x19, 0x03, 0x15, 0x61, 0x6E
实际密文:4R9U4RZt5xcwDRkDF2Fu
正常base64密文:4R9U4RZt5xcwDRkDF2Fu

对比了多组,确信了码表是对的ABCDEFGHIJKLMNOPwxyz0123456789+/ghijklmnopqrstuvQRSTUVWXYZabcdef,但是调试这个加密的过程中发现,一个线程至少会有两组明文,上面就是出自同一个线程。并且这个函数加密后的密文跟实际文件密文不同,下面是提取的两组密文和实际文件密文

 复制代码 隐藏代码
//34288线程
文件密文:4WFU4Wht52lm+2dV4WFu
密文1   :Fx8C4Wg7ERcwDWdV4WE4
密文2   :4WFUFxZt52lm+xkDFx9u

现在还有很多问题在里面,首先是明文为什么一个线程有两组,加密得到的两组密文跟文件密文有什么关系?两组密文实在是太奇怪了,我试着把他们联系起来,于是我将两组密文用base64还原得到明文也进行了对比,发现了一个惊喜。

2023腾讯游戏安全PC决赛复现

2023腾讯游戏安全PC决赛复现


这两个明文的有效字符组合起来刚好是catchmeifyoucan!!! 接着就是其他无效字符了,提取出十六进制看看有没有联系

Fx8C4Wg7ERcwDWdV4WE4->0x15,0x17,0x02,0x63,0x68,0x1b,0x13,0x1f,0x10,0x0f,0x6f,0x75,0x63,0x61,0x18
4WFUFxZt52lm+xkDFx9u->0x63,0x61,0x74,0x15,0x1e,0x6d,0x65,0x69,0x66,0x79,0x19,0x03,0x15,0x17,0x6e

貌似没什么关系,考虑到多个文件的写入密文不同,继续提取其他线程的密文

 复制代码 隐藏代码
//14032 线程
文件密文:Fx8CFxY7ERcwDRkDFx84
密文1   :Fx8C4Wg7ERcwDWdV4WE4
密文2   :4WFUFxZt52lm+xkDFx9u

拿这个线程的文件密文拿去解密,得到十六进制数组:0x15,0x17,0x02,0x15,0x1e,0x1b,0x13,0x1f,0x10,0x0f,0x19,0x03,0x15,0x17,0x18,刚好由34288线程的两组非可视字符十六进制组成。根据这个思路我把之前提取的密文进行了解密,发现线程之间的非字符不一定是成对的,但是非字符的十六进制跟所在索引是一定的,也就是说所有非可视字符的十六进制都是从上面这个数组里取,我试图多运行几遍进程反驳我这个猜想,因为这个十六进制数组并没有什么规律,但是运行结果都显示这个数组是一定的。

所以现在可以确定的是,明文catchmeifyoucan会和这个数组以某种方式进行混合,最后组成真正的明文进行base64换表加密得到文件密文。现在就需要继续研究这个混合的方式,因为进程会启动十六个线程来写日志,而且在之前的观察中看到在CreateFileA函数之前,密文就已经储存在寄存器中,所以是CreateThread和CreateFileA函数之间进行了明文混合和base64加密,对这三个关键地方下断点调试

2023腾讯游戏安全PC决赛复现


 复制代码 隐藏代码
{
0x15, 0x17, 0x02, 0x15, 0x1E, 0x1B, 0x13, 0x1F, 0x10, 0x0F, 0x19, 0x03, 0x15, 0x17, 0x18
};

可以看到,第一个参数即为之前分析的十六进制数组的指针,做一层栈回溯找明文替换方式

2023腾讯游戏安全PC决赛复现


对base64的第一个参数位置(明文指针)下硬件访问断点,找到写入明文内存的位置

2023腾讯游戏安全PC决赛复现


2023腾讯游戏安全PC决赛复现


明文进行了一个字节一个字节的写入,仔细看这段VM给的混淆汇编代码, 其实就是一个

 复制代码 隐藏代码
mov r8, qword ptr ds:[rdi]
mov r9b, byte ptr ds:[rdi+0x8]
mov byte ptr ds:[r8], r9b

那就意味着我们要追踪[RDI]看看是怎么来的,由于这段代码已经被VM了,控制流混淆导致根本没法手动追踪,用xdbg自动追踪功能打印日志看看

2023腾讯游戏安全PC决赛复现


足足有30000行汇编

2023腾讯游戏安全PC决赛复现


往上回溯第一层发现[rdi]从RFLAGS寄存器来的,如果真要手动回溯的话,就要回溯十多个标志位,手动分析逻辑写源码不大现实(不禁再次感叹VM的强大)

根据题目要求

提取WorkingService.exe的主体功能可以以二进制指令块的形式,也可以以自写源码的形式

可以考虑提取二进制指令块的形式写功能,那我们就可以把这段追踪得来的汇编代码转成ShellCode写入到自己程序的内存里,然后通过VirtualProtect修改成可执行的运行一下从而提取这份功能。只要获取这份功能就可以完成第三问了,感觉工程量挺大的就不写了。

程序分析总结

启动程序后会先默认按照参数daemon restart启动一个守护进程,守护进程通过设置参数working和调用ShellExcuteA来启动工作进程,工作进程会创建16个线程来循环调用CreateFileA保持文件存在(第二问需要通过修改参数实现相同功能并减少CPU占用率)并调用一次WriteFile来写入密文,密文由两个明文和两种加密组成,明文是catchmeifyoucan字符串和{
0x15, 0x17, 0x02, 0x15, 0x1E, 0x1B, 0x13, 0x1F, 0x10, 0x0F, 0x19, 0x03, 0x15, 0x17, 0x18
}数组,第一种加密是某种替换加密,第二种加密是base64变表加密。


-官方论坛

www.52pojie.cn


👆👆👆

公众号设置“星标”,不会错过新的消息通知
开放注册、精华文章和周边活动等公告

2023腾讯游戏安全PC决赛复现

原文始发于微信公众号(吾爱破解论坛):2023腾讯游戏安全PC决赛复现

版权声明:admin 发表于 2024年8月16日 上午10:32。
转载请注明:2023腾讯游戏安全PC决赛复现 | CTF导航

相关文章