我试图理解使用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 $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发生了什么:
$ 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字节)。
让我们继续:
(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 $0x18,%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并不过分。唯一的区别是堆栈的大小。发生什么事了呢?
其他信息:
dumb_function,则不会发生此行为$ 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发布于 2020-07-13 02:59:50
你不是要覆盖保存的eip,这是真的。但是,您正在覆盖一个指针,该指针是函数用来查找保存的eip的。您实际上可以在您的i f输出中看到这一点;查看“前一帧的sp”,并注意这两个低字节是00 35;ASCI0x35是5,00是终止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,可能会将堆栈顶部标记为回溯),因此之后不会发生任何不好的事情。
https://stackoverflow.com/questions/62859245
复制相似问题