--- name: re-windbg description: > WinDbg 调试(Windows 用户态+内核)。触发词:WinDbg、windbg、内核调试、kd、!analyze capabilities: [debugging] --- # WinDbg 动态调试(Windows 用户态 + 内核) ## 何时使用 / 何时不用 - 用:Windows 用户态进程调试(启动/附加、断点、单步、寄存器);崩溃/异常分析(`!analyze -v`);内核调试(双机/VM/本地 kd);驱动与内核模块分析(配合 [[re-kernel]]);崩溃 dump(minidump/full dump)分析 - 用:时间旅行调试(TTD)——`ttd.exe` 录制 .run 轨迹后任意回放,适合「复现一次慢慢查」的校验/解密逻辑(见 [[commands]] TTD 节) - 不用:Linux 目标(走 [[re-gdb]]);macOS 目标([[re-lldb]]);只需轻量 GUI 调试([[re-x64dbg]] 更快) - 不用:只做静态分析/反编译([[re-ida]] / [[re-ghidra]] / [[re-binaryninja]]);无需与运行态互动的纯静态流程不需要调试器 - 不用:WSL 内跨边界 attach Windows 进程([[re-analyze/platform-tips]] WSL 分支:跨边界走 Windows 侧工具) ## 工具准备 参考 [[re-analyze/platform-tips]] Windows 分支——attach 需要管理员权限;调试属动态执行,默认沙箱内进行([[re-analyze/platform-tips]] 最高原则)。 ### WinDbg(微软商店版 / Windows SDK / WDK) - 商店版(新 UI WinDbg):`winget install Microsoft.WinDbg`(或 Microsoft Store 搜索 "WinDbg") - 经典版:Windows SDK 安装器勾选 "Debugging Tools for Windows";`choco install windbg` - WDK(含内核调试工具 kd.exe/kdnet.exe):`winget install Microsoft.WindowsWDK` 或 Visual Studio 安装器勾选 WDK - 验证: 打开 WinDbg,`File > Open Executable` 加载任意 exe 能停在初始断点;命令窗口 `ver` 输出版本号 - 版本差异: 商店版(WinDbg 1.x,Chromium UI)与经典版(WinDbg 10.0.x)命令兼容,UI 与附加流程不同;dump 分析两者通用(见 [[gotchas]]) ### Windows 符号(Windows SDK 符号 / 公共符号服务器) - 符号路径: `.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols`(或设环境变量 `_NT_SYMBOL_PATH`) - 内核模块符号: 内核调试下 `.reload /f nt`(强制从符号服务器拉取) - 验证: `!analyze -v` 输出中出现函数名而非 `+0x1a` 裸偏移 ### 双机/虚拟机内核调试环境 - 目标机(被调试): `bcdedit /debug on` + `bcdedit /dbgsettings serial debugport:1 baudrate:115200`(串口)或 `bcdedit /dbgsettings net hostip:<宿主机IP> port:50000 key:`(NET 网卡,Windows 10+ 推荐) - VM 场景: VMware/Hyper-V 给目标 VM 添加串口设备(Named Pipe `\\.\pipe\com_1` / `-vmconnect`),宿主 WinDbg `File > Kernel Debug > COM` 填管道名 - 本地内核调试(仅测试机,需测试签名): `bcdedit /set testsigning on` 重启后,WinDbg `File > Kernel Debug > Local` - 验证: 目标机重启停在 "Waiting to reconnect" 或进入调试等待后,宿主 WinDbg 附加成功、`g` 后 `Ctrl+Break` 能中断、`lm` 列出模块 ### TTD(时间旅行调试,Windows 10 1903+) - 录制: `ttd.exe -out C:\traces target.exe`(或 `-attach ` 附加录制;`-monitor <进程名>` 监控启动) - 打开: WinDbg `File > Open` 选 `.run` 轨迹文件(或 `Open Trace File`) - 验证: `ttd.exe -help` 有输出;录一段后用 WinDbg 打开,`!tt` 能跳到任意位置、`g-` 能往回跑(详见 [[commands]] TTD 节) ## 操作步骤 1. **附加/启动用户态目标**: - 启动: `File > Open Executable` 选目标 exe,停在初始断点(ntdll 加载完成处) - 附加: `File > Attach to Process` 按 PID/进程名选目标(**管理员权限**启动 WinDbg 才能附加高权限进程) - 命令行: `windbg -p ` / `windbg 目标.exe`;`-g` 跳过初始断点直接运行 2. **断点 / 单步 / 寄存器**: - 符号断点: `bp kernel32!CreateFileW`;地址断点: `bp 0x401000`;`bl` 列表、`bd`/`be` 禁用/启用、`bc *` 清空 - 未加载模块的符号用 `bu`(unresolved,模块加载后自动绑定)——目标 DLL 按需加载时 `bu` 是正解 - 命令断点: `bp 地址 "r rax; k; gc"`——命中时打印现场后自动继续,日志式观察不打断流程 - 单步: `g` 继续、`p` 步过、`t` 步入、`gu` 执行到当前函数返回 - 寄存器: `r` 全部、`r rax` 查看、`r eax=0` 修改(绕过校验常用) - 内存/反汇编: `u 0x401000` 反汇编、`dd`/`db` 读内存、`dps rsp` 看栈内容 - 搜索: `s -d 0x0 L?10000000 0xdeadbeef`(值)、`s -a 0x0 L?10000000 "text"`(ASCII 串)——找硬编码常量/密钥用 - 符号搜索: `x 模块!*关键字*`(如 `x kernel32!*Virtual*`)——不确定精确符号名时模糊匹配 3. **异常分析 `!analyze -v`**: - 目标崩溃停下后: `!analyze -v` → 读 `EXCEPTION_CODE`、`FAULTING_IP`、`STACK_TEXT`、`PROCESS_NAME` - 内核蓝屏对应 bugcheck 码(`BUGCHECK_CODE`,如 0xD1 驱动访问无效内存)——先记码再往下查,常见码含义要能对上故障方向 - 现场恢复: 从 `STACK_TEXT` 取 `EXCEPTION_RECORD` 地址 `.exr `(异常记录),再取 `CONTEXT` 地址 `.cxr ` 恢复寄存器现场,之后 `k` 出真实调用栈(栈损坏时唯一可靠的栈回溯方式) - dump 现场更简单: 打开 dump 后直接 `.ecxr`(自动用异常上下文设寄存器现场)→ `k`——minidump 分析标准开局 - dump 分析: `File > Open Crash Dump` 打开 `.dmp`(用户态用 `.dump /ma` 生成;内核蓝屏用 `%SystemRoot%\Minidump\*.dmp` 或完全 dump),同样 `!analyze -v` 出 bugcheck 码与故障模块 4. **内核调试(双机 / VM / 本地 kd)**: - 目标机配置见「工具准备」;配置后重启,宿主 `File > Kernel Debug` 按所用传输(COM/NET)连接 - VM 管道串口流程: 目标 VM 加串口 Named Pipe → 目标机 `bcdedit /dbgsettings serial` → 宿主 `File > Kernel Debug > COM` 填管道名(`\\.\pipe\com_1`)→ 目标机重启等连 - 连接后: `g` 让目标继续跑;`Ctrl+Break` 随时中断回调试器;`!process 0 0` 确认会话活着 - 本地调试: `bcdedit /set testsigning on` 重启后 `File > Kernel Debug > Local`(仅测试机,不影响生产机的用法) 5. **扩展命令**: - `!process 0 0`(全部进程列表)、`!process 1`(单进程详情+线程);`.process /p ` 切到目标进程上下文后再 `k`/`!thread` - `!teb`(当前线程 TEB)、`!peb`(当前进程 PEB——命令行、加载模块、环境块) - `lm`(模块列表,行尾 `(deferred)`=符号未加载、`(no symbols)`=无符号)、`lmv m <模块>`(版本+路径)、`lmi <模块>`(镜像头) - `kv`(带符号完整栈回溯,驱动分析常用)、`!analyze -v` 后按 `STACK_TEXT` 逐帧核对 - 驱动专项: `lmv m 驱动名` 拿基址 → `.reload /f 驱动名` 加载符号 → 深挖转 [[re-kernel]] 6. **TTD 回放(校验/解密逻辑慢查)**: ``` ttd.exe -out C:\traces target.exe # 录制 WinDbg 打开 .run → g- / t- / p- # 往回跑 !tt 50 # 跳到轨迹 50% 处 !tt 7213:36 # 跳指定位置(事件序号:步数) ``` - 断点前移/后移: `!tt br` 在某寄存器值变化处停;`!tt bm` 在内存访问处停 - 首次打开轨迹先 `!tt.index` 建索引(之后打开跳过);回放中途也可断点 `bp`(在轨迹上工作) - 查询 API: `dx @$cursession.TTD.Calls("模块!函数")` 统计调用次数/参数(如 `...Calls("ntdll!mem*")`) - TTD 限制: 需管理员运行、用户态专用、轨迹文件很大(数 GB)、回放只读不能写内存(见 [[gotchas]]) 7. **伪寄存器与脚本化(简单自动化)**: - 伪寄存器: `r $t0 = 0` 起临时变量;`$$ > <文件>` 把命令输出重定向到文件(如 `$$ > c:\log.txt`) - 条件执行: `.if (rax==0) { ... } .else { ... }`——配合 `gc` 做断点内逻辑 - `!runaway` 看线程 CPU 时间(找忙等/死循环线程);`!locks` 查锁状态(挂起排查) ## 跨域联合 - [[re-binary-core]]:工作流第 6 步 Windows 调试器(用户态+内核) - [[re-kernel]]:内核调试与驱动/rootkit 运行时验证固定调用本技能 - [[re-anti-analysis]]:反调试对抗(内核级检测点)、脱壳后验证 - [[re-malware]]:恶意样本崩溃 dump 分析与内核组件 - [[re-cracking]]:授权校验崩溃定位(`!analyze -v` 快速找崩溃原因) - 与 [[re-x64dbg]] 互补:轻量 GUI 断点调试用 x64dbg,内核/异常现场恢复/轨迹回放用 WinDbg ## 常见坑与陷阱 - **符号未配置 → 全是裸偏移无函数名**:现象——`lm` 行尾 `(no symbols)`,`k` 输出 `ntdll!+0x1a`;原因——符号路径为空或没 `_NT_SYMBOL_PATH`;对策——`.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols` 后 `.reload /f`,内核模块同理(先确认符号服务器可达) - **内核调试配置错 → 目标机蓝屏死等或连不上**:现象——目标机重启后停在 debug 等待或直接进系统未中断;原因——bcdedit 传输类型/端口与宿主不一致,或物理机单机本地调试受限(PatchGuard);对策——用 VM 串口管道最稳(两边都对 `\\.\pipe\com_1`),`bcdedit /dbgsettings` 重查配置,物理机场景优先 KDNET - **32/64 位命令与寄存器差异**:现象——64 位目标下 `r eax` 改错值、`!teb` 字段对不上;原因——x64 上下文用 `rax/rbx...`(低 32 位是 `eax` 等)、wow64 目标线程栈在 x86 层;对策——先确认目标位数,wow64 场景 `.load wow64exts` + `!wow64exts.sw` 切到 x86 上下文 - **栈损坏时 `k` 是假象**:现象——调用栈乱(地址不像代码);原因——栈指针被破坏/内存踩踏,常规回溯不可信;对策——用 `.exr` 拿异常记录、`.cxr` 恢复 CONTEXT 后再 `k`,必要时 `!analyze -v` 的 `STACK_TEXT` 里直接取现场 - **蓝屏 dump 打不开**:现象——`Open Crash Dump` 报格式错;原因——minidump 不完整(磁盘满)或用的 32 位 WinDbg 读 64 位 dump;对策——目标机确认内核页交换文件(crash dump 需要 pagefile)足够大,改用同位数 WinDbg - **RPC 调用统一入口 NdrStubCall3(svchost 功能逆向)**:现象——要拦/分析某个系统功能(如 Toast 通知),不知道它经什么通道进 svchost;原因——功能实现经 RPC 到 svchost 进程,入口难找;对策——Process Monitor 看目标进程加载 rpcrt4.dll + RpcView 确认 RPC 目标进程;**所有 RPC 服务端 stub 统一走 rpcrt4 的 NdrStubCall3**(x64;NdrStubCall2 是旧版 x86,ReactOS/Wine 源码可印证)——在其下断 `kvn` 看调用来源;筛选目标调用:第三参数(r8)指向 RPC_MESSAGE(+0x1C 是 ProcNum 方法编号、+0x28 是接口描述指针 RpcInterfaceInformation → RPC_SERVER_INTERFACE 的 InterfaceId 在 +0x04);**opnum 由 MIDL 按方法声明顺序从 0 编号**(如 PostNotification3 排第 0x29 位 → opnum=0x29),ObjectStublessClientN 中 rax=N 即 ProcNum;坑:NdrStubCall3 处理 svchost 内所有 RPC 接口,**必须按 InterfaceId + ProcNum 双重筛选**,只拦目标方法(拦错会把 Edge 等其他功能搞挂) - TTD 限制、版本差异与符号服务器细节见 [[gotchas]]