调试与逆向
在这小节,我们将学习简单的调试以及逆向技能,这对于恶意软件开发领域有这些帮助:理解当下的恶意软件的技术、功能、原理,分析恶意软件并且改进,开发漏洞利用以及 Shellcode,理解安全产品的原理以规避检测等。
我们将主要使用 WinDBG 作为动态调试工具,IDA 作为逆向工具。两者相辅相成,动态与静态相结合。WinDBG 可以在微软商店中获取:
IDA 可以从 https://hex-rays.com/ida-free/ 下载,拥有专业版自然更好。
WinDBG
调试器是插入在目标应用程序和 CPU 之间的计算机程序,充当类似代理的角色。使用调试器使我们能够查看应用程序的内存和执行流程并与之交互。接下来的课程内容中,我们将与用户模式交互。
CPU以二进制级别处理代码,这对人类来说很难阅读和理解,而汇编语言引入了二进制内容和编程语言之间的一对一映射。尽管汇编语言应该是人类可读的,但它仍然是一种低级语言,并且需要时间来掌握。 操作码是由 CPU 解释为特定指令的二进制序列,这在调试器中显示为十六进制值以及汇编语言的翻译。
接下来,利用 WinDbg,我们将学习如何使用断点来单步执行和控制应用程序的流程。
自定义界面
为了能有一个舒适的调试环境,我们可以对 WinDBG 的界面进行自定义,这样当我们调试应用程序的时候,可以在用户界面中查看到所需的信息,例如内存、汇编代码、断点信息等。
在导航栏中选中 View,我们可以添加多个窗口,这里面对我们调试过程会十分有帮助的有 WinDBG 命令 (Command)、寄存器 (Registers)、内存 (Memory)、栈 (Stack)、汇编代码 (Disassembly)。
点中之后,对应的窗口会浮现出来,可以使其与 WinDBG 主窗口相互独立,也可以将其嵌入至 WinDBG 主窗口,我会更推荐将它们嵌入至主窗口。不过因为主窗口空间有限,如果添加的窗口数量过多,也会影响我们对信息的提取效率,诸如模块 (Module)、断点 (Breakpoints) 等窗口我们不一定要添加到主页面,而是通过 WinDBG 的命令来查看相关信息。
下图是个人偏好的一个界面布局,你们可能注意到我添加了 2 个内存窗口,因为我们还想查看 RSP 的状态。
当我们想要调试一个进程或者应用的时候,可以选择使用 WinDBG 启动目标程序,或者附加到正常运行的进程中。
附加上之后,进程会自动被设置一个软件断点,对应的汇编指令为 int 3。
我们可以执行 WinDBG 命令 g 来继续执行。
在进入对 WinDBG 基本命令的学习之前,我们还需要知道调试符号。符号 (Symbol) 文件允许 WinDbg 使用名称而不是地址来引用内部函数、结构体和全局变量。例如,我们想给 kernelbase.dll 中的 CreateProcessA 函数设置软件断点,我们不需要先得到载入的 kernelbase.dll 中的 CreateProcessA 函数的地址,使用名称即可,命令为 bp kernelbase!CreateProcessA。在后面,我们也可以用符号来查看一些结构体。
符号文件以 .pdb 为拓展名,当应用程序的 pdb 与 PE 文件在同一目录下,符号文件会被自动载入从而识别应用程序中的符号。
这样,我们可以使用 process_calc!Main 来定位到 process_calc.exe 程序中的 Main 函数。
基本命令
有了对 WinDBG 的初始了解以及配置了最适合自己的界面布局后,我们来学习 WinDBG 的基本常用命令。WinDBG 内置的命令数量十分庞大,并且考虑到该小节的内容是作为恶意软件开发的前置知识,因此我们不会过于深入。接下来,我们依次介绍以下这些常用命令。
反汇编
在 WinDBG 中,使用命令 u <内存地址> 来检视给定内存地址的汇编代码。例如,我们可以查看 user32.dll 中的 MessageBoxA 函数的汇编实现:
0:000> u user32!messageboxa
USER32!MessageBoxA:
00007ffb`7e9a7a90 4883ec38 sub rsp,38h
00007ffb`7e9a7a94 4533db xor r11d,r11d
00007ffb`7e9a7a97 44391dcaf70300 cmp dword ptr [USER32!gfEMIEnable (00007ffb`7e9e7268)],r11d
00007ffb`7e9a7a9e 742e je USER32!MessageBoxA+0x3e (00007ffb`7e9a7ace)
00007ffb`7e9a7aa0 65488b042530000000 mov rax,qword ptr gs:[30h]
00007ffb`7e9a7aa9 4c8b5048 mov r10,qword ptr [rax+48h]
00007ffb`7e9a7aad 33c0 xor eax,eax
对于内存地址,可以是符号、内存地址、寄存器的形式,只要是合法的内存地址。可以通过 l* 来指定显示的行数。
0:000> u 00007ffb`7e9a7a90 l3
USER32!MessageBoxA:
00007ffb`7e9a7a90 4883ec38 sub rsp,38h
00007ffb`7e9a7a94 4533db xor r11d,r11d
00007ffb`7e9a7a97 44391dcaf70300 cmp dword ptr [USER32!gfEMIEnable (00007ffb`7e9e7268)],r11d
读取内存
我们可以使用命令 d* <内存地址> 来读取给定内存地址,星号可以是不同的数据类型,例如 byte,word,dword 等。我们可以在微软文档 https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/d--da--db--dc--dd--dd--df--dp--dq--du--dw--dw--dyb--dyd--display-memor 中了解更加详细的用法。
读取结构体
修改和写入内存
搜索内存空间
检视寄存器
计算器
列举模块
调用栈
TEB
内存状态
软件断点
硬件断点
单步执行
t
p
执行至
ph 和 th
pa 和 ta