前言
最近一直在做往年腾讯游戏安全PC端的复现,但是2023年决赛的题做到第三问就开始有点做不动了,想在网上找找题解但是并没有找到,于是便想分享一下自己的解题经验和第三问的思路。鉴于知识的局限性,文中可能存在疏漏或不足之处,如果发现任何错误或不准确之处,请不吝赐教。
题目
(一)解题过程
第一问让我们杀死进程,可以尝试根据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;
}
以管理员身份启动测试
启动前:
启动后
(二)解题过程
先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的压力了呢?写份代码跑一下看看
#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 = %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占比显著下降
现在我们能够确定,是这十六个线程循环打开文件(也可能做了其他的事情)占用CPU大量资源。但是直接线程退出的方式也会导致日志文件被删除,题目意思是说在保持WorkingService.exe正常运行不崩溃、主体功能无损的前提下,使之占用CPU下降到平均5%以下,WorkingService.exe的主体功能为写入信息。在没有源码的情况下提取这个部分,自行编写一个性能更佳的服务,现在直接退出线程的方式会导致日志文件删除过快,虽然写入了信息,但是根本没法看啊,所以我们为了日志文件更方便的浏览,应该在WriteFile之后保存文件。那我们得先hook一下删除文件的函数,看一下在哪调用的,然后选择过滤掉这个函数从而保存日志文件。
尝试hook一下DeleteFileA这个函数
发现没有被调用。
翻阅了下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:%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能耗,写一个程序测试一下
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%,但是程序告诉我注入成功。???疑惑住了
因为之前注入的时候可以成功降低CPU能耗的,我很确信我的dll是没问题的,所以问题可能就是注入的方式了。考虑到之前注入使用工具注入,在进程启动的同时注入dll,而现在注入是在程序完全跑起来之后,所以怀疑是被下了钩子破坏了注入。在ARK工具里扫一下钩子看一眼。
豁然开朗!第一个钩子勾住了NtProtectVirtualMemory函数,而我们注入模块的时候就会调用这个内核函数去修改内存权限,第二个钩子勾住了DbgUiRemoteBreakin函数,我也不知道这个函数是干什么的,问一下GPT得知
DbgUiRemoteBreakin
函数是一个Windows API函数,它的作用是向指定的进程发送一个远程调试器的断点请求。当这个函数被调用时,目标进程会触发一个断点异常(通常是访问违规异常),如果目标进程已经有一个调试器附加,那么调试器就会捕获这个异常并进行处理。
这个函数通常用于以下情况:
- 附加调试器:如果你想要附加一个调试器到一个正在运行的进程,你可以调用
DbgUiRemoteBreakin
来使进程触发断点,然后使用调试器来附加到该进程。
- 检测调试:如果一个进程检测到自己被调试,它可以调用
DbgUiRemoteBreakin
来尝试断开任何远程调试器的连接。
看来是能附加调试的,勾住这个来反调试,哇哦,不错的方式。但是我们就先不管它了,先成功注入dll再说。所以现在看来还得先还原NtProtectVirtualMemory函数取消钩子,再调用注入函数,修改一下代码跑一下发现日志文件不能循环写入,应该是线程都退出之后,进程并未停止,猜测是线程函数里有计数的东西,到某个值之后会通知进程结束,从而让守护进程再生工作进程,于是我改了一下hook代码,让线程终止16次后终止进程,从而实现日志文件循环再生。
效果图:
题目二的结果应该就是这样了,能保持写入日志文件功能的同时不破坏工作进程循环启动的机制,大功告成!!!
附一份exe源码和一份dll源码
//main.cpp
#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");//获取所有进程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 <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 = %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 <= 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());
//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下个断点看一眼密文的大致模样。
猜测有可能是base64加密,因为之前查看字符串有看到base64的码表,还是一个魔改码表,直接丢cyberchef试一下解密
好熟悉的字符串,这不正是初赛题的明文嘛,这个明显就是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编码规则,如果码表和明文都一致的话,那密文就只能有一个,说明每个线程的码表和明文至少有一个不一致。那现在只能继续分析他的加密方式,在现有的码表处下断追踪到加密函数
在内存窗口发现传入的第一个参数是一个字符串地址->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还原得到明文也进行了对比,发现了一个惊喜。
这两个明文的有效字符组合起来刚好是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加密,对这三个关键地方下断点调试
{
0x15, 0x17, 0x02, 0x15, 0x1E, 0x1B, 0x13, 0x1F, 0x10, 0x0F, 0x19, 0x03, 0x15, 0x17, 0x18
};
可以看到,第一个参数即为之前分析的十六进制数组的指针,做一层栈回溯找明文替换方式
对base64的第一个参数位置(明文指针)下硬件访问断点,找到写入明文内存的位置
明文进行了一个字节一个字节的写入,仔细看这段VM给的混淆汇编代码, 其实就是一个
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变表加密。