为什么较小的堆栈边界不会发生分段错误?

Why segmentation fault doesn't occur with smaller stack boundary?

我试图了解使用 GCC 选项 -mpreferred-stack-boundary=2 编译的代码与默认值 -mpreferred-stack-boundary=4.

之间的行为差​​异

我已经阅读了很多关于此选项的 Q/A 但我无法理解我将在下面描述的情况。

让我们考虑一下这段代码:

#include <stdio.h>
#include <string.h>

void dumb_function() {}

int main(int argc, char** argv) {
    dumb_function();

    char buffer[24];
    strcpy(buffer, argv[1]);

    return 0;
}

在我的 64 位架构上,我想将其编译为 32 位,因此我将使用 -m32 选项。因此,我创建了两个二进制文件,一个带有 -mpreferred-stack-boundary=2,一个带有默认值:

sysctl -w kernel.randomize_va_space=0
gcc -m32 -g3 -fno-stack-protector -z execstack -o default vuln.c
gcc -mpreferred-stack-boundary=2 -m32 -g3 -fno-stack-protector -z execstack -o align_2 vuln.c

现在,如果我用两个字节的溢出来执行它们,我会遇到默认对齐的分段错误,但在其他情况下不会:

$ ./default 1234567890123456789012345
Segmentation fault (core dumped)
$ ./align_2 1234567890123456789012345
$

我试图用 default 来探究为什么会出现这种行为。下面是主要功能的反汇编:

08048411 <main>:
 8048411:   8d 4c 24 04             lea    0x4(%esp),%ecx
 8048415:   83 e4 f0                and    [=13=]xfffffff0,%esp
 8048418:   ff 71 fc                pushl  -0x4(%ecx)
 804841b:   55                      push   %ebp
 804841c:   89 e5                   mov    %esp,%ebp
 804841e:   53                      push   %ebx
 804841f:   51                      push   %ecx
 8048420:   83 ec 20                sub    [=13=]x20,%esp
 8048423:   89 cb                   mov    %ecx,%ebx
 8048425:   e8 e1 ff ff ff          call   804840b <dumb_function>
 804842a:   8b 43 04                mov    0x4(%ebx),%eax
 804842d:   83 c0 04                add    [=13=]x4,%eax
 8048430:   8b 00                   mov    (%eax),%eax
 8048432:   83 ec 08                sub    [=13=]x8,%esp
 8048435:   50                      push   %eax
 8048436:   8d 45 e0                lea    -0x20(%ebp),%eax
 8048439:   50                      push   %eax
 804843a:   e8 a1 fe ff ff          call   80482e0 <strcpy@plt>
 804843f:   83 c4 10                add    [=13=]x10,%esp
 8048442:   b8 00 00 00 00          mov    [=13=]x0,%eax
 8048447:   8d 65 f8                lea    -0x8(%ebp),%esp
 804844a:   59                      pop    %ecx
 804844b:   5b                      pop    %ebx
 804844c:   5d                      pop    %ebp
 804844d:   8d 61 fc                lea    -0x4(%ecx),%esp
 8048450:   c3                      ret    
 8048451:   66 90                   xchg   %ax,%ax
 8048453:   66 90                   xchg   %ax,%ax
 8048455:   66 90                   xchg   %ax,%ax
 8048457:   66 90                   xchg   %ax,%ax
 8048459:   66 90                   xchg   %ax,%ax
 804845b:   66 90                   xchg   %ax,%ax
 804845d:   66 90                   xchg   %ax,%ax
 804845f:   90                      nop

感谢 sub [=26=]x20,%esp 指令,我们可以了解到编译器为堆栈分配了 32 个字节,这是一致的 -mpreferred-stack-boundary=4 选项:32 是 16 的倍数。

第一个问题:为什么,如果我有一个 32 字节的堆栈(24 个字节用于缓冲区和其余的垃圾),我会得到一个仅溢出一个字节的分段错误?

让我们看看 gdb 发生了什么:

$ gdb default
(gdb) b 10
Breakpoint 1 at 0x804842a: file vuln.c, line 10.

(gdb) b 12
Breakpoint 2 at 0x8048442: file vuln.c, line 12.

(gdb) r 1234567890123456789012345
Starting program: /home/pierre/example/default 1234567890123456789012345

Breakpoint 1, main (argc=2, argv=0xffffce94) at vuln.c:10
10      strcpy(buffer, argv[1]);

