1. 动态调试的概念
动态调试是指在程序运行中,通过附加在程序进程上的调试器(debugger),获知程序的执行流程、内存数据和寄存器信息等的一种手段。
我们还有其他的监控程序运行的手段,比如动态插装和内存修改器(CE 大法好)
对于逆向工程,动态调试可以帮助使用者验证静态分析时的一些猜测,有时可以直接解决静态分析下表面上复杂的算法,例如对于某个本质上是可逆的算法,可以直接将密文作为输入,在动态调试中查看原程序处理后的明文。这比从头写解密脚本要快速得多,而且不容易出错。
建议可以优先尝试动态调试,看看能不能通过原程序直接得到 flag。
我们还可以通过动态调试完成对花指令和壳的分析。
IDA 可以应对大部分动态调试任务。如果出现 IDA 无法调试的情况,可以换用 olly dbg (32-bits) 和 x64dbg,因为后两者有比较完善的反调试插件(OpenSource 魅力时刻)。
2.IDA 支持的调试特性
2.1 断点
-
软件断点
IDA 将程序目标地址上的原字节覆盖为调试字节。当断点命中时,IDA 再恢复原字节
-
硬件断点
IDA 利用 CPU 提供的断点机制,将目标地址存入特殊的几个寄存器。CPU 执行到目标地址时,会向操作系统发送异常指令,IDA 就会捕获这个断点
-
条件断点
用户预设一段代码,程序执行到断点处,执行这段代码。如果代码返回 “True”,命中断点;如果代码返回 “False”,不命中断点。
-
脚本断点
用户预设一个脚本,每命中断点一次就执行一次脚本。脚本执行完后可以返回 “False”
比如我们希望循环获取程序中某个寄存器的值,并且要求自动处理断点,我们就可以设置脚本断点,并让其返回 “False”
2.2 控制程序运行
-
步入
-
步过
-
步出函数
-
执行到光标位置
2.3 调试代码
- 汇编级
- 伪代码级
- 源码级
2.4 内存操作
- 寄存器读写
- 内存读写
2.5 特殊调试
- 进程调试
- 附加调试
3.IDA 动态调试演示
BUUCTF - Reverse1
我们先查看程序的伪代码:
1 | printf("input the flag:"); |
本段程序的目的是比较 Str1
和 Str2
。Str1
由我们输入,而 Str2
作为目标 flag,是不依附于任何输入的。我们可以对
1 | if ( !strncmp(Str1, Str2, v5) ) |
这一行设置断点,当断点命中时,查看 Str2
的值
(在伪代码窗口直接查看)
(汇编代码窗口查看)
FCTF 2025 - Tree
本题如果只靠静态分析加密算法的话是很吓人的,毕竟这里的加密算法又是二叉树又是 DFS 的,一看就搞不定。
真的搞不定吗?可以动态调试看看。
将密文作为输入,下断点在加密函数执行后,查看用户输入变量,惊奇地发现我们已经得到了 flag—— 不需要任何的密码和算法经验。
为什么?如果去问 DeepSeek,可以找到一些答案(但不要寄希望于小鲸鱼可以直接写出解密脚本,它也说不清楚):所有子节点指针都是 NULL
,实际上没有构建出任何树结构,这不过是出题者写的纸老虎。此时该算法退化成简单的对称加密。
尽管如此,我们提供的信息仍然无法让 DeepSeek 给出正确的解密脚本(至少我是如此)。DeepSeek 不厌其烦地跟我提示这里还有其他未发现的加密流程,可我无法定位。动态调试的建议直到我明确表述加密和二叉树无关后才提及。
4.IDA 附加调试演示
还是上面的例子,设置断点,但我们不从 IDA 运行程序。我们在外部先运行程序。
然后进入 Debugger > Attach to process… ,选择我们要调试的进程:
开始调试后,IDA 会先将程序停在某个系统 dll 库中,我们一般需要继续运行程序。
5. 调试代码演示
F7 是**步入(Step into)**,程序会进入下一个函数。如果需要撤回,可以按 Ctrl+ F7 (Run until return)。 F8是**步过(Step over)**,程序会跳过函数(`scanf`等具有阻塞机制的函数除外)。 F4是**执行到光标处(Run to cursor)**,程序可以执行到当前位置之*后*的任意位置。6. 内存修改演示
调试时,我们一个比较常用的窗口是内存窗口 (Hex view)。
在 Hex View 窗口按下 G 键,或进入 Jump > Jump to address…,出现跳转窗口,输入你需要跳转到的地址即可。
IDA 提供了修改内存的功能。
我们跳到 Str1
处。
按 F2 或者右键 > Edit… 进入修改模式
通过修改对应位上的十六进制值,我们就可以修改 Str1
的内容:
修改完成之后再按一次 F2 或者右键 > Apply Changes,保存修改结果。
有时我们需要一次性复制大量内存数据,这时就可以用其他开发者提供的插件了,比如此处使用的 LazyIDA 插件:
插件提供了十六进制输入、Base64 编码输入和 ASCII 码输入三种输入方式:
我们的内存就被改好了。
需要注意的是,插件并不会自动添加字符串结尾的‘0’。如果你想要输入的是字符串,请手动添加末尾的‘0’。
如果需要修改寄存器的值,可以在 General registers 窗口点击目标寄存器的值进行修改。
7. 模块窗口演示
进入 Debugger > Debugger windows > Module list,弹出的窗口展示了当前程序加载的所有模块。在 Windows 系统上一般为.dll 文件,在 Linux 系统上一般为.so 文件。
调试.dll 或.so 库时需要关注这个窗口。
8. 跨平台调试
8.1 Windows 动态调试
如果要调试 PE 文件,我们直接使用 IDA 的 Windows 本地调试 (Local Windows debugger) 即可(当然 IDA 也支持远程调试 Windows 程序)
一般步骤:
- 选择调试器:Debugger > Select Debugger > Local Windows debugger
- 开始调试:Debugger > Start Process
注意调试前先设好断点。
8.2 Linux 动态调试
Linux 的 ELF 文件无法在 Windows 系统上运行,更不用说动态调试了。所以 IDA 提供了远程调试,在 Windows 上将调试指令发送到 Linux 上,在 Linux 中调试程序。
远程调试需要在被调试机上安装一个远程调试服务端。实际的调试指令由该服务端执行,IDA 作为调试前端展示调试结果。
IDA 和服务端之间通过 tcp 通信发送调试指令。
调试机和被调试机之间要保持网络畅通,一般两机是在同一个内网部署。
打开已经部署的 Linux 系统,将 IDA 安装目录 (Windows) 下 dbgsvr 的 linux_server 和 linux_server64 上传到 Linux 系统,运行 server
对于 Windows 用户,可以通过以下方式部署 Linux 系统:
- VMware、VirtualBox 等虚拟机软件
- WSL2 (⚠️必须是 WSL2,WSL1 不能远程调试)
- 本机双系统
- 云服务
再买一台主机装 Linux
你应该看到这样的输出:
之后我们打开 IDA。
我们还要将 Windows 上需要调试的文件上传到 Linux 中。我们可以手动上传,但在配置好远程调试后,IDA 也可以帮我们上传文件到 Linux。
选择 Debugger > Process options…,在弹出的窗口中按照 Linux_server 监听 (Listen) 的 IP 地址和端口设置 Hostname 和 Port。
0.0.0.0 表示监听所有可用地址。由于我们是本机调试,所以 Hostname 应填写 127.0.0.1,表示本地调试。
接下来选择 Remote Linux debugger,开始调试。由于 Linux 系统中没有目标文件,因此 IDA 会问我们需不需要上传到 Linux 中,这里我们选择 Yes,然后就进入我们熟悉的调试界面了:
9. 动态调试技巧
- Ctrl + N 设置代码执行的位置(用来跳过不想运行的代码)
-
使用插件(如 LazyIDA)批量修改内存
-
附加调试(附加到一个正在运行的进程进行调试,一般附加成功后程序会断在一个固定的库函数处)
-
一般用来调试动态库
-
绕过一些反调试
-