radare2 使用 winkd 调试插件通过 KD 协议调试 Windows 内核

radare2 使用 winkd 调试插件通过 KD 协议调试 Windows 内核

【免费下载链接】radare2 UNIX-like reverse engineering framework and command-line toolset 【免费下载链接】radare2 项目地址: https://gitcode.com/gh_mirrors/ra/radare2

导读

本文介绍 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):

  1. winkd_sync():与目标同步,建立 KD 会话;
  2. winkd_read_ver():读取目标版本信息(对应 KD 的 DbgKdGetVersionApi 请求,见 kd.h 中的请求类型定义),从中解析出目标位数、机器类型(KD_MACH_I386 / KD_MACH_AMD64 等,见 kd.h);
  3. 根据握手结果设置 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 调试原因
0x80000003STATUS_BREAKPOINT(断点命中)R_DEBUG_REASON_BREAKPOINT
0x80000004STATUS_SINGLE_STEP(单步)R_DEBUG_REASON_STEP
0xc0000005STATUS_ACCESS_VIOLATION(访问违例)R_DEBUG_REASON_SEGFAULT
0xc000001dSTATUS_ILLEGAL_INSTRUCTION(非法指令)R_DEBUG_REASON_ILLEGAL
0xc0000094STATUS_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 / SP3x86
Windows Vista SP0 / SP1 / SP2x86 / x64
Windows Server 2003 SP0 / SP1 / SP2x86 / x64
Windows 7 SP0 / SP1x86 / x64
Windows 8 / 8.1 SP0 / SP1x86 / x64
Windows 10 RS1 / RS4 / 19H1x64

例如 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 子系统的调用链,便于读者在仓库中按图索骥:

  1. 连接建立: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 上下文。
  2. 协议编解码: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)。
  3. 调试语义:libr/debug/p/debug_winkd.c 把 r2 的调试回调(attach / detach / cont / step / wait / reg_read / reg_write / breakpoint / pids / threads / modules_get / select)逐一映射到 winkd_* API。
  4. 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)模式,读写目标从内核地址变为所选进程的用户地址空间。

十、进一步阅读

【免费下载链接】radare2 UNIX-like reverse engineering framework and command-line toolset 【免费下载链接】radare2 项目地址: https://gitcode.com/gh_mirrors/ra/radare2

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值