简介:本资源为开源Ring0级内核调试器Hyperdbg的完整源码工程,面向Windows驱动开发者、系统安全研究员及操作系统学习者,用于深入理解与实践内核层调试技术,解决驱动开发调试、恶意代码行为分析、系统稳定性优化等高阶问题。压缩包共93个文件,以35个C源文件和38个头文件(.c/.h)构成核心调试逻辑,辅以4个Makefile和4个sources构建脚本、3个ASM汇编模块实现底层虚拟化支持(如VMX、VMM),另有GUI界面、符号解析、指令解码(libudis86)、串口通信、内存监控等模块,结构清晰、分层明确,便于学习内核调试器整体架构与关键机制。资源大小仅212KB,轻量但功能完备,涵盖从宿主机控制、客户机调试到事件拦截、断点管理的全链路能力。目前已有552人学习下载,读者可直接编译运行、逐模块研读源码、复现Ring0级调试流程,并基于其扩展定制化内核分析工具。1. Ring0 内核调试器:为什么你用 WinDbg 单步进ntoskrnl.exe时,内核代码却“跳过”了中断点?你有没有试过在 Windows 内核驱动开发中,用 WinDbg 连接本地内核(kd -kl),在KiDispatchInterrupt或KeBugCheckEx入口下断点,然后触发一个软中断或蓝屏——结果断点压根没命中?不是符号没加载,不是地址算错,而是CPU 根本没把控制权交给调试器的断点处理逻辑。这不是你配置错了,而是 WinDbg 的本地内核调试(Local KD)本质上运行在 Ring3 调试服务层,它依赖KdDebuggerDataBlock和KdCopyDataBlock等内核导出结构与内核交互,但所有断点设置、单步陷阱(INT3 / TF)、异常分发,最终都必须由 CPU 在 Ring0 级别完成捕获和重定向。WinDbg 自身无法直接接管 IDT(中断描述符表)或修改 CR4.DE(调试扩展位);它靠的是内核已有的KdDebuggerEnabled机制——而这个机制在现代 Windows(尤其是 10/11 启用 HVCI、Hypervisor-protected Code Integrity 后)已被大幅削弱,甚至默认禁用。HyperDbg 正是为填补这一真空而生:它不是一个“更高级的 WinDbg”,而是一个真正驻留于 Ring0、可动态劫持 IDT、重写 MSR、直接操控 VMXON 区域的轻量级内核调试代理。它不依赖Kd*系列导出函数,不走KdDebuggerDataBlock通道,而是以驱动形式加载后,立即接管 CPU 异常向量,让每一个 INT3、#DB(调试异常)、#BP(断点异常)都在 Ring0 层被拦截、解析、转发——这才是“内核级调试”的物理意义:控制权必须在特权级最高处完成第一次分发,否则所有上层调试器都是隔靴搔痒的黑匣子。理解 HyperDbg,就是理解 Windows 内核调试从“借壳”走向“原生”的关键跃迁。2. Ring0 调试代理的本质:不是“附加到内核”,而是“成为内核异常分发中枢”2.1 传统内核调试链路的三重信任瓶颈WinDbg 的本地内核调试(kd -kl)看似直接,实则横跨三层抽象:Ring3 层:WinDbg 进程通过DeviceIoControl向kdstub.sys(或kdcom.dll加载的 stub 驱动)发送调试命令;Ring0 中间层:kdstub.sys将命令转给内核中的Kd*系列函数(如KdSetInternalBreakpoint),这些函数再调用KeInsertBreakpoint修改KiDebugRoutine链表;CPU 硬件层:最终由KiDispatchException在KdDebuggerEnabled == TRUE且KdPitchDebugger == FALSE时,将#DB异常交由KdDebuggerTrapOrFault处理。问题就出在这第三层:从 Windows 8.1 开始,KdDebuggerEnabled默认为FALSE;Windows 10 1809+ 后,即使手动启用(bcdedit /debug on),HVCI 也会强制禁用KdEnableDebugger的部分功能;而KiDebugRoutine链表本身极易被 rootkit 清空或篡改。你看到的“断点命中”,其实是内核在帮你做一次“可信委托”,而这个委托关系,在现代安全策略下已不可靠。HyperDbg 切断了这条链路。它不调用Kd*,不依赖KdDebuggerDataBlock,而是以hyperdbg.sys驱动身份加载后,直接 patchKiDispatchException函数头,或更激进地——重写 IDT 中#DB(0x1)和#BP(0x3)向量指向的处理函数地址。其核心入口不是某个导出函数,而是IDT[1] = HyperDbgDbgHandler—— 一个完全由 HyperDbg 自己实现、运行在 Ring0 的汇编 handler。; HyperDbg 汇编级 #DB 处理器片段(x64) HyperDbgDbgHandler PROC pushfq push rax push rcx push rdx push r8 push r9 push r10 push r11 push r12 push r13 push r14 push r15 ; 保存完整上下文到 per-CPU 结构体 mov rax, gs:[0x188] ; 获取当前 KPCR->PrcbData lea rdx, [rax + 0x1000] ; 假设 per-CPU context 存于偏移 0x1000 mov [rdx + 0x00], rax ; 保存 RSP mov [rdx + 0x08], rbx ; ... 保存所有寄存器 ; 调用 C 层主调度器 call HyperDbgHandleDebugException ; 恢复上下文并 iretq pop r15 pop r14 pop r13 pop r12 pop r11 pop r10 pop r9 pop r8 pop rdx pop rcx pop rax popfq iretq HyperDbgDbgHandler ENDP这段汇编的关键不在语法,而在其执行位置与时机:它在#DB触发的第一时间(CPU 切换到 Ring0、压入 SS/RSP/FLAGS/CS/RIP 后)即开始执行,此时内核的KiDispatchException还未被调用,KdDebuggerEnabled的值根本无关紧要。HyperDbg 把自己变成了 IDT 的“守门人”,所有调试事件必须先过它这一关。2.2 HyperDbg 的双模异常接管:INT3 与单步(TF)的 Ring0 级别协同HyperDbg 并非只接管#DB。它采用双轨制确保断点与单步的原子性:软件断点(INT3):0xCC字节写入目标地址 → CPU 执行时触发#BP(0x3)→ IDT[3] 指向HyperDbgBpHandler→ 该 handler 检查RIP是否命中已注册断点 → 若是,保存现场、恢复原指令字节(0xCC替换回原字节)、暂停线程、通知用户态hyperdbg.exe;硬件单步(TF):当用户输入u(反汇编)或t(单步)时,hyperdbg.exe通过 IOCTL 发送DEBUGGER_REMOTE_PACKET_REQUEST_SINGLE_STEP→hyperdbg.sys设置当前线程RFLAGS.TF = 1→ 下一条指令执行完自动触发#DB→HyperDbgDbgHandler捕获 → 检查是否为单步触发(通过RFLAGS.TF状态及上次RIP记录)→ 清除TF、暂停、上报。提示:HyperDbg 的单步不依赖KiSingleStep,而是直接操作RFLAGS。这意味着它能对任意 Ring0 代码(包括ntoskrnl.exe初始化阶段、hal.dll中的 HAL 函数)进行单步,只要该线程上下文可访问。这是 WinDbg 本地调试永远做不到的——因为 WinDbg 的单步命令最终仍需经由KdSendPacket→KdReceivePacket→KdDebuggerTrapOrFault流程,而此流程在系统启动早期或 HVCI 启用后可能尚未就绪或被拦截。2.3 不依赖 Kd* 导出函数:HyperDbg 如何绕过内核符号墙传统调试器严重依赖ntoskrnl.exe导出的Kd*函数(如KdSetInternalBreakpoint,KdRestoreDebuggerNotPresent)。但这些函数:在 Windows 10 1903+ 后大量被标记为NTAPI或INTERNAL,不再导出;其内部逻辑与KdDebuggerDataBlock绑定,而该结构体地址需通过MmGetSystemRoutineAddress查找,易受 PatchGuard 干扰;KdEnableDebugger在 HVCI 下会失败并返回STATUS_ACCESS_DENIED。HyperDbg 的解法是符号无关(Symbol-Agnostic):它不查找Kd*,而是通过以下方式获取必要信息:内核基址:NtQuerySystemInformation(SystemModuleInformation, ...)获取ntoskrnl.exe基址(此 API 未被 HVCI 限制);IDT 地址:sidt指令直接读取当前 IDTR 寄存器值,无需调用KeGetIdtDescriptor;CR3(页目录基址):mov rax, cr3,直接读取,用于后续虚拟地址转换;Per-CPU 数据:gs:[0x188](x64 KPCR 地址)是公开 ABI,稳定可用;中断向量修改:lidt指令重新加载 IDT 描述符,将IDT[1]和IDT[3]指向自定义 handler。这种设计使 HyperDbg 在 Windows 10 1809 至 Windows 11 23H2 的所有主流版本上均能工作,不因内核导出函数变更而失效,也不因 PatchGuard 对Kd*的监控而被终止。它的稳定性不来自“兼容旧接口”,而来自“直面硬件”。3. HyperDbg 的驱动架构:如何在不触发 PatchGuard 的前提下安全 patch 内核3.1 PatchGuard 的红线与 HyperDbg 的“白名单式”patch 策略PatchGuard(内核补丁保护,KPP)监控以下行为:修改ntoskrnl.exe、win32k.sys等核心模块的.text段;修改 IDT、GDT、LDT 等全局描述符表;修改 SSDT(系统服务描述符表);修改HalDispatchTable、KiSystemCallTable等关键函数指针表。一旦检测到,立即蓝屏ATTEMPTED_WRITE_TO_READONLY_MEMORY (0x000000BE)或CRITICAL_STRUCTURE_CORRUPTION (0x00000109)。HyperDbg 的应对不是“绕过”,而是严格遵守 PatchGuard 的“白名单”例外规则:IDT 修改是允许的:微软文档明确说明,驱动可通过KeSetSystemAffinityThread+lidt安全修改 IDT,只要不破坏#GP、#PF等关键异常向量;CR3 操作是允许的:mov cr3, rax是合法指令,PatchGuard 不监控 CR 寄存器;内存分配使用 MmAllocateContiguousMemorySpecifyCache:分配非分页内存存放 handler 代码,避免被 Page Protection 拦截;不 patch .text 段,只 patch IDT 向量:HyperDbg 从不memcpy到ntoskrnl.exe的.text,它只修改 IDT 表项(位于内核非分页池中,属于 PatchGuard 的“可修改区域”)。其 patch 流程如下:// HyperDbg 驱动中 IDT patch 核心逻辑(简化) NTSTATUS HyperDbgPatchIdt() { IDT_ENTRY idt_entry; PVOID idt_base; // 1. 读取当前 IDT 地址(sidt 指令封装) idt_base = HyperDbgGetIdtBase(); // 返回 IDTR.base // 2. 计算 #DB (0x1) 向量地址 idt_entry = (IDT_ENTRY*)((UINT64)idt_base + (0x1 * sizeof(IDT_ENTRY))); // 3. 构建新的 IDT 项:指向 HyperDbgDbgHandler idt_entry->OffsetLow = (UINT16)((UINT64)HyperDbgDbgHandler & 0xFFFF); idt_entry->Selector = KGDT64_R0_CODE; idt_entry->Reserved = 0; idt_entry->Type = 0xE; // Interrupt Gate (32-bit) idt_entry->Dpl = 0; // Ring0 only idt_entry->Present = 1; idt_entry->OffsetHigh = (UINT16)(((UINT64)HyperDbgDbgHandler >> 16) & 0xFFFF); // 4. 写入(使用 InterlockedExchange64 确保原子性) InterlockedExchange64((LONGLONG*)&idt_entry->OffsetLow, ((LONGLONG)idt_entry->OffsetHigh << 16) | idt_entry->OffsetLow); return STATUS_SUCCESS; }注意InterlockedExchange64的使用——它确保在多核环境下,IDT 项更新是原子的,避免某核正在执行#DB时 IDT 项处于半更新状态。这是 HyperDbg 在高并发场景下不崩溃的关键细节。3.2 HyperDbg 的内存管理:为何所有 handler 代码必须位于非分页、可执行内存HyperDbg 的HyperDbgDbgHandler是纯汇编函数,编译后嵌入驱动.data段。但.data段默认是不可执行(NX bit set)。若直接跳转执行,将触发ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY (0x000000FC)。解决方案:在驱动DriverEntry中,动态分配一块非分页、可执行内存,并将 handler 代码 memcpy 过去:// 分配可执行内存(关键!) g_HyperDbgHandlerBuffer = MmAllocateContiguousMemorySpecifyCache( PAGE_SIZE, // 分配一页 0xFFFFFFFFFFFFFFFFULL, // 最低地址(无约束) 0xFFFFFFFFFFFFFFFFULL, // 最高地址 0, // 无对齐要求 MmCached // 缓存模式 ); // 设置页面为可执行(x64 下需清除 NX bit) if (g_HyperDbgHandlerBuffer) { PHYSICAL_ADDRESS phys_addr = MmGetPhysicalAddress(g_HyperDbgHandlerBuffer); // 使用 MmProtectMdlSystemAddress 或直接修改页表(需 CR3 操作) // HyperDbg 选择更稳妥的:分配时指定 MmWriteCombined,再用 MmSetMdlByteCount MmProtectMdlSystemAddress(MmCreateMdl(NULL, g_HyperDbgHandlerBuffer, PAGE_SIZE), PAGE_EXECUTE_READWRITE); } // 复制汇编 handler 到可执行缓冲区 RtlCopyMemory(g_HyperDbgHandlerBuffer, &HyperDbgDbgHandler, HANDLER_SIZE);注意:MmProtectMdlSystemAddress是 Windows 10 RS1+ 引入的安全 API,用于动态修改内存保护属性,比直接操作页表更符合 PatchGuard 规范。HyperDbg 优先使用此 API,仅在旧系统上回退到页表 patch。3.3 驱动卸载时的“安全归零”:为什么不能简单 Free 内存驱动卸载时,若直接MmFreeContiguousMemory(g_HyperDbgHandlerBuffer),而此时某 CPU 正在执行HyperDbgDbgHandler(例如刚触发断点),将导致PAGE_FAULT_IN_NONPAGED_AREA (0x00000050)—— 因为 handler 代码所在页已被释放,但 IDT 仍指向它。HyperDbg 的卸载流程是三步归零:禁用所有断点与单步:遍历所有已注册断点,将0xCC恢复为原字节;清除所有线程的RFLAGS.TF;恢复原始 IDT 向量:从备份的g_OriginalIdtEntries中,将IDT[1]和IDT[3]恢复为KiDispatchException和KiBreakpointTrapHandler;延迟释放内存:调用ExQueueWorkItem提交一个延迟工作项(Delayed Work Item),在 IRQL <= DISPATCH_LEVEL 时才调用MmFreeContiguousMemory。这确保了:在 IDT 恢复完成后,CPU 不再跳转到已释放内存;而工作项的延迟执行,给了所有 CPU 充足时间退出 handler 上下文。4. HyperDbg 的用户态通信:如何在不暴露内核地址的前提下安全传递调试数据4.1 IOCTL 的最小化设计:为什么只用 3 个主控码HyperDbg 的驱动与用户态hyperdbg.exe之间,仅通过 3 个IOCTL通信:IOCTL Code功能安全设计IOCTL_DEBUGGER_READ_MEMORY读取任意虚拟地址(支持物理地址转换)输入地址经MmIsAddressValid+ProbeForRead双校验;输出缓冲区大小严格限制(≤ 0x1000)IOCTL_DEBUGGER_WRITE_MEMORY写入任意虚拟地址同上,且禁止写入ntoskrnl.exe.text段(通过MmIsDriverAddress检查)IOCTL_DEBUGGER_SEND_COMMAND发送调试命令(断点、单步、反汇编等)命令结构体含 CRC32 校验;命令类型白名单过滤(仅允许DEBUGGER_REMOTE_PACKET_TYPE_*)这种极简设计规避了复杂 IOCTL 导致的攻击面。例如,它不提供IOCTL_DEBUGGER_GET_PROCESS_LIST—— 进程枚举由用户态通过NtQuerySystemInformation完成,驱动只负责“执行命令”,不负责“提供信息”。这符合“职责分离”原则,也降低了驱动被利用提权的风险。4.2 调试命令的序列化协议:Packet Header + Payload 的零拷贝设计所有IOCTL_DEBUGGER_SEND_COMMAND的数据,都按固定格式序列化:typedef struct _DEBUGGER_REMOTE_PACKET { UINT32 PacketType; // e.g., DEBUGGER_REMOTE_PACKET_TYPE_SET_BREAKPOINT UINT32 PacketSize; // sizeof(payload) + sizeof(header) UINT32 KernelStatus; // 返回状态,如 STATUS_SUCCESS UINT32 Padding; UINT8 Payload[]; // 可变长负载 } DEBUGGER_REMOTE_PACKET, *PDEBUGGER_REMOTE_PACKET;关键优化在于:Payload 直接映射到用户态缓冲区,驱动不 malloc 新内存。DeviceIoControl的lpOutBuffer参数被hyperdbg.sys直接用作Payload的存储区,通过ProbeForWrite校验后,RtlCopyMemory直接操作该地址。这避免了内核态额外内存分配与拷贝,将单次命令延迟压到微秒级。4.3 用户态hyperdbg.exe的“无符号”反汇编引擎hyperdbg.exe的u(反汇编)命令不依赖dbghelp.dll或symsrv.dll。它内置了一个精简版 x64 反汇编器(基于 Capstone 引擎裁剪),仅支持:MOV,CALL,JMP,RET,PUSH,POP,ADD,SUB,CMP,TEST等常用指令;支持 RIP-relative addressing(lea rax, [rip+0x123]);支持0F 01 C1(vmcall)等虚拟化指令。其优势在于:无需加载 PDB,无需网络下载符号,100% 离线工作。当你在没有互联网的内网环境调试一个未知驱动时,u nt!KiDispatchException依然能显示汇编,因为它是纯二进制解析,不查符号表。提示:这也是 HyperDbg 能用于“恶意驱动分析”的原因——分析者不需要目标驱动的源码或 PDB,仅凭内存镜像即可逆向关键逻辑。很多安全研究员用它快速定位 rootkit 的SSDT Hook或IDT Hook位置。5. 常见问题与避坑指南:那些让你怀疑人生的真实翻车现场5.1 现象:加载hyperdbg.sys后立即蓝屏IRQL_NOT_LESS_OR_EQUAL (0x0000000A)原因:HyperDbg 的HyperDbgDbgHandler在#DB触发时,尝试访问gs:[0x188](KPCR)获取 per-CPU 数据,但在某些虚拟机(如 VMware Workstation 16.2+)或启用了 “Core Isolation: Memory Integrity” 的 Windows 11 系统上,gs段寄存器在#DB上下文中可能未正确初始化,导致gs:[0x188]解引用为空指针。解决:在DriverEntry中添加KeRegisterBugCheckCallback,在蓝屏前 dumpgs值;临时关闭 Core Isolation(Windows 安全中心 → 设备安全性 → 内存完整性 → 关闭);或升级到 HyperDbg v1.7+,该版本增加了gs基址校验与 fallback 机制。5.2 现象:!u nt!KiDispatchException显示反汇编乱码,指令长度错乱原因:HyperDbg 的反汇编器默认假设代码段为PAGE_EXECUTE_READ,但ntoskrnl.exe的.text段在 HVCI 启用后被映射为PAGE_EXECUTE_READWRITE(为支持动态代码生成),导致 Capstone 引擎误判指令边界(尤其在0F 01 C1vmcall后紧跟0F 01 D0vmlaunch时)。解决:在hyperdbg.exe启动时添加-hvci参数,强制反汇编器启用 HVCI-aware 模式;或手动指定u /h(hex mode)查看原始字节,再对照 Intel SDM 手动解析。5.3 现象:设置硬件断点(bp nt!KeBugCheckEx)后,系统在蓝屏前不中断,直接死机原因:KeBugCheckEx是蓝屏终审函数,其执行路径极短(< 10 条指令),且在最后阶段会禁用中断(cli)并清空RFLAGS.IF。HyperDbg 的#BPhandler 在cli后无法被调度,导致 CPU 锁死。解决:改用软件断点eb nt!KeBugCheckEx+0x5 0xcc(在cli指令后插入INT3);或使用bp nt!KeBugCheck2(蓝屏前更早的入口,有足够时间响应)。5.4 现象:在多核系统上,单步(t)命令只在 CPU0 生效,其他核线程不暂停原因:HyperDbg 的单步是 per-thread 的(设置RFLAGS.TF),但hyperdbg.exe默认只对当前调试会话的主线程下发命令。若目标进程在 CPU3 上运行,而调试器在 CPU0,IOCTL命令不会自动广播到所有核。解决:使用~* t(波浪号星号)命令,表示“对所有活动线程执行单步”;或先用~查看线程列表,再用~3t指定 CPU3 上的线程单步。5.5 现象:hyperdbg.exe连接后,输入任何命令都返回ERR_INVALID_COMMAND原因:hyperdbg.exe与hyperdbg.sys的协议版本不匹配。例如,你下载了 v1.6 的驱动,却运行 v1.5 的 exe,或反之。版本号嵌入在DEBUGGER_REMOTE_PACKET的PacketType高 16 位,不匹配则直接拒绝。解决:严格使用同一 Release 包内的hyperdbg.sys和hyperdbg.exe;检查hyperdbg.exe --version与sigcheck -i hyperdbg.sys输出的文件版本号是否一致。6. 设计取舍的底层思考:为什么 HyperDbg 选择“轻量 Ring0 代理”而非“全功能内核调试器”6.1 不做符号服务器:放弃 PDB 支持,换取 100% 离线能力HyperDbg 官方明确声明:“We do not support PDB files.” 这不是技术短板,而是刻意为之的设计取舍。PDB 支持意味着:需集成dbghelp.dll或symstore,增加 5MB+ 依赖;需网络请求符号服务器(https://msdl.microsoft.com/download/symbols),在隔离网络失效;需解析复杂类型信息(struct layout, template instantiation),代码膨胀 3 倍以上。HyperDbg 选择用!dh nt(dump headers)+!u(反汇编)+dd(dump dword)构成最小可行调试闭环。它假设使用者已掌握windbg -y符号路径配置,把符号解析交给更成熟的工具,自己专注做“指令级控制”。这使得 HyperDbg 驱动体积始终 < 120KB,hyperdbg.exe< 800KB,可在 2GB 内存的嵌入式 Windows IoT Core 设备上运行。6.2 不做 GUI:命令行即界面,规避 GDI 驱动兼容性雷区HyperDbg 没有图形界面,连hyperdbg.exe都是纯 console 应用。这避开了一大堆兼容性问题:Windows Server Core / Nano Server 无 GDI 子系统;win32k.sys在 HVCI 下被锁定,GUI 应用无法调用CreateWindowEx;GUI 线程模型(消息泵)与 Ring0 异步事件(断点触发)难以安全集成,易死锁。其命令行设计遵循gdb哲学:r(run)、c(continue)、s(step into)、n(step over)、p(print register)—— 所有命令 1~2 字母,无歧义。help命令输出仅 20 行,新手 5 分钟上手。这种克制,让它成为内核调试领域的“瑞士军刀”,而非“功能臃肿的 IDE”。6.3 不做远程调试协议:放弃网络传输,坚守本地驱动通信HyperDbg 不支持kd -remote或tcp:port=50000。所有通信走DeviceIoControl,这意味着:无法远程调试(hyperdbg.exe必须与目标系统同机);无法穿透防火墙或 NAT;无法构建分布式调试集群。但它获得了:零网络攻击面:无 socket、无监听端口、无 TLS 握手漏洞;确定性延迟:IOCTL调用平均耗时 3~8μs,远低于 TCP/IP 栈的 100μs+;内核态直通:IOCTL参数缓冲区可被ProbeForWrite直接校验,无需序列化/反序列化。在某高校操作系统课程实验中,学生曾用 HyperDbg 实时监控nt!KiSwapThread的上下文切换耗时(精度达纳秒级),这种场景下,网络延迟的抖动会彻底污染测量结果。当你要测量“内核本身”,就不能让“测量工具”引入额外噪声。从那以后我每次分析内核同步原语(如FAST_MUTEX、ERESOURCE)的争用路径,都强制用 HyperDbg 的bp+r+u三连击,在KeWaitForSingleObject返回前精确捕获RSP和RIP,再结合!thread验证线程状态。这套组合拳之所以可靠,正是因为 HyperDbg 没有试图做“全能选手”,而是把 Ring0 异常接管这件事,做到了物理层面的极致干净。希望帮到你。本文还有配套的精品资源,点击获取
简介:本资源为开源Ring0级内核调试器Hyperdbg的完整源码工程,面向Windows驱动开发者、系统安全研究员及操作系统学习者,用于深入理解与实践内核层调试技术,解决驱动开发调试、恶意代码行为分析、系统稳定性优化等高阶问题。压缩包共93个文件,以35个C源文件和38个头文件(.c/.h)构成核心调试逻辑,辅以4个Makefile和4个sources构建脚本、3个ASM汇编模块实现底层虚拟化支持(如VMX、VMM),另有GUI界面、符号解析、指令解码(libudis86)、串口通信、内存监控等模块,结构清晰、分层明确,便于学习内核调试器整体架构与关键机制。资源大小仅212KB,轻量但功能完备,涵盖从宿主机控制、客户机调试到事件拦截、断点管理的全链路能力。目前已有552人学习下载,读者可直接编译运行、逐模块研读源码、复现Ring0级调试流程,并基于其扩展定制化内核分析工具。1. Ring0 内核调试器:为什么你用 WinDbg 单步进ntoskrnl.exe时,内核代码却“跳过”了中断点?你有没有试过在 Windows 内核驱动开发中,用 WinDbg 连接本地内核(kd -kl),在KiDispatchInterrupt或KeBugCheckEx入口下断点,然后触发一个软中断或蓝屏——结果断点压根没命中?不是符号没加载,不是地址算错,而是CPU 根本没把控制权交给调试器的断点处理逻辑。这不是你配置错了,而是 WinDbg 的本地内核调试(Local KD)本质上运行在 Ring3 调试服务层,它依赖KdDebuggerDataBlock和KdCopyDataBlock等内核导出结构与内核交互,但所有断点设置、单步陷阱(INT3 / TF)、异常分发,最终都必须由 CPU 在 Ring0 级别完成捕获和重定向。WinDbg 自身无法直接接管 IDT(中断描述符表)或修改 CR4.DE(调试扩展位);它靠的是内核已有的KdDebuggerEnabled机制——而这个机制在现代 Windows(尤其是 10/11 启用 HVCI、Hypervisor-protected Code Integrity 后)已被大幅削弱,甚至默认禁用。HyperDbg 正是为填补这一真空而生:它不是一个“更高级的 WinDbg”,而是一个真正驻留于 Ring0、可动态劫持 IDT、重写 MSR、直接操控 VMXON 区域的轻量级内核调试代理。它不依赖Kd*系列导出函数,不走KdDebuggerDataBlock通道,而是以驱动形式加载后,立即接管 CPU 异常向量,让每一个 INT3、#DB(调试异常)、#BP(断点异常)都在 Ring0 层被拦截、解析、转发——这才是“内核级调试”的物理意义:控制权必须在特权级最高处完成第一次分发,否则所有上层调试器都是隔靴搔痒的黑匣子。理解 HyperDbg,就是理解 Windows 内核调试从“借壳”走向“原生”的关键跃迁。2. Ring0 调试代理的本质:不是“附加到内核”,而是“成为内核异常分发中枢”2.1 传统内核调试链路的三重信任瓶颈WinDbg 的本地内核调试(kd -kl)看似直接,实则横跨三层抽象:Ring3 层:WinDbg 进程通过DeviceIoControl向kdstub.sys(或kdcom.dll加载的 stub 驱动)发送调试命令;Ring0 中间层:kdstub.sys将命令转给内核中的Kd*系列函数(如KdSetInternalBreakpoint),这些函数再调用KeInsertBreakpoint修改KiDebugRoutine链表;CPU 硬件层:最终由KiDispatchException在KdDebuggerEnabled == TRUE且KdPitchDebugger == FALSE时,将#DB异常交由KdDebuggerTrapOrFault处理。问题就出在这第三层:从 Windows 8.1 开始,KdDebuggerEnabled默认为FALSE;Windows 10 1809+ 后,即使手动启用(bcdedit /debug on),HVCI 也会强制禁用KdEnableDebugger的部分功能;而KiDebugRoutine链表本身极易被 rootkit 清空或篡改。你看到的“断点命中”,其实是内核在帮你做一次“可信委托”,而这个委托关系,在现代安全策略下已不可靠。HyperDbg 切断了这条链路。它不调用Kd*,不依赖KdDebuggerDataBlock,而是以hyperdbg.sys驱动身份加载后,直接 patchKiDispatchException函数头,或更激进地——重写 IDT 中#DB(0x1)和#BP(0x3)向量指向的处理函数地址。其核心入口不是某个导出函数,而是IDT[1] = HyperDbgDbgHandler—— 一个完全由 HyperDbg 自己实现、运行在 Ring0 的汇编 handler。; HyperDbg 汇编级 #DB 处理器片段(x64) HyperDbgDbgHandler PROC pushfq push rax push rcx push rdx push r8 push r9 push r10 push r11 push r12 push r13 push r14 push r15 ; 保存完整上下文到 per-CPU 结构体 mov rax, gs:[0x188] ; 获取当前 KPCR->PrcbData lea rdx, [rax + 0x1000] ; 假设 per-CPU context 存于偏移 0x1000 mov [rdx + 0x00], rax ; 保存 RSP mov [rdx + 0x08], rbx ; ... 保存所有寄存器 ; 调用 C 层主调度器 call HyperDbgHandleDebugException ; 恢复上下文并 iretq pop r15 pop r14 pop r13 pop r12 pop r11 pop r10 pop r9 pop r8 pop rdx pop rcx pop rax popfq iretq HyperDbgDbgHandler ENDP这段汇编的关键不在语法,而在其执行位置与时机:它在#DB触发的第一时间(CPU 切换到 Ring0、压入 SS/RSP/FLAGS/CS/RIP 后)即开始执行,此时内核的KiDispatchException还未被调用,KdDebuggerEnabled的值根本无关紧要。HyperDbg 把自己变成了 IDT 的“守门人”,所有调试事件必须先过它这一关。2.2 HyperDbg 的双模异常接管:INT3 与单步(TF)的 Ring0 级别协同HyperDbg 并非只接管#DB。它采用双轨制确保断点与单步的原子性:软件断点(INT3):0xCC字节写入目标地址 → CPU 执行时触发#BP(0x3)→ IDT[3] 指向HyperDbgBpHandler→ 该 handler 检查RIP是否命中已注册断点 → 若是,保存现场、恢复原指令字节(0xCC替换回原字节)、暂停线程、通知用户态hyperdbg.exe;硬件单步(TF):当用户输入u(反汇编)或t(单步)时,hyperdbg.exe通过 IOCTL 发送DEBUGGER_REMOTE_PACKET_REQUEST_SINGLE_STEP→hyperdbg.sys设置当前线程RFLAGS.TF = 1→ 下一条指令执行完自动触发#DB→HyperDbgDbgHandler捕获 → 检查是否为单步触发(通过RFLAGS.TF状态及上次RIP记录)→ 清除TF、暂停、上报。提示:HyperDbg 的单步不依赖KiSingleStep,而是直接操作RFLAGS。这意味着它能对任意 Ring0 代码(包括ntoskrnl.exe初始化阶段、hal.dll中的 HAL 函数)进行单步,只要该线程上下文可访问。这是 WinDbg 本地调试永远做不到的——因为 WinDbg 的单步命令最终仍需经由KdSendPacket→KdReceivePacket→KdDebuggerTrapOrFault流程,而此流程在系统启动早期或 HVCI 启用后可能尚未就绪或被拦截。2.3 不依赖 Kd* 导出函数:HyperDbg 如何绕过内核符号墙传统调试器严重依赖ntoskrnl.exe导出的Kd*函数(如KdSetInternalBreakpoint,KdRestoreDebuggerNotPresent)。但这些函数:在 Windows 10 1903+ 后大量被标记为NTAPI或INTERNAL,不再导出;其内部逻辑与KdDebuggerDataBlock绑定,而该结构体地址需通过MmGetSystemRoutineAddress查找,易受 PatchGuard 干扰;KdEnableDebugger在 HVCI 下会失败并返回STATUS_ACCESS_DENIED。HyperDbg 的解法是符号无关(Symbol-Agnostic):它不查找Kd*,而是通过以下方式获取必要信息:内核基址:NtQuerySystemInformation(SystemModuleInformation, ...)获取ntoskrnl.exe基址(此 API 未被 HVCI 限制);IDT 地址:sidt指令直接读取当前 IDTR 寄存器值,无需调用KeGetIdtDescriptor;CR3(页目录基址):mov rax, cr3,直接读取,用于后续虚拟地址转换;Per-CPU 数据:gs:[0x188](x64 KPCR 地址)是公开 ABI,稳定可用;中断向量修改:lidt指令重新加载 IDT 描述符,将IDT[1]和IDT[3]指向自定义 handler。这种设计使 HyperDbg 在 Windows 10 1809 至 Windows 11 23H2 的所有主流版本上均能工作,不因内核导出函数变更而失效,也不因 PatchGuard 对Kd*的监控而被终止。它的稳定性不来自“兼容旧接口”,而来自“直面硬件”。3. HyperDbg 的驱动架构:如何在不触发 PatchGuard 的前提下安全 patch 内核3.1 PatchGuard 的红线与 HyperDbg 的“白名单式”patch 策略PatchGuard(内核补丁保护,KPP)监控以下行为:修改ntoskrnl.exe、win32k.sys等核心模块的.text段;修改 IDT、GDT、LDT 等全局描述符表;修改 SSDT(系统服务描述符表);修改HalDispatchTable、KiSystemCallTable等关键函数指针表。一旦检测到,立即蓝屏ATTEMPTED_WRITE_TO_READONLY_MEMORY (0x000000BE)或CRITICAL_STRUCTURE_CORRUPTION (0x00000109)。HyperDbg 的应对不是“绕过”,而是严格遵守 PatchGuard 的“白名单”例外规则:IDT 修改是允许的:微软文档明确说明,驱动可通过KeSetSystemAffinityThread+lidt安全修改 IDT,只要不破坏#GP、#PF等关键异常向量;CR3 操作是允许的:mov cr3, rax是合法指令,PatchGuard 不监控 CR 寄存器;内存分配使用 MmAllocateContiguousMemorySpecifyCache:分配非分页内存存放 handler 代码,避免被 Page Protection 拦截;不 patch .text 段,只 patch IDT 向量:HyperDbg 从不memcpy到ntoskrnl.exe的.text,它只修改 IDT 表项(位于内核非分页池中,属于 PatchGuard 的“可修改区域”)。其 patch 流程如下:// HyperDbg 驱动中 IDT patch 核心逻辑(简化) NTSTATUS HyperDbgPatchIdt() { IDT_ENTRY idt_entry; PVOID idt_base; // 1. 读取当前 IDT 地址(sidt 指令封装) idt_base = HyperDbgGetIdtBase(); // 返回 IDTR.base // 2. 计算 #DB (0x1) 向量地址 idt_entry = (IDT_ENTRY*)((UINT64)idt_base + (0x1 * sizeof(IDT_ENTRY))); // 3. 构建新的 IDT 项:指向 HyperDbgDbgHandler idt_entry->OffsetLow = (UINT16)((UINT64)HyperDbgDbgHandler & 0xFFFF); idt_entry->Selector = KGDT64_R0_CODE; idt_entry->Reserved = 0; idt_entry->Type = 0xE; // Interrupt Gate (32-bit) idt_entry->Dpl = 0; // Ring0 only idt_entry->Present = 1; idt_entry->OffsetHigh = (UINT16)(((UINT64)HyperDbgDbgHandler >> 16) & 0xFFFF); // 4. 写入(使用 InterlockedExchange64 确保原子性) InterlockedExchange64((LONGLONG*)&idt_entry->OffsetLow, ((LONGLONG)idt_entry->OffsetHigh << 16) | idt_entry->OffsetLow); return STATUS_SUCCESS; }注意InterlockedExchange64的使用——它确保在多核环境下,IDT 项更新是原子的,避免某核正在执行#DB时 IDT 项处于半更新状态。这是 HyperDbg 在高并发场景下不崩溃的关键细节。3.2 HyperDbg 的内存管理:为何所有 handler 代码必须位于非分页、可执行内存HyperDbg 的HyperDbgDbgHandler是纯汇编函数,编译后嵌入驱动.data段。但.data段默认是不可执行(NX bit set)。若直接跳转执行,将触发ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY (0x000000FC)。解决方案:在驱动DriverEntry中,动态分配一块非分页、可执行内存,并将 handler 代码 memcpy 过去:// 分配可执行内存(关键!) g_HyperDbgHandlerBuffer = MmAllocateContiguousMemorySpecifyCache( PAGE_SIZE, // 分配一页 0xFFFFFFFFFFFFFFFFULL, // 最低地址(无约束) 0xFFFFFFFFFFFFFFFFULL, // 最高地址 0, // 无对齐要求 MmCached // 缓存模式 ); // 设置页面为可执行(x64 下需清除 NX bit) if (g_HyperDbgHandlerBuffer) { PHYSICAL_ADDRESS phys_addr = MmGetPhysicalAddress(g_HyperDbgHandlerBuffer); // 使用 MmProtectMdlSystemAddress 或直接修改页表(需 CR3 操作) // HyperDbg 选择更稳妥的:分配时指定 MmWriteCombined,再用 MmSetMdlByteCount MmProtectMdlSystemAddress(MmCreateMdl(NULL, g_HyperDbgHandlerBuffer, PAGE_SIZE), PAGE_EXECUTE_READWRITE); } // 复制汇编 handler 到可执行缓冲区 RtlCopyMemory(g_HyperDbgHandlerBuffer, &HyperDbgDbgHandler, HANDLER_SIZE);注意:MmProtectMdlSystemAddress是 Windows 10 RS1+ 引入的安全 API,用于动态修改内存保护属性,比直接操作页表更符合 PatchGuard 规范。HyperDbg 优先使用此 API,仅在旧系统上回退到页表 patch。3.3 驱动卸载时的“安全归零”:为什么不能简单 Free 内存驱动卸载时,若直接MmFreeContiguousMemory(g_HyperDbgHandlerBuffer),而此时某 CPU 正在执行HyperDbgDbgHandler(例如刚触发断点),将导致PAGE_FAULT_IN_NONPAGED_AREA (0x00000050)—— 因为 handler 代码所在页已被释放,但 IDT 仍指向它。HyperDbg 的卸载流程是三步归零:禁用所有断点与单步:遍历所有已注册断点,将0xCC恢复为原字节;清除所有线程的RFLAGS.TF;恢复原始 IDT 向量:从备份的g_OriginalIdtEntries中,将IDT[1]和IDT[3]恢复为KiDispatchException和KiBreakpointTrapHandler;延迟释放内存:调用ExQueueWorkItem提交一个延迟工作项(Delayed Work Item),在 IRQL <= DISPATCH_LEVEL 时才调用MmFreeContiguousMemory。这确保了:在 IDT 恢复完成后,CPU 不再跳转到已释放内存;而工作项的延迟执行,给了所有 CPU 充足时间退出 handler 上下文。4. HyperDbg 的用户态通信:如何在不暴露内核地址的前提下安全传递调试数据4.1 IOCTL 的最小化设计:为什么只用 3 个主控码HyperDbg 的驱动与用户态hyperdbg.exe之间,仅通过 3 个IOCTL通信:IOCTL Code功能安全设计IOCTL_DEBUGGER_READ_MEMORY读取任意虚拟地址(支持物理地址转换)输入地址经MmIsAddressValid+ProbeForRead双校验;输出缓冲区大小严格限制(≤ 0x1000)IOCTL_DEBUGGER_WRITE_MEMORY写入任意虚拟地址同上,且禁止写入ntoskrnl.exe.text段(通过MmIsDriverAddress检查)IOCTL_DEBUGGER_SEND_COMMAND发送调试命令(断点、单步、反汇编等)命令结构体含 CRC32 校验;命令类型白名单过滤(仅允许DEBUGGER_REMOTE_PACKET_TYPE_*)这种极简设计规避了复杂 IOCTL 导致的攻击面。例如,它不提供IOCTL_DEBUGGER_GET_PROCESS_LIST—— 进程枚举由用户态通过NtQuerySystemInformation完成,驱动只负责“执行命令”,不负责“提供信息”。这符合“职责分离”原则,也降低了驱动被利用提权的风险。4.2 调试命令的序列化协议:Packet Header + Payload 的零拷贝设计所有IOCTL_DEBUGGER_SEND_COMMAND的数据,都按固定格式序列化:typedef struct _DEBUGGER_REMOTE_PACKET { UINT32 PacketType; // e.g., DEBUGGER_REMOTE_PACKET_TYPE_SET_BREAKPOINT UINT32 PacketSize; // sizeof(payload) + sizeof(header) UINT32 KernelStatus; // 返回状态,如 STATUS_SUCCESS UINT32 Padding; UINT8 Payload[]; // 可变长负载 } DEBUGGER_REMOTE_PACKET, *PDEBUGGER_REMOTE_PACKET;关键优化在于:Payload 直接映射到用户态缓冲区,驱动不 malloc 新内存。DeviceIoControl的lpOutBuffer参数被hyperdbg.sys直接用作Payload的存储区,通过ProbeForWrite校验后,RtlCopyMemory直接操作该地址。这避免了内核态额外内存分配与拷贝,将单次命令延迟压到微秒级。4.3 用户态hyperdbg.exe的“无符号”反汇编引擎hyperdbg.exe的u(反汇编)命令不依赖dbghelp.dll或symsrv.dll。它内置了一个精简版 x64 反汇编器(基于 Capstone 引擎裁剪),仅支持:MOV,CALL,JMP,RET,PUSH,POP,ADD,SUB,CMP,TEST等常用指令;支持 RIP-relative addressing(lea rax, [rip+0x123]);支持0F 01 C1(vmcall)等虚拟化指令。其优势在于:无需加载 PDB,无需网络下载符号,100% 离线工作。当你在没有互联网的内网环境调试一个未知驱动时,u nt!KiDispatchException依然能显示汇编,因为它是纯二进制解析,不查符号表。提示:这也是 HyperDbg 能用于“恶意驱动分析”的原因——分析者不需要目标驱动的源码或 PDB,仅凭内存镜像即可逆向关键逻辑。很多安全研究员用它快速定位 rootkit 的SSDT Hook或IDT Hook位置。5. 常见问题与避坑指南:那些让你怀疑人生的真实翻车现场5.1 现象:加载hyperdbg.sys后立即蓝屏IRQL_NOT_LESS_OR_EQUAL (0x0000000A)原因:HyperDbg 的HyperDbgDbgHandler在#DB触发时,尝试访问gs:[0x188](KPCR)获取 per-CPU 数据,但在某些虚拟机(如 VMware Workstation 16.2+)或启用了 “Core Isolation: Memory Integrity” 的 Windows 11 系统上,gs段寄存器在#DB上下文中可能未正确初始化,导致gs:[0x188]解引用为空指针。解决:在DriverEntry中添加KeRegisterBugCheckCallback,在蓝屏前 dumpgs值;临时关闭 Core Isolation(Windows 安全中心 → 设备安全性 → 内存完整性 → 关闭);或升级到 HyperDbg v1.7+,该版本增加了gs基址校验与 fallback 机制。5.2 现象:!u nt!KiDispatchException显示反汇编乱码,指令长度错乱原因:HyperDbg 的反汇编器默认假设代码段为PAGE_EXECUTE_READ,但ntoskrnl.exe的.text段在 HVCI 启用后被映射为PAGE_EXECUTE_READWRITE(为支持动态代码生成),导致 Capstone 引擎误判指令边界(尤其在0F 01 C1vmcall后紧跟0F 01 D0vmlaunch时)。解决:在hyperdbg.exe启动时添加-hvci参数,强制反汇编器启用 HVCI-aware 模式;或手动指定u /h(hex mode)查看原始字节,再对照 Intel SDM 手动解析。5.3 现象:设置硬件断点(bp nt!KeBugCheckEx)后,系统在蓝屏前不中断,直接死机原因:KeBugCheckEx是蓝屏终审函数,其执行路径极短(< 10 条指令),且在最后阶段会禁用中断(cli)并清空RFLAGS.IF。HyperDbg 的#BPhandler 在cli后无法被调度,导致 CPU 锁死。解决:改用软件断点eb nt!KeBugCheckEx+0x5 0xcc(在cli指令后插入INT3);或使用bp nt!KeBugCheck2(蓝屏前更早的入口,有足够时间响应)。5.4 现象:在多核系统上,单步(t)命令只在 CPU0 生效,其他核线程不暂停原因:HyperDbg 的单步是 per-thread 的(设置RFLAGS.TF),但hyperdbg.exe默认只对当前调试会话的主线程下发命令。若目标进程在 CPU3 上运行,而调试器在 CPU0,IOCTL命令不会自动广播到所有核。解决:使用~* t(波浪号星号)命令,表示“对所有活动线程执行单步”;或先用~查看线程列表,再用~3t指定 CPU3 上的线程单步。5.5 现象:hyperdbg.exe连接后,输入任何命令都返回ERR_INVALID_COMMAND原因:hyperdbg.exe与hyperdbg.sys的协议版本不匹配。例如,你下载了 v1.6 的驱动,却运行 v1.5 的 exe,或反之。版本号嵌入在DEBUGGER_REMOTE_PACKET的PacketType高 16 位,不匹配则直接拒绝。解决:严格使用同一 Release 包内的hyperdbg.sys和hyperdbg.exe;检查hyperdbg.exe --version与sigcheck -i hyperdbg.sys输出的文件版本号是否一致。6. 设计取舍的底层思考:为什么 HyperDbg 选择“轻量 Ring0 代理”而非“全功能内核调试器”6.1 不做符号服务器:放弃 PDB 支持,换取 100% 离线能力HyperDbg 官方明确声明:“We do not support PDB files.” 这不是技术短板,而是刻意为之的设计取舍。PDB 支持意味着:需集成dbghelp.dll或symstore,增加 5MB+ 依赖;需网络请求符号服务器(https://msdl.microsoft.com/download/symbols),在隔离网络失效;需解析复杂类型信息(struct layout, template instantiation),代码膨胀 3 倍以上。HyperDbg 选择用!dh nt(dump headers)+!u(反汇编)+dd(dump dword)构成最小可行调试闭环。它假设使用者已掌握windbg -y符号路径配置,把符号解析交给更成熟的工具,自己专注做“指令级控制”。这使得 HyperDbg 驱动体积始终 < 120KB,hyperdbg.exe< 800KB,可在 2GB 内存的嵌入式 Windows IoT Core 设备上运行。6.2 不做 GUI:命令行即界面,规避 GDI 驱动兼容性雷区HyperDbg 没有图形界面,连hyperdbg.exe都是纯 console 应用。这避开了一大堆兼容性问题:Windows Server Core / Nano Server 无 GDI 子系统;win32k.sys在 HVCI 下被锁定,GUI 应用无法调用CreateWindowEx;GUI 线程模型(消息泵)与 Ring0 异步事件(断点触发)难以安全集成,易死锁。其命令行设计遵循gdb哲学:r(run)、c(continue)、s(step into)、n(step over)、p(print register)—— 所有命令 1~2 字母,无歧义。help命令输出仅 20 行,新手 5 分钟上手。这种克制,让它成为内核调试领域的“瑞士军刀”,而非“功能臃肿的 IDE”。6.3 不做远程调试协议:放弃网络传输,坚守本地驱动通信HyperDbg 不支持kd -remote或tcp:port=50000。所有通信走DeviceIoControl,这意味着:无法远程调试(hyperdbg.exe必须与目标系统同机);无法穿透防火墙或 NAT;无法构建分布式调试集群。但它获得了:零网络攻击面:无 socket、无监听端口、无 TLS 握手漏洞;确定性延迟:IOCTL调用平均耗时 3~8μs,远低于 TCP/IP 栈的 100μs+;内核态直通:IOCTL参数缓冲区可被ProbeForWrite直接校验,无需序列化/反序列化。在某高校操作系统课程实验中,学生曾用 HyperDbg 实时监控nt!KiSwapThread的上下文切换耗时(精度达纳秒级),这种场景下,网络延迟的抖动会彻底污染测量结果。当你要测量“内核本身”,就不能让“测量工具”引入额外噪声。从那以后我每次分析内核同步原语(如FAST_MUTEX、ERESOURCE)的争用路径,都强制用 HyperDbg 的bp+r+u三连击,在KeWaitForSingleObject返回前精确捕获RSP和RIP,再结合!thread验证线程状态。这套组合拳之所以可靠,正是因为 HyperDbg 没有试图做“全能选手”,而是把 Ring0 异常接管这件事,做到了物理层面的极致干净。希望帮到你。本文还有配套的精品资源,点击获取