(gdb) i f
Stack level 0, frame at 0xffffce00:
 eip = 0x804842a in main (vuln.c:10); saved eip = 0xf7e07647
 source language c.
 Arglist at 0xffffcde8, args: argc=2, argv=0xffffce94
 Locals at 0xffffcde8, Previous frame's sp is 0xffffce00
 Saved registers:
  ebx at 0xffffcde4, ebp at 0xffffcde8, eip at 0xffffcdfc

(gdb) x/6x buffer
0xffffcdc8: 0xf7e1da60  0x080484ab  0x00000002  0xffffce94
0xffffcdd8: 0xffffcea0  0x08048481

(gdb) x/x buffer+36
0xffffcdec: 0xf7e07647

就在调用strcpy之前,我们可以看到保存的eip是0xf7e07647。我们可以从缓冲区地址(堆栈堆栈的 32 个字节 + esp 的 4 个字节 = 36 个字节)返回此信息。

让我们继续:

(gdb) c
Continuing.

Breakpoint 2, main (argc=0, argv=0x0) at vuln.c:12
12      return 0;

(gdb) i f
Stack level 0, frame at 0xffff0035:
 eip = 0x8048442 in main (vuln.c:12); saved eip = 0x0
 source language c.
 Arglist at 0xffffcde8, args: argc=0, argv=0x0
 Locals at 0xffffcde8, Previous frame's sp is 0xffff0035
 Saved registers:
  ebx at 0xffffcde4, ebp at 0xffffcde8, eip at 0xffff0031

(gdb) x/7x buffer
0xffffcdc8: 0x34333231  0x38373635  0x32313039  0x36353433
0xffffcdd8: 0x30393837  0x34333231  0xffff0035

(gdb) x/x buffer+36
0xffffcdec: 0xf7e07647

我们可以看到缓冲区后的下一个字节溢出:0xffff0035。此外,存储 eip 的位置没有任何变化:0xffffcdec: 0xf7e07647 因为溢出仅为两个字节。但是,info frame 保存的eip 发生了变化:saved eip = 0x0 如果我继续,就会出现分段错误:

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()

发生了什么事?为什么我保存的eip变了,而溢出只有两个字节?

现在,让我们将其与使用另一种对齐方式编译的二进制文件进行比较:

$ objdump -d align_2
...
08048411 <main>:
...
 8048414:   83 ec 18                sub    [=17=]x18,%esp
...

堆栈恰好是 24 个字节。这意味着 2 个字节的溢出将覆盖 esp(但仍然不是 eip)。让我们用 gdb 检查一下:

(gdb) b 10
Breakpoint 1 at 0x804841c: file vuln.c, line 10.

(gdb) b 12
Breakpoint 2 at 0x8048431: file vuln.c, line 12.

(gdb) r 1234567890123456789012345
Starting program: /home/pierre/example/align_2 1234567890123456789012345

Breakpoint 1, main (argc=2, argv=0xffffce94) at vuln.c:10
10      strcpy(buffer, argv[1]);

(gdb) i f
Stack level 0, frame at 0xffffce00:
 eip = 0x804841c in main (vuln.c:10); saved eip = 0xf7e07647
 source language c.
 Arglist at 0xffffcdf8, args: argc=2, argv=0xffffce94
 Locals at 0xffffcdf8, Previous frame's sp is 0xffffce00
 Saved registers:
  ebp at 0xffffcdf8, eip at 0xffffcdfc

(gdb) x/6x buffer
0xffffcde0: 0xf7fa23dc  0x080481fc  0x08048449  0x00000000
0xffffcdf0: 0xf7fa2000  0xf7fa2000

(gdb) x/x buffer+28
0xffffcdfc: 0xf7e07647

(gdb) c
Continuing.

Breakpoint 2, main (argc=2, argv=0xffffce94) at vuln.c:12
12      return 0;

(gdb) i f
Stack level 0, frame at 0xffffce00:
 eip = 0x8048431 in main (vuln.c:12); saved eip = 0xf7e07647
 source language c.
 Arglist at 0xffffcdf8, args: argc=2, argv=0xffffce94
 Locals at 0xffffcdf8, Previous frame's sp is 0xffffce00
 Saved registers:
  ebp at 0xffffcdf8, eip at 0xffffcdfc

