Linux中國

使用 GDB 查看程序的棧空間

昨天我和一些人在閑聊的時候,他們說他們並不真正了解棧是如何工作的,而且也不知道如何去查看棧空間。

這是一個快速教程,介紹如何使用 GDB 查看 C 程序的棧空間。我認為這對於 Rust 程序來說也是相似的。但我這裡仍然使用 C 語言,因為我發現用它更簡單,而且用 C 語言也更容易寫出錯誤的程序。

我們的測試程序

這裡是一個簡單的 C 程序,聲明了一些變數,從標準輸入讀取兩個字元串。一個字元串在堆上,另一個字元串在棧上。

#include <stdio.h>
#include <stdlib.h>

int main() {
    char stack_string[10] = "stack";
    int x = 10;
    char *heap_string;

    heap_string = malloc(50);

    printf("Enter a string for the stack: ");
    gets(stack_string);
    printf("Enter a string for the heap: ");
    gets(heap_string);
    printf("Stack string is: %sn", stack_string);
    printf("Heap string is: %sn", heap_string);
    printf("x is: %dn", x);
}

這個程序使用了一個你可能從來不會使用的極為不安全的函數 gets 。但我是故意這樣寫的。當出現錯誤的時候,你就知道是為什麼了。

第 0 步:編譯這個程序

我們使用 gcc -g -O0 test.c -o test 命令來編譯這個程序。

-g 選項會在編譯程序中將調式信息也編譯進去。這將會使我們查看我們的變數更加容易。

-O0 選項告訴 gcc 不要進行優化,我要確保我們的 x 變數不會被優化掉。

第一步:啟動 GDB

像這樣啟動 GDB:

$ gdb ./test

它列印出一些 GPL 信息,然後給出一個提示符。讓我們在 main 函數這裡設置一個斷點:

(gdb) b main

然後我們就可以運行程序:

(gdb) b main
Starting program: /home/bork/work/homepage/test
Breakpoint 1, 0x000055555555516d in main ()

(gdb) run
Starting program: /home/bork/work/homepage/test

