新聞中心
昨天我和一些人在閑聊的時(shí)候,他們說(shuō)他們并不真正了解棧是如何工作的,而且也不知道如何去查看??臻g。

這是一個(gè)快速教程,介紹如何使用 GDB 查看 C 程序的棧空間。我認(rèn)為這對(duì)于 Rust 程序來(lái)說(shuō)也是相似的。但我這里仍然使用 C 語(yǔ)言,因?yàn)槲野l(fā)現(xiàn)用它更簡(jiǎn)單,而且用 C 語(yǔ)言也更容易寫出錯(cuò)誤的程序。
我們的測(cè)試程序
這里是一個(gè)簡(jiǎn)單的 C 程序,聲明了一些變量,從標(biāo)準(zhǔn)輸入讀取兩個(gè)字符串。一個(gè)字符串在堆上,另一個(gè)字符串在棧上。
#include#includeint 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: %s\n", stack_string);printf("Heap string is: %s\n", heap_string);printf("x is: %d\n", x);}
這個(gè)程序使用了一個(gè)你可能從來(lái)不會(huì)使用的極為不安全的函數(shù) gets 。但我是故意這樣寫的。當(dāng)出現(xiàn)錯(cuò)誤的時(shí)候,你就知道是為什么了。
第 0 步:編譯這個(gè)程序
我們使用 gcc -g -O0 test.c -o test 命令來(lái)編譯這個(gè)程序。
-g 選項(xiàng)會(huì)在編譯程序中將調(diào)式信息也編譯進(jìn)去。這將會(huì)使我們查看我們的變量更加容易。
-O0 選項(xiàng)告訴 gcc 不要進(jìn)行優(yōu)化,我要確保我們的 x 變量不會(huì)被優(yōu)化掉。
第一步:?jiǎn)?dòng) GDB
像這樣啟動(dòng) GDB:
$ gdb ./test
它打印出一些 GPL 信息,然后給出一個(gè)提示符。讓我們?cè)?nbsp;main 函數(shù)這里設(shè)置一個(gè)斷點(diǎn):
(gdb) b main
然后我們就可以運(yùn)行程序:
(gdb) b mainStarting program: /home/bork/work/homepage/testBreakpoint 1, 0x000055555555516d in main ()(gdb) runStarting program: /home/bork/work/homepage/testBreakpoint 1, main () at test.c:44 int main() {
好了,現(xiàn)在程序已經(jīng)運(yùn)行起來(lái)了。我們就可以開始查看棧空間了。
第二步:查看我們變量的地址
讓我們從了解我們的變量開始。它們每個(gè)都在內(nèi)存中有一個(gè)地址,我們可以像這樣打印出來(lái):
(gdb) p &x$3 = (int *) 0x7fffffffe27c(gdb) p &heap_string$2 = (char **) 0x7fffffffe280(gdb) p &stack_string$4 = (char (*)[10]) 0x7fffffffe28e
因此,如果我們查看那些地址的堆棧,那我們應(yīng)該能夠看到所有的這些變量!
概念:棧指針
我們將需要使用棧指針,因此我將盡力對(duì)其進(jìn)行快速解釋。
有一個(gè)名為 ESP 的 x86 寄存器,稱為“棧指針stack pointer”。 基本上,它是當(dāng)前函數(shù)的棧起始地址。 在 GDB 中,你可以使用 $sp 來(lái)訪問(wèn)它。 當(dāng)你調(diào)用新函數(shù)或從函數(shù)返回時(shí),棧指針的值會(huì)更改。
第三步:在 main 函數(shù)開始的時(shí)候,我們查看一下在棧上的變量
首先,讓我們看一下 main 函數(shù)開始時(shí)的棧。 現(xiàn)在是我們的堆棧指針的值:
(gdb) p $sp$7 = (void *) 0x7fffffffe270
因此,我們當(dāng)前函數(shù)的棧起始地址是 0x7fffffffe270,酷極了。
現(xiàn)在,讓我們使用 GDB 打印出當(dāng)前函數(shù)堆棧開始后的前 40 個(gè)字(即 160 個(gè)字節(jié))。 某些內(nèi)存可能不是棧的一部分,因?yàn)槲也惶_定這里的堆棧有多大。 但是至少開始的地方是棧的一部分。
我已粗體顯示了 stack_string,heap_string 和 x 變量的位置,并改變了顏色:
x是紅色字體,并且起始地址是0x7fffffffe27cheap_string是藍(lán)色字體,起始地址是0x7fffffffe280stack_string是紫色字體,起始地址是0x7fffffffe28e
你可能會(huì)在這里注意到的一件奇怪的事情是 x 的值是 0x5555,但是我們將 x 設(shè)置為 10! 那是因?yàn)橹钡轿覀兊?nbsp;main 函數(shù)運(yùn)行之后才真正設(shè)置 x ,而我們現(xiàn)在才到了 main 最開始的地方。
第三步:運(yùn)行到第十行代碼后,再次查看一下我們的堆棧
讓我們跳過(guò)幾行,等待變量實(shí)際設(shè)置為其初始化值。 到第 10 行時(shí),x 應(yīng)該設(shè)置為 10。
首先我們需要設(shè)置另一個(gè)斷點(diǎn):
(gdb) b test.c:10Breakpoint 2 at 0x5555555551a9: file test.c, line 11.
然后繼續(xù)執(zhí)行程序:
(gdb) continueContinuing.Breakpoint 2, main () at test.c:1111 printf("Enter a string for the stack: ");
好的! 讓我們?cè)賮?lái)看看堆棧里的內(nèi)容! gdb 在這里格式化字節(jié)的方式略有不同,實(shí)際上我也不太關(guān)心這些(LCTT 譯注:可以查看 GDB 手冊(cè)中 x 命令,可以指定 c 來(lái)控制輸出的格式)。 這里提醒一下你,我們的變量在棧上的位置:
x是紅色字體,并且起始地址是0x7fffffffe27cheap_string是藍(lán)色字體,起始地址是0x7fffffffe280stack_string是紫色字體,起始地址是0x7fffffffe28e
在繼續(xù)往下看之前,這里有一些有趣的事情要討論。
stack_string 在內(nèi)存中是如何表示的
現(xiàn)在(第 10 行),stack_string 被設(shè)置為字符串stack。 讓我們看看它在內(nèi)存中的表示方式。
我們可以像這樣打印出字符串中的字節(jié)(LCTT 譯注:可以通過(guò) c 選項(xiàng)直接顯示為字符):
(gdb) x/10x stack_string0x7fffffffe28e: 0x73 0x74 0x61 0x63 0x6b 0x00 0x00 0x000x7fffffffe296: 0x00 0x00
stack 是一個(gè)長(zhǎng)度為 5 的字符串,相對(duì)應(yīng) 5 個(gè) ASCII 碼- 0x73、0x74、0x61、0x63 和 0x6b。0x73 是字符 s 的 ASCII 碼。 0x74 是 t 的 ASCII 碼。等等...
同時(shí)我們也使用 x/1s 可以讓 GDB 以字符串的方式顯示:
(gdb) x/1s stack_string0x7fffffffe28e: "stack"
heap_string 與 stack_string 有何不同
你已經(jīng)注意到了 stack_string 和 heap_string 在棧上的表示非常不同:
stack_string是一段字符串內(nèi)容(stack)heap_string是一個(gè)指針,指向內(nèi)存中的某個(gè)位置
這里是 heap_string 變量在內(nèi)存中的內(nèi)容:
0xa0 0x92 0x55 0x55 0x55 0x55 0x00 0x00
這些字節(jié)實(shí)際上應(yīng)該是從右向左讀:因?yàn)?x86 是小端模式,因此,heap_string 中所存放的內(nèi)存地址 0x5555555592a0
另一種方式查看 heap_string 中存放的內(nèi)存地址就是使用 p 命令直接打印 :
(gdb) p heap_string$6 = 0x5555555592a0 ""
整數(shù) x 的字節(jié)表示
x 是一個(gè) 32 位的整數(shù),可由 0x0a 0x00 0x00 0x00 來(lái)表示。
我們還是需要反向來(lái)讀取這些字節(jié)(和我們讀取 heap_string 需要反過(guò)來(lái)讀是一樣的),因此這個(gè)數(shù)表示的是 0x000000000a 或者是 0x0a,它是一個(gè)數(shù)字 10;
這就讓我把把 x 設(shè)置成了 10。
第四步:從標(biāo)準(zhǔn)輸入讀取
好了,現(xiàn)在我們已經(jīng)初始化我們的變量,我們來(lái)看一下當(dāng)這段程序運(yùn)行的時(shí)候,??臻g會(huì)如何變化:
printf("Enter a string for the stack: ");gets(stack_string);printf("Enter a string for the heap: ");gets(heap_string);
我們需要設(shè)置另外一個(gè)斷點(diǎn):
(gdb) b test.c:16Breakpoint 3 at 0x555555555205: file test.c, line 16.
然后繼續(xù)執(zhí)行程序:
(gdb) continueContinuing.
我們輸入兩個(gè)字符串,為棧上存儲(chǔ)的變量輸入 123456789012 并且為在堆上存儲(chǔ)的變量輸入 bananas;
讓我們先來(lái)看一下 stack_string(這里有一個(gè)緩存區(qū)溢出)
(gdb) x/1s stack_string0x7fffffffe28e: "123456789012"
這看起來(lái)相當(dāng)正常,對(duì)嗎?我們輸入了 12345679012,然后現(xiàn)在它也被設(shè)置成了 12345679012(LCTT 譯注:實(shí)測(cè) gcc 8.3 環(huán)境下,會(huì)直接段錯(cuò)誤)。
但是現(xiàn)在有一些很奇怪的事。這是我們程序的??臻g的內(nèi)容。有一些紫色高亮的內(nèi)容。
令人奇怪的是 stack_string 只支持 10 個(gè)字節(jié)。但是現(xiàn)在當(dāng)我們輸入了 13 個(gè)字符以后,發(fā)生了什么?
這是一個(gè)典型的緩沖區(qū)溢出,stack_string 將自己的數(shù)據(jù)寫在了程序中的其他地方。在我們的案例中,這還沒(méi)有造成問(wèn)題,但它會(huì)使你的程序崩潰,或者更糟糕的是,使你面臨非常糟糕的安全問(wèn)題。
例如,假設(shè) stack_string 在內(nèi)存里的位置剛好在 heap_string 之前。那我們就可能覆蓋 heap_string 所指向的地址。我并不確定 stack_string 之后的內(nèi)存里有一些什么。但我們也許可以用它來(lái)做一些詭異的事情。
確實(shí)檢測(cè)到了有緩存區(qū)溢出
當(dāng)我故意寫很多字符的時(shí)候:
./testEnter a string for the stack: 01234567891324143Enter a string for the heap: adsfStack string is: 01234567891324143Heap string is: adsfx is: 10*** stack smashing detected ***: terminatedfish: Job 1, './test' terminated by signal SIGABRT (Abort)
這里我猜是 stack_string 已經(jīng)到達(dá)了這個(gè)函數(shù)棧的底部,因此額外的字符將會(huì)被寫在另一塊內(nèi)存中。
當(dāng)你故意去使用這個(gè)安全漏洞時(shí),它被稱為“堆棧粉碎”,而且不知何故有東西在檢測(cè)這種情況的發(fā)生。
我也覺得這很有趣,雖然程序被殺死了,但是當(dāng)緩沖區(qū)溢出發(fā)生時(shí)它不會(huì)立即被殺死——在緩沖區(qū)溢出之后再運(yùn)行幾行代碼,程序才會(huì)被殺死。 好奇怪!
這些就是關(guān)于緩存區(qū)溢出的所有內(nèi)容。
現(xiàn)在我們來(lái)看一下 heap_string
我們?nèi)匀粚?nbsp;bananas 輸入到 heap_string 變量中。讓我們來(lái)看一下內(nèi)存中的樣子。
這是在我們讀取了字符串以后,heap_string 在??臻g上的樣子:
需要注意的是,這里的值是一個(gè)地址。并且這個(gè)地址并沒(méi)有改變,但是我們來(lái)看一下指向的內(nèi)存上的內(nèi)容。
(gdb) x/10x 0x5555555592a00x5555555592a0: 0x62 0x61 0x6e 0x61 0x6e 0x61 0x73 0x000x5555555592a8: 0x00 0x00
看到了嗎,這就是字符串 bananas 的字節(jié)表示。這些字節(jié)并不在??臻g上。他們存在于內(nèi)存中的堆上。
堆和棧到底在哪里?
我們已經(jīng)討論過(guò)棧和堆是不同的內(nèi)存區(qū)域,但是你怎么知道它們?cè)趦?nèi)存中的位置呢?
每個(gè)進(jìn)程都有一個(gè)名為 /proc/$PID/maps 的文件,它顯示了每個(gè)進(jìn)程的內(nèi)存映射。 在這里你可以看到其中的棧和堆。
$ 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 開頭。 所以很容易區(qū)分棧上的地址和堆上的地址之間的區(qū)別。
像這樣使用 gdb 真的很有幫助
這有點(diǎn)像旋風(fēng)之旅,雖然我沒(méi)有解釋所有內(nèi)容,但希望看到數(shù)據(jù)在內(nèi)存中的實(shí)際情況可以使你更清楚地了解堆棧的實(shí)際情況。
我真的建議像這樣來(lái)把玩一下 gdb —— 即使你不理解你在內(nèi)存中看到的每一件事,我發(fā)現(xiàn)實(shí)際上像這樣看到我程序內(nèi)存中的數(shù)據(jù)會(huì)使抽象的概念,比如“?!焙汀岸选焙汀爸羔槨备菀桌斫?。
更多練習(xí)
一些關(guān)于思考棧的后續(xù)練習(xí)的想法(沒(méi)有特定的順序):
- 嘗試將另一個(gè)函數(shù)添加到
test.c并在該函數(shù)的開頭創(chuàng)建一個(gè)斷點(diǎn),看看是否可以從main中找到堆棧! 他們說(shuō)當(dāng)你調(diào)用一個(gè)函數(shù)時(shí)“堆棧會(huì)變小”,你能在 gdb 中看到這種情況嗎? - 從函數(shù)返回一個(gè)指向棧上字符串的指針,看看哪里出了問(wèn)題。 為什么返回指向棧上字符串的指針是不好的?
- 嘗試在 C 中引起堆棧溢出,并嘗試通過(guò)在 gdb 中查看堆棧溢出來(lái)準(zhǔn)確理解會(huì)發(fā)生什么!
- 查看 Rust 程序中的堆棧并嘗試找到變量!
- 在 噩夢(mèng)課程 中嘗試一些緩沖區(qū)溢出挑戰(zhàn)。每個(gè)問(wèn)題的答案寫在 README 文件中,因此如果你不想被寵壞,請(qǐng)避免先去看答案。 所有這些挑戰(zhàn)的想法是給你一個(gè)二進(jìn)制文件,你需要弄清楚如何導(dǎo)致緩沖區(qū)溢出以使其打印出
flag字符串。
網(wǎng)頁(yè)題目:使用GDB查看程序的棧空間
分享路徑:http://www.5511xx.com/article/coghgdg.html


咨詢
建站咨詢
