radare2 使用 winkd 调试插件通过 KD 协议调试 Windows 内核
导读
本文介绍 radare2(r2)内建的 WinDBG / KD 调试支持:通过 winkd IO 插件与调试插件,radare2 可以经由虚拟串口(命名管道 / Unix socket)附着到运行在 VirtualBox、QEMU 等虚拟机中的 Windows 系统,使用微软内核调试器(KD)协议远程读写内核内存、控制执行流、设置断点并枚举进程与模块。阅读本文后,你将掌握从 Windows 侧开启内核调试、在虚拟机中创建串口管道、用 r2 命令行连接并完成首次附着、越过初始断点陷阱、以及使用 dp / dpa 枚举与切换进程的完整实战流程,同时理解 KD 协议在 r2 中的底层实现脉络。
说明:原文档位于 doc/windbg.md,本文以其为骨架展开,并补充仓库源码层面的实现细节。该功能当前仍处于 work-in-progress 状态,属于初版实现,会随版本迭代不断完善。
一、功能概览:用 KD 协议把 r2 变成 Windows 内核调试器
WinDBG 支持让 radare2 通过命名 socket 文件(未来会支持更多 IO 方式)附着到运行 Windows 的虚拟机,利用 KD 接口 通过串口完成内核级调试。这套能力的完整实现分布在两处:
- IO 层:
winkd://URI 对应的 IO 插件,位于 libr/io/p/io_winkd.c,负责建立与虚拟机串口管道的连接,并提供read/write/seek等内存读写原语; - 调试层:名为
winkd的调试插件,位于 libr/debug/p/debug_winkd.c,负责附着、单步、继续执行、寄存器读写、断点、进程/线程/模块枚举等调试语义; - 协议核心:KD 协议的编解码与传输逻辑集中在共享子库 shlr/winkd 目录下(
kd.c/kd.h、winkd.c/winkd.h、transport.c/transport.h、iob_pipe.c、iob_net.c、profiles.h)。
在 IO 插件中,winkd:// 前缀的 URL 会被自动识别(io_winkd.c),并根据路径中是否包含 : 来自动选择传输后端:包含冒号时走 UDP 网络后端(iob_net),否则走 管道后端(iob_pipe)。值得注意的是,IO 插件还支持 winkd://192.168.1.33:1234:key 形式的 UDP 附着,其中 key 是 base36 编码的 AES-256 密钥,格式形如 x.x.x.x,用于 KDNET 网络调试(UDP 传输在 shlr/winkd/iob_net.c 中实现)。
调试插件的描述为 "winkd debug plugin",架构限定为 x86,同时支持 32 位与 64 位目标(debug_winkd.c 中 bits = R_SYS_BITS_PACK2 (32, 64))。这意味着 r2 可调试 32 位与 64 位的 Windows 内核。
二、Windows 侧开启内核调试
2.1 Windows Vista 及更高版本
在目标 Windows 系统(Vista 及以上)中以管理员身份打开命令提示符,执行:
bcdedit /debug on
bcdedit /dbgsettings serial debugport:1 baudrate:115200
这两条命令分别启用内核调试,并将调试端口设置为串口 1(COM1)、波特率设为 115200。
2.2 Windows XP
XP 没有 bcdedit,需要手工编辑 boot.ini,在操作系统条目末尾追加 /debug /debugport=COM1 /baudrate=115200:
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Debugging with Cable" /fastdetect /debug /debugport=COM1 /baudrate=57600
注意:文档中的示例把波特率写成了 57600,而 radare2 侧默认以 115200 建立会话,实际使用时应保持两侧一致(统一 115200),否则握手可能失败。
三、虚拟机串口管道配置
3.1 VirtualBox
在 VirtualBox 中为目标虚拟机配置串口:
Preferences -> Serial Ports -> Port 1
[V] Enable Serial Port
Port Number: [_COM1_______]
Port Mode: [_Host_Pipe__]
[v] Create Pipe
Port/File Path: [_/tmp/winkd.pipe____]
要点是启用 Host Pipe 模式并勾选 Create Pipe,将管道文件指向 /tmp/winkd.pipe。这样 VirtualBox 会创建一个命名管道,供宿主机上的 r2 连接。
3.2 QEMU
也可以直接用 QEMU 启动 Windows 虚拟机并创建对应的 socket 管道:
$ qemu-system-x86_64 -chardev socket,id=serial0,\
path=/tmp/winkd.pipe,nowait,server \
-serial chardev:serial0 -hda Windows7-VM.vdi
-chardev socket,path=/tmp/winkd.pipe,nowait,server 让 QEMU 作为服务器端创建一个 Unix socket(nowait 表示不等待客户端连接即可继续启动,server 表示本端为服务端),-serial chardev:serial0 把虚拟串口接到该 socket 上。
四、用 r2 连接并附着到内核
radare2 通过 winkd IO 插件连接 VirtualBox / QEMU 创建的 socket 文件,并通过 winkd 调试插件进行调试。连接时需要同时指定 x86 架构与位数(32 位与 64 位调试均受支持):
$ r2 -a x86 -b 32 -D winkd winkd:///tmp/winkd.pipe
-a x86:指定目标架构为 x86;-b 32:指定目标位数为 32 位;-D winkd:使用 winkd 调试插件启动;winkd:///tmp/winkd.pipe:通过 Unix socket 管道连接虚拟机。
在 Windows 宿主机上直接运行 radare2 时,连接命令对应 Windows 命名管道语法:
$ radare2 -D winkd winkd://\\.\pipe\com_1
这里的 \\.\pipe\com_1 是 Windows 命名管道路径,对应 shlr/winkd/iob_pipe.c 中 CreateFile 打开的管道句柄(该文件在 Windows 分支下使用 CreateFile + 重叠 IO(FILE_FLAG_OVERLAPPED)实现异步读写,超时通过 WaitForSingleObject 控制)。
附着过程中的协议握手
从源码看,调试插件在附着时会执行一次 KD 协议握手(debug_winkd.c):
winkd_sync():与目标同步,建立 KD 会话;winkd_read_ver():读取目标版本信息(对应 KD 的DbgKdGetVersionApi请求,见 kd.h 中的请求类型定义),从中解析出目标位数、机器类型(KD_MACH_I386/KD_MACH_AMD64等,见 kd.h);- 根据握手结果设置
dbg->bits,随后由r_debug_winkd_reg_profile依据位数选择 x86 或 x64 的寄存器 profile(native/reg/windows-x86.h/windows-x64.h)。
KD 协议包由 kd_packet_t 描述(kd.h):16 字节定长头部(leader、type、length、id、checksum)加上可变长度负载;协议层的请求/响应结构 kd_req_t 被精确对齐到 56 字节(源码中有 ct_assert(sizeof(kd_req_t)==56) 编译期断言)。控制包与数据包分别通过 kd_send_ctrl_packet / kd_send_data_packet 发送。
五、首次附着后的中断处理
连接并附着成功后,r2 会停在内核的初始断点处:
[0x828997b8]> pd 20
;-- eip:
0x828997b8 cc int3
0x828997b9 c20400 ret 4
0x828997bc cc int3
0x828997bd 90 nop
0x828997be c3 ret
0x828997bf 90 nop
pd 20 反汇编了当前 eip 附近的 20 条指令,可以看到典型的 int3(cc)软中断指令,即内核调试器在系统启动早期插入的陷阱指令。
跳过陷阱恢复系统
要跳过该陷阱,需要修改 eip 并连续执行两次 dc(continue):
dr eip=eip+1
dc
dr eip=eip+1
dc
dr eip=eip+1:把 eip 指向int3之后的下一条指令;dc:继续执行,遇到下一个断点再次停下;- 重复一次后,Windows 虚拟机恢复交互,系统正常继续运行。
此时如果想要重新夺回内核控制权,需要退出 r2(kill 掉 r2 进程)并再次附着。
调试插件如何映射异常
dc 停下的原因可以在调试插件的 wait 逻辑中找到依据(debug_winkd.c):插件持续等待 KD_PACKET_TYPE_STATE_CHANGE64 状态变更包,解析其中的 kd_stc_64 结构(kd.h),并把 Windows NTSTATUS 异常码映射为 r2 的调试原因:
| NTSTATUS 异常码 | 含义 | r2 调试原因 |
|---|---|---|
0x80000003 | STATUS_BREAKPOINT(断点命中) | R_DEBUG_REASON_BREAKPOINT |
0x80000004 | STATUS_SINGLE_STEP(单步) | R_DEBUG_REASON_STEP |
0xc0000005 | STATUS_ACCESS_VIOLATION(访问违例) | R_DEBUG_REASON_SEGFAULT |
0xc000001d | STATUS_ILLEGAL_INSTRUCTION(非法指令) | R_DEBUG_REASON_ILLEGAL |
0xc0000094 | STATUS_INTEGER_DIVIDE_BY_ZERO(除零) | R_DEBUG_REASON_DIVBYZERO |
除此之外,DbgKdLoadSymbolsStateChange 状态会被映射为 R_DEBUG_REASON_NEW_LIB(加载新模块事件)。当前 eip 处的 int3 正是 STATUS_BREAKPOINT,因此 r2 会以断点原因停在初始位置。
六、进程枚举与切换:dp / dpa / dp=
附着后可以用 dp 命令列出目标内核中的所有进程:
[0x828997b8]> dp
底层实现是调试插件的 r_debug_winkd_pids(debug_winkd.c),它调用 winkd_list_process 遍历内核 EPROCESS 链表,为每个进程构造一个 RDebugPid(pid、路径、状态)。
dpa 或 dp= 用于附着到指定进程:
[0x828997b8]> dpa <pid>
附着后 r2 会显示该进程在物理内存布局中的基地址。这个行为对应 r_debug_winkd_select(debug_winkd.c):调用 winkd_set_target 切换目标进程,再通过 winkd_get_target_base 获取进程基地址并打印 Process base is 0x...。
附着到进程后,内存读写会切换到 UVA(User Virtual Address,用户虚拟地址) 模式:IO 插件的 __read / __write 在 winkd_get_target 有值时会调用 winkd_read_at_uva / winkd_write_at_uva(io_winkd.c),否则走内核虚拟地址(winkd_read_at)。也就是说,dp / dpa 不仅是查看进程列表,还会影响后续内存访问的地址空间解释。
此外,调试插件还提供了 r_debug_winkd_threads(线程枚举)、r_debug_winkd_modules(内核模块枚举,把 WindModule 映射为 RDebugMap)以及 r_debug_winkd_breakpoint(软/硬件断点设置,b->hw 决定使用内核侧断点句柄还是 DRn 寄存器槽位)。
七、系统版本支持与内核结构体偏移 Profile
KD 调试需要解析 Windows 内核的 EPROCESS / ETHREAD / PEB 等结构体,不同 Windows 版本这些结构体的字段偏移不同。radare2 通过一组内置的 Profile 来适配不同系统版本(shlr/winkd/profiles.h),每个 Profile 记录 build 号、SP(Service Pack)、位数以及 15 个关键字段偏移(对应 winkd.h 中定义的 E_ActiveProcessLinks、E_UniqueProcessId、E_Peb、E_ImageFileName、P_DirectoryTableBase 等枚举,以及 K_PsActiveProcessHead、K_PaeEnabled、K_CmNtCSDVersion 等内核符号偏移常量)。
当前仓库内置的 Profile 覆盖范围(节选):
| 系统 | 架构 |
|---|---|
| Windows XP SP2 / SP3 | x86 |
| Windows Vista SP0 / SP1 / SP2 | x86 / x64 |
| Windows Server 2003 SP0 / SP1 / SP2 | x86 / x64 |
| Windows 7 SP0 / SP1 | x86 / x64 |
| Windows 8 / 8.1 SP0 / SP1 | x86 / x64 |
| Windows 10 RS1 / RS4 / 19H1 | x64 |
例如 WIN7_SP1_X64 的 Profile 为:build 7601、SP 1、64 位,字段偏移数组以 { 0x0188, 0x0180, 0x0338, ... } 描述;WIN10_19H1_X64 的 build 号为 18362。这意味着该实现面向较老的 Windows 版本(最高到 Windows 10 19H1 时期)做过结构体偏移适配,对更新版本的系统可能因内核结构体变化而需要额外的 Profile 支持——这也是文档标注该功能仍为初版 work-in-progress 的原因之一。
八、从源码看 winkd 的完整工作链路
汇总整个 winkd 子系统的调用链,便于读者在仓库中按图索骥:
- 连接建立:r2 解析
winkd://URI → libr/io/p/io_winkd.c 的__open选择iob_pipe或iob_net后端 → 通过 shlr/winkd/transport.c 的io_desc_t包装 → 调用winkd_ctx_new创建WindCtx上下文。 - 协议编解码:shlr/winkd/kd.c 负责组装/解析 KD 包(控制包、数据包、ACK、校验和
kd_data_checksum),shlr/winkd/kd.h 定义了全部 KD 操作码(DbgKdReadVirtualMemoryApi、DbgKdWriteVirtualMemoryApi、DbgKdGetContextApi、DbgKdSetContextApi、DbgKdWriteBreakPointApi、DbgKdReadPhysicalMemoryApi、DbgKdGetVersionApi等)与错误码(KD_E_OK/KD_E_BADCHKSUM/KD_E_TIMEOUT/KD_E_MALFORMED/KD_E_IOERR)。 - 调试语义:libr/debug/p/debug_winkd.c 把 r2 的调试回调(
attach/detach/cont/step/wait/reg_read/reg_write/breakpoint/pids/threads/modules_get/select)逐一映射到winkd_*API。 - IO 语义:libr/io/p/io_winkd.c 把 r2 的读/写/寻址映射到
winkd_read_at/winkd_write_at(内核地址)与winkd_read_at_uva/winkd_write_at_uva(进程用户地址)。
备选方案:远程 GDB
除了原生 winkd 通道外,radare2 也可以使用远程 GDB 接口连接并调试 Windows 内核,从而不依赖 Windows 自身的调试能力(如 GDB 服务器位于可访问目标内核的位置时)。这条路径属于 r2 既有的 GDB 调试支持,与 winkd 是并列的两条可选路线。
九、常见问题与使用限制
- 功能成熟度:文档明确说明 WinDBG 支持仍处于 work-in-progress 阶段,属于初版实现,某些边界场景(如新版本 Windows 内核结构、KDNET 加密网络传输的完整支持)可能不完善。
- 波特率一致性:Windows 侧
bcdedit设置的baudrate必须与虚拟串口、以及 r2 连接会话的参数保持一致,文档示例中的 XP 配置(57600)与主线配置(115200)不一致时应统一。 - 陷阱处理:首次附着停在
int3处是预期行为,需按上文方式修改 eip 并dc两次才能恢复系统交互;若要重新调试,需要重启 r2 重新附着。 - 架构与位数:winkd 调试插件仅支持 x86 架构(
arch = "x86"),位数支持 32 / 64;-a x86与-b 32/64需要在启动时正确指定。 - 进程附着影响地址空间:执行
dpa后,IO 读写会切换到用户虚拟地址(UVA)模式,读写目标从内核地址变为所选进程的用户地址空间。
十、进一步阅读
- 原文档:doc/windbg.md
- IO 插件实现:libr/io/p/io_winkd.c
- 调试插件实现:libr/debug/p/debug_winkd.c
- KD 协议核心:shlr/winkd/kd.h、shlr/winkd/kd.c
- winkd 上下文与 API:shlr/winkd/winkd.h、shlr/winkd/winkd.c
- 传输层与后端:shlr/winkd/transport.h、shlr/winkd/iob_pipe.c、shlr/winkd/iob_net.c
- 系统版本 Profile:shlr/winkd/profiles.h
- 相关调试文档:doc/debug.md、doc/debug-internals.md
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



