2023年腾讯游戏安全pc端初赛wp

2023年腾讯游戏安全pc端初赛wp

练练手

所使用到的工具

1、x64dbg 2、IDA 3、ScyllaHide 4、frida 5、vmpdump https://github.com/0xnobody/vmpdump 6、Procmon/火绒剑 7、drstrace https://github.com/DynamoRIO/drmemory 8、010editor 9、ce修改器

寻踪觅迹

die看一下,发现程序被加了vmp

image-20241203153333453

运行一下程序,可以发现程序会一直向当前目录的 contest.txt 文件内写入 ImVkImx9JG12OGtlImV+

用火绒剑或者procmon看一下进程,可以可以发现程序每各一秒就会轮流的向\#\.4%34n484contest.txt写入数据

image-20241203153634637

下面我们就考虑先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程序,我们对 GetSystemTimeAsFileTimeQueryPerformanceCounter 这两个api下断点

之后F9运行,在断在GetSystemTimeAsFileTime的时候回溯返回地址,找到第一个返回地址在 .txt段的位置:

我们尝试运行,第一次 GetSystemTimeAsFileTime被断下来后,回溯返回地址,可发现地址在 vmp段内,那我们只能继续F9知道断在 GetSystemTimeAsFileTime后返回地址在 .txt断

image-20241203155353328 image-20241203155538853

在第二次断到 GetSystemTimeAsFileTime 的时候,可以发现返回地址在 .text段

image-20241203155756163

image-20241203155833944

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

image-20241203160010435

之后我们就可以找一下入口点的特征,事实上,经过我的多次尝试,0x0007FF62CF8945C 就是程序真正的入口点。

我们我们需要dump下来程序并恢复IAT表,这里常规的dump + iat修复法是不起作用的。经过我的浏览器检索,我找到了一个开源项目:

vmpdump: https://github.com/0xnobody/vmpdump

我们直接 执行:

.\VMPDump.exe 22164 "" ep=0x0007FF62CF8945C -disable-reloc

image-20241203160855016

即可dump出程序并修复好IAT表

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

image-20241203160601208

下面我们的任务就是分析源程序了

层层深入

用IDA打开,最好给IDA也配一个ScyllaHide插件

之后搜索字符串,可以定位到一个可疑字符串:

image-20241203160926765

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

image-20241203160951825

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

image-20241203161108991

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

image-20241203161235909

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

image-20241203161203990

接下来开始我们漫长的调试工作,我们对call下断点,一点点分析

经过分析,我们可以发现,程序首先遍历进程对 idax32dbgx64dbgida64x86 64-SSE4-AVX2.exewiresharkprocesshackernetstat.exenetmontcpview.exefilemon.exereqmon.execain.exe等函数进行检查

之后去明文进行逐字节异或:

image-20241203162149173

然后进行魔改base64编码,随后调用某个函数,进行了文件读写操作,最后关闭文件句柄,Sleep 1秒

程序的大体流程已经很清楚了,接下来就是要写代码进行 hook。

为了方便hook,我用 drstrace工具 trace了一下系统调用

image-20241203162910152

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

image-20241203163047066

同理,也可以通过hook NtCreateFile(ZwCreateFile)的方式来实现任意文件写明文。

但通过调试,我发现,我对 ntdll中的 NtCreateFileNtWriteFile 下断点,并不能断下来,但我的火绒剑、Procmon、drstrace工具上明明显示了这些系统调用的执行。

莫非是程序自实现了 syscall? 我尝试全局搜索 syscall,也并未找到对应汇编。

柳暗花明

事实上,程序先alloc了一块内存,再把ntdll的相关函数copy了进去并直接执行这个堆中的代码,而不是 ntdll中对应的函数代码

直接对VirtualProtectEx下断点,经过调试我发现,在main函数执行之前,程序会分配一个 0x100000 大小的内存,之后调用

image-20241203164249596

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

image-20241203164319612

那么我们用ida打开 ntdll.dll,并在对应函数(ZwWriteFile / NtCreateFile)上搜索特征码,之后用ce修改器定位一下,可以发现确实能定位函数到这个堆块中。之后下内存执行断点,能成功断下来!

落叶归根

那么我的hook思路就是:

程序运行之后,写脚本,先搜索全局内存,在所有具有rwx属性段的内存段中搜索特征码,定位NtCreateFileZwWriteFile函数地址,之后hook 函数参数即可。

简单起见,这里写了个frida脚本

frida_hook.py:

import frida import sys import time import subprocess Jscode = None def get_js_code(): global Jscode fp = open("./frida_hook.js","r",encoding="utf8") Jscode = fp.read() fp.close() filepath = "tlsn.txt" def exe_searchcode(pid): exe_path = "./search_code.exe" process = subprocess.Popen([exe_path,str(pid)]) process.wait() def rewrite_filepath(): global filepath fp = open("./data.log","r") data = fp.read() fp.close() fp = open("./data.log","w") fp.write(data + "filepath: \n" + filepath + "\n") fp.close() # Attach 方式 local = frida.get_local_device() # session = local.attach("contest.VMPDump.exe") # contest.VMPDump.exe session = local.attach("contest.exe") # contest.exe pid = session._impl.pid exe_searchcode(pid) rewrite_filepath() get_js_code() script = session.create_script(Jscode) script.load() sys.stdin.read()

