关于局部变量的基本倒车问题

逆向工程 拆卸 x86
2021-06-19 06:27:12

我不明白以下内容:

在一个非常简单的虚拟 C 函数中:

void vulnerable_function(char* string) { 
    char buffer[100];
}

当我gdb用来拆卸它时,我得到:

0x08048464 <+0>:  push   %ebp
0x08048465 <+1>:  mov    %esp,%ebp
0x08048467 <+3>:  sub    $0x88,%esp

我真的不明白为什么堆栈指针会减少0x88。

我相信它会0x64代替0x88. 你能解释一下吗?

非常感谢你!

2个回答

编译器为进程运行时堆栈上的函数堆栈帧分配了多少空间涉及多个因素:

  • 在堆栈帧中保存函数的参数副本所需的空间
  • 在堆栈帧中存储局部变量所需的空间
  • 堆栈对齐到 16 字节边界(i386 架构的 GCC 默认值)

背景

i386 ABI

堆栈帧规范

x86 机器上堆栈帧的规范在System V 应用程序二进制接口 Intel386 架构处理器增补第四版的第 3 章:“低级系统信息”中给出标题为“函数调用序列”部分。

笔记:

在本规范中,术语半字指的是 16 位对象,术语指的是 32 位对象,术语双字指的是 64 位对象。

以下是相关摘录:

  • 堆栈是字对齐的。尽管该体系结构不需要任何堆栈对齐,但软件约定和操作系统要求堆栈在字边界上对齐。

  • 如有必要,增加参数的大小以使其成为单词的倍数。这可能需要尾部填充,具体取决于参数的大小。

  • 其他方面取决于编译器和正在编译的代码。标准调用序列不定义最大堆栈帧大小,也不限制语言系统如何使用标准堆栈帧的“未指定”区域。

堆栈帧中的“未指定”区域是为局部变量创建的空间以及函数参数被复制到的空间。这个空间由编译器管理。

这是来自 ABI 的图表: 标准堆栈帧,i386

结盟

管理堆栈帧的是编译器,为了对齐堆栈帧,还必须知道堆栈帧内变量的对齐方式。

变量的对齐方式取决于它们的类型和 CPU 的架构。这也在 ABI 中指定: 基本类型,i386

有一些约定特别适用于数组、结构和联合的对齐:

聚合(结构和数组)和联合假定其最严格对齐的组件对齐。任何对象(包括聚合和联合)的大小始终是对象对齐的倍数。数组使用与其元素相同的对齐方式。结构和联合对象可能需要填充以满足大小和对齐约束。任何填充的内容都是未定义的。

但是,在 i386 架构系统上,GCC 默认将堆栈对齐到 16 字节的边界:

-mpreferred-stack-boundary=num
尝试将堆栈边界与 2 提升到 num 字节边界对齐。如果-mpreferred-stack-boundary未指定,则默认值为 4(16 字节或 128 位)。

这意味着编译器会在堆栈帧上为类型大小小于 16 字节的变量分配 16 字节的空间。例如,即使inti386 系统上的an是 4 个字节,编译器仍会在堆栈帧上为其分配 16 个字节的空间。

易受攻击的函数()的堆栈帧

让我们用两个简单的例子来分析编译器如何在函数的堆栈帧上分配空间:一个带有char指针局部变量的函数和一个带有 100 字节char数组的函数

pointer_test使用char指针局部变量调用的函数

void pointer_test(void) {
    char *i = "test";
}

gcc+生成的汇编代码as

Dump of assembler code for function pointer_test:
   0x080483db <+0>:     push   %ebp
   0x080483dc <+1>:     mov    %esp,%ebp
   0x080483de <+3>:     sub    $0x10,%esp  <-- 16 bytes of space created for 4-byte pointer
   0x080483e1 <+6>:     movl   $0x8048480,-0x4(%ebp)
   0x080483e8 <+13>:    nop
   0x080483e9 <+14>:    leave  
   0x080483ea <+15>:    ret  

这里我们看到为 4 字节指针分配了 16 字节的空间。

char_array_test使用 char 数组局部变量调用的函数

