长亭百川云 - 文章详情

VMP源码分析:反调试与绕过方法

看雪学苑

76

2024-07-13

vmp反调试相关源码部分


1.1 如何检索反调试源码

我们都知道,当vmp检测到被调试,会有如下弹框。

通过这条报错信息,不难在源码中找到:

然后通过它的消息传递机制,不难找到:

void LoaderMessage(MessageType type, const void *param1 = NULL, const void *param2 = NULL)
{
const VMP_CHAR *message;
bool need_format = false;
switch (type) {
case mtDebuggerFound:
message = reinterpret_cast<const VMP_CHAR *>(FACE_DEBUGGER_FOUND);
break;
case mtVirtualMachineFound:
message = reinterpret_cast<const VMP_CHAR *>(FACE_VIRTUAL_MACHINE_FOUND);
break;
case mtFileCorrupted:
message = reinterpret_cast<const VMP_CHAR *>(FACE_FILE_CORRUPTED);
break;
case mtUnregisteredVersion:
message = reinterpret_cast<const VMP_CHAR *>(FACE_UNREGISTERED_VERSION);
break;
case mtInitializationError:
message = reinterpret_cast<const VMP_CHAR *>(FACE_INITIALIZATION_ERROR);
need_format = true;
break;
case mtProcNotFound:
message = reinterpret_cast<const VMP_CHAR *>(FACE_PROC_NOT_FOUND);
need_format = true;
break;
case mtOrdinalNotFound:
message = reinterpret_cast<const VMP_CHAR *>(FACE_ORDINAL_NOT_FOUND);
need_format = true;
break;
default:
return;
}

然后查找mtDebuggerFound的引用即可检索到各处反调试相关源码,也就是此文将要详细说的,至于其他部分的检测,感兴趣的童鞋可以自行研究。

1.2 源码阅读:反调试手段

1.2.1 系统版本号的判断

if (!os_build_number) {
if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}
tmp_loader_data->set_is_debugger_detected(true);
}

那么这个os_build_number怎么获取的呢?

简单说,一共两种获取方式,1. 从peb里直接去取得;2.从ntdll.dll的头部获取文件版本号从而确定系统版本。

可能有的童鞋会问了,系统版本号拿来判断反调试是不是有点什么大病,其实不是,私以为,这边判断系统版本号纯纯的只是为了方便取 syscall 所使用的系统调用号。

如下,vmp应该是把全量的发行版系统都是硬编码了:

当系统版本号不在 vmp 适配过的范围(比如测试版 windows),他则会去 map 一份新的 ntdll ,然后从中找他要的NT函数的系统调用号,至于系统调用号是什么,这里就不赘述了。

1.2.2 peb->BeingDebugged 标记

if (peb->BeingDebugged) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}

会心一笑,peb里的这个位就不用过多解释了。

1.2.3 ProcessDebugPort

if (NT_SUCCESS(reinterpret_cast<tNtQueryInformationProcess *>(syscall | sc_query_information_process)(process, ProcessDebugPort, &debug_object, sizeof(debug_object), NULL)) && debug_object != 0) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}

查询 ProcessDebugPort,如果查到了,自然是被调试了,也是很常见的反调。

1.2.4 ProcessDebugObjectHandle

if (NT_SUCCESS(reinterpret_cast<tNtQueryInformationProcess *>(syscall | sc_query_information_process)(process, ProcessDebugObjectHandle, &debug_object, sizeof(debug_object), reinterpret_cast(&debug_object)))
|| debug_object == 0) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}

查询 ProcessDebugObjectHandle, 如果 存在调试对象句柄,那也是被调试了,也属于常见反调。

1.2.5 SystemKernelDebuggerInformation

SYSTEM_KERNEL_DEBUGGER_INFORMATION info;
NTSTATUS status = nt_query_system_information(SystemKernelDebuggerInformation, &info, sizeof(info), NULL);
if (NT_SUCCESS(status) && info.DebuggerEnabled && !info.DebuggerNotPresent) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}

针对内核调试器的监测,也属常见。

1.2.6 针对驱动模块名的匹配

SYSTEM_MODULE_INFORMATION *buffer = NULL;
ULONG buffer_size = 0;
status = nt_query_system_information(SystemModuleInformation, &buffer, 0, &buffer_size);
if (buffer_size) {
buffer = reinterpret_cast<SYSTEM_MODULE_INFORMATION *>(LoaderAlloc(buffer_size * 2));
if (buffer) {
status = nt_query_system_information(SystemModuleInformation, buffer, buffer_size * 2, NULL);
if (NT_SUCCESS(status)) {
for (size_t i = 0; i < buffer->Count && !is_found; i++) {
SYSTEM_MODULE_ENTRY *module_entry = &buffer->Module[i];
for (size_t j = 0; j < 5 ; j++) {
const char *module_name;
switch (j) {
case 0:
module_name = reinterpret_cast<const char *>(FACE_SICE_NAME);
break;
case 1:
module_name = reinterpret_cast<const char *>(FACE_SIWVID_NAME);
break;
case 2:
module_name = reinterpret_cast<const char *>(FACE_NTICE_NAME);
break;
case 3:
module_name = reinterpret_cast<const char *>(FACE_ICEEXT_NAME);
break;
case 4:
module_name = reinterpret_cast<const char *>(FACE_SYSER_NAME);
break;
}
if (Loader_stricmp(module_name, module_entry->Name + module_entry->PathLength, true) == 0) {
is_found = true;
break;
}
}
}
}
LoaderFree(buffer);
}
}

这也是针对了一些常见的内核级调试器的检测,他们的驱动名。

1.2.7 线程隐藏

if (sc_set_information_thread)
reinterpret_cast<tNtSetInformationThread *>(syscall | sc_set_information_thread)(thread, ThreadHideFromDebugger, NULL, 0);

对调试器隐藏了当前线程。

1.2.8 函数头 0xCC断点检测

tNtOpenFile *open_file = reinterpret_cast<tNtOpenFile *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_OPEN_FILE_NAME), true));
tNtCreateSection *create_section = reinterpret_cast<tNtCreateSection *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_CREATE_SECTION_NAME), true));
tNtMapViewOfSection *map_view_of_section = reinterpret_cast<tNtMapViewOfSection *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_MAP_VIEW_OF_SECTION), true));
tNtUnmapViewOfSection *unmap_view_of_section = reinterpret_cast<tNtUnmapViewOfSection *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_UNMAP_VIEW_OF_SECTION), true));
tNtClose *close = reinterpret_cast<tNtClose *>(LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_NT_CLOSE), true));

if (!create_section || !open_file || !map_view_of_section || !unmap_view_of_section || !close) {
LoaderMessage(mtInitializationError, INTERNAL_GPA_ERROR);
return LOADER_ERROR;
}

// check breakpoint
uint8_t *ckeck_list[] = { reinterpret_cast<uint8_t*>(create_section),
reinterpret_cast<uint8_t*>(open_file),
reinterpret_cast<uint8_t*>(map_view_of_section),
reinterpret_cast<uint8_t*>(unmap_view_of_section),
reinterpret_cast<uint8_t*>(close) };
for (i = 0; i < _countof(ckeck_list); i++) {
if (*ckeck_list[i] == 0xcc) {
if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}
tmp_loader_data->set_is_debugger_detected(true);
}
}

if (*reinterpret_cast<uint8_t*>(virtual_protect) == 0xcc) {
if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}
tmp_loader_data->set_is_debugger_detected(true);
}

检测自己要调用的函数有没有被下0xCC断点。

1.2.9 内存断点检测

if (old_protect & PAGE_GUARD) {
if (data.options() & LOADER_OPTION_CHECK_DEBUGGER) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}
tmp_loader_data->set_is_debugger_detected(true);
}

1.2.10 假句柄

tCloseHandle *close_handle = reinterpret_cast<tCloseHandle *>(LoaderGetProcAddress(kernel32, reinterpret_cast<const char *>(FACE_CLOSE_HANDLE_NAME), true));
if (close_handle) {
__try {
if (close_handle(HANDLE(INT_PTR(0xDEADC0DE)))) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}
} __except(EXCEPTION_EXECUTE_HANDLER) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}
}

通过关闭无效句柄来判断是否成功,如果成功则中了陷阱。

1.2.11 TrapFlag 与 硬件断点检测

__try {
__writeeflags(__readeflags() | 0x100);
val = __rdtsc();
__nop();
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
} __except(ctx = (GetExceptionInformation())->ContextRecord,
drx = (ctx->ContextFlags & CONTEXT_DEBUG_REGISTERS) ? ctx->Dr0 | ctx->Dr1 | ctx->Dr2 | ctx->Dr3 : 0,
EXCEPTION_EXECUTE_HANDLER) {
if (drx) {
LoaderMessage(mtDebuggerFound);
return LOADER_ERROR;
}
}

可还行,两个检测写在一起了,通过设置flags的TrapFlag触发异常,然后在异常处理里检查硬件断点寄存器是否设置。

至此,反调弹框部分基本看完了。

1.3 源码部分总结

◆用了10+种反调手段,基本都属于常见范畴。

◆有检测普通调试器的,有检测内核调试器的。

◆一些查询api使用的syscall,难以从 r3 直接突破。

