在启动调试会话之前,通过在管理程序 CMD.exe 中运行以下命令来设置希望 WinDBG 使用的符号路径。如果已经设置了变量 _NT_SYMBOL_PATH,则无需运行此命令。
setx _NT_SYMBOL_PATH SRV*C:\symsrv*http://msdl.microsoft.com/download/symbols
2.启动目标进程
现在已经准备好启动调试器了。使用 WinDBG 调试进程有几种不同的方法,最常见的是附加到正在运行的进程和从 WinDBG 启动进程。在本篇文章中,将从 WinDBG 启动本地 64 位可执行文件。为了便于学习,这里选择每个 Windows 10 系统中都有的应用程序,即 notepad.exe。64 位 notepad.exe 位于 c:\windows\system32 目录中。
从 Windows 10 的 "开始 "菜单启动 WinDBG。启动 WinDBG 后,选择 "文件",然后选择 "启动可执行文件"。
图片
在 "启动可执行文件 "对话框中,浏览到 c:\Windows\System32 目录,选择 notepad.exe,然后单击 "打开"。
图片
当 WinDBG 启动应用程序时,它会停在执行应用程序主入口点之前的初始断点处。当 WinDBG 启动 notepad.exe 时,WinDBG 的命令窗口中将显示以下几行。这样,我们就可以运行一些初始命令,并在调用主入口点之前设置所需的断点。
3.调试器流程架构
WinDBG Preview 是一款 UWP 应用程序,对系统的访问非常有限,当然不足以调试进程。因此,WinDBG UI 和 WinDBG 调试器工作主程序分属于不同的进程,它们使用命名管道进程间通信(IPC)机制进行通信。WinDBG UI 预览进程是 DBG.X.Shell.exe,它通过命名管道连接到 EngHost.exe,后者是负责附加或启动被调试进程的进程。
图片
以下命令显示传递给调试器进程 (EngHost.exe) 的命令行选项。DbgX.Shell.exe 使用名称为 DbgX_c07674536fa94c33bdf0af63c782f816 的命名管道与 EngHost.exe 通信。
图片
4.进程初步调查
先获取一些有关操作系统版本和正在调试的进程的基本信息。显示目标计算机版本 (vertarget) 命令可显示 Windows 版本信息和调试会话时间信息。
(vertarget)命令显示系统中有 4 个 CPU(内核),使用(!cpuid)调试器扩展命令进一步了解系统中 CPU/内核的系列(F)、型号(M)、步进(S)和速度。注意,(!cpuid)命令前的(!)符号表示该命令不受调试器本机支持,而是位于调试器扩展 DLL 中。
图片
在启动 WinDBG 之前,已经将环境变量 _NT_SYMBOL_PATH 设置为符号路径。WinDBG 应该会自动使用该变量中设置的符号路径, 使用 Set Symbol Path (.sympath) 命令来验证一下。注意,命令前面的 (.) 表示这是一条元命令。大多数此类命令都会改变调试器的行为。在这种特殊情况下,(.sympath) 命令可以与 (+) 选项一起使用,在当前符号路径上附加另一个路径。
图片
要查找调试器在记事本中使用的符号文件(.PDB)的路径,可以使用调试器扩展命令(!lmi)。该命令会解析 PE 头文件,并显示从 PE 文件的调试目录中获取的信息。
图片
5.进程和线程状态
进程状态 (|) 命令可用于查找正在调试的进程 ID 和进程名称。
图片
当 WinDBG 作为用户模式调试器使用时,线程状态(~)命令会显示当前进程中所有线程的信息。此时,记事本进程中只有一个线程,该线程的状态如下所示。这些信息包括线程 ID = 0x1df4、线程的 TEB 地址 0x000000e979a72000、线程的挂起次数以及线程的冻结状态信息。
图片
6.模块信息
要查找内存中已加载模块的虚拟地址,可以运行(List Loaded Modules)(lm)命令。运行 lm 命令本身将显示 notepad.exe 进程地址空间中每个模块加载的起始和终止地址范围:
图片
使用 (m) 选项运行 (lm) 命令可将输出限制在特定模块上。用它来检索分配给 notepad.exe 模块的 VA 范围。
要获取存储在资源(.rsrc)部分的模块版本信息,使用 lm 命令的 (v) 选项。
图片
在 WinDBG 中,任何模块的名称都会被视为一个表达式,如果在模块 "记事本 "上使用 MASM 表达式求值运算符(?),该表达式会求值到模块加载到内存的起始 VA。
图片
7.设置一个断点
我们已经找到了被调试进程的一些合理信息。现在将在 notepad.exe PE 文件的主入口点上设置一个断点。为此,必须找到代表 notepad.exe 主入口点的符号。要获得所有此类符号的列表,可以使用 Examine Symbol (x) 命令,该命令接受通配符,因此可以非常方便地获得以 main 结尾的函数列表。传递给 (x) 命令的参数包括:模块名称(不含扩展名)、作为分隔符的感叹号(!)以及符号名称(包含通配符)。
图片
在上述输出中,第一列显示的是符号所在的地址,后面是与给定通配符匹配的符号全名。
设置断点(bp)命令可以在符号名称或地址上设置断点。我们用它在 notepad.exe 的主入口点上设置断点。
图片
使用断点列表 (bl) 命令来验证断点是否设置正确。
图片
值得注意的是,bp 命令能够将符号 notepad!wWinMain 解析到适当的地址,即 00007ff6`f883b090,并且能够设置执行断点并启用它,如上面输出中的 (e) 所示。
图片
一旦继续执行,设置断点的函数就会被调用,断点触发,WinDBG 将进程控制权交还给我们。
8.触发断点
在函数 notepad!wWinMain 的第一条指令处,WinDBG 停止了 notepad.exe 的执行。通过检索所有 x64 CPU 寄存器的值来确定这一点。寄存器中的值还有助于检索传递给该函数的参数,因为在 x64 中,前 4 个参数都是通过 CPU 寄存器传递的。要检索 CPU 寄存器,可以使用寄存器 (r) 命令。
图片
上述输出结果证实,指令指针 (RIP) 中的地址确实指向函数 notepad!wWinMain 的第一条指令。函数 wWinMain 的原型以及包含相应参数的 CPU 寄存器如下所示。
图片
从 (r) 命令的输出中可以看到 hInstance = 0x00007ff6f8830000、hPrevInstance = 0x 0000000000000000 (NULL)、lpCmdLine=0x0000023771d528b6、nCmdShow=0000000000000a (SW_SHOWDEFAULT) 的值。
9.显示栈内容
RSP 寄存器指向当前线程的堆栈顶部。对于 64 位进程,堆栈中存储的每个值都是 64 位,即指针大小的值。要显示从 RSP 寄存器地址开始的内存内容,可以使用显示内存命令的 (dp) 变体。
注意,如果当前的表达式求值器是 C++,寄存器前面的 (@) 符号是必需的,这里默认选择C++。
图片
默认情况下,(dp) 命令在两列中显示 64 位数值。可以使用 (dp) 命令的列 (/c) 选项来更改,以 4 列显示内存内容,如下图所示。
图片
要显示多于默认的 16 个值,我们可以使用对象计数 (L) 选项,后面跟上要显示的值的个数。
图片
如果希望 WinDBG 自动尝试将显示的每一个值映射到符号,可以使用显示引用内存命令的 (dps) 。
图片
现在可以使用显示堆栈回溯(k)命令及其变体,按照调试器的预期方式查看堆栈。
图片
从该线程开始执行(ntdll!RtlUserThreadStart)一直到设置断点的当前函数(notepad!wWinMain)。显示的信息是调用链,现在深入研究一下显示的调用堆栈。
上面显示的每一行都代表一个函数的堆栈框架。最下面的一帧是最近的一帧,最上面的一帧是最近的一帧。
Child-SP 下列出的值是该帧的栈指针(RSP)寄存器值。这是在调用站点列所列函数的序逻辑执行完毕后 RSP 寄存器的值。RSP 寄存器的值在整个函数体中保持不变。函数的局部变量和基于堆栈的参数使用 RSP 中的这个值进行访问。
RetAddr 是当前函数(即调用站点下所列函数)执行完毕后的返回地址。该地址对应于下一个(较低)堆栈帧中显示的位置。例如,在最上面一帧的 RetAddr 上运行 List Nearest Symbols(ln)命令,就会映射到最上面一帧下面一帧的 Call Site 下所列函数和偏移量。
图片
既然已经了解了如何解释 (k) 命令所显示的信息,那么尝试一下它的一些变体。要在堆栈显示中包含帧号,请使用显示堆栈回溯(k)命令的(kn)变体。
图片
要显示仅列出模块和函数名称的简洁堆栈跟踪,请使用显示堆栈跟踪 (k) 命令的 (kc) 变体。
图片
最后,要显示包含传递给堆栈上每个函数的堆栈参数的详细堆栈跟踪,请使用显示堆栈跟踪 (k) 命令的 (kv) 变体。需要注意的是,根据 x64 调用约定,函数的前四个参数是通过 CPU 寄存器而不是堆栈传递的。因此,"Args to Child(子参数)"下显示的值是堆栈上的值,并不代表函数的实际参数,使用这些值可能会产生误导。因此,(kv) 命令在 x64 上的使用非常有限。
图片
10.显示字符串
现在来看看一些 WinDBG 命令,它们可用于显示应用程序使用的不同类型的字符串,如 ASCII 字符串、宽字符串和 Unicode 字符串。查找此类字符串的一种相对直接的方法是在 notepad.exe 或 NTDLL.dll 等模块中查找代表数据值(而非函数)的符号,这些符号的名称表明它们代表字符串。使用带 (/d) 选项的 "检查符号 (x)" 命令可以找到此类变量名的列表。我们还添加了 (/a) 选项,该选项将按地址升序显示输出结果。
图片
在 notepad.exe 上使用上述技术,我们得到了 notepad!_sz_ADVAPI32_dll 符号。数据变量名中的 "sz "意味着内存中包含一个以 NULL 结尾的 ASCII 字符串。根据这一假设,我们运行显示内存命令的 (da) 变体。
图片
再次使用上述符号列表技术,我们得到了 ntdll!SlashSystem32SlashString 符号。与前一种情况不同的是,这个名称并没有暗示这个数据变量所代表的字符串类型。它可能是 ASCII 字符串、宽字符串或 Unicode 字符串。如果事先不知道内存中的数据格式,可以使用显示内存命令的 (dc) 变体,以 DWORD(32 位)格式和 ASCII 字符显示内存内容,前提是这些字符是可打印的。
图片
通过观察内存位置的内容和识别模式 - 16 位整数、16 位整数、32 位 NULL、64 位地址,可以假设内存中有一个 Unicode 字符串头。为了确定这一点,可以使用显示类型 (dt) 命令来显示 Unicode 字符串头的数据类型。
图片
现在已经确认 ntdll!SlashSystem32SlashString 包含一个 Unicode 字符串头,继续使用显示字符串命令的 (dS) 变体来显示该字符串。
图片
除了使用 UNICODE_STRING 结构本身的地址,还可以使用 UNICODE_STRING 结构的 Buffer 字段(即 0x00007ff9`ff1b75c8)中的地址来显示字符串。可以使用显示内存命令的 (du) 变体。注意,宽字符串必须以 NULL 结尾。
图片
11.显示内存内容
符号 notepad!_sz_ADVAPI32_dll 中的内存包含 ASCII 字符串,可以以其他各种格式显示相同的内存,如 8 位字节 (db)、16 位字 (dw)、32 位双字 (dd) 和 64 位四元字 (dq)。注意,只有 (db) 命令以 ASCII 和十六进制数字显示输出。
显示为字节 (char)。
图片
显示为short类型:
图片
显示为long类型:
图片
显示为int64类型:
图片
12.在汇编程序中导航
虽然有比 WinDBG 更好的逆向工程工具,如 Ghidra,但 WinDBG 确实提供了浏览汇编函数的功能。WinDBG 缺少的最明显的功能是执行交叉引用的能力。
要反汇编从当前指令指针(RIP)开始的指令,我们使用反汇编(u)命令,并将 RIP 寄存器作为地址参数。(u)命令使用线性扫描算法反汇编接下来8条指令的操作码。
图片
为了反向反汇编指令,我们使用反汇编命令的 (ub) 变体,并再次指定 RIP 寄存器作为地址参数,反汇编 RIP 寄存器地址之前的 8 条指令。下面的列表显示了在函数 notepad!wWinMain 开始之前的一系列 INT 3 指令。编译器添加了这些 INT 3 指令,以确保 notepad!wWinMain 以 16 (0x10) 字节边界开始,并提供了一个小代码洞穴,可用于内联挂接的潜在用途。
图片
要反汇编 8 条以上的指令,可以指定一个地址范围,其中包括地址 (RIP) 和对象计数 (L),然后是要显示的指令数。
图片
(u) 和 (ub) 变体都使用线性扫描算法来反汇编函数,因此不知道函数边界或函数内的基本模块。而反汇编函数 (uf)命令则使用递归算法,通过评估函数中的每个基本模块来查找其他基本模块。为简洁起见,对以下输出进行了编辑。
图片
如果感兴趣的只是函数的调用而不是实际的反汇编,(uf) 命令的 (/c) 标志可以列出这些调用。为简洁起见,对以下输出进行了编辑。
图片
13.继续执行
上面已经完成了所有调试步骤,让 WinBDG 再次使用 Go (g) 命令继续执行进程 notepad.exe。
Windbg命令列表
vercommand | 显示调试器命令行 |
vertarget | 显示目标计算机版本 |
!cpuid | 显示CPU相关信息 |
.sympath | 设置符号路径 |
!lmi | 显示模块相关的详细信息 |
| | 进程状态 |
~ | 线程状态 |
lm | 列出已加载模块 |
? | 评估表达式 |
x | 检查符号 |
bp | 设置断点 |
bl | 启用断点 |
g | 执行 |
r | 寄存器 |
dp | 显示内存 |
dps | 显示已知符号的内存引用 |
k | 显示堆栈回溯 |
ln | 列出最近的符号 |
da | 以ASCII字符显示内存 |
dc | 以DWORD和ASCII显示内存 |
dt | 显示类型 |
dS | 显示UNICODE_STRING 结构字符串 |
du | 以宽字符显示内存 |
db | ASCII字符显示内存 |
dw | Word类型显示内存 |
dd | DWORD类型显示内存 |
dq | 四字格式显示内存 |
u | 反汇编 |
ub | 向后反汇编 |
uf | 反汇编函数 |