首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >为什么在较小的堆栈边界上不发生分段故障?

为什么在较小的堆栈边界上不发生分段故障?
EN

Stack Overflow用户
提问于 2020-07-12 09:17:07
回答 1查看 311关注 0票数 1

我试图理解使用GCC选项-mpreferred-stack-boundary=2编译的代码与默认值-mpreferred-stack-boundary=4之间的行为差异。

我已经读过很多关于这个选项的Q/A,但是我无法理解我将在下面描述的情况。

让我们考虑一下下面的代码:

代码语言:javascript
运行
复制
#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,一个具有默认值:

代码语言:javascript
运行
复制
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

现在,如果我以两个字节的溢出执行它们,则对于默认的对齐有分段错误,但在另一种情况下没有:

代码语言:javascript
运行
复制
$ ./default 1234567890123456789012345
Segmentation fault (core dumped)
$ ./align_2 1234567890123456789012345
$

我试图探究为什么使用default时会出现这种行为。以下是主要功能的分解:

代码语言:javascript
运行
复制
08048411 <main>:
 8048411:   8d 4c 24 04             lea    0x4(%esp),%ecx
 8048415:   83 e4 f0                and    $0xfffffff0,%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    $0x20,%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    $0x4,%eax
 8048430:   8b 00                   mov    (%eax),%eax
 8048432:   83 ec 08                sub    $0x8,%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    $0x10,%esp
 8048442:   b8 00 00 00 00          mov    $0x0,%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 $0x20,%esp指令,我们可以了解编译器为堆栈分配了32个字节--这是-mpreferred-stack-boundary=4选项: 32是16的倍数。

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

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

代码语言:javascript
运行
复制
$ 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 = 36字节为4字节)。

让我们继续:

代码语言:javascript
运行
复制
(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,如果我继续,则会发生分段错误:

代码语言:javascript
运行
复制
(gdb) c
Continuing.

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

发生什么事了呢?为什么在溢出只有两个字节的情况下,保存的eip会改变呢?

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

代码语言:javascript
运行
复制
$ objdump -d align_2
...
08048411 <main>:
...
 8048414:   83 ec 18                sub    $0x18,%esp
...

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

代码语言:javascript
运行
复制
(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并不过分。唯一的区别是堆栈的大小。发生什么事了呢?

其他信息:

  • 如果不存在dumb_function,则不会发生此行为
  • 我用的是GCC的以下版本:
代码语言:javascript
运行
复制
$ gcc -v
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
  • 关于我的系统的一些信息:
代码语言:javascript
运行
复制
$ 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
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2020-07-13 02:59:50

你不是要覆盖保存的eip,这是真的。但是,您正在覆盖一个指针,该指针是函数用来查找保存的eip的。您实际上可以在您的i f输出中看到这一点;查看“前一帧的sp”,并注意这两个低字节是00 35;ASCI0x35是500是终止null。因此,虽然保存的eip完好无损,但机器正在从其他地方获取其返回地址,从而导致崩溃。

更详细的是:

GCC显然不信任启动代码将堆栈对齐为16个字节,因此它将事情掌握在自己的手中(and $0xfffffff0,%esp)。但是它需要跟踪前一个堆栈指针值,以便在需要时找到它的参数和返回地址。这是lea 0x4(%esp),%ecx,它加载ecx时,dword的地址就在堆栈上保存的eip之上。gdb将此地址称为“前一个帧的sp",我猜是因为它是调用方执行其call main指令之前堆栈指针的值。我简称它为P。

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

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

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

如果使用dumb_function(),则堆栈布局略有更改。不再需要围绕对dumb_function()的调用推动ebx,因此来自ecx的P指针现在在-4(%ebp)结束,有4字节的未使用空间(用于维护对齐),然后buffer位于-0x20(%ebp)。因此,你的两个字节的溢出进入了空间,完全没有使用,因此没有崩溃。

这里是使用-mpreferred-stack-boundary=2生成的程序集。现在没有必要重新对齐堆栈,因为编译器确实信任启动代码将堆栈对齐为至少4个字节(如果不这样做是不可想象的)。堆栈布局更简单:推ebp,并为buffer再减去24个字节。因此,您的溢出覆盖保存的ebp的两个字节。这最终会从堆栈中弹出,返回到ebp中,因此main返回给调用者,其中ebp中的值与条目上的值不一样。这很顽皮,但系统启动代码没有将ebp中的值用于任何东西(实际上,在我的测试中,它在输入为main时被设置为0,可能会将堆栈顶部标记为回溯),因此之后不会发生任何不好的事情。

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/62859245

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档