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
运行一下程序,可以发现程序会一直向当前目录的 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
我们直接 执行:
.\VMPDump.exe 22164 "" ep=0x0007FF62CF8945C -disable-reloc
即可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:
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;
}