frida_hook.js:

var data = [ 0x26, 0x24, 0x31, 0x26, 0x2D, 0x28, 0x20, 0x2C, 0x23, 0x3C, 0x2A, 0x30, 0x26, 0x24, 0x2B]; var out_buf_addr = 0 var out_length_addr = 0 var ans =0 var xbase64 = "" function scan_xbase64(){ // var moduleName = "contest.exe"; var moduleName = "contest.VMPDump.exe"; var module = Process.getModuleByName(moduleName) var searchPattern = "41 56 56 57 53 89 D0 41 BA AB AA AA AA" Memory.scan(module.base,module.size,searchPattern,{ onMatch: function(address,size){ xbase64 = address console.log("Found xbase64 addr: " + address.toString(16)); }, onComplete:function(){ console.log("scan xbase64 finish"); hook_xbase64() }, onError: function(){ console.log("scan error"); } }); } function readLines(filePath) { var alldata = File.readAllText(filePath); var lines = alldata.split('\n'); // 按行拆分文件内容 return lines } function hook_WriteFile(){ var path = "./data.log" var lines = readLines(path); var WriteFileBuffAddr = lines[5].trim(); console.log("ZwWriteFile is: " + WriteFileBuffAddr) Interceptor.attach(ptr(WriteFileBuffAddr), { onEnter: function(args) { console.log("Enter writefile hook") var rsp = this.context.rsp var outstraddr = rsp.add(0x30).readPointer() var length = data.length for(var i=0;i<data.length;i++){ outstraddr.add(i).writeU8(data[i]); } rsp.add(0x38).writeU64(length); console.log("leave writefile hook") } }); } var newFilePath = "" // 为了防止alloc的内存被释放,把变量名写道全局变量上也行 function hook_CreateFile(){ var path = "./data.log" var lines = readLines(path); var hook_file_path = lines[7].trim(); console.log("hook new file path is:",hook_file_path) var CreateFileBuffAddr = lines[3].trim(); newFilePath = Memory.allocUtf16String(hook_file_path) var length = hook_file_path.length * 2 var UNICODE_STRING_file_path_struct = Memory.alloc(0x100) UNICODE_STRING_file_path_struct.writeU16(length) UNICODE_STRING_file_path_struct.add(2).writeU16(length) UNICODE_STRING_file_path_struct.add(8).writePointer(newFilePath) Interceptor.attach(ptr(CreateFileBuffAddr), { onEnter: function(args) { var struct_addr = args[2]; // 我们需要提前检测一下,写入的是 #\.4%34n484 还是其他的什么 var old_unicode_struct_addr = struct_addr.add(0x10).readPointer() var old_path_addr = old_unicode_struct_addr.add(8).readPointer() var old_path = old_path_addr.readUtf16String(); if (old_path == "contest.txt"){ struct_addr.add(0x10).writePointer(UNICODE_STRING_file_path_struct) console.log("finish hook_CreateFile"); }else{ console.log("no hook_CreateFile"); } } }); } hook_WriteFile() hook_CreateFile() // __kernel_entry NTSTATUS NtCreateFile( // [out] PHANDLE FileHandle, // [in] ACCESS_MASK DesiredAccess, // [in] POBJECT_ATTRIBUTES ObjectAttributes, // [out] PIO_STATUS_BLOCK IoStatusBlock, // [in, optional] PLARGE_INTEGER AllocationSize, // [in] ULONG FileAttributes, // [in] ULONG ShareAccess, // [in] ULONG CreateDisposition, // [in] ULONG CreateOptions, // [in] PVOID EaBuffer, // [in] ULONG EaLength // ); // typedef struct _UNICODE_STRING { // USHORT Length; // USHORT MaximumLength; // PWSTR Buffer; // } UNICODE_STRING; // 3、hook CreateFile

其中 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]); // 目标进程ID 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 }; // CreateFile char pattern2[] = { 0x4C, 0x8B, 0xD1, 0xB8, 0x08, 0x00, 0x00, 0x00 }; // WriteFile 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; // 每次读取1MB 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; } //printf("baseaddress: 0x%x \t size: 0x%x \t protect: 0x%x\n", mbi.BaseAddress, mbi.RegionSize, mbi.Protect); //if (mbi.BaseAddress == 0x2070000) { // printf("here\n"); //} 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; //fprintf(fp, "NtCreateFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result - (char*)mem); //break; } 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; //fprintf(fp, "ZwWriteFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result2 - (char*)mem); //break; } } 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:

import frida import sys import time import subprocess Jscode = None def get_js_code(): global Jscode fp = open("./frida_hook.js","r",encoding="utf8") Jscode = fp.read() fp.close() filepath = "tlsn.txt" def exe_searchcode(pid): exe_path = "./search_code.exe" process = subprocess.Popen([exe_path,str(pid)]) process.wait() def rewrite_filepath(): global filepath fp = open("./data.log","r") data = fp.read() fp.close() fp = open("./data.log","w") fp.write(data + "filepath: \n" + filepath + "\n") fp.close() # Attach 方式 local = frida.get_local_device() # session = local.attach("contest.VMPDump.exe") # contest.VMPDump.exe session = local.attach("contest.exe") # contest.exe pid = session._impl.pid exe_searchcode(pid) rewrite_filepath() get_js_code() script = session.create_script(Jscode) script.load() sys.stdin.read()