Breakpoint 1, main () at test.c:4
4   int main() {

好了,現在程序已經運行起來了。我們就可以開始查看棧空間了。

第二步:查看我們變數的地址

讓我們從了解我們的變數開始。它們每個都在內存中有一個地址,我們可以像這樣列印出來:

(gdb) p &x
$3 = (int *) 0x7fffffffe27c
(gdb) p &heap_string
$2 = (char **) 0x7fffffffe280
(gdb) p &stack_string
$4 = (char (*)[10]) 0x7fffffffe28e

因此,如果我們查看那些地址的堆棧,那我們應該能夠看到所有的這些變數!

概念:棧指針

我們將需要使用棧指針,因此我將儘力對其進行快速解釋。

有一個名為 ESP 的 x86 寄存器,稱為「 棧指針 stack pointer 」。 基本上,它是當前函數的棧起始地址。 在 GDB 中,你可以使用 $sp 來訪問它。 當你調用新函數或從函數返回時,棧指針的值會更改。

第三步:在 main 函數開始的時候,我們查看一下在棧上的變數

首先,讓我們看一下 main 函數開始時的棧。 現在是我們的堆棧指針的值:

(gdb) p $sp
$7 = (void *) 0x7fffffffe270

因此,我們當前函數的棧起始地址是 0x7fffffffe270,酷極了。

現在,讓我們使用 GDB 列印出當前函數堆棧開始後的前 40 個字(即 160 個位元組)。 某些內存可能不是棧的一部分,因為我不太確定這裡的堆棧有多大。 但是至少開始的地方是棧的一部分。

我已粗體顯示了 stack_stringheap_stringx 變數的位置,並改變了顏色:

  • x 是紅色字體,並且起始地址是 0x7fffffffe27c
  • heap_string 是藍色字體,起始地址是 0x7fffffffe280
  • stack_string 是紫色字體,起始地址是 0x7fffffffe28e

你可能會在這裡注意到的一件奇怪的事情是 x 的值是 0x5555,但是我們將 x 設置為 10! 那是因為直到我們的 main 函數運行之後才真正設置 x ,而我們現在才到了 main 最開始的地方。

第三步:運行到第十行代碼後,再次查看一下我們的堆棧

讓我們跳過幾行,等待變數實際設置為其初始化值。 到第 10 行時,x 應該設置為 10

首先我們需要設置另一個斷點:

(gdb) b test.c:10
Breakpoint 2 at 0x5555555551a9: file test.c, line 11.

然後繼續執行程序:

(gdb) continue
Continuing.

Breakpoint 2, main () at test.c:11
11      printf("Enter a string for the stack: ");

好的! 讓我們再來看看堆棧里的內容! gdb 在這裡格式化位元組的方式略有不同,實際上我也不太關心這些(LCTT 譯註:可以查看 GDB 手冊中 x 命令,可以指定 c 來控制輸出的格式)。 這裡提醒一下你,我們的變數在棧上的位置:

  • x 是紅色字體,並且起始地址是 0x7fffffffe27c
  • heap_string 是藍色字體,起始地址是 0x7fffffffe280
  • stack_string 是紫色字體,起始地址是 0x7fffffffe28e

在繼續往下看之前,這裡有一些有趣的事情要討論。

stack_string 在內存中是如何表示的

現在(第 10 行),stack_string 被設置為字元串stack。 讓我們看看它在內存中的表示方式。

我們可以像這樣列印出字元串中的位元組(LCTT 譯註:可以通過 c 選項直接顯示為字元):

(gdb) x/10x stack_string
0x7fffffffe28e: 0x73    0x74    0x61    0x63    0x6b    0x00    0x00    0x00
0x7fffffffe296: 0x00    0x00

stack 是一個長度為 5 的字元串,相對應 5 個 ASCII 碼- 0x730x740x610x630x6b0x73 是字元 s 的 ASCII 碼。 0x74t 的 ASCII 碼。等等...

同時我們也使用 x/1s 可以讓 GDB 以字元串的方式顯示:

(gdb) x/1s stack_string
0x7fffffffe28e: "stack"

heap_stringstack_string 有何不同

你已經注意到了 stack_stringheap_string 在棧上的表示非常不同:

  • stack_string 是一段字元串內容(stack
  • heap_string 是一個指針,指向內存中的某個位置

這裡是 heap_string 變數在內存中的內容:

0xa0  0x92  0x55  0x55  0x55  0x55  0x00  0x00

這些位元組實際上應該是從右向左讀:因為 x86 是小端模式,因此,heap_string 中所存放的內存地址 0x5555555592a0

另一種方式查看 heap_string 中存放的內存地址就是使用 p 命令直接列印 :

(gdb) p heap_string
$6 = 0x5555555592a0 ""

整數 x 的位元組表示

x 是一個 32 位的整數,可由 0x0a 0x00 0x00 0x00 來表示。

我們還是需要反向來讀取這些位元組(和我們讀取 heap_string 需要反過來讀是一樣的),因此這個數表示的是 0x000000000a 或者是 0x0a,它是一個數字 10;

這就讓我把把 x 設置成了 10

第四步:從標準輸入讀取

好了,現在我們已經初始化我們的變數,我們來看一下當這段程序運行的時候,棧空間會如何變化:

printf("Enter a string for the stack: ");
gets(stack_string);
printf("Enter a string for the heap: ");
gets(heap_string);

我們需要設置另外一個斷點:

(gdb) b test.c:16
Breakpoint 3 at 0x555555555205: file test.c, line 16.

然後繼續執行程序:

(gdb) continue
Continuing.

我們輸入兩個字元串,為棧上存儲的變數輸入 123456789012 並且為在堆上存儲的變數輸入 bananas;

讓我們先來看一下 stack_string(這裡有一個緩存區溢出)

(gdb) x/1s stack_string
0x7fffffffe28e: "123456789012"

這看起來相當正常,對嗎?我們輸入了 12345679012,然後現在它也被設置成了 12345679012(LCTT 譯註:實測 gcc 8.3 環境下,會直接段錯誤)。

但是現在有一些很奇怪的事。這是我們程序的棧空間的內容。有一些紫色高亮的內容。

令人奇怪的是 stack_string 只支持 10 個位元組。但是現在當我們輸入了 13 個字元以後,發生了什麼?

這是一個典型的緩衝區溢出,stack_string 將自己的數據寫在了程序中的其他地方。在我們的案例中,這還沒有造成問題,但它會使你的程序崩潰,或者更糟糕的是,使你面臨非常糟糕的安全問題。

例如,假設 stack_string 在內存里的位置剛好在 heap_string 之前。那我們就可能覆蓋 heap_string 所指向的地址。我並不確定 stack_string 之後的內存里有一些什麼。但我們也許可以用它來做一些詭異的事情。

確實檢測到了有緩存區溢出

當我故意寫很多字元的時候:

 ./test
Enter a string for the stack: 01234567891324143
Enter a string for the heap: adsf
Stack string is: 01234567891324143
Heap string is: adsf
x is: 10
*** stack smashing detected ***: terminated
fish: Job 1, &apos;./test&apos; terminated by signal SIGABRT (Abort)

這裡我猜是 stack_string 已經到達了這個函數棧的底部,因此額外的字元將會被寫在另一塊內存中。

當你故意去使用這個安全漏洞時,它被稱為「堆棧粉碎」,而且不知何故有東西在檢測這種情況的發生。

我也覺得這很有趣,雖然程序被殺死了,但是當緩衝區溢出發生時它不會立即被殺死——在緩衝區溢出之後再運行幾行代碼,程序才會被殺死。 好奇怪!

這些就是關於緩存區溢出的所有內容。

現在我們來看一下 heap_string

我們仍然將 bananas 輸入到 heap_string 變數中。讓我們來看一下內存中的樣子。

這是在我們讀取了字元串以後,heap_string 在棧空間上的樣子:

需要注意的是,這裡的值是一個地址。並且這個地址並沒有改變,但是我們來看一下指向的內存上的內容。

(gdb) x/10x 0x5555555592a0
0x5555555592a0: 0x62    0x61    0x6e    0x61    0x6e    0x61    0x73    0x00
0x5555555592a8: 0x00    0x00

看到了嗎,這就是字元串 bananas 的位元組表示。這些位元組並不在棧空間上。他們存在於內存中的堆上。

堆和棧到底在哪裡?

我們已經討論過棧和堆是不同的內存區域,但是你怎麼知道它們在內存中的位置呢?

每個進程都有一個名為 /proc/$PID/maps 的文件,它顯示了每個進程的內存映射。 在這裡你可以看到其中的棧和堆。

$ cat /proc/24963/maps
... lots of stuff omitted ...
555555559000-55555557a000 rw-p 00000000 00:00 0                          [heap]
... lots of stuff omitted ...
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]

需要注意的一件事是,這裡堆地址以 0x5555 開頭,棧地址以 0x7fffff 開頭。 所以很容易區分棧上的地址和堆上的地址之間的區別。

像這樣使用 gdb 真的很有幫助

這有點像旋風之旅,雖然我沒有解釋所有內容,但希望看到數據在內存中的實際情況可以使你更清楚地了解堆棧的實際情況。

我真的建議像這樣來把玩一下 gdb —— 即使你不理解你在內存中看到的每一件事,我發現實際上像這樣看到我程序內存中的數據會使抽象的概念,比如「棧」和「堆」和「指針」更容易理解。

更多練習

一些關於思考棧的後續練習的想法(沒有特定的順序):

  • 嘗試將另一個函數添加到 test.c 並在該函數的開頭創建一個斷點,看看是否可以從 main 中找到堆棧! 他們說當你調用一個函數時「堆棧會變小」,你能在 gdb 中看到這種情況嗎?
  • 從函數返回一個指向棧上字元串的指針,看看哪裡出了問題。 為什麼返回指向棧上字元串的指針是不好的?
  • 嘗試在 C 中引起堆棧溢出,並嘗試通過在 gdb 中查看堆棧溢出來準確理解會發生什麼!
  • 查看 Rust 程序中的堆棧並嘗試找到變數!
  • 噩夢課程 中嘗試一些緩衝區溢出挑戰。每個問題的答案寫在 README 文件中,因此如果你不想被寵壞,請避免先去看答案。 所有這些挑戰的想法是給你一個二進位文件,你需要弄清楚如何導致緩衝區溢出以使其列印出 flag 字元串。

via: https://jvns.ca/blog/2021/05/17/how-to-look-at-the-stack-in-gdb/

作者:Julia Evans 選題:lujun9972 譯者:amwps290 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國