vmp 反调试的 bypass (纯r3)


2.1 几句废话

vmp的反调试基本是一些常见的反调试手段,
其中比较棘手的是一些NT函数的调用,他使用了SYSCALL,通过自实现的系统调用规避了我们从 r3 hook 然后绕过的可能。

通过网上一顿检索,确实看到了不少从 r0 来过 vmp 反调的插件/工具/源码。
但是!难道!我们就只能上驱动了么?它是r3却把我们逼到了r0,有没有纯纯的三环方法还能绕过他的呢?

答案当然是,当然存在(狗头),不然我也就不写这个分享了。

2.2 bypass 关键点

通过不死心的源码阅读,终于让我看到了这块代码。

也就是关键的这一句

LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_WINE_GET_VERSION_NAME), true)

此时,小伙伴就会问了,VMP在搞啥?

这其实是 vmp 在给 wine 环境做兼容,如果发现 ntdll.dll 的导出表存在 wine_get_version 函数,则会关闭使用系统调用的特性!

关闭系统调用以后,那还不是随便我们hook?

所以理论上只要给 ntdll.dll 的导出表做点手脚即可。理论可行,开始动手。

2.3 bypasss 代码

首先,重复造轮子的事不要做,针对vmp的那些常见的反调,已经有很多大佬写好插件并开源了,
我在这里拿这个x64dbg官方的插件做例子https://github.com/x64dbg/ScyllaHide

找到x64dbg插件的代码 ScyllaHide\InjectorCLI\ApplyHooking.cpp
在其中插入一段新的代码。

