从0开始开发VT调试器(二)

admin 2026-04-02 05:05:20 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文是《从0开始开发VT调试器》系列的第二篇,主要介绍了如何在VT环境下拦截并处理Ring3层的#DB异常。作者通过修改VMCS的异常位图,使得当Guest触发#DB异常时,VMM(虚拟机监控程序)能够接管该异常。文中详细说明了处理流程,包括保存关键寄存器(如eflags、DR寄存器等)、注入自定义的Gueststub代码以执行vmcall指令,以及后续的现场恢复机制。此外,文章还讨论了KVA(KernelVirtualAddress)功能对代码注入的影响,并提供了相应的判断方法。 综合评分: 85 文章分类: 二进制安全,恶意软件,红队,渗透测试,逆向分析


cover_image

从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调试器(二)》

评论:0   参与:  0