返回介绍

20.4 调试版与发行版二进制文件

发布于 2024-10-11 21:05:48 字数 3893 浏览 0 评论 0 收藏 0

通常,微软的 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文