(gdb) x/7x buffer
0xffffcde0: 0x34333231  0x38373635  0x32313039  0x36353433
0xffffcdf0: 0x30393837  0x34333231  0x00000035

(gdb) x/x buffer+28
0xffffcdfc: 0xf7e07647

(gdb) c
Continuing.
[Inferior 1 (process 6118) exited normally]

不出所料,这里没有分段错误,因为我没有覆盖 eip。

我不明白这种行为差异。在这两种情况下,eip 都不会被覆盖。唯一的区别是堆栈的大小。发生什么事了?


附加信息:

$ gcc -v
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
$ uname -a
Linux pierre-Inspiron-5567 4.15.0-107-generic #108~16.04.1-Ubuntu SMP Fri Jun 12 02:57:13 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

您没有覆盖保存的 eip,这是真的。但是您正在覆盖函数用来查找保存的 eip 的指针。您实际上可以在 i f 输出中看到这一点;查看“Previous frame's sp”并注意两个低字节是如何00 35; ASCII 0x35 是 5 并且 00 是终止空值。因此,尽管保存的 eip 完好无损,但机器正在从其他地方获取其 return 地址,因此崩溃。


更详细:

GCC 显然不信任将堆栈对齐到 16 字节的启动代码,因此它自己处理事情 (and [=14=]xfffffff0,%esp)。但它需要跟踪之前的堆栈指针值,以便在需要时可以找到它的参数和 return 地址。这是 lea 0x4(%esp),%ecx,它用堆栈上保存的 eip 上方 的双字地址加载 ecx。 gdb 称此地址为“前一帧的 sp”,我猜是因为它是调用者执行其 call main 指令前 立即 的堆栈指针的值。我就简称为P吧。

对齐堆栈后,编译器将 -0x4(%ecx)(即 argv 参数)从堆栈中压入,以便于访问,因为稍后将需要它。然后它用 push %ebp; mov %esp, %ebp 设置它的栈帧。从现在开始,我们可以跟踪所有相对于 %ebp 的地址,就像编译器在不优化时通常做的那样。

push %ecx 几行之后将地址 P 存储在堆栈中的偏移量 -0x8(%ebp) 处。 sub [=23=]x20, %esp 在堆栈上增加了 32 个字节的 space(以 -0x28(%ebp) 结尾),但问题是,space 在哪里结束 buffer被安置?我们看到它发生在调用 dumb_function 之后,使用 lea -0x20(%ebp), %eax; push %eax;这是 strcpy 被推送的第一个参数,即 buffer,因此 buffer 实际上位于 -0x20(%ebp),而不是您可能猜到的 -0x28。因此,当您在那里写入 24 (=0x18) 个字节时,您会覆盖 -0x8(%ebp) 处的两个字节,这是我们存储的 P 指针。

从这里开始都是下坡路。 P 的损坏值(称为 Px)被弹出到 ecx 中,就在 return 之前,我们执行 lea -0x4(%ecx), %esp。现在 %esp 是垃圾,指向不好的地方,所以下面的 ret 肯定会带来麻烦。也许 Px 指向未映射的内存,只是试图从那里获取 return 地址会导致错误。也许它指向可读内存,但从该位置获取的地址并不指向可执行内存,因此控制传输出错。也许后者确实指向可执行内存,但位于那里的指令不是我们想要执行的指令。


如果您 take out the call to dumb_function(),堆栈布局会略有变化。不再需要围绕对 dumb_function() 的调用推送 ebx,因此来自 ecx 的 P 指针现在结束在 -4(%ebp),有 4 个未使用的字节 space(以保持对齐),然后 buffer-0x20(%ebp)。所以你的两个字节溢出进入 space 根本没有使用,因此没有崩溃。

here是用-mpreferred-stack-boundary=2生成的程序集。现在不需要重新对齐堆栈,因为编译器确实相信启动代码会将堆栈对齐到至少 4 个字节(如果不是这种情况是不可想象的)。堆栈布局更简单:压入 ebp,并为 buffer 再减去 24 个字节。因此,您的溢出会覆盖保存的 ebp 的两个字节。这最终从堆栈弹出回到 ebp,因此 main returns 到它的调用者,ebp 中的值是 和入境时不一样。这很顽皮,但碰巧系统启动代码没有将 ebp 中的值用于任何事情(实际上在我的测试中它在进入 main 时设置为 0,可能标记堆栈顶部以进行回溯),并且所以之后没有什么不好的事情发生。