
简介Hyperdbg 是一套面向 ring0 内核级调试的完整源码资料适合驱动程序开发者、操作系统学习者与安全研究人员用来理解内核调试器的实现思路应对系统调用跟踪、中断处理分析、内核内存异常排查等场景。压缩包共 93 个文件约 212KB以 38 个 .h 头文件和 35 个 .c 源文件为主体辅以汇编、构建脚本、Makefile、说明文档及图标等资源整体轻量但模块划分明确。已有 553 人学习下载可见其在同类小众内核调试主题中具有一定关注度。源码围绕 VMX 虚拟化、MMU 内存管理、事件与断点处理、符号检索、反汇编以及图形界面等部分展开读者可据此研究软硬件断点设置、内核内存查看与修改、寄存器状态监控、调试日志记录与回放等机制的落地方式也能借鉴其工程组织与构建流程为自行开发调试工具或深入分析系统底层行为提供参考。1. 蓝屏之后只剩一个 dumpring0 内核调试器 HyperDbg 到底解决什么问题HyperDbg 是一个开源的 ring0 内核调试器kernel debugger但它跟大多数人印象里的内核调试不是一回事不需要第二台机器、不需要串口线、不需要 KDNET 网络握手调试器和被调试系统跑在同一台物理机上。它靠硬件虚拟化把当前系统变成虚拟机监控器下的一层 guest用 EPT 翻转页权限来接管执行流因此挂钩内核函数时不需要往代码段里写 0xCC也就绕开了 PatchGuard 那一套自校验。适合谁写驱动的人、做内核态对抗分析的人、以及那些被改一个寄存器就蓝屏、重启后现场只剩一个 dump折磨过、想在本机直接把断点打下去的人。它的门槛不在命令而在机器上那块 VT-x 能不能被它独占。2. 架构选型为什么它用 VT-x/EPT 而不是软件断点2.1 三条内核调试路线的取舍内核态调试这件事业内能走的路其实只有三条每条路的代价完全不同。路线依赖条件断点实现方式典型限制双机调试成熟调试器 串口/网线第二台物理机或虚拟机宿主写 0xCC、硬件断点环境搭建麻烦调试期间目标机基本停摆纯软件断点改指令首字节无额外依赖覆盖为 0xCC 再回填只读段改不了、被自校验发现、触发内核完整性检查虚拟化调试HyperDbg 这一类VT-x / AMD-V 独占EPT 页权限翻转与 Hyper-V、WSL2、内存完整性抢资源第一条路最稳问题是它把复现变成了两件设备的事你在 A 机上敲命令B 机上跑现场任何一次环境漂移都要重新对齐。第二条路最轻但只要目标函数在只读页里或者代码段有自己的 CRC 校验改字节这一步就直接翻车。HyperDbg 选的是第三条。它在系统启动后把自己加载成一个轻量 VMM然后不再改动任何客户机代码字节只改 EPT 里那一条页表项的权限位——执行位一清目标页一被执行就触发 EPT violationCPU 从 guest 退出到 VMM控制权就落到了调试器手里。指令内存看起来一个字节都没变rdmsr 读出来的 MSR、内存里的代码段、CRC 校验全是对的这是它在对抗场景里最实际的价值。代价也很明确VT-x 只有一个谁先占谁用。这台机器上只要有别的组件在用它HyperDbg 就加载不起来。2.2 VMM 层与调试器模块两层各自负责什么它的运行结构是两层加载的时候也是两条命令很多人第一次用会卡在这里。底层是 VMM 模块负责真正的脏活检测 CPU 虚拟化能力、建立 VMCS/EPT 表、接管 VM exit、维护一张哪些地址被挂了钩子的表。这一层加载成功后整台机器已经跑在虚拟化环境下了但这时候你还什么都干不了因为没有东西去消费它抛上来的事件。上层是调试器模块负责命令解析、脚本解释、事件注册与生命周期管理以及把 VMM 抛上来的退出原因翻译成第几次命中、命中在哪个地址、当前进程线程是谁。脚本引擎也在这层!epthook、!monitor、!syscall这些命令背后都是一套统一的事件 脚本模型。两层分离的好处是VMM 只做最小必要的事逻辑越少越不容易在 VM exit 上下文里出问题调试器模块可以随时卸载重载不用动已经跑起来的虚拟化层。血泪经验是——出问题时先分清是VMM 没起来还是事件没挂上这两类故障的排查路径完全不一样。2.3 一次 EPT Hook 从挂上到命中的完整路径把这条路径在脑子里跑通一遍后面所有参数和报错都能对上号。第一步命令里给的是一个虚拟地址调试器先把它翻译成物理地址再顺着 EPT 的层级找到对应的页表项把原始权限位记下来存档。第二步清掉那一项的执行位或读写位然后发一次 EPT TLB 失效保证 CPU 不再用旧的缓存翻译。第三步目标指令被取指CPU 发现 EPT 里这一页不可执行触发 EPT violationVM exit 到 VMM。第四步VMM 查钩子表确认这个地址在监控范围内把控制权交给调试器模块执行用户写的那段脚本。第五步脚本跑完之后不能就这么返回——页权限还是关着的直接返回会无限重复触发。常见做法是临时恢复该页权限同时打开单步 trap或利用监视 trap flag 的机制让 guest 只执行一条指令再重新把权限关掉。所以一次 hook 命中实际上包含两次以上的 VM exit这也是为什么挂钩热路径函数会把系统拖慢。第六步脚本里的print结果输出到调试器控制台事件计数加一。理解了第五步就能明白两个常见现象为什么在!epthook的脚本里写复杂逻辑会明显掉帧以及为什么某些指令比如 rep 前缀的长指令在单步恢复时会表现得不太自然。3. 环境落地把 HyperDbg 跑起来的最小闭环3.1 前置条件VT-x 独占、测试签名与内存完整性这一段是纯排雷跳过它后面全是蓝屏。先确认硬件虚拟化在 BIOS 里是开着的然后处理软件层的抢占。Windows 上最常见的三个抢占者是Hyper-V 平台本身包括 WSL2 背后的虚拟机平台、基于虚拟化的代码完整性保护、以及某些安全软件的驱动。三者的共同点都是先一步拿走了 VT-x。# 让宿主系统不要把 VT-x 占走交给 HyperDbg 的 VMM 使用 bcdedit /set hypervisorlaunchtype off # 自己编译出来的驱动没有正式签名必须打开测试签名模式 bcdedit /set testsigning on # 关掉内存完整性HVCI它同样建立在虚拟化之上 reg add HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity /v Enabled /t REG_DWORD /d 0 /f # 以上都需要重启才生效 shutdown /r /t 0这三条命令改的都是启动期状态必须重启。重启后有两个验证动作一是用系统信息工具看已检测到虚拟机监控程序这一项是否还是是如果仍然是说明还有东西没关干净二是确认测试签名模式已经生效否则驱动加载会直接返回签名错误。提示打开测试签名后Secure Boot 在某些配置下会阻止加载需要在固件设置里先关掉。这一步属于环境准备做完之后建议单独打一个系统还原点。3.2 编译与加载load vmm 与 load dm 的两步闭环源码树里同时给了 VMM 驱动工程和调试器可执行工程构建脚本和解决方案都在按仓库说明装好编译器工具链与驱动开发包即可。版本对齐很关键驱动开发包的版本和编译器版本不匹配时编出来的驱动会在加载阶段报一个完全不相干的错误码浪费半天。编完之后产物是两样东西VMM 驱动和调试器主程序。把主程序用管理员权限启动进入它自己的命令行控制台。# 第一步把机器虚拟化加载 VMM 层 load vmm # 第二步加载调试器模块脚本引擎和事件管理在这一层 load dm # 验证能看到进程列表说明上层通路已经打通 !process # 验证事件列表此时应该是空的 eventsload vmm成功时会打印出虚拟化初始化的信息包括检测到的特性支持情况。如果这一步报错99% 是 3.1 里的抢占问题没解决别去翻驱动代码。load dm失败则更多是路径或版本问题——调试器主程序和驱动之间的通信接口对不上通常出现在驱动是旧版本编的、主程序是新版本这种混搭场景。两步都成功之后这台机器已经处于被监控状态但还没有任何事件在跑性能开销接近零。这一点比双机调试舒服它是懒加载的挂多少钩子付多少代价。3.3 最小验证集断点、单步、内存与反汇编在挂 EPT 钩子之前先用常规断点确认整条链路是通的。命令名基本对齐大家熟悉的调试器习惯bp下断、bl列表、g继续、p单步、u反汇编、dq看内存。# 在一个内核函数上下一号断点地址换成你环境里的真实地址 bp fffff801a1b2c3d4 # 确认断点已经登记 bl # 放行等待命中 g # 命中后先看指令确认落点是不是你以为的那个函数 u fffff801a1b2c3d4 # 再看栈上的参数x64 下前四个参数在 rcx/rdx/r8/r9 dq rsp # 单步一条观察寄存器变化 p这里的重点是地址必须用运行时真实地址。内核模块有基址随机化静态反汇编里看到的地址直接抄过来断点会挂在一个完全不相关的页上而且不会报错——这是新手最常见的断点不命中。u出来的指令要和你的预期比对如果第一个字节是e9之类说明地址落在了跳转桩上如果整段反汇编对不上任何已知函数说明地址算错了。确认断点、单步、内存读取这三件事都能正常工作才算具备了挂 EPT 钩子的资格。这一步不通过就往后面走后面所有失败都无法归因。4. 事件与脚本引擎把「看一眼」变成「自动抓」4.1 内置事件清单与选型它把所有可拦截的东西抽象成统一的事件不同命令只是不同拦截点的语法糖。命令拦截点典型用途相对开销!epthookEPT 页级执行/读写挂钩内核函数、监控调用中每次命中两次以上 VM exit!epthook2更细粒度的指令级跟踪需要避开常见检测、粒度更小高!monitor一段内存的读/写/执行抓谁改了某个结构体字段中!syscall系统调用进出过滤调用、抓参数低到中!cpuidcpuid 指令分析虚拟化探测行为低!msrread/!msrwriterdmsr / wrmsr抓驱动改 MSR低!ioin/!ioout端口读写指令追踪设备访问低!exception异常分发抓缺页、通用保护中!dr调试寄存器访问反调试对抗分析低选型的判断标准只有一条你要观察的是某个地址被执行了还是某块内存被改了。前者用钩子后者用监控块。用钩子去观察数据变化会漏掉非执行路径的写入用监控块去观察函数调用会把同一页上所有无关访问全部引进来日志直接爆炸。4.2 !epthook 与 !monitor脚本参数逐项拆开先看执行钩子。语法是地址加一段脚本块脚本在每次命中时执行。# 在目标内核函数上挂执行钩子命中时打印当前进程和第一个参数 !epthook fffff801a1b2c3d4 { printf(hit pid%x tid%x arg1%llx\n, $pid, $tid, $rcx); }三个部分要分开理解。地址必须是运行时虚拟地址和bp的要求一样。花括号里是脚本体在 VM exit 上下文里执行所以只能做纯内存读取和输出不能调用会睡眠或触发缺页的操作。$pid、$tid、$rcx是伪寄存器由调试器在命中时刻填充不需要自己从结构体里扒。再看内存监控。它多了一个长度和权限掩码。# 监控某结构体里一个 8 字节字段的写入把写者打出来 !monitor fffff801b2c3d4e0 8 w { printf(writer pid%x tid%x rip%llx val%llx\n, $pid, $tid, $rip, poi(fffff801b2c3d4e0)); }8是监控长度粒度是字节但实际生效的页级保护意味着同一页上任何访问都可能把你唤醒脚本里必须自己再判一次地址。w是权限掩码可选读、写、执行及其组合。poi()是解引用把地址里的值取出来——不套poi打出来的是地址本身这个坑几乎每个人都会踩一次。脚本执行完事件会继续挂着直到你显式清理。# 列出所有活跃事件拿到 ID events # 按 ID 精确清掉不带参数是全清 flush 14.3 伪寄存器、表达式与 poi 解引用脚本引擎里有两类东西要分清伪寄存器和普通变量。伪寄存器以$开头由调试器在命中瞬间填值$pid、$tid、$proc、$thread、$rip、$rcx这些都属于这一类是最省事的取值方式。普通表达式支持算术、位运算和比较可以在脚本里做过滤也可以直接在控制台用?求值。// 只在指定进程里才输出避免全局钩子把日志刷爆 if ($pid 0x1a4) { printf(target pid hit rip%llx\n, $rip); } // 读一个指针再解引用注意 x64 下先确认它是有效地址 if (poi($rsp) ! 0) { printf(arg0%llx\n, poi(poi($rsp))); }if过滤是控制日志量的第一手段。把过滤条件放在脚本最前面比事后 grep 日志高效得多因为不满足条件的命中连格式化字符串的开销都省了。解引用要格外小心在 VM exit 上下文里如果poi()的地址是无效页行为不一定像用户态那样干脆地报个错。我一般会在脚本里加一个地址范围判断只在确认属于内核空间时才去读。注意伪寄存器的具体名称和各命令支持的参数会随版本有小幅差异命令名和参数以你手上这版help输出为准别照抄别人的截图。4.4 透明模式把调试痕迹藏到什么程度透明模式通过!hide指定进程让调试器对这个进程隐藏自身的存在调试端口、调试寄存器、以及一部分可被枚举到的痕迹会被处理掉。分析带反调试的样本时不打开这个模式样本一上来就能发现自己在被调。# 对指定进程启用透明模式参数是进程 ID !hide 0x1a4 # 分析结束后恢复 !unhide 0x1a4但要清楚它的边界。第一它藏的是状态类痕迹藏不住时序类痕迹——EPT violation 带来的额外开销、cpuid 和 rdtsc 的微小差异这些物理层面的事实改不掉。第二透明模式和常规断点是互相干扰的你一旦在目标进程里下普通断点被隐藏的状态就会被自己破坏。第三它作用范围是进程级的内核全局的钩子不受影响。所以正确的用法是静态分析定好点位透明模式下去挂 EPT 钩子观察行为全程不碰普通断点。想要边跑边停下看现场就得接受暴露。5. 避坑与排查六个把人劝退的地方5.1 加载阶段蓝屏与加载失败现象敲完load vmm机器直接蓝屏或者卡死重启。原因是 VT-x 已经被别的组件占着VMM 初始化时拿不到根模式。检查顺序是Hyper-V 平台有没有关、内存完整性有没有关、有没有安全软件的驱动在跑。三条命令都在 3.1 里改完必须重启改完不重启等于没改。还有一种更隐蔽的情况——BIOS 里的虚拟化选项是开的但受信任执行相关的开关把它锁住了这种情况下系统信息工具会给出误导性的显示。现象重启之后加载仍然失败报的是签名错误。原因是测试签名没生效或者 Secure Boot 把它挡住了。解决分两步先确认测试签名模式的状态再去固件里关掉 Secure Boot。自己编译的驱动一定要走这条路别想着绕过。现象加载成功但机器整体变卡鼠标都开始飘。原因是残留了之前没清理的钩子。events看一眼如果列表里有一堆不认识的事件 IDflush全清。日常习惯是每次分析结束前先events确认一遍别指望重启解决问题——重启当然能清但你会丢掉现场。5.2 调试阶段断点不命中与脚本静默现象!epthook挂上去返回成功目标函数明显在跑脚本一次都没打印。先怀疑地址。内核模块有基址随机化静态地址必然错。用进程/模块枚举拿运行时基址再把偏移加上去。确认方法是命中前先用dq把那个地址的头几个字节读出来和你反汇编里看到的对一遍——对不上就是地址错了。还有一种情况是目标页已经被别的钩子占用两个钩子叠在同一页上后挂的那个不生效。现象脚本语法没错但输出里全是乱码或者值明显不对。大概率是poi()忘了套把地址当值打出来了或者解引用的层级不对poi的次数少了一层。x64 下指针是 8 字节取结构体里第 N 个字段要按实际偏移算不能凭感觉加。现象挂上钩子后目标程序行为变了或者干脆崩了。看脚本里有没有做重量级操作。VM exit 上下文里不适合做复杂计算和跨页访问脚本应该只做读几个值 判断 打印。另外检查是不是挂了热路径函数——一个每秒被调几十万次的函数上挂 EPT 钩子系统会被拖到不可用。这种情况换成条件更精确的地址或者改用!monitor只在数据被写时才醒。现象机器随机卡死日志停在半截。这是钩子风暴的典型表现脚本里的printf触发了新的内存访问又被自己的监控块捕获形成递归。解决方式是在脚本里对当前线程或当前指令指针做一次排除判断命中时先设一个标记位直接返回。写日志这件事在虚拟机监控层是要付代价的不能像用户态那样随手打。现象重启之后所有事件都没了之前的配置白做。事件是运行时状态不落盘这是设计如此。解决办法是把所有注册命令写成一个脚本文件每次启动后按顺序重放一遍。这也是我现在的固定动作——分析过程中的每一条命令都同步记进那个文件下次复现直接跑。6. 把一次偶发蓝屏变成可回放的证据链偶发问题最烦的地方在于再来一次这个词。我的做法是把整个观察过程固化成一段可以重复执行的注册脚本而不是每次凭记忆敲命令。# 复现脚本先清干净再按顺序挂事件最后确认 flush !hide 0x1a4 !monitor fffff801b2c3d4e0 8 w { if ($pid 0x1a4) { printf(WRITE pid%x tid%x rip%llx old%llx\n, $pid, $tid, $rip, poi(fffff801b2c3d4e0)); } } !epthook fffff801a1b2c3d4 { if ($pid 0x1a4) { printf(CALL pid%x tid%x arg1%llx\n, $pid, $tid, $rcx); } } events这段脚本解决的是三件事。第一顺序固定先清残留再挂新事件避免上一轮的钩子污染这一轮的观测。第二过滤前置$pid判断放在最外面只有目标进程的命中才会走到格式化输出日志量能压下来一到两个数量级。第三输出带rip光有进程线程信息不够定位加上触发点的指令地址才能和反汇编对上才能判断是我预期的那个调用路径还是另一条完全没想到的路径。验证这套配置是否可信别直接上真实样本。先找一个自己写的、明确会写那个字段、明确会调那个函数的测试驱动跑一遍看日志条数和预期是否一致。条数对得上再换真实场景。这个先在自己人身上试一遍的习惯能把大量假阴性挡在外面——最怕的不是没日志是日志有但漏了关键的那一次。几个具体技巧。events返回的 ID 要记下来flush的时候按 ID 精确清理全清会把别的分析任务挂的事件一起干掉。挂在热路径上的监控块要给一个明确的结束条件或者干脆用时间窗口控制跑够样本量就flush。日志输出建议控制在每次命中一行多行输出会把控制台变成瓶颈反而改变时序。还有一点是我踩过最深的不要在脚本里顺便读太多东西。每多一次内存解引用就多一次可能触发异常的机会而在 VM exit 上下文里排查异常代价比用户态高一个数量级。从那以后我每次挂事件之前都强制先用?或dq把地址和头几个字节核一遍确认落点无误再写脚本宁可多花两分钟也不要在蓝屏之后靠猜。希望帮到你。本文还有配套的精品资源点击获取