void AddWineFunctionName(HANDLE hProcess)
{
BYTE* remote_ntdll = (BYTE*)GetModuleBaseRemote(hProcess, L"ntdll.dll");
// check input
if (!remote_ntdll)
return;

SIZE\_T readed = 0;  
// check module's header  
IMAGE\_DOS\_HEADER dos\_header;  
ReadProcessMemory(hProcess, remote\_ntdll, &dos\_header, sizeof(IMAGE\_DOS\_HEADER), &readed);  
if (dos\_header.e\_magic != IMAGE\_DOS\_SIGNATURE)  
    return;  

// check NT header  
IMAGE\_NT\_HEADERS pe\_header;  
ReadProcessMemory(hProcess, (BYTE\*)remote\_ntdll + dos\_header.e\_lfanew, &pe\_header, sizeof(IMAGE\_NT\_HEADERS), &readed);  
if (pe\_header.Signature != IMAGE\_NT\_SIGNATURE)  
    return;  

// get the export directory  
DWORD export\_adress = pe\_header.OptionalHeader.DataDirectory\[IMAGE\_DIRECTORY\_ENTRY\_EXPORT\].VirtualAddress;  
if (!export\_adress)  
    return;  

DWORD export\_size = pe\_header.OptionalHeader.DataDirectory\[IMAGE\_DIRECTORY\_ENTRY\_EXPORT\].Size;  

BYTE\* new\_export\_table = (BYTE\*)VirtualAllocEx(hProcess, remote\_ntdll + 0x1000000, export\_size + 0x1000, MEM\_COMMIT | MEM\_RESERVE, PAGE\_EXECUTE\_READWRITE);  

IMAGE\_EXPORT\_DIRECTORY export\_directory;  
ReadProcessMemory(hProcess, remote\_ntdll + export\_adress, &export\_directory, sizeof(IMAGE\_EXPORT\_DIRECTORY), &readed);  

BYTE\* tmp\_table = (BYTE\*)malloc(export\_size + 0x1000);  
if (tmp\_table == nullptr)return;  

//copy functions table  
BYTE\* new\_functions\_table = new\_export\_table;  
ReadProcessMemory(hProcess, remote\_ntdll + export\_directory.AddressOfFunctions, tmp\_table, export\_directory.NumberOfFunctions \* sizeof(DWORD), &readed);  
WriteProcessMemory(hProcess, new\_functions\_table, tmp\_table, export\_directory.NumberOfFunctions \* sizeof(DWORD), &readed);  
g\_log.LogInfo(L"\[VMPBypass\] new\_functions\_table: %p", new\_functions\_table);  

//copy ordinal table  
BYTE\* new\_ordinal\_table = new\_functions\_table + export\_directory.NumberOfFunctions \* sizeof(DWORD) + 0x100;  
ReadProcessMemory(hProcess, remote\_ntdll + export\_directory.AddressOfNameOrdinals, tmp\_table, export\_directory.NumberOfNames \* sizeof(WORD), &readed);  
WriteProcessMemory(hProcess, new\_ordinal\_table, tmp\_table, export\_directory.NumberOfNames \* sizeof(WORD), &readed);  
g\_log.LogInfo(L"\[VMPBypass\] new\_ordinal\_table: %p", new\_ordinal\_table);  
  
//copy name table  
BYTE\* new\_name\_table = new\_ordinal\_table + export\_directory.NumberOfNames \* sizeof(WORD) + 0x100;  
ReadProcessMemory(hProcess, remote\_ntdll + export\_directory.AddressOfNames, tmp\_table, export\_directory.NumberOfNames \* sizeof(DWORD), &readed);  
WriteProcessMemory(hProcess, new\_name\_table, tmp\_table, export\_directory.NumberOfNames \* sizeof(DWORD), &readed);  
g\_log.LogInfo(L"\[VMPBypass\] new\_name\_table: %p", new\_name\_table);  

free(tmp\_table);  
tmp\_table = nullptr;  

//setup new name  & name offset  
BYTE\* wine\_func\_addr = new\_name\_table + export\_directory.NumberOfNames \* sizeof(DWORD) + 0x100;  
WriteProcessMemory(hProcess, wine\_func\_addr, "wine\_get\_version\\x00", 17, &readed);  
DWORD wine\_func\_offset = (DWORD)(wine\_func\_addr - remote\_ntdll);  
WriteProcessMemory(hProcess, new\_name\_table + export\_directory.NumberOfNames \* sizeof(DWORD), &wine\_func\_offset, 4, &readed);  

//set fake ordinal  
WORD last\_ordinal = export\_directory.NumberOfNames;  
WriteProcessMemory(hProcess, new\_ordinal\_table + export\_directory.NumberOfNames \* sizeof(WORD), &last\_ordinal, 2, &readed);  

//set fake function offset  
BYTE\* query\_information\_process = reinterpret\_cast<BYTE\*>(GetProcAddress(hNtdll, "NtCurrentProcess"));  
DWORD function\_offset = (DWORD)(query\_information\_process - remote\_ntdll);  
WriteProcessMemory(hProcess, new\_functions\_table + export\_directory.NumberOfFunctions \* sizeof(DWORD), &function\_offset, 4, &readed);  

//setup new directory  
export\_directory.NumberOfNames++;  
export\_directory.NumberOfFunctions++;  

DWORD name\_table\_offset = (DWORD)(new\_name\_table - remote\_ntdll);  
export\_directory.AddressOfNames = name\_table\_offset;  

DWORD function\_talble\_offset = (DWORD)(new\_functions\_table - remote\_ntdll);  
export\_directory.AddressOfFunctions = function\_talble\_offset;  

DWORD ordinal\_table\_offset = (DWORD)(new\_ordinal\_table - remote\_ntdll);  
export\_directory.AddressOfNameOrdinals = ordinal\_table\_offset;  

//// change the offset of header data  
DWORD old\_prot;  
VirtualProtectEx(hProcess, remote\_ntdll + export\_adress, sizeof(IMAGE\_EXPORT\_DIRECTORY), PAGE\_EXECUTE\_READWRITE, &old\_prot);  
WriteProcessMemory(hProcess, remote\_ntdll + export\_adress, &export\_directory, sizeof(IMAGE\_EXPORT\_DIRECTORY), &readed);  
VirtualProtectEx(hProcess, remote\_ntdll + export\_adress, sizeof(IMAGE\_EXPORT\_DIRECTORY), old\_prot, &old\_prot);  

}

通过这段代码,复制了一份ntdll.dll的导出表,并在里边添加了wine_get_version的导出项,实际调用其实是调用的 NtCurrentProcess。
然后,调用这个函数。

然后编译产物,放到x64dbg插件目录。

至此,vmp的反调保护已经被我们从纯纯的 r3 bypass了(狗头)。

结语

加个导出表还是个比较简单的操作,vmp 想要兼容 wine 环境,但是又只进行了很简单的校验,而且源码又泄露了,这才给了我们可乘之机。

此外 vmp 源码种还有很多别的地方值得学习,这点下次有机会再说。

ps:我本次实验用的vmp版本是 最我能找到的最高版本的 3.8.4。

看雪ID:JoJoRun

https://bbs.kanxue.com/user-home-995602.htm

*本文为看雪论坛优秀文章,由 JoJoRun 原创,转载请注明来自看雪社区

# 往期推荐

1、Win10和Win11内存区域划分及动态随机的本质

2、Windows主机入侵检测与防御内核技术深入解析

3、反沙箱钓鱼远控样本分析

4、安全浏览器历史记录数据库解密算法逆向

5、APP小说VIP功能分析

球分享

球点赞

球在看

点击阅读原文查看更多

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2