- 献词
- 致谢
- 前言
- 第一部分 IDA 简介
- 第 1 章 反汇编简介
- 第 2 章 逆向与反汇编工具
- 第 3 章 IDA Pro 背景知识
- 第二部分 IDA 基本用法
- 第 4 章 IDA 入门
- 第 5 章 IDA 数据显示窗口
- 第 6 章 反汇编导航
- 第 7 章 反汇编操作
- 第 8 章 数据类型与数据结构
- 第 9 章 交叉引用与绘图功能
- 第 10 章 IDA 的多种面孔
- 第三部分 IDA 高级应用
- 第 11 章 定制 IDA
- 第 12 章 使用 FLIRT 签名来识别库
- 第 13 章 扩展 IDA 的知识
- 第 14 章 修补二进制文件及其他 IDA 限制
- 第四部分 扩展 IDA 的功能
- 第 15 章 编写 IDA 脚本
- 第 16 章 IDA 软件开发工具包
- 第 17 章 IDA 插件体系结构
- 第 18 章 二进制文件与 IDA 加载器模块
- 第 19 章 IDA 处理器模块
- 第五部分 实际应用
- 第 20 章 编译器变体
- 第 21 章 模糊代码分析
- 第 22 章 漏洞分析
- 第 23 章 实用 IDA 插件
- 第六部分 IDA 调试器
- 第 24 章 IDA 调试器
- 第 25 章 反汇编器/ 调试器集成
- 第 26 章 其他调试功能
- 附录 A 使用 IDA 免费版本 5.0
- 附录 B IDC/SDK 交叉引用
20.4 调试版与发行版二进制文件
通常,微软的 Visual Studio 项目能够构建调试版本或发行版本的二进制程序。要了解这两个版本之间的差异,可以比较一个项目的调试版本和发行版本所指定的构建选项。二者之间的细微差异包括:发行版本通常经过优化1 ,而调试版本则没有;调试版本链接有其他符号信息和运行库,而发行版本则没有。增加与调试有关的符号,有助于调试器将汇编语言语句转换成它们对应的源代码,还有助于确定局部变量的名称2 。通常,这些信息会在编译过程中丢失。在编译调试版本的微软运行库时,我们还可以包含调试符号、禁用优化并启用其他安全检查,以核实一些函数参数是否有效。
1. 优化通常是指删除代码中的多余代码,或选择更快但可能更大的代码序列,以满足开发者的期望,即创建更快或更小的可执行文件。与未优化代码相比,分析经过优化的代码要困难一些,因此,在程序的开发和调试阶段,使用优化代码被视为是一个错误的选择。
2. 在编译过程中,gcc 也能够插入调试符号。
使用 IDA 进行反汇编时,Visual Studio 项目的调试版本与发行版本之间存在着明显的差异。这是仅调试版本指定了编译器和链接器选项的结果,如基本的运行时检查(/RTCx3 )选项,它在最终的二进制文件中引入了额外的代码。这些额外的代码会造成一个“副作用”:它会破坏 IDA 的启动签名匹配过程,导致 IDA 总是无法自动定位调试版本的二进制文件中的 main
函数。
3. 参见 http://msdn.microsoft.com/en-us/library/8wtf2dfz.aspx 。
在调试版本的二进制文件中,一个明显的不同是几乎所有的函数都通过 jump
函数(也叫做 thunk
函数)调用,如下面的代码片段所示:
➎ .text:00411050 sub_411050 proc near ; CODE XREF: start_0+3↓p .text:00411050 ➏jmp sub_412AE0 .text:00411050 sub_411050 endp ... ➊ .text:0041110E start proc near .text:0041110E ➋jmp start_0 .text:0041110E start endp ... ➌ .text:00411920 start_0 proc near ; CODE XREF: start↑j .text:00411920 push ebp .text:00411921 mov ebp, esp .text:00411923 ➌call sub_411050 .text:00411928 call sub_411940 .text:0041192D pop ebp .text:0041192E retn .text:0041192E start_0 endp
在这个例子中,程序入口点(➊ )除了跳转(➋ )到真正的启动函数(➌ )外,其他什么也不做。启动函数又调用(➍ )另一个函数(➎ ),而后者则跳转到(➏ )启动函数的实现位置。➊ 和➎ 这两个函数中没有其他内容,只有一个叫做 thunk 函数的跳转语句。在调试版本的二进制文件中大量使用 thunk 函数给 IDA 的签名匹配过程造成一个很大的障碍。虽然 thunk
函数会大大减慢你的分析进程,但使用前一节中描述的技巧,你仍然可以跟踪到二进制文件的 main
函数。
在调试版本的二进制文件中进行基本的运行时检查,会导致在执行任何函数时都增加其他一些操作。一个调试版本的二进制文件的扩充版“序言”如下所示:
.text:00411500 push ebp .text:00411501 mov ebp, esp .text:00411503 ➊ sub esp, 0F0h .text:00411509 push ebx .text:0041150A push esi .text:0041150B push edi .text:0041150C ➋lea edi, [ebp+var_F0] .text:00411512 mov ecx, 3Ch .text:00411517 mov eax, 0CCCCCCCCh .text:0041151C rep stosd .text:0041151E ➌ mov [ebp+var_8], 0 .text:00411525 mov [ebp+var_14], 1 .text:0041152C mov [ebp+var_20], 2 .text:00411533 mov [ebp+var_2C], 3
这个例子中的函数使用 4 个局部变量,它们仅仅需要 16 个字节的栈空间。但是,我们看到,这个函数分配了 240 个字节(➊)的栈空间,然后用 0xCC
这个值填充这 240 个字节。从➋开始的 4 行代码等同于下面的函数调用:
memset(&var_F0, 0xCC, 240);
字节值 0xCC
对应于 int
3 的 x86 操作码, int 3
是一个软件中断,它会使一个程序进入调试器中。用大量 0xCC
值填充栈帧,是为了确保当程序试图执行栈中的指令时(在调试版本的二进制文件中可能会遇到的错误条件),调试器将被调用。
这个函数的局部变量从➌处开始初始化,从那里我们注意到,这些变量并不是彼此相邻。它们之间的多余空间由前面的 memset
操作用 0xC
C 值填充。但是,变量之间的额外空间会使我们更易于检测出一个变量的溢出情况,这种溢出可能会波及另一个变量,并给它造成破坏。在正常情况下,任何已声明变量之外用作填充符的 0xCC
值,绝不可能被覆写。为了便于比较,上面代码的发行版本如下所示:
.text:004018D0 push ebp .text:004018D1 mov ebp, esp .text:004018D3 ➊ sub esp, 10h .text:004018D6 ➋ mov [ebp+var_4], 0 .text:004018DD mov [ebp+var_C], 1 .text:004018E4 mov [ebp+var_8], 2 .text:004018EB mov [ebp+var_10], 3
我们看到,发行版本仅为局部变量分配了所需的空间(➊),而且所有 4 个变量彼此相邻(➋)。还要注意的是,这时已不再需要使用 0xCC
作为填充值。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论