frida_hook.js:

var data = [ 0x26, 0x24, 0x31, 0x26, 0x2D, 0x28, 0x20, 0x2C, 0x23, 0x3C, 0x2A, 0x30, 0x26, 0x24, 0x2B]; var out_buf_addr = 0 var out_length_addr = 0 var ans =0 var xbase64 = "" function scan_xbase64(){ // var moduleName = "contest.exe"; var moduleName = "contest.VMPDump.exe"; var module = Process.getModuleByName(moduleName) var searchPattern = "41 56 56 57 53 89 D0 41 BA AB AA AA AA" Memory.scan(module.base,module.size,searchPattern,{ onMatch: function(address,size){ xbase64 = address console.log("Found xbase64 addr: " + address.toString(16)); }, onComplete:function(){ console.log("scan xbase64 finish"); hook_xbase64() }, onError: function(){ console.log("scan error"); } }); } function readLines(filePath) { var alldata = File.readAllText(filePath); var lines = alldata.split('\n'); // 按行拆分文件内容 return lines } function hook_WriteFile(){ var path = "./data.log" var lines = readLines(path); var WriteFileBuffAddr = lines[5].trim(); console.log("ZwWriteFile is: " + WriteFileBuffAddr) Interceptor.attach(ptr(WriteFileBuffAddr), { onEnter: function(args) { console.log("Enter writefile hook") var rsp = this.context.rsp var outstraddr = rsp.add(0x30).readPointer() var length = data.length for(var i=0;i<data.length;i++){ outstraddr.add(i).writeU8(data[i]); } rsp.add(0x38).writeU64(length); console.log("leave writefile hook") } }); } var newFilePath = "" // 为了防止alloc的内存被释放,把变量名写道全局变量上也行 function hook_CreateFile(){ var path = "./data.log" var lines = readLines(path); var hook_file_path = lines[7].trim(); console.log("hook new file path is:",hook_file_path) var CreateFileBuffAddr = lines[3].trim(); newFilePath = Memory.allocUtf16String(hook_file_path) var length = hook_file_path.length * 2 var UNICODE_STRING_file_path_struct = Memory.alloc(0x100) UNICODE_STRING_file_path_struct.writeU16(length) UNICODE_STRING_file_path_struct.add(2).writeU16(length) UNICODE_STRING_file_path_struct.add(8).writePointer(newFilePath) Interceptor.attach(ptr(CreateFileBuffAddr), { onEnter: function(args) { var struct_addr = args[2]; // 我们需要提前检测一下,写入的是 #\.4%34n484 还是其他的什么 var old_unicode_struct_addr = struct_addr.add(0x10).readPointer() var old_path_addr = old_unicode_struct_addr.add(8).readPointer() var old_path = old_path_addr.readUtf16String(); if (old_path == "contest.txt"){ struct_addr.add(0x10).writePointer(UNICODE_STRING_file_path_struct) console.log("finish hook_CreateFile"); }else{ console.log("no hook_CreateFile"); } } }); } hook_WriteFile() hook_CreateFile() // __kernel_entry NTSTATUS NtCreateFile( // [out] PHANDLE FileHandle, // [in] ACCESS_MASK DesiredAccess, // [in] POBJECT_ATTRIBUTES ObjectAttributes, // [out] PIO_STATUS_BLOCK IoStatusBlock, // [in, optional] PLARGE_INTEGER AllocationSize, // [in] ULONG FileAttributes, // [in] ULONG ShareAccess, // [in] ULONG CreateDisposition, // [in] ULONG CreateOptions, // [in] PVOID EaBuffer, // [in] ULONG EaLength // ); // typedef struct _UNICODE_STRING { // USHORT Length; // USHORT MaximumLength; // PWSTR Buffer; // } UNICODE_STRING; // 3、hook CreateFile

其中 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]); // 目标进程ID 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 }; // CreateFile char pattern2[] = { 0x4C, 0x8B, 0xD1, 0xB8, 0x08, 0x00, 0x00, 0x00 }; // WriteFile 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; // 每次读取1MB 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; } //printf("baseaddress: 0x%x \t size: 0x%x \t protect: 0x%x\n", mbi.BaseAddress, mbi.RegionSize, mbi.Protect); //if (mbi.BaseAddress == 0x2070000) { // printf("here\n"); //} 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; //fprintf(fp, "NtCreateFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result - (char*)mem); //break; } 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; //fprintf(fp, "ZwWriteFile address: \n0x%llx\n", (SIZE_T)mbi.BaseAddress + (char*)result2 - (char*)mem); //break; } } 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__

本文作者_TLSN
本文链接https://www.cnblogs.com/lordtianqiyi/p/18584631.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   TLSN  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示