文章总结: 本文是《从0开始开发VT调试器》系列的第二篇,主要介绍了如何在VT环境下拦截并处理Ring3层的#DB异常。作者通过修改VMCS的异常位图,使得当Guest触发#DB异常时,VMM(虚拟机监控程序)能够接管该异常。文中详细说明了处理流程,包括保存关键寄存器(如eflags、DR寄存器等)、注入自定义的Gueststub代码以执行vmcall指令,以及后续的现场恢复机制。此外,文章还讨论了KVA(KernelVirtualAddress)功能对代码注入的影响,并提供了相应的判断方法。 综合评分: 85 文章分类: 二进制安全,恶意软件,红队,渗透测试,逆向分析
从0开始开发VT调试器(二)
原创
CrazyHarb CrazyHarb
冲鸭安全
2026年3月29日 10:01 北京
从0开始开发VT调试器(二)
背景:
上一章中,我们完成了基础的VT环境搭建,我们已经可以从guest中拦截一些简单的指令跳转到host了,本期我们加快速度将对调试链路进行进一步编写
原理:
在Ring3层,我们经常会用到各种调试工具,例如vs、x64dbg等,他们的单步原理基本上就是两种异常——#DB、#BP,这两种异常在触发时(其实所有异常都会这么走),会触发IDT表中的1号中断和3号中断,中断触发后,会先触发调试器,也就是windbg、x64dbg等(First Chance); 如果调试器未处理,会自动分发到触发异常的进程的ntdll的RtlDispatchException,最后如果进程没有处理,会再次触发Ring3调试器
我们简单画一下异常处理的流程图,即:
VT下的异常流程:
通过原理已知在Ring3进程触发异常时,首先会通过触发#DB #BP,然后借助IDT进入windows内核,这便是我们可以触发VT调试的点,即,当触发#DB #BP时,VT host会接管异常,并分发到我们自己的调试器,当调试器处理完成后,VT负责注入异常或者继续执行
代码修改:
1. Exception map
首先,我们找到setup_vmcs中的针对_vt_vmcs_exceptionbitmap的写入值,这个字段主要的作用是它记录的值中如果某一位为1,那么当idt触发对应的中断时,会进入vt host,例如当#DB触发时,如果_vt_vmcs_exceptionbitmap的值为 1 << 1,那么#DB会进入VT的host的代码中,这里我们把exception_map的值改为 1 << 1
| | | — | | C++ uintptr_t exception_bitmap = 1 << 1; nerror |= __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_exceptionbitmap, exception_bitmap); |
2. VT处理异常代码
我们在代码中需要对_vt_exitreason_exceptionornmi进行处理,即vt_vmm_handleexception函数,在该函数中,我们需要对#DB进行判断,并打印一条日志,并按原属性进行注入事件,在我们修改的代码中可以看到,判断了type(Hardware)和vector(#DB),这个type和vector是一一对应的,具体值需要参考intel的手册,这里就不再展开了
| | | — | | C++ void vt_vmm_handleexception(_vt_vmhandle_guestcontext* guest_context) { size_t exit_exception_value = 0; __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_vmexitintrinfo, &exit_exception_value); const _vt_vmexit_interruptioninformationfield exception = { exit_exception_value }; const _vt_interruption_type interruption_type = (_vt_interruption_type)(exception.fields.interruption_type); const _vt_interruption_vector vector = (_vt_interruption_vector)(exception.fields.vector); ULONG_PTR guest_inst_length; __vmx_vmread((ULONG_PTR)_vt_vmcs_field::_vt_vmcs_vmexitinstructionlen, &guest_inst_length); ULONG_PTR error_code = 0; __vmx_vmread((ULONG32)_vt_vmcs_field::_vt_vmcs_vmexitintrerrorcode, &error_code); if (interruption_type == _vt_interruption_type::_vt_interruption_hardwareexception) { // Hardware exception if (vector == _vt_interruption_vector::_vt_intteruptionvec_debugexception) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, “GetInt1 %p\n”, PsGetCurrentProcessId()); vt_vmm_injectinterruption(_vt_interruption_type::_vt_interruption_hardwareexception, vector, exception.fields.error_code_valid, error_code); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_vmentryinstructionlen, guest_inst_length); } else { vt_vmm_injectinterruption(interruption_type, vector, exception.fields.error_code_valid, error_code); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_vmentryinstructionlen, guest_inst_length); } } else { vt_vmm_injectinterruption(interruption_type, vector, exception.fields.error_code_valid, error_code); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_vmentryinstructionlen, guest_inst_length); } } |
3. 代码测试
我们将驱动在测试环境中加载起来,此时我们需要触发#DB,才能在驱动的输出中看是否拦截到了,所以,在这里需要打开调试器对文件进行单步,笔者使用了x64dbg对一个exe进行调试,可以看到,笔者单步两次后,debug中是可以打印出对应的日志的,证明此时host拦截到正确的数据了
Guest代码转向
现在,我们开始编写guest代码,guest的流程主要为,host注入guest转向,guest代码跳转到我们自己的stub上,stub最后触发host处理,最后host恢复环境,然后ring3继续执行,这里触发host处理,我们用vmcall指令即可
首先,我们在汇编中添加:
| | | — | | C++ asm_dbg_entry proc mov rcx, 1340h vmcall int 3 ;不该执行到这里 asm_dbg_entry endp |
其次,我们在#DB中添加注入RIP的代码
| | | — | | C++ …. if (interruption_type == _vt_interruption_type::_vt_interruption_hardwareexception) { // Hardware exception if (vector == _vt_interruption_vector::_vt_intteruptionvec_debugexception) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, “GetInt1 %p\n”, PsGetCurrentProcessId()); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestrip, (ULONG_PTR)&asm_dbg_entry); } else { vt_vmm_injectinterruption(interruption_type, vector, exception.fields.error_code_valid, error_code); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_vmentryinstructionlen, guest_inst_length); } |
KVA功能的影响
在注入代码后,我们需要考虑的一个问题,就是KVA的问题,如果windows启动了KVA,那么我们还需要更改cr3到kernel态下的CR3,毕竟shadow Cr3只有当前进程的ring3地址,以及ntkernel中的.KVSCode段的地址,我们的驱动不在这个段中,所以需要切CR3;如果没有开启,那么就不需要切了,可以用下面的ps脚本进行判断
| | | — | | C++ Install-Module SpeculationControl -Force Import-Module SpeculationControl Get-SpeculationControlSettings |
笔者的电脑,执行后发现没有开启KVA,查阅相关资料发现在最新的CPU上,已经修复了这个漏洞,所以,windows不再开启了
需要保留的寄存器:
以上的代码展示了核心部分,但不是完整流程,因为在vmcall后,host需要恢复现场内容,所以需要将现场数据进行保存
1. eflags寄存器
该寄存器必然会被修改,因为至少tf位会被置为0,否则当注入guest后,会再次触发#DB,最后走入异常状态,我们根据Intel手册中对eflags的描述,我们将Eflags置为2,原始值保存
2. DR寄存器
和eflags的理由相同,当启动Dr时,如果地址被ring3精心构造,那么我们会在stub的某个地方触发#DB异常,导致异常状态,我们把Dr7改为0x400,屏蔽掉其他的Dr寄存器,避免触发
3. Guest context寄存器
包含cs和ss、rip、rsp、rcx,为了在vmcall的时候直接恢复,所以这几个段需要保存,这里可能会有读者有些疑问,为什么rcx也需要保存呢?因为我们在再次进入host时,用的vmcall,为了判断编号,所以用的时rcx,所以这个rcx是需要保存的
4. guest_exceptionreason、error_code、指令长度等
这些都和guest注入异常有关系,所以也需要进行保存,这样可以避免后续未处理时,注入错了异常
我们总结一下整体的结构
| | | — | | C++ struct DBG_Stack { ULONG_PTR Dr7; ULONG_PTR guest_inst_length; ULONG_PTR exception_type; ULONG_PTR exception_vector; ULONG_PTR csbase; ULONG_PTR cslimit; ULONG_PTR csselector; ULONG_PTR ssselector; ULONG_PTR csarbytes; ULONG_PTR ssbase; ULONG_PTR sslimit; ULONG_PTR ssarbytes; ULONG_PTR ip; ULONG_PTR eflags; ULONG_PTR sp; ULONG_PTR original_rcx; }; |
申请并填充栈
为了保存我们需要的数据,我们需要将我们的数据保存在栈上,并为了避免返回Ring3的时候,Ring3找到相关的数据,所以我们需要自己申请一块地址,保存context
| | | — | | C++ PHYSICAL_ADDRESS phys = { 0 }; phys.QuadPart = ~0ULL; auto stack = (ULONG_PTR)MmAllocateContiguousMemory(PAGE_SIZE, phys); DBG_Stack* guest_stack = (DBG_Stack*)(stack + PAGE_SIZE – sizeof(DBG_Stack)); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestdr7, &guest_stack->Dr7); guest_stack->guest_inst_length = guest_inst_length; guest_stack->exception_type = (ULONG_PTR)interruption_type; guest_stack->exception_vector = (ULONG_PTR)vector; guest_stack->ip = (ULONG_PTR)guest_context->ip; guest_stack->eflags = guest_context->flag_reg.all; guest_stack->sp = (ULONG_PTR)guest_context->stack->gp_regs.Rsp; guest_stack->original_rcx = guest_context->stack->gp_regs.Rcx; __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestcsbase, &guest_stack->csbase); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestssbase, &guest_stack->ssbase); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestcsarbytes, &guest_stack->csarbytes); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestssarbytes, &guest_stack->ssarbytes); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestcslimit, &guest_stack->cslimit); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestsslimit, &guest_stack->sslimit); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestcsselector, &guest_stack->csselector); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestssselector, &guest_stack->ssselector); __vmx_vmread((size_t)_vt_vmcs_field::_vt_vmcs_guestdr7, &guest_stack->Dr7); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestcsselector, asm_readcs()); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestssselector, asm_readss()); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestcsarbytes, vmx_getsegment_accessright(asm_readcs())); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestssarbytes, vmx_getsegment_accessright(asm_readss())); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestcslimit, __segmentlimit(asm_readcs())); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestsslimit, __segmentlimit(asm_readss())); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestdr7, 0x400); __vmx_vmwrite((size_t)_vt_vmcs_field::_vt_vmcs_guestrflags, 2); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestrsp, (ULONG_PTR)guest_stack); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestrip, (ULONG_PTR)&asm_dbg_entry); |
VMCALL代码的实现:
首先,我们先针对VMACALL做一下接管,之前的逻辑是直接注入GP异常,现在不需要了
| | | — | | C++ void vt_vmm_handlevmexit(_vt_vmhandle_guestcontext* guest_context) { …. case _vt_vmx_exitreason::_vt_exitreason_vmcall: vt_vmm_handle_vmcall(guest_context); break; } |
我们开始继续封装handle_vmcall函数,当得到rcx为0x1340时,我们直接注入系统异常,后续ring0、ring3的调试器就可以接到内容了
| | | — | | C++ #define vmcall_db_dispatch_system 0x1340 void vt_vmm_handle_vmcall(_vt_vmhandle_guestcontext* guest_context) { const auto hypercall_number = guest_context->stack->gp_regs.Rcx; switch (hypercall_number) { case vmcall_db_dispatch_system: vt_vmm_handle_DB_exception(guest_context, guest_context->stack->gp_regs.Rsp, true); break; default: break; } } |
我们继续封装vt_vmm_handle_DB_exception用于处理相关逻辑,其中封装了一个vt_vmm_restore_context函数,用于处理堆栈上的数据恢复guest的状态,段、栈、ip、flags、原始rcx等
| | | — | | C++ void vt_vmm_restore_context(_vt_vmhandle_guestcontext* guest_context, ULONG_PTR guestRsp, bool dispatch_into_system, ULONG_PTR& exception_reason, ULONG_PTR& error_code) { DBG_Stack* dbgFrame = (DBG_Stack*)guestRsp; guest_context->stack->gp_regs.Rcx = dbgFrame->original_rcx; exception_reason = (ULONG_PTR)dbgFrame->exception_type; __vmx_vmwrite((ULONG_PTR)_vt_vmcs_field::_vt_vmcs_guestrip, (ULONG_PTR)dbgFrame->ip); __vmx_vmwrite((ULONG_PTR)_vt_vmcs_field::_vt_vmcs_guestcsselector, (ULONG_PTR)dbgFrame->csselector); __vmx_vmwrite((ULONG_PTR)_vt_vmcs_field::_vt_vmcs_guestrflags, (ULONG_PTR)dbgFrame->eflags); __vmx_vmwrite((ULONG_PTR)_vt_vmcs_field::_vt_vmcs_guestrsp, (ULONG_PTR)dbgFrame->sp); __vmx_vmwrite((ULONG_PTR)_vt_vmcs_field::_vt_vmcs_guestssselector, (ULONG_PTR)dbgFrame->ssselector); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_vmentryinstructionlen, dbgFrame->guest_inst_length); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestdr7, dbgFrame->Dr7); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestcslimit, dbgFrame->cslimit); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestcsarbytes, dbgFrame->csarbytes); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestsslimit, dbgFrame->sslimit); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestssarbytes, dbgFrame->ssarbytes); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestcsbase, dbgFrame->csbase); __vmx_vmwrite((ULONG32)_vt_vmcs_field::_vt_vmcs_guestssbase, dbgFrame->ssbase); } void vt_vmm_handle_DB_exception(_vt_vmhandle_guestcontext* guest_context, ULONG_PTR guestRsp, bool inject_into_system) { ULONG_PTR excetpion_reason; ULONG_PTR error_code; vt_vmm_restore_context(guest_context, guestRsp, inject_into_system, excetpion_reason, error_code); if (inject_into_system) { vt_vmm_injectinterruption(_vt_interruption_type::_vt_interruption_hardwareexception, _vt_interruption_vector::_vt_intteruptionvec_debugexception, false, error_code); DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, “vt_vmm_handle_DB_exception\n”); } } |
运行效果:
我们加载驱动后,打开x64dbg,并开始调试任意exe,我们在代码中,接到#DB异常时,打印了一条GetInt,在vmcall恢复的时候,打印了一条vt_vmm_handle_DB_exception,我们在x64dbg下执行两次单步后,发现windbg中日志已经可以正常打印了,切x64dbg下一切正常,没有任何的调试问题,我们再执行两次,发现rcx也是没有被变动的,证明,本次代码改动已经完成了
工程代码:
https://git.key08.com/CrazyHarb/VirtualizeDBG v0.0.2
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:冲鸭安全 CrazyHarb CrazyHarb《从0开始开发VT调试器(二)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。












评论