打算寫這篇文章是因?yàn)樵诰W(wǎng)上看過一篇論文软能,講了緩沖區(qū)溢出破壞堆棧來(lái)執(zhí)行惡意程序的漏洞。該論文請(qǐng)見參考資料1慌闭。這篇文章會(huì)涉及一些匯編的基礎(chǔ)知識(shí)别威,以及虛擬內(nèi)存的一些基本概念等。當(dāng)然用來(lái)調(diào)試程序的系統(tǒng)是linux驴剔,工具是gcc省古。很久沒有看過匯編和C語(yǔ)言了,錯(cuò)漏之處丧失,還請(qǐng)指正豺妓。這篇文章最早也是發(fā)在CSDN,有一些參考文章鏈接可能漏掉了,如果原作者看到科侈,麻煩指出载佳。
1 概要
文章標(biāo)題有提到堆棧和緩沖區(qū),那么就先來(lái)探討下這幾個(gè)名詞的定義臀栈。這里的緩沖區(qū),指的就是計(jì)算機(jī)內(nèi)一塊連續(xù)的內(nèi)存區(qū)域挠乳,可以保存相同數(shù)據(jù)類型的多個(gè)實(shí)例权薯。C程序員最常見的緩沖區(qū)就是字符數(shù)組了。與C語(yǔ)言中其他變量一樣睡扬,數(shù)組也可以聲明為靜態(tài)或動(dòng)態(tài)的盟蚣,靜態(tài)變量在程序加載時(shí)位于數(shù)據(jù)段,動(dòng)態(tài)變量位于堆棧之中(這一點(diǎn)我們可以很容易的寫個(gè)程序來(lái)驗(yàn)證卖怜,見exmple.c
,使用命令gcc -m32 -S example.c
將其編譯成32位匯編代碼,查看example.s
即可看到數(shù)組a的數(shù)據(jù)分布在數(shù)據(jù)段中屎开,而數(shù)組b的數(shù)據(jù)則分布在堆棧中)。本文只探討動(dòng)態(tài)緩沖區(qū)的溢出問題马靠,即基于堆棧的緩沖區(qū)溢出奄抽。
/*exapmle.c*/
int main() {
static int a[4] = {1, 2, 3, 4};
int b[4] = {5, 6, 7, 8};
}
2 基礎(chǔ)知識(shí)
2.1 進(jìn)程內(nèi)存組織形式
既然本文要討論基于堆棧的緩沖區(qū)溢出,首先就來(lái)看看進(jìn)程的內(nèi)存組織結(jié)構(gòu)甩鳄。我們基本都知道逞度,進(jìn)程在內(nèi)存中的結(jié)構(gòu)可以簡(jiǎn)單的分為代碼段,數(shù)據(jù)段和堆棧段妙啃。代碼段位于內(nèi)存低地址档泽,而堆棧位于內(nèi)存高地址。當(dāng)然我們這里說(shuō)的內(nèi)存地址是指虛擬地址揖赴,具體物理地址是需要經(jīng)過MMU(內(nèi)存管理單元)進(jìn)行轉(zhuǎn)換得到馆匿。下面是一個(gè)進(jìn)程的內(nèi)存組織結(jié)構(gòu)圖:
圖2.1 進(jìn)程的內(nèi)存組織結(jié)構(gòu)圖
從圖2.1中可以看到,除了基本的代碼段燥滑,數(shù)據(jù)段渐北,還有未初始化數(shù)據(jù)段bss,堆heap突倍,內(nèi)存映射區(qū)域等腔稀。當(dāng)然我們這里的段的概念跟程序加載時(shí)的段是不一樣的,具體區(qū)別可以參見
《Linux C一站式編程》18.5 ELF文件格式
那一節(jié)的說(shuō)明羽历。
2.2 堆棧
堆棧是一種計(jì)算機(jī)中常用的抽象數(shù)據(jù)模型焊虏,其特征就是先進(jìn)先出,支持的操作主要就是PUSH和POP秕磷。PUSH操作是在堆棧頂部壓入一個(gè)元素诵闭,而POP操作則是彈出堆棧的頂部元素。
為什么會(huì)使用堆棧則是跟現(xiàn)代計(jì)算機(jī)設(shè)計(jì)相關(guān)。在高級(jí)編程語(yǔ)言如C語(yǔ)言疏尿,JAVA語(yǔ)言瘟芝,Python語(yǔ)言等編寫程序時(shí),經(jīng)常會(huì)用到函數(shù)(function)或者過程(procedure)褥琐。通常锌俱,一個(gè)函數(shù)調(diào)用可以像跳轉(zhuǎn)命令那樣改變程序的執(zhí)行流程,而函數(shù)執(zhí)行完畢后敌呈,又需要把控制權(quán)返回給函數(shù)之后的代碼指令贸宏,這種實(shí)現(xiàn)需要依靠堆棧來(lái)實(shí)現(xiàn)。當(dāng)然在函數(shù)的局部變量中磕洪,以及函數(shù)傳遞參數(shù)和返回值中都要用到堆棧吭练。
堆棧是一塊連續(xù)的內(nèi)存區(qū)域,堆棧既可以向上也可以向下增長(zhǎng)析显,這個(gè)依賴于具體實(shí)現(xiàn)鲫咽。在大部分的處理器如Intel,Motorola谷异,SPARC和MIPS中分尸,堆棧都是向下增長(zhǎng)的,即堆棧指針SP指向堆棧的頂部晰绎,堆棧底部是一個(gè)固定的地址寓落,堆棧大小在運(yùn)行時(shí)由內(nèi)核動(dòng)態(tài)調(diào)整。CPU實(shí)現(xiàn)指令PUSH和POP荞下,向堆棧中添加和移除元素伶选。
除了堆棧指針SP,為了方便還有一個(gè)指向幀內(nèi)固定地址的指針BP尖昏。從理論上來(lái)說(shuō)仰税,局部變量可以通過SP加偏移量來(lái)引用,然而抽诉,當(dāng)有字被壓入棧和出棧后陨簇,這些偏移就變化了。盡管有些情況下編譯器能夠跟蹤棧內(nèi)的操作變化迹淌,修正偏移量河绽,但是還有很多情況不能跟蹤,而且為了跟蹤偏移量的變化需要引入額外的管理開銷唉窃。因此很多編譯器會(huì)使用第二個(gè)寄存器BP耙饰,局部變量和函數(shù)參數(shù)都可以引用它,因?yàn)榫植孔兞亢秃瘮?shù)參數(shù)到BP的距離不受PUSH和POP操作的影響纹份。
2.3 函數(shù)調(diào)用中棧幀分析
為了利用緩沖區(qū)溢出苟跪,需要知道函數(shù)調(diào)用中棧幀變化和布局情況廷痘,這里就不分析了,已經(jīng)有很好的文章詳細(xì)說(shuō)過這個(gè)問題件已,參見宋勁松老師的《linux C一站式編程》19.1節(jié)函數(shù)調(diào)用笋额。
3 緩沖區(qū)溢出
好了,做了一些準(zhǔn)備工作后篷扩,可以來(lái)看看這個(gè)緩沖區(qū)溢出的問題了兄猩。 首先看下面的代碼example1.c,我們分析下函數(shù)棧幀的分布鉴未。
/*example2.c*/
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
運(yùn)行命令:gcc -S -fno-stack-protector example1.c
,通過分析example1.s文件得出 函數(shù)棧幀分布如下所示(我的運(yùn)行環(huán)境是32位的ubuntu11.04):
c (高地址)
b
a
ret
ebp
buffer1
buffer2 (低地址)
接下來(lái)看一個(gè)通過覆蓋返回地址造成段錯(cuò)誤的情況厦滤。見example2.c。
/*example2.c*/
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
void main() {
char large_string[256];
int i;
for( i = 0; i < 255; i++)
large_string[i] = 'A';
function(large_string);
}
example2.c是一個(gè)典型的緩沖區(qū)溢出的例子歼狼,strcpy拷貝的數(shù)據(jù)超過了16個(gè)字節(jié),導(dǎo)致溢出代碼覆蓋了棧中保存的ebp值以及返回地址ret享怀,而函數(shù)返回時(shí)會(huì)從棧中取返回地址ret接著執(zhí)行下一條指令羽峰,該地址不合法,從而導(dǎo)致段錯(cuò)誤添瓷。而如果用一個(gè)合法地址來(lái)覆蓋返回地址ret梅屉,這樣就可以修改程序執(zhí)行流程了。
接下來(lái)鳞贷,修改example2.c坯汤,通過緩沖區(qū)溢出修改返回地址ret來(lái)修改程序執(zhí)行流程。如example3.c所示搀愧。
/*example3.c*/
void function(int a, int b, int c)
{
int *ret;
char buffer1[5];
char buffer2[10];
ret = buffer1 + 13;
(*ret) += 8;
}
void main()
{
int x = 0;
function(1,2,3);
x = 1;
printf("%d\n", x);
}
使用命令gcc -o example3 -fno-stack-protector example3.c
編譯惰聂,可以看到棧幀分布如下所示:
c (高地址)| b | a | ret(返回地址) | ebp | ret (局部變量ret) | buffer1 | buffer2 (低地址)|
因此,通過ret=buffer1+13
咱筛,可以獲得返回地址ret的地址搓幌。這里之所以加13,是buffer1的5字節(jié)+局部變量ret的4字節(jié)+ebp的4字節(jié)
迅箩。調(diào)用function函數(shù)后溉愁,返回地址本應(yīng)該是x=1指令地址,(*ret) += 8將返回地址ret加8饲趋,這樣就跳過了x=1這條指令拐揭,example3.c編譯后執(zhí)行的結(jié)果是0。注意必須加上-fno-stack-protector
奕塑,因?yàn)間cc默認(rèn)存在堆棧保護(hù)技術(shù)堂污,那樣會(huì)防止返回地址被改寫,如果返回地址被惡意修改爵川,會(huì)報(bào)段錯(cuò)誤敷鸦。GCC編譯器堆棧保護(hù)技術(shù)詳見該文鏈接。
接下來(lái)可以通過緩沖區(qū)溢出來(lái)執(zhí)行shell代碼,這個(gè)留待下一篇文章再說(shuō)了扒披,內(nèi)容太長(zhǎng)值依,現(xiàn)在還沒有看完。