【Reverse】2023腾讯游戏安全决赛WriteUp (未完成)
题解报告
脱壳
首先,程序加了一个VMP3.0.0-VMP3.5.0的壳,要对程序进行脱壳,我选用的是断点在CreateToolhelp32Snapshot这个api的后几位,程序到达这里之后再对vmp0段下内存执行断点。
再次f9即可到达oep。
程序执行流分析1
程序oep被虚拟化保护了,因此想要静态分析依然很困难,可以通过x64dbg的dump工具dump出来,可以看到一些敏感字符,由于程序被虚拟化保护了,我们只能通过这些数据的地址来定位函数地址。
首先对下方的error信息进行信息收集,可以很快的发现这个错误出自远程线程注入的代码中。那么其实可以知道程序中其实是带有一个远程线程注入的功能的。而对程序的进程信息进行分析,可以了解到程序将DLL注入到了taskmgr.exe和explorer.exe中,并且会反复的对程序进行重启。
杀死进程思路
通过以上的分析,可以得到该程序进程守护的原理,那么想要杀死进程则可以通过一个简单的方法即在杀死程序进程的同时一起杀死taskmgr.exe和explorer.exe。
示例代码
#include <stdio.h>
#include <windows.h>
#include "tchar.h"
#include <TlHelp32.h>
//通过进程快照获取PID
int* _GetProcessPID(LPCTSTR lpProcessName)
{
int Ret[10];
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("获取进程快照失败,请重试! Error:%d", ::GetLastError());
return Ret;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
int i = 0;
do {
if (!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret[i] = p32.th32ProcessID;
i++;
}
} while (::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
return Ret;
}
BOOL MyTerminate(int pid) {
int ret;
HANDLE hProcess;
if (pid > 32768 || pid <= 0) {
return 0;
}
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (INVALID_HANDLE_VALUE == hProcess) {
fprintf(stderr, "Error: Can not open specified process!\n");
fprintf(stderr, "Error code: %d\n", GetLastError());
return 0;
}
ret = TerminateProcess(hProcess, 0);
if (ret == FALSE) {
printf("Error: Can not terminate specified process!\n");
printf("Error code: %d\n", GetLastError());
}
CloseHandle(hProcess);
}
int main(int argc, char* argv[])
{
int* explorerProcessId;
int* WorkingServiceProcessId;
int* taskmgrProcessId;
//获取三个需要杀死的进程的pid
explorerProcessId = _GetProcessPID(L"explorer.exe");
WorkingServiceProcessId = _GetProcessPID(L"WorkingService.exe");
taskmgrProcessId = _GetProcessPID(L"taskmgr.exe");
//杀死所有名为WorkingService.exe的进程(已测试不会同时出现10个WorkingService.exe的进程,简化处理)
for(int i = 0;i < 10;i++){
printf("%d\n", WorkingServiceProcessId[i]);
MyTerminate(WorkingServiceProcessId[i]);
}
//杀死explorer和taskmgr
MyTerminate(explorerProcessId[0]);
MyTerminate(taskmgrProcessId[0]);
system("pause");
return 0;
}
程序执行流分析2
通过使用Windows SysinternalsSuite工具集中的api monitor,可以收集到各个WorkingService.exe的信息,通过正在运行的程序进行附加,可以得到调用过的API信息。可以看到的是,程序启动了两个不同的子进程,一个只有一个线程,另一个有16个线程。
通过追踪同一个线程的API调用,不难发现程序一直调用NtCreateFile,创建名为contest+threadid+.txt的文件,次数非常多。疑似写入死循环,重复创建文件并写入 ,大量的IO是导致程序CPU占用率高的主要原因。
由于程序主要工作是写文件,并不需要进行死循环,可以对程序进行patch跳出循环。
由于个人原因(去网鼎杯线下赛了)没有时间做程序的分析了,由于关键代码被虚拟化,应该需要使用unicorn进行模拟查看调用,很可惜没能写出来。但是大体可以使用DLL注入的方式对内存中的位置进行修改或者使用某些框架进行修改。
程序执行流分析3
再来到程序核心功能的分析。
首先对程序进行运行,马上就能看到的效果就是程序在目录下生成了多个txt文件,文本由于被占用的问题无法打开。那么如何获取到程序写入的内容呢,答案是可以通过调试器获取到。
在对程序进行调试的过程中,我们通过先前dump出来的脱壳后的程序可以发现到一些字符串,并且调试过程中发现字符串地址固定,程序要想写入文件,肯定需要经过读取文件名的过程,那么可以将硬件访问断点打在contest这个字符串上。即offset = 09C8
的位置。
具体过程如下:
- 按照脱壳的方法进入到oep
- 计算出offset = 09C8的位置
- 转到内存窗口并下硬件读写断点
- 启动进程,通过x64dbg附加调试进程
- 进入断点,单步执行观察堆栈
观察堆栈可以发现栈中有疑似密文的文本,通过火绒解除占用的方式发现这的确就是密文,但是后面还拼接上了当前线程的pid号
通过数据区中的表可以判断出是base64加密,可以通过b64进行解密操作。
可以发现输出的并不完整,通过黑盒的方式进行多次测试发现密文总是有一部分不完整,并且该部分总是满足规律,即无法正常显示的部分与0x76异或能得到明文即catchmeifyoucan
那么可以猜测程序核心操作:
- 启动两个进程并且通过远程线程注入的方式将保活DLL注入explorer.exe和taskmgr.exe
- 启动两个进行,其中一个用于重启进程(附加进程可以发现调用了Sleep函数,推测是等待10秒后重启另一个用于写入文件的进程),另一个用于循环创建与写入文件
- 对明文catchmeifyoucan中某些位进行异或0x76的操作
- 将异或后的数据使用更换过的base64表进行加密操作
- 通过某种调用CreateFileA并创建名为contest+threadid+.txt的文件
- 将密文写入文件
- 线程结束时删除文件
可以得到核心代码并且重新实现函数功能:
// RemoteThreadInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
#include "tchar.h"
char* base64Encode(char const* origSigned, unsigned origLength)
{
static const char base64Char[] = "ABCDEFGHIJKLMNOPwxyz0123456789+/ghijklmnopqrstuvQRSTUVWXYZabcdef";
unsigned char const* orig = (unsigned char const*)origSigned;
if (orig == NULL) return NULL;
unsigned const numOrig24BitValues = origLength / 3;
bool havePadding = origLength > numOrig24BitValues * 3;
bool havePadding2 = origLength == numOrig24BitValues * 3 + 2;
unsigned const numResultBytes = 4 * (numOrig24BitValues + havePadding);
char* result = new char[numResultBytes + 1];
// Map each full group of 3 input bytes into 4 output base-64 characters:
unsigned i;
for (i = 0; i < numOrig24BitValues; ++i)
{
result[4 * i + 0] = base64Char[(orig[3 * i] >> 2) & 0x3F];
result[4 * i + 1] = base64Char[(((orig[3 * i] & 0x3) << 4) | (orig[3 * i + 1] >> 4)) & 0x3F];
result[4 * i + 2] = base64Char[(((orig[3 * i + 1] & 0x0f) << 2) | (orig[3 * i + 2] >> 6)) & 0x3F];
result[4 * i + 3] = base64Char[(orig[3 * i + 2] & 0x3f) & 0x3F];
}
// Now, take padding into account. (Note: i == numOrig24BitValues)
if (havePadding)
{
result[4 * i + 0] = base64Char[(orig[3 * i] >> 2) & 0x3F];
if (havePadding2)
{
result[4 * i + 1] = base64Char[(((orig[3 * i] & 0x3) << 4) | (orig[3 * i + 1] >> 4)) & 0x3F];
result[4 * i + 2] = base64Char[((orig[3 * i + 1] & 0x0f) << 2) & 0x3F];
}
else
{
result[4 * i + 1] = base64Char[((orig[3 * i] & 0x3) << 4) & 0x3F];
result[4 * i + 2] = '=';
}
result[4 * i + 3] = '=';
}
result[numResultBytes] = '/0';
return result;
}
DWORD _CreateProcessPath(LPWSTR szEXEPath) {
LPWSTR szCommandLine = szEXEPath;//或者WCHAR
//LPWSTR szCommandLine = TEXT("NOTEPAD");//错误
//STARTUPINFO si = { sizeof(si) };
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
szCommandLine = (LPWSTR)szCommandLine;
si.dwFlags = STARTF_USESHOWWINDOW; // 指定wShowWindow成员有效
si.wShowWindow = TRUE; // 此成员设为TRUE的话则显示新建进程的主窗口,
// 为FALSE的话则不显示
BOOL bRet = ::CreateProcess(
NULL, // 不在此指定可执行文件的文件名
szCommandLine, // 命令行参数
NULL, // 默认进程安全性
NULL, // 默认线程安全性
FALSE, // 指定当前进程内的句柄不可以被子进程继承
CREATE_NEW_CONSOLE, // 为新进程创建一个新的控制台窗口
NULL, // 使用本进程的环境变量
NULL, // 使用本进程的驱动器和目录
&si,
&pi);
return pi.dwProcessId;
}
//通过进程快照获取PID
DWORD _GetProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret = 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("获取进程快照失败,请重试! Error:%d", ::GetLastError());
return Ret;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do {
if (!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret = p32.th32ProcessID;
}
} while (::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
return Ret;
}
//打开一个进程并为其创建一个线程
DWORD _RemoteThreadInject(DWORD _Pid, LPCWSTR DllName)
{
//打开进程
HANDLE hprocess;
HANDLE hThread;
DWORD _Size = 0;
BOOL Write = 0;
LPVOID pAllocMemory = NULL;
DWORD DllAddr = 0;
FARPROC pThread;
hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
//Size = sizeof(string_inject);
_Size = (_tcslen(DllName) + 1) * sizeof(TCHAR);
//远程申请空间
pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
if (pAllocMemory == NULL)
{
printf("VirtualAllocEx - Error! %d", GetLastError());
return FALSE;
}
// 写入内存
Write = ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL);
if (Write == FALSE)
{
printf("WriteProcessMemory - Error!");
return FALSE;
}
//获取LoadLibrary的地址
pThread = ::GetProcAddress(::GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pThread;
//在另一个进程中创建线程
hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL);
if (hThread == NULL)
{
printf("CreateRemoteThread - Error!");
return FALSE;
}
//等待线程函数结束,获得退出码
WaitForSingleObject(hThread, -1);
GetExitCodeThread(hThread, &DllAddr);
//释放DLL空间
VirtualFreeEx(hprocess, pAllocMemory, _Size, MEM_DECOMMIT);
//关闭线程句柄
::CloseHandle(hprocess);
return TRUE;
}
DWORD WINAPI ThreadFunc(LPVOID p)
{
char* encoded = (char*)"";
char enc[16] = "catchmeifyoucan";
char txt[5] = ".txt";
DWORD dwBytesWritten = 0;
BOOL bErrorFlag = FALSE;
srand(GetCurrentTime());
for (int i = 0; i < 16; i++) {
int r = ((double)rand() / RAND_MAX) * (16 - 0) + 0;
enc[r] ^= 0x76;
}
char contest[16] = "contest";
*(contest+7) = GetCurrentThreadId();
*(contest + 12) = *txt;
HANDLE hFile = CreateFileA(
contest, // 文件名
GENERIC_READ | GENERIC_WRITE, // 打开方式
0, // 文件共享模式
NULL, // 安全属性
CREATE_ALWAYS, // 文件创建或打开标志
FILE_ATTRIBUTE_NORMAL, // 文件属性
NULL // 模板文件句柄
);
if (hFile == INVALID_HANDLE_VALUE) {
// 处理错误
return 1;
}
// 对文件进行操作
DWORD dwBytesToWrite = (DWORD)strlen(encoded);
CloseHandle(hFile);
encoded = base64Encode(enc, 16);
*(contest + 21) = GetCurrentThreadId();
bErrorFlag = WriteFile(
hFile, // open file handle
encoded, // start of data to write
dwBytesToWrite, // number of bytes to write
&dwBytesWritten, // number of bytes that were written
NULL); // no overlapped structure
Sleep(10000);
DeleteFile((LPCWSTR)contest);
return 0;
}
BOOL MyTerminate(int pid) {
int ret;
HANDLE hProcess;
if (pid > 32768 || pid <= 0) {
return 0;
}
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (INVALID_HANDLE_VALUE == hProcess) {
fprintf(stderr, "Error: Can not open specified process!\n");
fprintf(stderr, "Error code: %d\n", GetLastError());
return 0;
}
ret = TerminateProcess(hProcess, 0);
if (ret == FALSE) {
printf("Error: Can not terminate specified process!\n");
printf("Error code: %d\n", GetLastError());
}
CloseHandle(hProcess);
}
int main(int argc, char* argv[])
{
char path1[92] = "D:\\CTF\\ctf比赛文件\\2023txgame\\WorkService.exe a";
if (argv[1] == "a") {
HANDLE hThread;
DWORD threadId;
for (int i = 0; i < 16; i++) {
hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, &threadId); // 创建线程
}
}
else if (argv[1] == "b") {
DWORD WorkingServiceProcessId;
Sleep(10000);
WorkingServiceProcessId = _GetProcessPID(L"WorkingService.exe");
MyTerminate(WorkingServiceProcessId);
system(path1);
}
else {
DWORD explorerProcessId;
DWORD taskmgrProcessId;
//获取pid
explorerProcessId = _GetProcessPID(L"explorer.exe");
taskmgrProcessId = _GetProcessPID(L"taskmgr.exe");
//注入
_RemoteThreadInject(explorerProcessId, L"D:\\CTF\\ctf比赛文件\\2023txgamefinal\\WorkServiceDLL\\x64\\Release\\WorkServiceDLL.dll");
_RemoteThreadInject(taskmgrProcessId, L"D:\\CTF\\ctf比赛文件\\2023txgamefinal\\WorkServiceDLL\\x64\\Release\\WorkServiceDLL.dll");
}
}
dll.cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <windows.h>
#include <TlHelp32.h>
#include "tchar.h"
#include <iostream>
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
char path[92] = "D:\\CTF\\ctf比赛文件\\2023txgamefinal\\WorkService\\x64\\WorkService.exe b";
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
while (1) {
Sleep(10000);
system(path);
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}