从函数调用过程中的堆栈变化理解缓冲区溢出
一、說(shuō)明
本來(lái)是想直接寫一個(gè)緩沖區(qū)溢出的例子,但是一是當(dāng)前編譯器和操作系統(tǒng)有溢出的保護(hù)措施沒(méi)有完全弄清怎么取消,二是strcpy等遇到00會(huì)截?cái)嘈枰M(jìn)行編碼這比較難搞,所以最終沒(méi)有實(shí)現(xiàn)。
但已經(jīng)雙看了一陣函數(shù)的調(diào)用過(guò)程,如果全然就此放棄那以后再研究緩沖區(qū)溢出又得從0開(kāi)始研究函數(shù)的調(diào)用,所以就記些東西下來(lái),免得以后雙得從0開(kāi)始。
在緩沖區(qū)溢出中堆棧變化是最為關(guān)鍵的,本文從堆棧入手。
?
二、函數(shù)調(diào)用過(guò)程中的堆棧變化
2.1 使用程序
本文使用程序源代碼如下(編寫時(shí)我創(chuàng)建了一個(gè)StackChange工程,將源代碼保存為StackChange.c),使用vc++ 6.0編譯,使用olldbg逆向。
#include <stdio.h>int get_sum(int a, int b) {int sum;sum = a + b;printf("get_sum: calc sum success, now will return\n");return sum; }int main(int argc, char **argv) {int a = 1;int b = 2;int sum;sum = get_sum(a,b);printf("main: the sum is %d\n", sum);return 1; }?
2.2 堆棧在內(nèi)存地址空間中的位置
今早等公交的時(shí)候看到一個(gè)問(wèn)題大意是,如果堆棧不可執(zhí)行那么程序?yàn)槭裁催€可以執(zhí)行,從概念上說(shuō)堆棧段和代碼段沒(méi)關(guān)系所以堆棧段不可執(zhí)行代碼段還可以執(zhí)行。
但我想這位小哥根本上是想知道,堆棧段和代碼段分別在內(nèi)存的什么位置,為什么堆棧段不可執(zhí)行代碼段還可執(zhí)行。
將2.1中的程序編譯后,使用olldbg載入exe,然后打開(kāi)內(nèi)存窗口(M),如下圖:
圖中各種信息都很明了了,具體到堆棧段和代碼段的位置,堆棧段為0x0018e000-0x00190000(因?yàn)閟ize為0x2000),代碼段為0x00401000-0x00422000(因?yàn)閟ize為0x2100)。所以堆棧段不可執(zhí)行,不影向代碼段可不可執(zhí)行。
?
2.3 函數(shù)調(diào)用在匯編上的過(guò)程
main函數(shù)匯編解析,說(shuō)明看其中的注釋:
get_sum函數(shù)匯編解析如下,流程和main函數(shù)是類似的(應(yīng)該說(shuō)所有函數(shù)的流程框架都是這樣的)不重復(fù)注釋,需要注意的就是獲取參數(shù)是使用ebp+8的形式回頭獲取的
?
2.4 函數(shù)調(diào)用過(guò)程中的堆棧變化
應(yīng)該很多人和我一樣2.3中的函數(shù)調(diào)用過(guò)程聽(tīng)了一萬(wàn)遍了,只是下次還是記不住;想以此去理解緩沖區(qū)溢出更是浮沙筑臺(tái),一知半解,過(guò)后又忘。
我想了想其癥結(jié)在于我們總嘗試去死記硬背這個(gè)過(guò)程,重點(diǎn)放在代碼段上。
而如果我們從整個(gè)程序內(nèi)存空間在程序執(zhí)行過(guò)程中的變化情況,就會(huì)發(fā)現(xiàn)代碼段區(qū)域和數(shù)據(jù)段區(qū)域內(nèi)容都是不變的,只有堆棧段內(nèi)容才會(huì)變(當(dāng)然寄存器也是變化的但寄存器不在內(nèi)存中)。
或者換言之,整個(gè)0x00000000-0xffffffff內(nèi)存地址空間,從程序運(yùn)行到進(jìn)程結(jié)束,只有堆棧部分的內(nèi)存即0x0018e000-0x00190000是會(huì)變的,其他部分在初始化后都是不變的。
所以也許重點(diǎn)放在堆棧段上,也許我們能更好更解函數(shù)調(diào)用過(guò)程。
(此時(shí)想起兩年前去面試,面試官問(wèn)輸入的用戶名密碼存到哪了,回答存內(nèi)存了,得到基礎(chǔ)薄弱的評(píng)價(jià),當(dāng)時(shí)是有些不忿的;現(xiàn)在想來(lái)內(nèi)存是0x00000000-0xffffffff,而別人想要的是0x0018e000-0x00190000這么一小段,差距確實(shí)是有點(diǎn)大)
?
2.4.1 堆棧在函數(shù)層次的變化
以下是olldbg進(jìn)入get_sum函數(shù)的printf函數(shù)內(nèi)部時(shí)的各函數(shù)堆棧情況(0x0018fe6c-0x0018ff4c):
以下是olldbg進(jìn)入main函數(shù)的printf函數(shù)內(nèi)部時(shí)各函數(shù)的堆棧情況(0x0018fe6c-0x0018ff4c):
從以上兩圖中的變化我們可以部結(jié)出以下幾點(diǎn):
1. 棧從高地址向低地址生長(zhǎng)。我們前邊說(shuō)過(guò)棧地址空間為0x0018e000-0x00190000,這里main并沒(méi)有從0x00190000開(kāi)始是因?yàn)閙ain函數(shù)其實(shí)是被系統(tǒng)函數(shù)調(diào)用啟動(dòng)的,并不能一開(kāi)始就是main函數(shù)。見(jiàn)下圖。
2. 層次為父子關(guān)系的函數(shù)(比如這里的main和get_sum、get_sum和其中的printf),父函數(shù)的堆棧在高地址子函數(shù)的堆棧在緊接父函數(shù)堆棧的低地址。
3. 層次為兄弟關(guān)系的函數(shù)(比如這里的get_sum和main函數(shù)的中的printf),前一函數(shù)調(diào)用完后其堆棧被釋放歸回未使用,后一函數(shù)執(zhí)行時(shí)使用的堆棧和前一函數(shù)一樣(開(kāi)始地址一樣,結(jié)束地址一般不一樣,畢竟各自局部變量需要的空間不一樣)
4.堆棧在使用時(shí)初始化在釋放時(shí)不初始化。比如上邊我們已進(jìn)入到main函數(shù)的printf,但get_sum未被占用的那部份和get_sum中的printf的堆棧共保留的內(nèi)容還和原來(lái)一樣。
?
2.4.2 單個(gè)函數(shù)內(nèi)部的堆棧空間分析
我們使用前邊“olldbg進(jìn)入get_sum函數(shù)的printf函數(shù)內(nèi)部時(shí)的各函數(shù)堆棧情況”時(shí)的圖來(lái)看get_sum函數(shù)堆棧的內(nèi)容
我們將get_sum堆棧空間轉(zhuǎn)化為以下表格:
| 對(duì)應(yīng)圖中地址 | 內(nèi)容 | 說(shuō)明 |
| 0x0018FE84 | ebp(get_sum函數(shù)的) | 這其實(shí)是printf函數(shù)中的push ebp |
| 0x0018FE88 | eip(printf返回get_sum的) | 這是get_sum函數(shù)中call printf壓入棧的 |
| 0x0018FE8C | 形參(printf的) | ? |
| 0x0018FE90 | edi(main函數(shù)的) | 如果不調(diào)用參數(shù),或調(diào)用參數(shù)返回后,esp指向此處 |
| 0x0018FE94 | esi(main函數(shù)的) | ? |
| 0x0018FE98 | ebx(main函數(shù)的) | ? |
| 0x0018FE9C-0x0018FED8 | 預(yù)留空間 | 編譯器總會(huì)給比參數(shù)需要的空間多一些的空間,malloc的話會(huì)從0x0018FE9C往0x0018FED8方向使用 |
| 0x0018FEDC | 局部變量已使用空間 | 所謂緩沖區(qū)溢出,就是此處的參數(shù)長(zhǎng)度溢出,向后覆蓋0x0018FEE4處的eip |
| 0x0018FEE0 | ebp(main函數(shù)的) | 這是get_sum函數(shù)形頭push ebp壓入棧的 |
| 0x0018FEE4 | eip(get_sum返回main的) | 這是main函數(shù)中的call get_sum壓入棧的,不屬于get_sum的堆棧空間 |
?
?
?
?
?
?
?
?
?
?
?
?
?
?
三、緩沖區(qū)溢出
3.1 緩沖區(qū)溢出利用的原理
緩沖區(qū)溢出:給本函數(shù)局部變量賦的值其長(zhǎng)度超過(guò)了變量定義的長(zhǎng)度
結(jié)合2.4.2最后的表格中我們可以得到緩沖區(qū)溢出利用的原理就是:給本函數(shù)局部變量賦長(zhǎng)度超過(guò)其定義長(zhǎng)度的值,使本函數(shù)返回上層函數(shù)的eip值被覆蓋成自己想去執(zhí)行的語(yǔ)句的地址。
如果這個(gè)定義還感覺(jué)不是很明了,那我們舉個(gè)例子:main函數(shù)調(diào)用了vuln_fun函數(shù),vuln_fun函數(shù)調(diào)用了strcpy,strcpy沒(méi)注意長(zhǎng)度會(huì)引發(fā)緩沖區(qū)溢出。此時(shí)溢出發(fā)生在vuln_fun函數(shù)的堆棧中,而被覆蓋的eip是vuln_fun執(zhí)行完返回main的eip。
所以可以給緩沖區(qū)溢出漏洞下一個(gè)更簡(jiǎn)單的定義:緩沖區(qū)溢出發(fā)生在調(diào)用strcpy/strcat/sprintf/vsprintf/gets/scanf的函數(shù)中,被覆蓋的eip是該函數(shù)返回上層函數(shù)的eip。
?
3.2 緩沖區(qū)溢出利用的難點(diǎn)
3.2.1 不考慮系統(tǒng)與編譯器無(wú)保護(hù)機(jī)制時(shí)的難點(diǎn)
在給變量賦的超長(zhǎng)字符串中包含以下三部份內(nèi)容:填充數(shù)據(jù)、注入的要執(zhí)行的匯編語(yǔ)句、注入的要執(zhí)行的匯編語(yǔ)句的地址。
填充數(shù)據(jù),隨便點(diǎn)就行了不是難點(diǎn)。
注入的要執(zhí)行的匯編語(yǔ)句,在不考慮編譯器和系統(tǒng)保護(hù)機(jī)制的情況下,大概是把自己要執(zhí)行的語(yǔ)句寫成c程序,然后編譯成exe,然后再把語(yǔ)句對(duì)應(yīng)的十六進(jìn)制dump出來(lái)就行了;不過(guò)strcpy等函數(shù)遇到00會(huì)認(rèn)為字符串結(jié)束,而期望dump出的十六進(jìn)制剛好沒(méi)有00是不太現(xiàn)實(shí)的,需要對(duì)其進(jìn)行編碼處理。
注入的要執(zhí)行的匯編語(yǔ)句的地址,這又有兩個(gè)難點(diǎn):一是要確定變量到eip的距離,以便剛好能在eip的位置寫入想要執(zhí)行的代碼的地址;二是要確定注入的、想要執(zhí)行的代碼的地址是多少。
?
3.2.2 編譯器與操作系統(tǒng)的保護(hù)措施
編譯器有棧不可執(zhí)行、棧保護(hù)兩種措施,編譯器與操作系統(tǒng)聯(lián)動(dòng)則還有內(nèi)存布局隨機(jī)化。更具體內(nèi)容可參考:https://www.jianshu.com/p/47d484b9227e
?
3.2.3 為什么很多overflow的cve沒(méi)有exp
長(zhǎng)期以來(lái),我都將存在緩沖區(qū)溢出漏洞等同于系統(tǒng)命令執(zhí)行、等同于系統(tǒng)淪陷。但很多overflow類型的cve都只是評(píng)分“很低”的dos而不是execute code,而且只有極少數(shù)才有exp,這很令人不解。
而基于以上難點(diǎn)的討論,這種現(xiàn)像就好理解了,緩沖區(qū)溢出到導(dǎo)致程序運(yùn)行出錯(cuò)所以基本都能dos,但由于編寫shellcode本身就比較困難再加上各種保護(hù)機(jī)制,有溢出不是必然就有exp的。
?
參考:
https://www.jianshu.com/p/47d484b9227e
https://www.shiyanlou.com/courses/231
https://www.cnblogs.com/yejianyong/p/7506465.html
https://blog.csdn.net/nicholas199109/article/details/8560988
轉(zhuǎn)載于:https://www.cnblogs.com/lsdb/p/9547380.html
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的从函数调用过程中的堆栈变化理解缓冲区溢出的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 洛谷 P2893 [USACO08FEB
- 下一篇: Linux 下qt 程序打包发布(使用l