决赛题目就是一个占用率很高的服务进程,让我们分析解决,前几天都有事,最后一天 all in 写完的。
腾讯游戏安全大赛决赛题解
参赛人员信息
略
结束进程
常规思路就是找进程匹配名字直接杀,但是发现会再生,用火绒剑分析了一下,发现有两个进程,应该是相互守护关系。并且 exploror.exe 进程一直在产生新的 working service 进程。猜测应该是运行了程序之后对这个进程下了钩子,于是火绒剑扫描一下钩子:
上下图对比下图是没运行之前扫描的结果,可以发现对 ZwProtectVirtualMemory 下了钩子,和之前一样,先把这个钩子修复,然后再杀掉所有进程名为 WorkingService 的进程即可。
但是之后又发现一些很奇怪的事情,有时候可以完全杀掉,有时候杀不掉,究其原因,原来它对很多进程都进行了 hook 尝试,有次我打开了任务管理器没有考虑到就又复活了,因此选择遍历进程的这个模块,识别钩子并去掉。
去掉之后再循环寻找进程杀掉即可。
代码实现:
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
| #include<windows.h> #include<iostream> #include<time.h> #include<stdlib.h> #include<TlHelp32.h> #define EXP L"explorer" #define PROCESSNAME L"WorkingService" DWORD old; SIZE_T written;
void UNHOOK() { DWORD ProcessId=0; BYTE INS[] = { 0x4C,0x8B,0xD1,0xB8,0x50 },buf; HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32; pe32 = { sizeof(pe32) }; BOOL ret = Process32First(hSnap, &pe32); while (ret) { if (TRUE) { ProcessId=pe32.th32ProcessID; HANDLE ths = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessId); if (!ths) { ret = Process32Next(hSnap, &pe32); continue; } MODULEENTRY32 me; me.dwSize = sizeof(me); UINT64 addr = 0; if (Module32First(ths, &me)) { do { if (addr = (UINT64)GetProcAddress(me.hModule, "ZwProtectVirtualMemory")) { break; } } while (Module32Next(ths, &me)); } CloseHandle(ths); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); if (!hProcess||addr==0) { ret = Process32Next(hSnap, &pe32); continue; } ReadProcessMemory(hProcess, (void *)addr, &buf, 1, &written); if (buf == 0xE9) { VirtualProtectEx(hProcess, (void*)addr, 0x5, PAGE_EXECUTE_READWRITE, &old); WriteProcessMemory(hProcess, (void*)addr, INS, 0x5, &written); printf("Process %d,hook addr:%p\n",ProcessId, addr); printf("written:%d\n", written); VirtualProtectEx(hProcess, (void*)addr, 0x5, old, &old); } CloseHandle(hProcess); } ret = Process32Next(hSnap, &pe32); } }
DWORD FindProcess() { HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32; pe32 = { sizeof(pe32) }; BOOL ret = Process32First(hSnap, &pe32); while (ret) { if (!wcsncmp(pe32.szExeFile, PROCESSNAME, lstrlenW(PROCESSNAME))) { printf("Find WorkingService.exe Process %d\n", pe32.th32ProcessID); return pe32.th32ProcessID; } ret = Process32Next(hSnap, &pe32); } return 0; } int main() { DWORD ProcessId = 0; UNHOOK(); do { ProcessId = FindProcess(); printf("pid:%p\n", ProcessId); if (!ProcessId) { break; } HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); printf("hProcess:%p\n", hProcess); if (!hProcess) { break; } TerminateProcess(hProcess, 0); } while (1); printf("Terminate OK\n"); system("pause"); }
|
运行结果:
直接使用main1.cpp编译出来的代码运行即可结束。
但是有一定概率失败,不知道是什么问题,但是有时候可以完美运行,因此这里我使用了另一种方法:检测到被注入的进程,然后直接结束它们,之后马上杀掉服务进程。
实现起来也非常简单:
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
| #include<windows.h> #include<iostream> #include<time.h> #include<stdlib.h> #include<TlHelp32.h> #define EXP L"explorer" #define PROCESSNAME L"WorkingService" #define DLLNAME L"WorkingServiceDll.dll" DWORD old; SIZE_T written;
void UNHOOK() { DWORD ProcessId = 0; BYTE INS[] = { 0x4C,0x8B,0xD1,0xB8,0x50 }, buf; HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32; pe32 = { sizeof(pe32) }; BOOL ret = Process32First(hSnap, &pe32); while (ret) { if (TRUE) { ProcessId = pe32.th32ProcessID; HANDLE ths = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessId); if (!ths) { ret = Process32Next(hSnap, &pe32); continue; } MODULEENTRY32 me; me.dwSize = sizeof(me); UINT64 addr = 0; if (Module32First(ths, &me)) { do { if (!memcmp(me.szModule,DLLNAME,sizeof(DLLNAME))) { printf("terminate pid=%d\n", ProcessId); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); TerminateProcess(hProcess, 0); CloseHandle(hProcess); } } while (Module32Next(ths, &me)); }
CloseHandle(ths); } ret = Process32Next(hSnap, &pe32); } }
DWORD FindProcess() { HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32; pe32 = { sizeof(pe32) }; BOOL ret = Process32First(hSnap, &pe32); while (ret) { if (!wcsncmp(pe32.szExeFile, PROCESSNAME, lstrlenW(PROCESSNAME))) { printf("Find WorkingService.exe Process %d\n", pe32.th32ProcessID); return pe32.th32ProcessID; } ret = Process32Next(hSnap, &pe32); } return 0; } int main() { DWORD ProcessId = 0; UNHOOK(); system("taskkill /f /im WorkingService.exe"); printf("Terminate OK\n"); system("pause"); }
|
此方式编译main1-2.cpp文件的代码运行即可。
降低CPU占用
通过火绒剑分析行为,发现一秒内就会有大量的创建文件请求。
因此想到使用 CE 去查找到 contest 字符串,找到创建文件的指令,hook出来每次创建文件之后sleep即可。
但是由于pid一直变化,因此尝试先卸载钩子,保证pid不变化,使用 CE 调试,找到内存访问的指令。
注入的时候同样发现进程存在钩子,因此也先在注入器当中手动把钩子去掉。
找到读 contest 内存的代码段,把代码段拷贝到一个自己分配的内存上面,赋予可执行权限,不过在指令中间可以加入一个 sleep函数,让它得以休息。不过这里遇到一个问题,就是32位偏移貌似跳转不了,因此使用
1 2
| mov rax,0x0000000000000000 call rax
|
的方式去调用休眠函数,这里的八字节整数直接填sleep函数的虚拟地址即可。
注入器代码:
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
| #include<windows.h> #include<iostream> #include<time.h> #include<stdlib.h> #include<TlHelp32.h> DWORD old; SIZE_T written;
void InjectModule(DWORD ProcessId, const char* szPath) { HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); printf("进程句柄:%p\n", hProcess); LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); SIZE_T dwWriteLength = 0; WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength); HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL); WaitForSingleObject(hThread, -1); VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE); CloseHandle(hProcess); CloseHandle(hThread); } void UNHOOK(DWORD ProcessId) { BYTE INS[] = { 0x4C,0x8B,0xD1,0xB8,0x50 }; HANDLE ths = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessId); MODULEENTRY32 me; me.dwSize = sizeof(me); UINT64 addr = 0; if (Module32First(ths, &me)) { do { if (addr = (UINT64)GetProcAddress(me.hModule, "ZwProtectVirtualMemory")) { printf("addr:%p\n", addr); break; }
} while (Module32Next(ths, &me)); } CloseHandle(ths); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); VirtualProtectEx(hProcess, (void*)addr, 0x5, PAGE_EXECUTE_READWRITE, &old); WriteProcessMemory(hProcess, (void*)addr, INS, 0x5, &written); printf("written:%d\n", written); VirtualProtectEx(hProcess, (void*)addr, 0x5, old, &old); CloseHandle(hProcess); } void FindProcess() { HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32; pe32 = { sizeof(pe32) }; BOOL ret = Process32First(hSnap, &pe32); while (ret) { if (!wcsncmp(pe32.szExeFile, L"WorkingService", 14)) { printf("Find WorkingService.exe Process %d\n", pe32.th32ProcessID); DWORD PID = pe32.th32ProcessID; printf("尝试去除钩子...\n"); UNHOOK(PID); printf("开始注入进程...\n"); InjectModule(PID, "T-Contest-final-2-dll.dll"); printf("注入完毕\n"); system("pause"); } ret = Process32Next(hSnap, &pe32); } } int main() { FindProcess(); }
|
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
| #include<Windows.h> #include<stdio.h> DWORD oldprot; BYTE code[] = { 0x8B,0x0A, 0x44,0x0F,0xB7,0x42,0x04, 0x44,0x0F,0xB6,0x4A,0x06, 0x89,0x08, 0x66,0x44,0x89,0x40,0x04, 0x44,0x88,0x48,0x06, 0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xFF,0xD0, 0xC3 }; void SSLEEP() { Sleep(500); } void HOOK() { UINT64 offset = 0x7C70,Base=(UINT64)GetModuleHandle(nullptr),offset2=0x19,InsLen=5; BYTE INS[] = {0xe9,0x00,0x00,0x00,0x00}; UINT64 PAGEADDR=(UINT64)VirtualAlloc((LPVOID)0x7FF6F8D90000, 0x200, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); printf("%p\n", PAGEADDR); *(DWORD*)(INS + 1) = PAGEADDR - (Base + offset) - 5;
*(UINT64*)(code + offset2) = (UINT64)SSLEEP;
memcpy((void*)PAGEADDR, code, sizeof(code));
VirtualProtect((void*)(Base + offset), InsLen, PAGE_EXECUTE_READWRITE, &oldprot); memcpy((void*)(Base + offset), INS, InsLen); VirtualProtect((void*)(Base + offset), InsLen, oldprot, &oldprot);
}
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: AllocConsole(); freopen("CONOUT$", "w", stdout);
HOOK(); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
|
内存利用率下降了很多。
使用main2.cpp和injector.cpp编译出exe文件,先执行main2取消被钩住的进程,再使用injector注入dllmain.cpp编译出的动态链接库,达到效果。
同样做的时候发现了不稳定的因素,有时候可以使用,有时候不能使用,因此采取第二种方式来解决。
同样要先把所有钩子卸掉,最好的办法还是重启那些已经被钩住的程序。
这部分程序与之前 main1-2.cpp 中的区别仅仅是少了 system("taskkill /f /im WorkingService.exe");
而已。
然后把上面的dll注入到两个进程中去,这一段代码没有变化。
通过火绒剑的事件分析可以发现,文件创建的频率明显下降很多(因为每次强制休息了500MS):
并且没有影响进程功能,CPU占用率也大大降低了。
实现相同功能
首先就要分析它写入了哪四个文件了,从火绒剑的分析来看,应该是有四个线程在不同的写,因为很容易注意到,四个文件几乎是同时打开的。
对进程进行内存dump,拖入ida分析,发现一串base64的表,于是在CE中找到主要编码的函数。
通过参数分析找到了内存的位置,通过仔细观察,发现和初赛的flag:catchmeifyoucan
十分相似。
经过分析,发现它们存在互补关系
通过某次虚拟机炸了之后,意外得到了两份落地的文件
经过base64解码发现,其中一个是 catchmeifyoucan
+ id,一个是不可见的字符,而且刚好等于上面不可见字符的补集。
本来并没有什么头绪,但是发现了一个细节,在火绒剑当中
可以发现 contestxxxx.txt 的 xxxx 就是线程的 id,
大概分析了一下,应该是有两串content,根据线程id,选择到底是 ASCII 字符还是非ASCII 字符,然后base64后+TID。
经过多次测试之后发现,只会出现两种:两种互补,就像这样子:
根据逻辑分析,是计算TID的总和,然后根据位数,选择这一位到底是选择 ASCII 还是非ASCII字符。
根据之前进程的行为写出了以下的程序,这里需要CREATE_ALWAYS和FILE_FLAG_DELETE_ON_CLOSE两个标志位,以便于达到和之前进程一样的行为。
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
| #include <stdio.h> #include <windows.h> char flag1[] = "catchmeifyoucan"; char flag2[] = "\x15\x17\x02\x15\x1e\x1b\x13\x1f\x10\x0f\x19\x03\x15\x17\x18"; DWORD written; const char* BASE64_CHARS = "ABCDEFGHIJKLMNOPwxyz0123456789+/ghijklmnopqrstuvQRSTUVWXYZabcdef"; DWORD all = 0; void base64_encode(const char* input, char* output) { int i, j; unsigned char buf[3]; int num_bits = 0, bits; size_t len = strlen(input);
for (i = 0, j = 0; i < len; i++) { buf[num_bits++] = input[i]; if (num_bits == 3) { bits = (buf[0] << 16) | (buf[1] << 8) | buf[2]; output[j++] = BASE64_CHARS[(bits >> 18) & 0x3F]; output[j++] = BASE64_CHARS[(bits >> 12) & 0x3F]; output[j++] = BASE64_CHARS[(bits >> 6) & 0x3F]; output[j++] = BASE64_CHARS[bits & 0x3F]; num_bits = 0; } }
if (num_bits > 0) { bits = (buf[0] << 16) | ((num_bits == 2) ? (buf[1] << 8) : 0); output[j++] = BASE64_CHARS[(bits >> 18) & 0x3F]; output[j++] = BASE64_CHARS[(bits >> 12) & 0x3F]; output[j++] = (num_bits == 1) ? '=' : BASE64_CHARS[(bits >> 6) & 0x3F]; output[j++] = '='; }
output[j] = '\0'; } void process(LPVOID f) { DWORD TID = GetCurrentThreadId(); HANDLE hFile = NULL; char filename[20]; char content[50]; char c[50]; char tmp[50]; sprintf(filename, "contest%d.txt", TID); while (!all); DWORD x = all; printf("%d\n", *(int*)f); for (int i = 0; i < 15; i++) { if ((*(int*)f % 2)^(x&1)) { c[i] = flag1[i]; } else { c[i] = flag2[i]; } x >>= 1; } c[15] = 0; base64_encode(c, tmp); sprintf(content, "%s%d", tmp, TID); do { hFile = CreateFileA(filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, NULL); if (hFile != (HANDLE) - 1)WriteFile(hFile, content, strlen(content), &written, 0); Sleep(1000); } while (1); CloseHandle(hFile); } int main() { HANDLE hThread[4]; DWORD ans=0; int k[] = { 0,1,2,3 }; for (int i = 0; i < 4; i++) { DWORD dwThreadId; hThread[i] = CreateThread( NULL, 0, (LPTHREAD_START_ROUTINE)process, &k[i], 0, &dwThreadId ); ans += dwThreadId; } all = ans&0xffff; for (int i = 0; i < 4; i++) { WaitForSingleObject(hThread[i], -1); } return 0; }
|
在附件中,编译 main3.cpp,运行可以直接达到效果,并且拥有很低的CPU占用率。