[英]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
現在,如果我以兩個字節的溢出執行它們,則默認 alignment 存在分段錯誤,但在其他情況下則沒有:
$ ./default 1234567890123456789012345
Segmentation fault (core dumped)
$ ./align_2 1234567890123456789012345
$
我試圖用default
挖掘這種行為的原因。 下面是主function的拆解:
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 字節和垃圾的 rest),我得到一個只有一個字節溢出的分段錯誤?
讓我們看看 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 改變了,而溢出只有兩個字節?
現在,讓我們將其與用另一個 alignment 編譯的二進制文件進行比較:
$ 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
你沒有覆蓋保存的eip,這是真的。 但是您正在覆蓋 function 用於查找保存的 eip 的指針。 您實際上可以在if
output; 查看“Previous frame's sp”並注意兩個低字節是00 35
; ASCII 0x35 是5
是終止00
。 因此,盡管保存的 eip 完好無損,但機器正在從其他地方獲取其返回地址,從而導致崩潰。
更詳細地說:
GCC 顯然不信任啟動代碼將堆棧對齊到 16 個字節,因此它需要自己處理( and $0xfffffff0,%esp
)。 但是它需要跟蹤之前的堆棧指針值,以便在需要時可以找到它的參數和返回地址。 這是lea 0x4(%esp),%ecx
,它使用堆棧中保存的 eip上方的 dword 的地址加載 ecx。 gdb 將此地址稱為“上一幀的 sp”,我猜是因為它是調用者執行其call main
指令之前堆棧指針的值。 我簡稱它為P。
對齊堆棧后,編譯器會從堆棧中推送-0x4(%ecx)
參數,這是argv
參數,以便於訪問,因為稍后會需要它。 然后它使用push %ebp; mov %esp, %ebp
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
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
指向未映射的 memory 並且只是嘗試從那里獲取返回地址會導致故障。 也許它指向可讀的 memory,但從該位置獲取的地址並不指向可執行的 memory,因此控制轉移錯誤。 也許后者確實指向可執行 memory,但位於那里的指令不是我們想要執行的指令。
如果您取消對dumb_function()
的調用,堆棧布局會略有變化。 不再需要在對dumb_function()
的調用周圍推動ebx,因此來自ecx 的P 指針現在以-4(%ebp)
結束,有4 個字節的未使用空間(以保持對齊),然后buffer
位於-0x20(%ebp)
。 所以你的兩字節溢出進入了根本沒有使用的空間,因此沒有崩潰。
這是使用-mpreferred-stack-boundary=2
生成的程序集。 現在不需要重新對齊堆棧,因為編譯器確實相信啟動代碼會將堆棧對齊到至少 4 個字節(如果不是這種情況,那將是不可想象的)。 堆棧布局更簡單:push ebp,並為buffer
減去 24 個字節。 因此,您的溢出覆蓋了已保存 ebp 的兩個字節。 這最終會從堆棧中彈出回 ebp,因此main
返回到其調用者時,ebp 中的值與 entry 中的值不同。 這很淘氣,但碰巧系統啟動代碼沒有將 ebp 中的值用於任何事情(實際上在我的測試中,它在進入 main 時設置為 0,可能會標記堆棧的頂部以進行回溯),並且所以之后沒有什么不好的事情發生。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.