2023年腾讯游戏安全pc端初赛wp
练练手
所使用到的工具
寻踪觅迹
die看一下,发现程序被加了vmp
运行一下程序,可以发现程序会一直向当前目录的 contest.txt
文件内写入 ImVkImx9JG12OGtlImV+
用火绒剑或者procmon看一下进程,可以可以发现程序每各一秒就会轮流的向\#\.4%34n484
与 contest.txt
写入数据
下面我们就考虑先dump出源程序
抽丝剥茧
笔者第一次脱vmp壳,在网上搜索了一些资料,发现市面上对于3.x的vmp壳的脱法基本大同小异。
我参考了这两篇文章:
https://www.52pojie.cn/thread-1764028-1-1.html
https://0x666.club/vmp3-unpack-trick/
由于程序具有反调试,我们需要用scyllaHide 插件反反调试。
我们需要先执行到 程序.text段再dump,即必须先让vmp恢复完毕所有的源程序代码。
我的实现步骤如下:
首先x64dbg载入 contest.exe程序,我们对 GetSystemTimeAsFileTime
与 QueryPerformanceCounter
这两个api下断点
之后F9运行,在断在GetSystemTimeAsFileTime
的时候回溯返回地址,找到第一个返回地址在 .txt段的位置:
我们尝试运行,第一次 GetSystemTimeAsFileTime
被断下来后,回溯返回地址,可发现地址在 vmp段内,那我们只能继续F9知道断在 GetSystemTimeAsFileTime
后返回地址在 .txt断
在第二次断到 GetSystemTimeAsFileTime
的时候,可以发现返回地址在 .text段


随后再次F8运行到 QueryPerformanceCounter
后开始单步运行,直到运行到这里:

之后我们就可以找一下入口点的特征,事实上,经过我的多次尝试,0x0007FF62CF8945C
就是程序真正的入口点。
我们我们需要dump下来程序并恢复IAT表,这里常规的dump + iat修复法是不起作用的。经过我的浏览器检索,我找到了一个开源项目:
vmpdump: https://github.com/0xnobody/vmpdump
我们直接 执行:

即可dump出程序并修复好IAT表
之后我们再用 010editor改一下程序的入口点即可正常运行程序。(可能还得固定一下基址)

下面我们的任务就是分析源程序了
层层深入
用IDA打开,最好给IDA也配一个ScyllaHide插件
之后搜索字符串,可以定位到一个可疑字符串:

交叉引用可以找到一个魔改的 base64编码算法,这个函数没有被vmp混淆,所以还是能很容易看出来的

之后我们进入main函数,可以发现main函数被vmp混淆的非常严重

接下来我们找一下函数分发器

尽可能的去人工恢复一下符号(我这里是已经修复过的)

接下来开始我们漫长的调试工作,我们对call下断点,一点点分析
经过分析,我们可以发现,程序首先遍历进程对 ida
、x32dbg
、x64dbg
、ida64
、x86 64-SSE4-AVX2.exe
、wireshark
、processhacker
、netstat.exe
、netmon
、tcpview.exe
、filemon.exe
、reqmon.exe
、cain.exe
等函数进行检查
之后去明文进行逐字节异或:

然后进行魔改base64编码,随后调用某个函数,进行了文件读写操作,最后关闭文件句柄,Sleep 1秒
程序的大体流程已经很清楚了,接下来就是要写代码进行 hook。
为了方便hook,我用 drstrace工具 trace了一下系统调用

实际上 0x39786d496b566d49 转化为字符串就是 "ImVkImx9"字符串,那我们可以直接 hook NtWriteFile函数来实现写明文。

同理,也可以通过hook NtCreateFile(ZwCreateFile)的方式来实现任意文件写明文。
但通过调试,我发现,我对 ntdll中的 NtCreateFile
、NtWriteFile
下断点,并不能断下来,但我的火绒剑、Procmon、drstrace工具上明明显示了这些系统调用的执行。
莫非是程序自实现了 syscall? 我尝试全局搜索 syscall,也并未找到对应汇编。
柳暗花明
事实上,程序先alloc了一块内存,再把ntdll的相关函数copy了进去并直接执行这个堆中的代码,而不是 ntdll中对应的函数代码
直接对VirtualProtectEx
下断点,经过调试我发现,在main函数执行之前,程序会分配一个 0x100000 大小的内存,之后调用

