最近一直在做往年腾讯游戏安全PC端的复现,但是2023年决赛的题做到第三问就开始有点做不动了,想在网上找找题解但是并没有找到,于是便想分享一下自己的解题经验和第三问的思路。鉴于知识的局限性,文中可能存在疏漏或不足之处,如果发现任何错误或不准确之处,请不吝赐教。
题目
(一)解题过程 第一问让我们杀死进程,可以尝试根据windowsAPI提供的TerminateProcess函数去停止进程,写份代码测试下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 ; }
以管理员身份启动测试
启动前:
启动后
(二)解题过程 先DIE查壳发现有VMP,只能动调入手,先观察程序功能。程序启动后会有两个进程启动,并且其中之一占用CPU性能较高;程序目录会创建十六个文件,并会做重复的删除和重新创建的操作;观察任务管理器发现进程会自动重启,手动杀死进程后就会重生一个新的进程。
拖进xdbg里观察符号,发现引入了ShellExcuteA这个函数,正好是启动进程函数,对这个函数下断点,发现确实断了下来,观察传参窗口。
第六个参数不同,对应的是lpParameters(传入进程的参数),一个是working
,一个是daemon restart
根据这个在控制台上运行加上参数working,发现只有一个工作进程,CPU占用100%,同理加上daemon restart,只有一个守护进程CPU占用不到1%。
调试参数为working的进程,观察线程发现有16个线程运行同一个函数,刚好对应16个txt日志文件的输出!!!
于是进入线程入口开始分析,对所有可能的写入文件的windowsAPI下断,发现都没有断下来,于是向CreateFile等API下断,
所有线程在CreateFileA上成功断下,观察传参确定这是对日志文件的操作函数
有了这个依据,能确认的就是16个线程会循环通过CreateFileA打开文件,这可能是占用CPU的一个主要因素。但是跟之前运行的结果不同,调试发现,写入文件的API一直没有被调用,而正常运行的结果是,十六个文件会循环的做一个过程,文件被创建->文件被写入->文件被删除->新文件创建…。我通过加working参数运行,只会有一个循环过程,即不会有新文件诞生,文件内容也不会被修改。所以,十六个线程循环CreateFileA打开文件过程是无用功,只会徒增CPU负担!那么我们是不是可以HOOK CreateFileA这个函数,然后每个线程调用它的第二次的时候我们进行线程终止,是不是可以减少CPU的压力了呢?写份代码跑一下看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 #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 { \ \ wsprintfA(out, fmt, __VA_ARGS__); \ MessageBoxA(NULL, out, "提示" , MB_OK); \ } while(0) char out[100 ];DWORD tlsIndex; 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 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 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 ) { LPVOID tlsValue = TlsGetValue(tlsIndex); if (tlsValue == NULL ) { #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()); tlsIndex = TlsAlloc(); 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); DetourTransactionCommit(); break ; } return TRUE; }
注入进去查看CPU,与不注入的CPU情况对比如图,进程CPU占比显著下降
现在我们能够确定,是这十六个线程循环打开文件(也可能做了其他的事情)占用CPU大量资源。但是直接线程退出的方式也会导致日志文件被删除,题目意思是说在保持WorkingService.exe正常运行不崩溃、主体功能无损的前提下,使之占用CPU下降到平均5%以下,WorkingService.exe的主体功能为写入信息。在没有源码的情况下提取这个部分,自行编写一个性能更佳的服务 ,现在直接退出线程的方式会导致日志文件删除过快,虽然写入了信息,但是根本没法看啊,所以我们为了日志文件更方便的浏览,应该在WriteFile之后保存文件。那我们得先hook一下删除文件的函数,看一下在哪调用的,然后选择过滤掉这个函数从而保存日志文件。
尝试hook一下DeleteFileA这个函数
发现没有被调用。
翻阅了下CreateFileA的文档,找到dwFlagsAndAttributes这个参数,发现这个参数包含了FILE_FLAG_DELETE_ON_CLOSE
这个属性,意思是文件将在关闭所有句柄后立即删除
1 2 3 4 5 6 7 8 9 HANDLE CreateFileA ( LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ) ;
FILE_FLAG_DELETE_ON_CLOSE 0x04000000
文件将在关闭所有句柄后立即删除,其中包括指定的句柄和任何其他打开或重复的句柄。
因此,只要我们改了这个属性,日志文件就不会再被删除,写代码测试下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 HANDLE WINAPI HookCreateFileA ( LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ) { LPVOID tlsValue = TlsGetValue(tlsIndex); if ((DWORD_PTR)tlsValue <= 0x10 ) { #ifdef _KDEBUG DBGMGEBOX("放行\nlpFileName:%s\n" , lpFileName); #endif dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; TlsSetValue(tlsIndex, (LPVOID)((DWORD_PTR)tlsValue + 1 )); } else { #ifdef _KDEBUG DBGMGEBOX("终止\nlpFileName:%s\n" , lpFileName); #endif ExitThread(0 ); } return TrueCreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }
测试得到文件确实没有被删除,并且密文也成功写入文件,CPU占用也少于5%(峰值3%,最后趋近于0)
到这工作进程已经差不多分析完了,但是别忘了还有一个守护进程,守护进程应该也算是正常功能的一部分,那我们现在要开始结合守护进程分析。根据之前分析得到工作进程启动是通过守护进程调用ShellExcuteExA来传入working参数实现的,而这个后面启动的进程才是我们应该去注入的。而且因为程序有循环自生自灭的一个功能,我们可能要考虑循环注入实现对所有工作进程的hook。首先考虑第一个问题,如何去注入ShellExcuteExA启动的工作进程,第一个方式是Hook ShellExcuteExA这个函数,在其启动前获取ShellExcuteExA返回值->进程句柄,然后通过进程句柄进行注入;第二个方式是编写一个辅助程序,循环对每一个WorkingService.exe进程进行注入,这个要考虑到hook是否会影响守护进程的正常功能。我想先试一下第二个方式,较为简单一点,先测试一下守护进程是否会调用CreateFileA函数,如果会的话,我们要进行一下过滤操作。Hook测试了下,守护进程先启动,然后是工作进程,但是迟迟没有调试弹窗出现,说明守护进程没有调用CreateFileA。
因此,采用方式二,只要对所有WorkingService.exe进程进行注入,Hook CreateFileA函数即可在正常程序功能执行的情况下,降低CPU能耗,写一个程序测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int main () { while (1 ) { int Num = 0 ; ve = GetPidByProcName("WorkingService.exe" ); if (!ve.empty()) { printf ("There are currently %d processes\n" , ve.size()); 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" ); } return 0 ; }
这里使用Vector去接受所有WorkingService.exe进程的pid,然后用Map去映射一个bool值来判断是否这个进程被注入过,程序运行起来发现CPU能耗还是100%,但是程序告诉我注入成功。???疑惑住了
因为之前注入的时候可以成功降低CPU能耗的,我很确信我的dll是没问题的,所以问题可能就是注入的方式了。考虑到之前注入使用工具注入,在进程启动的同时注入dll,而现在注入是在程序完全跑起来之后,所以怀疑是被下了钩子破坏了注入。在ARK工具里扫一下钩子看一眼。
豁然开朗!第一个钩子勾住了NtProtectVirtualMemory函数,而我们注入模块的时候就会调用这个内核函数去修改内存权限,第二个钩子勾住了DbgUiRemoteBreakin函数,我也不知道这个函数是干什么的,问一下GPT得知
DbgUiRemoteBreakin
函数是一个Windows API函数,它的作用是向指定的进程发送一个远程调试器的断点请求。当这个函数被调用时,目标进程会触发一个断点异常(通常是访问违规异常),如果目标进程已经有一个调试器附加,那么调试器就会捕获这个异常并进行处理。
这个函数通常用于以下情况:
附加调试器 :如果你想要附加一个调试器到一个正在运行的进程,你可以调用DbgUiRemoteBreakin
来使进程触发断点,然后使用调试器来附加到该进程。
检测调试 :如果一个进程检测到自己被调试,它可以调用DbgUiRemoteBreakin
来尝试断开任何远程调试器的连接。
看来是能附加调试的,勾住这个来反调试,哇哦,不错的方式。但是我们就先不管它了,先成功注入dll再说。所以现在看来还得先还原NtProtectVirtualMemory函数取消钩子,再调用注入函数,修改一下代码跑一下发现日志文件不能循环写入,应该是线程都退出之后,进程并未停止,猜测是线程函数里有计数的东西,到某个值之后会通知进程结束,从而让守护进程再生工作进程,于是我改了一下hook代码,让线程终止16次后终止进程,从而实现日志文件循环再生。
效果图:
题目二的结果应该就是这样了,能保持写入日志文件功能的同时不破坏工作进程循环启动的机制,大功告成!!!
附一份exe源码和一份dll源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 #include <windows.h> #include <tlhelp32.h> #include <stdio.h> #include <vector> #include <map> char dllPath[] = R"(C:\Users\15386\Desktop\temp\hookDll.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 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" ); if (!ve.empty()) { printf ("There are currently %d processes\n" , ve.size()); 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 ; if (RemoveHook(hProcess, (PVOID)unHookAddr, originBytes)) { 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 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 #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 { \ \ wsprintfA(out, fmt, __VA_ARGS__); \ MessageBoxA(NULL, out, "提示" , MB_OK); \ } while(0) char out[100 ];DWORD tlsIndex; 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 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 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 ) { LPVOID tlsValue = TlsGetValue(tlsIndex); if ((DWORD_PTR)tlsValue <= 0x10 ) { #ifdef _KDEBUG DBGMGEBOX("放行\nlpFileName:%s\n" , lpFileName); #endif dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; TlsSetValue(tlsIndex, (LPVOID)((DWORD_PTR)tlsValue + 1 )); } else { static int Num = 0 ; #ifdef _KDEBUG DBGMGEBOX("终止\nlpFileName:%s\n" , 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()); tlsIndex = TlsAlloc(); 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); DetourTransactionCommit(); break ; } return TRUE; }
(三)思路分享 接着是第三题,要求是WorkingService.exe的主体功能为写入信息。在没有源码的情况下提取这个部分,自行编写一个性能更佳的服务,完全替代WorkingService.exe写入同样的密文信息,且运行时平均占用CPU时间低于5%。(2分)
根据之前分析,现在一直的程序功能主要体现在,创建守护进程启动->启动工作进程->创建十六个线程->每个线程创建一个日志文件->向文件写入密文->进程退出->守护进程循环创建工作进程,大致功能应该就是这些,而现在要解决的问题就是日志文件名和日志密文是怎么产生的。
在WriteFile下个断点看一眼密文的大致模样。
猜测有可能是base64加密,因为之前查看字符串有看到base64的码表,还是一个魔改码表,直接丢cyberchef试一下解密
好熟悉的字符串,这不正是初赛题的明文嘛,这个明显就是base64加密明文了,但是还有一个疑问,就是不同的文件会打印出不同的密文,把十六个文件的密文提取出来对比一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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编码规则,如果码表和明文都一致的话,那密文就只能有一个,说明每个线程的码表和明文至少有一个不一致。那现在只能继续分析他的加密方式,在现有的码表处下断追踪到加密函数
在内存窗口发现传入的第一个参数是一个字符串地址->c tc me an,貌似跟catchmeifyoucan明文很像,所以这是修改了明文,然后再通过正常的码表进行base64加密?提取出这个明文,试一下对着这个码表加密,对比运行后的密文就知道了,
1 2 3 4 5 6 明文:0x15 , 0x61 , 0x02 , 0x15 , 0x68 , 0x1B , 0x13 , 0x69 , 0x66 , 0x79 , 0x6F , 0x75 , 0x63 , 0x17 , 0x18 实际密文:F2ECF2g7EWlm+2 dV4R84 正常base64密文:F2ECF2g7EWlm+2 dV4R84 明文:0x63 , 0x17 , 0x74 , 0x63 , 0x1E , 0x6D , 0x65 , 0x1F , 0x10 , 0x0F , 0x19 , 0x03 , 0x15 , 0x61 , 0x6E 实际密文:4 R9U4RZt5xcwDRkDF2Fu 正常base64密文:4 R9U4RZt5xcwDRkDF2Fu
对比了多组,确信了码表是对的ABCDEFGHIJKLMNOPwxyz0123456789+/ghijklmnopqrstuvQRSTUVWXYZabcdef
,但是调试这个加密的过程中发现,一个线程至少会有两组明文,上面就是出自同一个线程。并且这个函数加密后的密文跟实际文件密文不同,下面是提取的两组密文和实际文件密文
1 2 3 4 文件密文:4 WFU4Wht52lm+2 dV4WFu 密文1 :Fx8C4Wg7ERcwDWdV4WE4 密文2 :4 WFUFxZt52lm+xkDFx9u
现在还有很多问题在里面,首先是明文为什么一个线程有两组,加密得到的两组密文跟文件密文有什么关系?两组密文实在是太奇怪了,我试着把他们联系起来,于是我将两组密文用base64还原得到明文也进行了对比,发现了一个惊喜。
这两个明文的有效字符组合起来刚好是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
貌似没什么关系,考虑到多个文件的写入密文不同,继续提取其他线程的密文
1 2 3 4 文件密文:Fx8CFxY7ERcwDRkDFx84 密文1 :Fx8C4Wg7ERcwDWdV4WE4 密文2 :4 WFUFxZt52lm+xkDFx9u
拿这个线程的文件密文拿去解密,得到十六进制数组:0x15,0x17,0x02,0x15,0x1e,0x1b,0x13,0x1f,0x10,0x0f,0x19,0x03,0x15,0x17,0x18 ,刚好由34288线程的两组非可视字符十六进制组成。根据这个思路我把之前提取的密文进行了解密,发现线程之间的非字符不一定是成对的,但是非字符的十六进制跟所在索引是一定的,也就是说所有非可视字符的十六进制都是从上面这个数组里取,我试图多运行几遍进程反驳我这个猜想,因为这个十六进制数组并没有什么规律,但是运行结果都显示这个数组是一定的。
所以现在可以确定的是,明文catchmeifyoucan会和这个数组以某种方式进行混合,最后组成真正的明文进行base64换表加密得到文件密文。现在就需要继续研究这个混合的方式,因为进程会启动十六个线程来写日志,而且在之前的观察中看到在CreateFileA函数之前,密文就已经储存在寄存器中,所以是CreateThread和CreateFileA函数之间进行了明文混合和base64加密,对这三个关键地方下断点调试
1 2 3 { 0x15, 0x17, 0x02, 0x15, 0x1E, 0x1B, 0x13, 0x1F, 0x10, 0x0F, 0x19, 0x03, 0x15, 0x17, 0x18 };
可以看到,第一个参数即为之前分析的十六进制数组的指针,做一层栈回溯找明文混合方式
对base64的第一个参数位置(明文指针)下硬件访问断点,找到写入明文内存的位置
明文进行了一个字节一个字节的写入,仔细看这段VM给的混淆汇编代码, 其实就是一个
1 2 3 mov r8, qword ptr ds:[rdi] mov r9b, byte ptr ds:[rdi+0x8] mov byte ptr ds:[r8], r9b
那就意味着我们要追踪[RDI]看看是怎么来的,由于这段代码已经被VM了,控制流混淆导致根本没法手动追踪,用xdbg自动追踪功能打印日志看看
足足有30000行汇编
往上回溯第一层发现[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变表加密。