8、动态分析工具(下):x64dbg高级功能

好,咱们接着聊。上一节我们把 x64dbg 的基础操作过了一遍,断点、单步、内存查看这些基本功,算是入了门。但说实话,真正让 x64dbg 在逆向圈子里站稳脚跟的,是它那些「高级功能」。我刚开始用的时候,也觉得基础功能够用了,直到有一次在分析一个加壳的恶意样本时,被反调试搞得焦头烂额……嗯,从那以后我才认真研究起这些高级玩法。

8.1 追踪:让程序「边走边报」

追踪功能,说白了就是让调试器帮你记录程序每一步的执行轨迹。你想想看,有时候我们单步跟了几百步,突然发现某个关键跳转没走对,想回头看看之前发生了什么——这时候如果没有记录,就只能重来一遍。多痛苦。

x64dbg 提供了两种追踪模式:

  • 步进追踪(Step Trace):记录每一条指令的执行情况
  • 回溯追踪(Back Trace):记录函数调用和返回的路径

我个人习惯在分析关键算法时开启步进追踪。比如遇到一个加密函数,我想知道它到底调用了哪些子函数、走了哪些分支,直接开追踪跑一遍,然后看日志就行了。

小技巧:追踪日志默认会很大,建议先设置过滤条件。比如只记录 CALL 指令,或者只记录某个内存区域的访问。不然跑个几万步,日志文件能把你硬盘撑爆。

8.1.1 如何开启追踪

在 x64dbg 的菜单栏找到「Trace」→「Start Trace」。或者直接用快捷键 Ctrl+Alt+T。启动后,程序每执行一步,调试器就会在「Trace」标签页里追加一条记录。

每条记录包含:

  • 指令地址
  • 指令字节码
  • 指令反汇编
  • 寄存器状态(可选)
  • 内存访问信息(可选)

我曾经在分析一个勒索软件时,靠追踪功能找到了它的加密密钥生成逻辑。当时它用了大量混淆指令,单步跟根本看不出来规律。但把追踪日志导出来,用脚本一分析,模式就清晰了。

8.2 条件断点:精准打击

普通断点有个问题:它每次执行到那个地址都会停下来。如果某个函数被调用了十万次,你难道要按十万次 F9 吗?

条件断点就是解决这个问题的。你可以设置一个条件,只有条件满足时,断点才会触发。

举个例子:

// 假设我们要在 CreateFileW 函数上断点
// 但只关心打开 "config.ini" 这个文件的情况

// 条件表达式(在断点属性里设置):
[esp+4] == "config.ini"

嗯,这里要注意:x64dbg 的条件表达式语法和 C 语言很像,但有一些差异。比如字符串比较要用双引号,地址要用方括号括起来。

8.2.1 常用条件断点场景

场景 条件表达式示例
只断在特定参数值 [esp+4] == 0x12345678
只断在特定调用次数 hitcount == 100
只断在寄存器满足条件 eax > 0x1000
只断在内存值变化 ReadByte(0x401000) != 0x90
避坑指南:我曾经在分析一个网络协议时,设了一个条件断点 [esp+8] == 0x80,结果程序直接崩溃了。后来才发现,那个地址在某些调用路径下是无效指针。所以条件断点里的表达式一定要确保安全,最好先确认地址可读。

8.3 脚本:让调试自动化

如果你觉得手动操作太累,那脚本就是你的救星。x64dbg 内置了一套脚本引擎,支持变量、循环、条件判断、函数调用等。

我一般用脚本做三件事:

  1. 批量打补丁:比如把程序里所有 jz 改成 jnz
  2. 自动化分析:自动设置断点、运行、记录结果
  3. 内存搜索与修改:批量扫描特定模式并修改

来看一个简单的脚本示例:

// 自动搜索并修改字符串
var addr
find 0x400000, 0x500000, "Hello", addr
// 如果找到了,就把字符串改成 "World"
if addr != 0
  set addr, "World"
  log "String patched at {addr}"
else
  log "String not found"
end

这个脚本的逻辑很简单:在 0x400000 到 0x500000 的内存范围内搜索字符串 "Hello",如果找到了就改成 "World"。实际分析中,我经常用类似脚本批量修改程序的验证逻辑。

8.4 API 断点与调用栈分析