void char_array_test(void) {
    char buffer[100];
}

gcc+生成的汇编代码as

Dump of assembler code for function char_array_test:
   0x0804844b <+0>:     push   %ebp
   0x0804844c <+1>:     mov    %esp,%ebp
   0x0804844e <+3>:     sub    $0x78,%esp  <-- 120 bytes of space created for 100-byte array
   0x08048451 <+6>:     mov    %gs:0x14,%eax
   0x08048457 <+12>:    mov    %eax,-0xc(%ebp)
   0x0804845a <+15>:    xor    %eax,%eax
   0x0804845c <+17>:    nop
   0x0804845d <+18>:    mov    -0xc(%ebp),%eax
   0x08048460 <+21>:    xor    %gs:0x14,%eax
   0x08048467 <+28>:    je     0x804846e <char_array_test+35>
   0x08048469 <+30>:    call   0x8048310 <__stack_chk_fail@plt>
   0x0804846e <+35>:    leave  
   0x0804846f <+36>:    ret

这里我们看到为 100 字节的数组分配了 120 字节的空间。

在 的情况下void vulnerable_function(char *string),必须gcc为 4 字节指针和 100 字节数组分配堆栈帧中的空间

  • 正如我们在上面 中观察到的pointer_test(),由于gcc默认情况下将分配的空间与 16 字节边界对齐,因此还在堆栈帧上为 4 字节指针char *string(函数的参数)分配了 16 字节的空间
  • 我们在上面观察到char_array_test()的是gcc分配120个字节的空间为100字节阵列(120不是16的倍数,所以这不是一个16字节边界对齐。我不知道为什么编译器这一点)。同样,编译器为char buffer[100]in分配了 120 字节的空间vulnerable_function()

0x10 字节string+ 0x78 字节buffer[100]= 0x88

资源

Compiler Explorer是一个在浏览器中运行的交互式编译器。使用它比不断地重新编译和反汇编代码要快得多。

System V 应用二进制接口 Intel386 架构处理器增补,第四版

GCC 的 Intel 386 和 AMD x86-64 选项

cdecl和 x86 调用约定讨论了 x86 编译器中的调用约定

Poke-a-hole and Friends是一篇文章,讨论了如何填充结构以保持对齐以及这如何跨架构发生变化。

相关的问题

堆栈分配、填充和对齐

什么是“堆栈对齐”?

正如 SYS_V 在他的回答中正确引用的那样GCC 文档指出 GCC 将默认将堆栈指针与 16 字节边界对齐。

-mpreferred-stack-boundary=num

尝试将堆栈边界与 2 提升到 num 字节边界对齐。如果-mpreferred-stack-boundary未指定,则默认值为 4(16 字节或 128 位)。

我们找到了一些关于为什么要这样做的推理(注意:在 64 位架构上,16 字节对齐是强制性的):

当使用 16 字节堆栈对齐编译的函数(例如标准库中的函数)使用未对齐的堆栈调用时,[A different value] 会导致错误代码。在这种情况下,SSE 指令可能会导致未对齐的内存访问陷阱 [并且] 对于 16 字节对齐的对象,变量参数的处理不正确 [...] 您必须构建所有模块 [具有相同的值]。这包括系统库和启动模块。

但是请注意,这主要是关于堆栈(边界),不一定是堆栈上的单个对象。这种帧对齐不是发生在函数内部,而是发生在调用站点,在那里你会看到这样的东西(注意从 中的额外减法%esp):

 sub    $0xc,%esp                # pad stack by 12 bytes
 push   %eax                     # push 4-byte argument
 call   vulnerable_function

尽管如此,保持(某些)对象对齐也是有意义的。

在您的示例中,您会遇到为 100 字节缓冲区分配的 0x88 (=136) 字节,而 SYS_V 为相同的缓冲区分配了 0x78 (=120)。请注意,这两个值都是 8 模 16 全等的。之所以选择这个,是因为此时您的堆栈帧已经包含两个 4 字节值:返回地址和保存的帧指针。将这些结合起来,您最终会在分配后进行 16 字节对齐。