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

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


image-20240813190641633.png (135.45 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

(一)解题过程
第一问让我们杀死进程,可以尝试根据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;
}
以管理员身份启动测试
启动前:


image-20240813191830392.png (16.44 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

启动后


image-20240813191935364.png (46.86 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传



image-20240813192038178.png (14.34 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

(二)解题过程
先DIE查壳发现有VMP,只能动调入手,先观察程序功能。程序启动后会有两个进程启动,并且其中之一占用CPU性能较高;程序目录会创建十六个文件,并会做重复的删除和重新创建的操作;观察任务管理器发现进程会自动重启,手动杀死进程后就会重生一个新的进程。
拖进xdbg里观察符号,发现引入了ShellExcuteA这个函数,正好是启动进程函数,对这个函数下断点,发现确实断了下来,观察传参窗口。


image-20240810184600095.png (42.05 KB, 下载次数: 0)
下载附件
2024-8-13 20:02 上传

第六个参数不同,对应的是lpParameters(传入进程的参数),一个是working,一个是daemon restart
根据这个在控制台上运行加上参数working,发现只有一个工作进程,CPU占用100%,同理加上daemon restart,只有一个守护进程CPU占用不到1%。
调试参数为working的进程,观察线程发现有16个线程运行同一个函数,刚好对应16个txt日志文件的输出!!!


image-20240810190419709.png (21.63 KB, 下载次数: 0)
下载附件
2024-8-13 20:00 上传

于是进入线程入口开始分析,对所有可能的写入文件的windowsAPI下断,发现都没有断下来,于是向CreateFile等API下断,
所有线程在CreateFileA上成功断下,观察传参确定这是对日志文件的操作函数


image-20240810192612468.png (16.14 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

有了这个依据,能确认的就是16个线程会循环通过CreateFileA打开文件,这可能是占用CPU的一个主要因素。但是跟之前运行的结果不同,调试发现,写入文件的API一直没有被调用,而正常运行的结果是,十六个文件会循环的做一个过程,文件被创建->文件被写入->文件被删除->新文件创建…。我通过加working参数运行,只会有一个循环过程,即不会有新文件诞生,文件内容也不会被修改。所以,十六个线程循环CreateFileA打开文件过程是无用功,只会徒增CPU负担!那么我们是不是可以HOOK CreateFileA这个函数,然后每个线程调用它的第二次的时候我们进行线程终止,是不是可以减少CPU的压力了呢?写份代码跑一下看看
#include "pch.h"
#include
#include
#include
#include
#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 = %d\nhProcess = %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%p\n", 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:%s\n", lpFileName);
#endif
        TlsSetValue(tlsIndex, (LPVOID)1);
    }
    else
    {
        // 不是第一次运行,终止线程
#ifdef _DEBUG
        DBGMGEBOX("终止\nlpFileName:%s\n", 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占比显著下降


image-20240810205144922.png (14.8 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传



image-20240810205250729.png (23.48 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

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


image-20240810215436792.png (67.83 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

发现没有被调用。
翻阅了下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


image-20240810230358085.png (63.5 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传



image-20240810230525262.png (65.9 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

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


image-20240812004624693.png (16.44 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

因此,采用方式二,只要对所有WorkingService.exe进程进行注入,Hook CreateFileA函数即可在正常程序功能执行的情况下,降低CPU能耗,写一个程序测试一下
int main() {
    while (1)
    {
        int Num = 0;
        ve  = GetPidByProcName("WorkingService.exe");
        if (!ve.empty())
        {
            printf("There are currently %d processes\n", 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 Injected\n",i);
                    continue;
                }
            }
        }
        else
        {
            printf("Wating WorkingService.exe ...\n");
        }
        system("cls");
        //Sleep(200);
    }
    return 0;
}
这里使用Vector去接受所有WorkingService.exe进程的pid,然后用Map去映射一个bool值来判断是否这个进程被注入过,程序运行起来发现CPU能耗还是100%,但是程序告诉我注入成功。???疑惑住了


image-20240812014325913.png (81.62 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

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


image-20240812015018472.png (31.54 KB, 下载次数: 0)
下载附件
2024-8-13 20:01 上传

豁然开朗!第一个钩子勾住了NtProtectVirtualMemory函数,而我们注入模块的时候就会调用这个内核函数去修改内存权限,第二个钩子勾住了DbgUiRemoteBreakin函数,我也不知道这个函数是干什么的,问一下GPT得知
DbgUiRemoteBreakin函数是一个Windows API函数,它的作用是向指定的进程发送一个远程调试器的断点请求。当这个函数被调用时,目标进程会触发一个断点异常(通常是访问违规异常),如果目标进程已经有一个调试器附加,那么调试器就会捕获这个异常并进行处理。
这个函数通常用于以下情况:
[ol]
  • 附加调试器:如果你想要附加一个调试器到一个正在运行的进程,你可以调用DbgUiRemoteBreakin来使进程触发断点,然后使用调试器来附加到该进程。
  • 检测调试:如果一个进程检测到自己被调试,它可以调用DbgUiRemoteBreakin来尝试断开任何远程调试器的连接。
    [/ol]
    看来是能附加调试的,勾住这个来反调试,哇哦,不错的方式。但是我们就先不管它了,先成功注入dll再说。所以现在看来还得先还原NtProtectVirtualMemory函数取消钩子,再调用注入函数,修改一下代码跑一下发现日志文件不能循环写入,应该是线程都退出之后,进程并未停止,猜测是线程函数里有计数的东西,到某个值之后会通知进程结束,从而让守护进程再生工作进程,于是我改了一下hook代码,让线程终止16次后终止进程,从而实现日志文件循环再生。
    效果图:


    image-20240812031842620.png (18.02 KB, 下载次数: 1)
    下载附件
    2024-8-13 20:01 上传



    image-20240812033834444.png (198.74 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    题目二的结果应该就是这样了,能保持写入日志文件功能的同时不破坏工作进程循环启动的机制,大功告成!!!
    附一份exe源码和一份dll源码
    //main.cpp
    #include
    #include
    #include
    #include
    #include
    char dllPath[] = R"(C:\Users\15386\Desktop\temp\hookDll.dll)";
    std::vectorve;
    std::mapmp;
    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 GetPidByProcName(const char* processName) {
        HANDLE hProcessSnap = INVALID_HANDLE_VALUE;
        PROCESSENTRY32 pe32 = { 0 };
        std::vector 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 process\n");
            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 processes\n", 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
    #include
    #include
    #include
    #include
    #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 = %d\nhProcess = %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%p\n", 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
    (三)思路分享
    接着是第三题,要求是WorkingService.exe的主体功能为写入信息。在没有源码的情况下提取这个部分,自行编写一个性能更佳的服务,完全替代WorkingService.exe写入同样的密文信息,且运行时平均占用CPU时间低于5%。(2分)
    根据之前分析,现在一直的程序功能主要体现在,创建守护进程启动->启动工作进程->创建十六个线程->每个线程创建一个日志文件->向文件写入密文->进程退出->守护进程循环创建工作进程,大致功能应该就是这些,而现在要解决的问题就是日志文件名和日志密文是怎么产生的。
    在WriteFile下个断点看一眼密文的大致模样。


    image-20240812044203214.png (38.3 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

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


    image-20240812045110783.png (73.33 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传



    image-20240812045242653.png (39.55 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    好熟悉的字符串,这不正是初赛题的明文嘛,这个明显就是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编码规则,如果码表和明文都一致的话,那密文就只能有一个,说明每个线程的码表和明文至少有一个不一致。那现在只能继续分析他的加密方式,在现有的码表处下断追踪到加密函数


    image-20240812073322391.png (162.29 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    在内存窗口发现传入的第一个参数是一个字符串地址->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还原得到明文也进行了对比,发现了一个惊喜。


    image-20240812080716501.png (13.86 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传



    image-20240812080739360.png (14.17 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    这两个明文的有效字符组合起来刚好是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加密,对这三个关键地方下断点调试


    image-20240812095555791.png (157.75 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    {
    0x15, 0x17, 0x02, 0x15, 0x1E, 0x1B, 0x13, 0x1F, 0x10, 0x0F, 0x19, 0x03, 0x15, 0x17, 0x18
    };
    可以看到,第一个参数即为之前分析的十六进制数组的指针,做一层栈回溯找明文替换方式


    image-20240812100645901.png (145.28 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

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


    image-20240812101030609.png (154.01 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传



    image-20240812101825299.png (89.81 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    明文进行了一个字节一个字节的写入,仔细看这段VM给的混淆汇编代码, 其实就是一个
    mov r8, qword ptr ds:[rdi]
    mov r9b, byte ptr ds:[rdi+0x8]
    mov byte ptr ds:[r8], r9b
    那就意味着我们要追踪[RDI]看看是怎么来的,由于这段代码已经被VM了,控制流混淆导致根本没法手动追踪,用xdbg自动追踪功能打印日志看看


    image-20240813181835965.png (86.27 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    足足有30000行汇编


    image-20240813181952050.png (98.05 KB, 下载次数: 0)
    下载附件
    2024-8-13 20:01 上传

    往上回溯第一层发现[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变表加密。

    进程, 线程

  • wqstudyy   

    学习了,楼主分析的好清楚啊,不过除了xdbg,其他的工具能详细说下嘛,纯小白,不大懂中间的有些步骤,想自己复现下
    msmvc   

    在论坛里也算是顶流了
    chenyong2020   

    厉害了楼主,虽然看不懂,还是感谢你的分享。
    xhwlkj   

    学习了学习了
    Xiaf   

    收藏学习了
    jjx270288   

    学习了学习了
    Xiaomihu39   

    学习了,学习了
    binghe01   

    我只能说牛逼两个字
    hdbb   

    楼主完成这篇文章用了多长时间啊?
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部