API 断点是逆向分析中最常用的技术之一。你想知道程序什么时候读写文件?在 ReadFile 上下断。想知道它什么时候联网?在 sendrecv 上下断。

设置 API 断点很简单:

  1. 在「符号」标签页找到目标 API
  2. 右键 → 「切换断点」
  3. 或者直接在反汇编窗口输入 bp kernel32!CreateFileW

但光有断点还不够。很多时候你需要知道:是谁调用了这个 API? 这时候就要看调用栈了。

8.4.1 调用栈分析

当断点触发时,切换到「调用栈」标签页。你会看到类似这样的信息:

Address    | Return to | 函数/模块
0x00A3F8C0 | 0x00401234 | kernel32!CreateFileW
0x00A3F8E0 | 0x00401345 | myapp!ReadConfig+0x10
0x00A3F900 | 0x00401456 | myapp!InitApp+0x20
0x00A3F920 | 0x00401567 | myapp!WinMain+0x30

从下往上看:WinMain 调用了 InitAppInitApp 调用了 ReadConfigReadConfig 调用了 CreateFileW。这样你就知道程序是在初始化阶段读取配置文件。

个人经验:分析恶意软件时,我经常在 VirtualAllocWriteProcessMemoryCreateRemoteThread 这三个 API 上同时下断点。一旦触发,看调用栈就能快速定位到注入代码的源头。这招帮我抓过不少「偷渡式」注入。

8.5 动态分析实战:追踪程序关键逻辑

理论说完了,咱们来点实战。假设我们有一个程序,它会对输入进行验证,如果正确就显示「成功」,否则显示「失败」。我们的目标是找到正确的输入。

8.5.1 第一步:定位关键函数

先用 API 断点。程序要显示结果,大概率会调用 MessageBoxWSetWindowTextW。我们在这些 API 上下断点。

运行程序,输入一个错误的字符串,点击确定。断点触发,看调用栈:

user32!MessageBoxW
myapp!CheckPassword+0x45
myapp!ButtonClick+0x12
...

好,关键函数是 CheckPassword。我们回到反汇编窗口,定位到这个函数。

8.5.2 第二步:追踪比较逻辑

CheckPassword 函数入口下断点,重新运行。输入同样的错误字符串,断点触发。

单步跟踪,注意观察:

  • 它从哪里读取输入?
  • 它调用了什么比较函数?
  • 比较的结果如何影响跳转?

我一般会重点关注 strcmpmemcmplstrcmp 这类比较函数。如果程序没有直接调用这些 API,那可能是自己实现了比较逻辑——这时候就要看循环和条件跳转了。

8.5.3 第三步:提取正确密码

假设我们跟踪到程序调用了 strcmp,参数分别是我们的输入和一个内存地址。那这个内存地址里存的就是正确密码。

在调用 strcmp 之前,查看寄存器或栈上的参数:

// 假设 strcmp 的调用约定是 stdcall
// 参数1(输入)在 [esp+4]
// 参数2(正确密码)在 [esp+8]

// 在内存窗口查看 [esp+8] 指向的内容
dump [esp+8]

看到一串字符:P@ssw0rd_2024。这就是正确密码。

核心思路总结:动态分析的关键不是盲目单步,而是先通过 API 断点定位关键函数,再通过调用栈理清调用关系,最后用追踪和条件断点精准锁定比较逻辑。说白了,就是「先宏观,再微观」。

8.6 本章知识体系

下面这张图概括了本章的核心内容,方便你回顾:

x64dbg 高级功能知识体系 追踪 记录执行轨迹 条件断点 精准触发 脚本 自动化调试 API断点+调用栈 定位关键函数 步进追踪 记录每条指令 回溯追踪 记录调用路径 参数条件 [esp+4] == value 命中次数 hitcount == N 批量打补丁 修改指令/数据 自动化分析 断点+记录 API断点 bp kernel32!CreateFileW 调用栈分析 追溯调用来源 实战流程:定位关键函数 → 追踪比较逻辑 → 提取关键数据 API断点 → 调用栈 → 条件断点 → 单步追踪 → 内存/寄存器分析

好了,这一章的内容就到这儿。x64dbg 的高级功能其实远不止这些,但掌握了追踪、条件断点、脚本和 API 断点这四板斧,应付大部分逆向场景已经足够了。下一节我们会把这些技巧综合起来,做一个完整的实战案例。


公众号:蓝海资料掘金营,微信deep3321