簡體   English   中英

為什么較小的堆棧邊界不會發生分段錯誤?

[英]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:
$ 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM