【加壳脱壳-VMP源码分析:反调试与绕过方法】此文章归类为:加壳脱壳。
我们都知道,当vmp检测到被调试,会有如下弹框

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

然后通过它的消息传递机制,不难找到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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 3 4 5 6 7 | 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 3 4 | if (peb->BeingDebugged) { LoaderMessage(mtDebuggerFound); return LOADER_ERROR;} |
会心一笑,peb里的这个位就不用过多解释了
1 2 3 4 | 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 3 4 5 | if (NT_SUCCESS(reinterpret_cast<tNtQueryInformationProcess *>(syscall | sc_query_information_process)(process, ProcessDebugObjectHandle, &debug_object, sizeof(debug_object), reinterpret_cast<PULONG>(&debug_object))) || debug_object == 0) { LoaderMessage(mtDebuggerFound); return LOADER_ERROR;} |
查询 ProcessDebugObjectHandle, 如果 存在调试对象句柄,那也是被调试了,也属于常见反调
1 2 3 4 5 6 | 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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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 | if (sc_set_information_thread) reinterpret_cast<tNtSetInformationThread *>(syscall | sc_set_information_thread)(thread, ThreadHideFromDebugger, NULL, 0); |
对调试器隐藏了当前线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 breakpointuint8_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); }} |
1 2 3 4 5 6 7 | 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 3 4 5 6 7 | 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 3 4 5 6 7 8 9 10 11 12 | 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 3 4 5 6 7 8 9 10 11 12 13 14 | __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触发异常,然后在异常处理里检查硬件断点寄存器是否设置
至此,反调弹框部分基本看完了
vmp的反调试基本是一些常见的反调试手段,
其中比较棘手的是一些NT函数的调用,他使用了SYSCALL,通过自实现的系统调用规避了我们从 r3 hook 然后绕过的可能。
通过网上一顿检索,确实看到了不少从 r0 来过 vmp 反调的插件/工具/源码。
但是!难道!我们就只能上驱动了么? 它是r3却把我们逼到了r0,有没有纯纯的三环方法还能绕过他的呢?
答案当然是,当然存在(狗头),不然我也就不写这个分享了。
通过不死心的源码阅读,终于让我看到了这块代码

也就是关键的这一句
1 | LoaderGetProcAddress(ntdll, reinterpret_cast<const char *>(FACE_WINE_GET_VERSION_NAME), true) |
此时,小伙伴就会问了,VMP在搞啥?
这其实是 vmp 在给 wine 环境做兼容,如果发现 ntdll.dll 的导出表存在 wine_get_version 函数,则会关闭使用系统调用的特性!!!
关闭系统调用以后,那还不是随便我们hook?
所以理论上只要给 ntdll.dll 的导出表做点手脚即可
理论可行,开始动手
首先,重复造轮子的事不要做,针对vmp的那些常见的反调,已经有很多大佬写好插件并开源了,
我在这里拿这个x64dbg官方的插件做例子 https://github.com/x64dbg/ScyllaHide
找到x64dbg插件的代码 ScyllaHide\InjectorCLI\ApplyHooking.cpp
在其中插入一段新的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | 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
更多【加壳脱壳-VMP源码分析:反调试与绕过方法】相关视频教程:www.yxfzedu.com