VirtualProtectEx
函数,给其一个可读可写可执行的权限

那么我们用ida打开 ntdll.dll,并在对应函数(ZwWriteFile
/ NtCreateFile
)上搜索特征码,之后用ce修改器定位一下,可以发现确实能定位函数到这个堆块中。之后下内存执行断点,能成功断下来!
落叶归根
那么我的hook思路就是:
程序运行之后,写脚本,先搜索全局内存,在所有具有rwx属性段的内存段中搜索特征码,定位NtCreateFile
与ZwWriteFile
函数地址,之后hook 函数参数即可。
简单起见,这里写了个frida脚本
frida_hook.py:
frida_hook.js:
其中 search_code.exe
是我编写的特征码搜索程序
源码如下:
#include <windows.h>
#include <stdio.h>
void* optimized_search(void* mem, size_t mem_size, const char* pattern, size_t pattern_size) {
if (pattern_size > mem_size) {
return NULL;
}
for (size_t i = 0; i <= mem_size - pattern_size; ++i) {
if (memcmp((char*)mem + i, pattern, pattern_size) == 0) {
return (char*)mem + i;
}
}
return NULL;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
puts("error argv");
return 0;
}
FILE* fp = fopen("./data.log", "w");
DWORD pid = atoi(argv[1]);
fprintf(fp, "pid: \n%d\n", pid);
HANDLE process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid);
if (process == NULL) {
printf("OpenProcess failed. Error: %lu\n", GetLastError());
return 1;
}
char pattern1[] = { 0x4C, 0x8B, 0xD1, 0xB8, 0x55, 0x00, 0x00, 0x00 };
char pattern2[] = { 0x4C, 0x8B, 0xD1, 0xB8, 0x08, 0x00, 0x00, 0x00 };
size_t pattern_size1 = sizeof(pattern1);
size_t pattern_size2 = sizeof(pattern2);
SIZE_T startAddr = 0x0;
SIZE_T endAddr = 0x30000000000;
SIZE_T chunk_size = 0x100000;
void* mem = malloc(chunk_size);
if (mem == NULL) {
printf("Memory allocation failed.\n");
CloseHandle(process);
return 1;
}
SIZE_T addr = startAddr;
SIZE_T NtCreateFileAddr = 0;
SIZE_T ZwWriteFileAddr = 0;
while (addr < endAddr) {
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQueryEx(process, (LPCVOID)addr, &mbi, sizeof(mbi)) == 0) {
printf("VirtualQueryEx failed at address 0x%llx. Error: %lu\n", addr, GetLastError());
addr += chunk_size;
continue;
}
if (mbi.State == MEM_COMMIT && (mbi.Protect == PAGE_EXECUTE_READWRITE )) {
SIZE_T region_size = mbi.RegionSize < chunk_size ? mbi.RegionSize : chunk_size;
SIZE_T bytes_read;
if (ReadProcessMemory(process, mbi.BaseAddress, mem, region_size, &bytes_read)) {
void* result = optimized_search(mem, bytes_read, pattern1, pattern_size1);
if (result != NULL) {
printf("NtCreateFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result - (char*)mem);
NtCreateFileAddr = (SIZE_T)mbi.BaseAddress + (char*)result - (char*)mem;
}
void* result2 = optimized_search(mem, bytes_read, pattern2, pattern_size2);
if (result2 != NULL) {
printf("ZwWriteFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result2 - (char*)mem);
ZwWriteFileAddr = (SIZE_T)mbi.BaseAddress + (char*)result2 - (char*)mem;
}
}
else {
printf("ReadProcessMemory failed at address 0x%llx. Error: %lu\n", (SIZE_T)mbi.BaseAddress, GetLastError());
}
}
addr += mbi.RegionSize;
}
fprintf(fp, "NtCreateFile address: \n0x%llx\n", NtCreateFileAddr);
fprintf(fp, "ZwWriteFile address: \n0x%llx\n", ZwWriteFileAddr);
fclose(fp);
free(mem);
CloseHandle(process);
return 0;
}
通过修改 firda_hook.py中的 filepath
变量,可以实现任意文件写明文操作。
最后答案
找到明文的信息
明文应该为 catchmeifyoucan
或者 &$1&-( ,#<*0&$+
是由 catchmeifyoucan
异或得来的
写入密文信息变为写入明文、让contest.exe 往入自行指定的不同的文件里写入明文信息成功
简单起见,这里用的frida脚本 (用frida脚本的后果就是要修改大量内存,hhh,优点就是方便)
我这套可以实现目录下的任意文件名写明文。
frida_hook.py:
frida_hook.js:
其中 search_code.exe
是我编写的特征码搜索程序
源码如下:
#include <windows.h>
#include <stdio.h>
void* optimized_search(void* mem, size_t mem_size, const char* pattern, size_t pattern_size) {
if (pattern_size > mem_size) {
return NULL;
}
for (size_t i = 0; i <= mem_size - pattern_size; ++i) {
if (memcmp((char*)mem + i, pattern, pattern_size) == 0) {
return (char*)mem + i;
}
}
return NULL;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
puts("error argv");
return 0;
}
FILE* fp = fopen("./data.log", "w");
DWORD pid = atoi(argv[1]);
fprintf(fp, "pid: \n%d\n", pid);
HANDLE process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid);
if (process == NULL) {
printf("OpenProcess failed. Error: %lu\n", GetLastError());
return 1;
}
char pattern1[] = { 0x4C, 0x8B, 0xD1, 0xB8, 0x55, 0x00, 0x00, 0x00 };
char pattern2[] = { 0x4C, 0x8B, 0xD1, 0xB8, 0x08, 0x00, 0x00, 0x00 };
size_t pattern_size1 = sizeof(pattern1);
size_t pattern_size2 = sizeof(pattern2);
SIZE_T startAddr = 0x0;
SIZE_T endAddr = 0x30000000000;
SIZE_T chunk_size = 0x100000;
void* mem = malloc(chunk_size);
if (mem == NULL) {
printf("Memory allocation failed.\n");
CloseHandle(process);
return 1;
}
SIZE_T addr = startAddr;
SIZE_T NtCreateFileAddr = 0;
SIZE_T ZwWriteFileAddr = 0;
while (addr < endAddr) {
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQueryEx(process, (LPCVOID)addr, &mbi, sizeof(mbi)) == 0) {
printf("VirtualQueryEx failed at address 0x%llx. Error: %lu\n", addr, GetLastError());
addr += chunk_size;
continue;
}
if (mbi.State == MEM_COMMIT && (mbi.Protect == PAGE_EXECUTE_READWRITE )) {
SIZE_T region_size = mbi.RegionSize < chunk_size ? mbi.RegionSize : chunk_size;
SIZE_T bytes_read;
if (ReadProcessMemory(process, mbi.BaseAddress, mem, region_size, &bytes_read)) {
void* result = optimized_search(mem, bytes_read, pattern1, pattern_size1);
if (result != NULL) {
printf("NtCreateFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result - (char*)mem);
NtCreateFileAddr = (SIZE_T)mbi.BaseAddress + (char*)result - (char*)mem;
}
void* result2 = optimized_search(mem, bytes_read, pattern2, pattern_size2);
if (result2 != NULL) {
printf("ZwWriteFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result2 - (char*)mem);
ZwWriteFileAddr = (SIZE_T)mbi.BaseAddress + (char*)result2 - (char*)mem;
}
}
else {
printf("ReadProcessMemory failed at address 0x%llx. Error: %lu\n", (SIZE_T)mbi.BaseAddress, GetLastError());
}
}
addr += mbi.RegionSize;
}
fprintf(fp, "NtCreateFile address: \n0x%llx\n", NtCreateFileAddr);
fprintf(fp, "ZwWriteFile address: \n0x%llx\n", ZwWriteFileAddr);
fclose(fp);
free(mem);
CloseHandle(process);
return 0;
}
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现