x64dbg实战:手把手教你实现函数追踪与参数解析
你有没有遇到过这样的场景?面对一个闭源的加密程序,你想搞清楚它是如何调用核心加密函数的,但没有源码、没有符号信息,甚至连入口点都找不到。这时候静态分析就像在黑暗中摸索——你能看到一堆指令,却不知道它们何时被执行、传了什么参数。
而动态分析,就是那束光。
今天,我们就以x64dbg为武器,带你从零开始完成一次完整的函数追踪实战:定位目标函数 → 设置断点 → 提取输入参数 → 监控返回值 → 自动化日志记录。整个过程不依赖任何外部工具(除了x64dbg),适合所有正在学习逆向工程或安全研究的同学。
为什么是 x64dbg?
市面上的调试器不少,比如老牌的 OllyDbg、微软官方的 WinDbg,还有 IDA Pro 的调试模块。但如果你要选一个现代、免费、功能强大又易于上手的 Windows 调试器,x64dbg 是目前最值得推荐的选择。
它不只是“能用”,而是真正做到了:
- 支持 x86/x64 双架构
- 图形界面现代化,操作直观
- 内建脚本系统 + Python 插件扩展
- 社区活跃,GitHub 持续更新
- 完全开源,无后门风险
更重要的是,它的设计哲学非常贴近实际分析需求:让你把精力集中在逻辑推理上,而不是和工具较劲。
我们要做什么?一个真实案例
假设我们拿到了一个名为crypto_tool.exe的程序,它会对用户输入进行 AES 加密,并输出结果。但我们不知道加密发生在哪个函数里,也不知道密钥是怎么处理的。
我们的目标是:
1. 找到负责加密的核心函数(暂且叫它EncryptData)
2. 追踪每次调用时传入的明文和密钥
3. 观察返回后的密文是否写入正确位置
4. 实现自动化日志记录,避免手动重复操作
听起来复杂?别急,我们一步步来。
第一步:启动并附加目标程序
打开 x64dbg,点击 “File” → “Open”,选择你的目标程序。如果程序已经运行(例如服务进程),也可以通过 “Attach to Process” 方式附加。
加载完成后,你会看到几个关键窗口:
-CPU 窗口:显示反汇编代码、寄存器、堆栈、内存等
-模块列表(Module Map):查看程序加载了哪些 DLL
-字符串窗口(Strings):提取程序中出现的所有可读文本
先别急着运行,我们要找线索。
第二步:通过字符串定位关键函数
很多程序会在错误提示、日志输出中留下蛛丝马迹。我们不妨看看有没有和加密相关的字符串。
点击菜单栏 “Search” → “Current Module” → “String references”,或者直接按快捷键Alt+S。
很快你会发现类似这样的字符串:
"Encryption started" "Key length invalid" "AES encryption completed"太好了!右键点击"AES encryption completed"→ “Find references”,跳转到引用它的代码位置:
call sub_140001A00 test eax, eax jz short loc_140001B20 mov rcx, offset aAesEncryptionCompleted ; "AES encryption completed" call printf看到call sub_140001A00了吗?这个函数很可能就是我们要找的EncryptData!
双击进入该函数,你会发现它做了以下几件事:
- 检查参数合法性
- 初始化 AES 上下文
- 调用底层加密例程
- 返回状态码
现在我们确认了:0x140001A00 就是我们要追踪的目标函数地址。
第三步:理解 x64 调用约定 —— 参数从哪来?
在 x86 时代,不同编译器使用不同的调用约定(__cdecl、__stdcall、__fastcall),让人头疼。但在x64 下,Windows 统一采用 Microsoft x64 calling convention,规则清晰明确:
| 参数序号 | 整型/指针 | 浮点 |
|---|---|---|
| 第1个 | RCX | XMM0 |
| 第2个 | RDX | XMM1 |
| 第3个 | R8 | XMM2 |
| 第4个 | R9 | XMM3 |
| 第5+个 | 压栈(从右至左) | 压栈 |
返回值放在RAX中。
所以,如果我们想获取EncryptData的前两个参数(比如明文指针和密钥指针),只需要看RCX 和 RDX的值即可。
📌 提示:结构体指针也很常见。如果 RCX 指向一块内存,记得去数据转储窗口查看其内容,可能是一个包含多个字段的上下文结构。
第四步:设置断点并观察参数
回到 CPU 窗口,在地址0x140001A00处按下F2,设置一个软件断点(你会看到地址背景变红)。
然后按F9运行程序,触发加密操作。
程序中断后,观察右侧寄存器面板:
RCX: 000001A8D3F40000 ← 明文缓冲区 RDX: 000001A8D3F40100 ← 密钥缓冲区 R8: 0000000000000010 ← 明文长度 = 16 字节 R9: 0000000000000020 ← 密钥长度 = 32 字节接下来,我们可以验证这些地址的内容。
右键点击 RCX 的值 → “Follow in Dump”,在下方“数据转储”窗口中切换编码方式(ASCII/UTF-8),看到明文确实是"HelloWorld123456"。
同理查看 RDX,发现密钥是"603DEB1015CA71BE..."—— 典型的 AES-256 密钥格式。
这说明我们成功捕获了函数调用的真实输入!
第五步:监控函数返回与输出结果
仅仅知道输入还不够,我们还想知道加密后的密文写到了哪里。
有两种方法可以做到:
方法一:在调用方下一条指令设断点
返回到原来的call EncryptData处,找到下一条指令地址(比如0x140001A50),在那里再设一个断点(F2)。当函数执行完返回时,程序会再次中断。
此时你可以检查:
- RAX 是否为 0(表示成功)
- 输出缓冲区是否有新数据写入
- 是否调用了printf或文件写入 API
方法二:使用栈上的返回地址设内存断点
更高级的做法是利用栈机制自动监听函数返回。
我们知道,函数调用时会把返回地址压入栈顶([RSP])。因此可以在[RSP]处设置内存访问断点,当函数ret时弹出该地址就会触发中断。
操作步骤:
1. 在函数入口中断后,记下[RSP]的值(比如0x140001A50)
2. 右键 → “Breakpoint” → “Memory on access”
3. 地址填rsp, 大小8, 类型Read/Write
这样当函数执行ret指令时,就会命中这个断点,无需手动寻找调用者。
第六步:自动化!用脚本批量记录调用日志
如果函数被频繁调用(比如每秒几十次),手动检查显然不可行。我们必须借助自动化。
x64dbg 支持两种脚本方式:内置脚本语言 和 Python 插件(x64dbgpy)。
方案一:使用内建脚本(轻量级)
新建一个文件trace_encrypt.dbg:
; trace_encrypt.dbg ; 功能:自动记录 EncryptData 的参数与返回值 entry: msg "Setting up trace on EncryptData..." bp 0x140001A00, "on_entry" ret on_entry: log "=== ENCRYPT CALL ===" log "Time: %t" log "Input Buffer (RCX): 0x%I64X", rcx log "Key Buffer (RDX): 0x%I64X", rdx log "Plaintext: '%s'", string(rcx) log "Key: '%s'", string(rdx) ; 设置返回监控 bpr esp, 8, 2, "on_return" ret on_return: log "Return Value (RAX): 0x%I64X", rax log "Output likely at: 0x%I64X", rcx ; 假设原地加密 log "" bc esp ; 清除内存断点 go ; 继续运行保存后,在 x64dbg 命令行输入:
script load "C:\path\to\trace_encrypt.dbg" script run entry从此以后,每一次EncryptData被调用,都会在日志窗口自动生成一条完整记录。
💡 技巧:
%t可打印时间戳;string(addr)尝试解析 C 风格字符串;log ""输出空行便于阅读。
方案二:使用 Python 插件(灵活强大)
如果你启用了x64dbgpy插件,可以用 Python 写更复杂的逻辑。
示例脚本encrypt_tracer.py:
from x64dbg import * def on_function_enter(): print(f"[+] Entering EncryptData @ {GetEIP():X}") plaintext_addr = Register("rcx") key_addr = Register("rdx") # 读取前16字节作为样本 plain_data = ReadByteArray(plaintext_addr, 16) key_data = ReadByteArray(key_addr, 32) print(f" Plaintext: {bytes(plain_data).decode('latin1', 'ignore')!r}") print(f" Key: {key_data.hex()}") # 绑定断点 AddBreakpoint(0x140001A00, BPS_NORMAL, on_function_enter)这种方式更适合做数据分析、网络上报、甚至集成 fuzzing 框架。
常见问题与避坑指南
❌ 问题一:函数没符号怎么办?
不是所有函数都有名字。解决办法:
- 用字符串交叉引用定位
- 查找特征指令序列(如mov rax, 0x41414141)
- 使用插件 Scylla 扫描已知库特征
❌ 问题二:断点被检测或绕过?
某些程序会检测 INT3 指令(软件断点的本质)来反调试。
应对策略:
- 改用硬件断点(最多4个,不修改内存)
- 使用内存断点监控代码页变化
- 启用 TitanHide 插件隐藏调试器痕迹
❌ 问题三:参数是指针嵌套结构怎么办?
比如RCX -> struct { char* data; int len; void* ctx; }
解决方案:
- 在数据转储窗口跟随指针(Follow in Dump)
- 手动计算偏移(如RCX+8是 ctx 地址)
- 使用 “Analyze Structure” 插件辅助解析
最佳实践建议
善用标签与注释
- 右键地址 → “Label” 给关键函数命名
- 使用 “Comment” 添加分析备注,方便后续回顾结合 IDA 预分析
- 先用 IDA 分析控制流、识别函数边界
- 导出.sig文件供 x64dbg 加载,提升识别率控制日志频率
- 高频调用函数启用条件断点:bp EncryptData if rcx == 0x12345678
- 或在脚本中加入过滤逻辑,只记录特定情况保护原始程序
- 调试前备份原文件,防止误改造成崩溃
- 如需打补丁,使用“Copy Patch to Executable”功能生成新文件
结语:掌握这项技能意味着什么?
当你能在几分钟内定位一个未知函数、还原其调用参数、监控执行流程时,你就不再只是一个“看代码的人”,而是变成了一个程序行为的掌控者。
无论是分析恶意软件的通信协议,还是破解某个 DRM 机制,亦或是复现已知漏洞的触发路径,这套基于 x64dbg 的动态追踪方法都能成为你最可靠的起点。
而这套能力的核心,并不是记住多少快捷键,而是建立一种思维方式:
程序的行为是可以被观测的,只要你在正确的时机、正确的地点,放上一只“眼睛”。
那只眼睛,可以是一个断点,一行脚本,或一段 Python 回调。
现在,轮到你动手试试了。
如果你在实践中遇到了其他挑战 —— 比如无法解析结构体、断点失效、多线程干扰 —— 欢迎在评论区留言讨论,我们一起拆解每一个技术难题。