??
字號:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<!-- saved from url=(0074)http://sinbad.dhs.org/cgi-bin/bbstpc?board=UNIX&file=M.992565069.A&num=430 -->
<HTML><HEAD><TITLE>辛巴達文章</TITLE>
<META http-equiv=Content-Type content="text/html; charset=gb2312"><LINK
href="緩沖溢出原理.files/bbstyle.css" type=text/css rel=stylesheet>
<META content="MSHTML 6.00.2712.300" name=GENERATOR></HEAD>
<BODY><BR><FONT color=#006000 size=4>標(biāo)題:緩沖區(qū)溢出的原理和實踐(Phrack)</FONT><BR>
<CENTER>
<TABLE class=title width="100%">
<TBODY>
<TR>
<TD align=left>作者:Sinbad</A></TD>
<TD align=right><A class=bar href="javascript:history.back()">返 回</A> <A
class=bar
href="http://sinbad.dhs.org/cgi-bin/bbspst?board=UNIX&file=M.992565069.A&key=">我要評論</A></TD></TR></TBODY></TABLE>
<TABLE class=doc>
<TBODY>
<TR>
<TD class=doc2><PRE>發(fā)信人: Sinbad <MicroBin@263.net>
標(biāo) 題: 緩沖區(qū)溢出的原理和實踐(Phrack)
發(fā)信站: 辛巴達 (Fri Jun 15 08:31:09 2001)
.oO Phrack 49 Oo.
Volume Seven, Issue Forty-Nine
File 14 of 16
BugTraq, r00t, and Underground.Org
bring you
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Smashing The Stack For Fun And Profit
以娛樂和牟利為目的踐踏堆棧
(緩沖區(qū)溢出的原理和實踐)
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
原作 by Aleph One
aleph1@underground.org
翻譯 xuzq@chinasafer.com
www.chinasafer.com
'踐踏堆棧'[C語言編程] n. 在許多C語言的實現(xiàn)中,有可能通過寫入例程
中所聲明的數(shù)組的結(jié)尾部分來破壞可執(zhí)行的堆棧.所謂'踐踏堆棧'使用的
代碼可以造成例程的返回異常,從而跳到任意的地址.這導(dǎo)致了一些極為
險惡的數(shù)據(jù)相關(guān)漏洞(已人所共知).其變種包括堆棧垃圾化(trash the
stack),堆棧亂寫(scribble the stack),堆棧毀壞(mangle the stack);
術(shù)語mung the stack并不使用,因為這從來不是故意造成的.參閱spam?
也請參閱同名的漏洞,胡鬧內(nèi)核(fandango on core),內(nèi)存泄露(memory
leak),優(yōu)先權(quán)丟失(precedence lossage),螺紋滑扣(overrun screw).
簡 介
~~~~~~~
在過去的幾個月中,被發(fā)現(xiàn)和利用的緩沖區(qū)溢出漏洞呈現(xiàn)上升趨勢.例如syslog,
splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at等等.本文試圖
解釋什么是緩沖區(qū)溢出, 以及如何利用.
匯編的基礎(chǔ)知識是必需的. 對虛擬內(nèi)存的概念, 以及使用gdb的經(jīng)驗是十分有益
的, 但不是必需的. 我們還假定使用Intel x86 CPU, 操作系統(tǒng)是Linux.
在開始之前我們給出幾個基本的定義: 緩沖區(qū),簡單說來是一塊連續(xù)的計算機內(nèi)
存區(qū)域, 可以保存相同數(shù)據(jù)類型的多個實例. C程序員通常和字緩沖區(qū)數(shù)組打交道.
最常見的是字符數(shù)組. 數(shù)組, 與C語言中所有的變量一樣, 可以被聲明為靜態(tài)或動態(tài)
的. 靜態(tài)變量在程序加載時定位于數(shù)據(jù)段. 動態(tài)變量在程序運行時定位于堆棧之中.
溢出, 說白了就是灌滿, 使內(nèi)容物超過頂端, 邊緣, 或邊界. 我們這里只關(guān)心動態(tài)
緩沖區(qū)的溢出問題, 即基于堆棧的緩沖區(qū)溢出.
進程的內(nèi)存組織形式
~~~~~~~~~~~~~~~~~~~~
為了理解什么是堆棧緩沖區(qū), 我們必須首先理解一個進程是以什么組織形式在
內(nèi)存中存在的. 進程被分成三個區(qū)域: 文本, 數(shù)據(jù)和堆棧. 我們把精力集中在堆棧
區(qū)域, 但首先按照順序簡單介紹一下其他區(qū)域.
文本區(qū)域是由程序確定的, 包括代碼(指令)和只讀數(shù)據(jù). 該區(qū)域相當(dāng)于可執(zhí)行
文件的文本段. 這個區(qū)域通常被標(biāo)記為只讀, 任何對其寫入的操作都會導(dǎo)致段錯誤
(segmentation violation).
數(shù)據(jù)區(qū)域包含了已初始化和未初始化的數(shù)據(jù). 靜態(tài)變量儲存在這個區(qū)域中. 數(shù)
據(jù)區(qū)域?qū)?yīng)可執(zhí)行文件中的data-bss段. 它的大小可以用系統(tǒng)調(diào)用brk(2)來改變.
如果bss數(shù)據(jù)的擴展或用戶堆棧把可用內(nèi)存消耗光了, 進程就會被阻塞住, 等待有了
一塊更大的內(nèi)存空間之后再運行. 新內(nèi)存加入到數(shù)據(jù)和堆棧段的中間.
/------------------\ 內(nèi)存低地址
| |
| 文本 |
| |
|------------------|
| (已初始化) |
| 數(shù)據(jù) |
| (未初始化) |
|------------------|
| |
| 堆棧 |
| |
\------------------/ 內(nèi)存高地址
Fig. 1 進程內(nèi)存區(qū)域
什么是堆棧?
~~~~~~~~~~~~~
堆棧是一個在計算機科學(xué)中經(jīng)常使用的抽象數(shù)據(jù)類型. 堆棧中的物體具有一個特性:
最后一個放入堆棧中的物體總是被最先拿出來, 這個特性通常稱為后進先處(LIFO)隊列.
堆棧中定義了一些操作. 兩個最重要的是PUSH和POP. PUSH操作在堆棧的頂部加入一
個元素. POP操作相反, 在堆棧頂部移去一個元素, 并將堆棧的大小減一.
為什么使用堆棧?
~~~~~~~~~~~~~~~~
現(xiàn)代計算機被設(shè)計成能夠理解人們頭腦中的高級語言. 在使用高級語言構(gòu)造程序時
最重要的技術(shù)是過程(procedure)和函數(shù)(function). 從這一點來看, 一個過程調(diào)用可
以象跳轉(zhuǎn)(jump)命令那樣改變程序的控制流程, 但是與跳轉(zhuǎn)不同的是, 當(dāng)工作完成時,
函數(shù)把控制權(quán)返回給調(diào)用之后的語句或指令. 這種高級抽象實現(xiàn)起來要靠堆棧的幫助.
堆棧也用于給函數(shù)中使用的局部變量動態(tài)分配空間, 同樣給函數(shù)傳遞參數(shù)和函數(shù)返
回值也要用到堆棧.
堆棧區(qū)域
~~~~~~~~~~
堆棧是一塊保存數(shù)據(jù)的連續(xù)內(nèi)存. 一個名為堆棧指針(SP)的寄存器指向堆棧的頂部.
堆棧的底部在一個固定的地址. 堆棧的大小在運行時由內(nèi)核動態(tài)地調(diào)整. CPU實現(xiàn)指令
PUSH和POP, 向堆棧中添加元素和從中移去元素.
堆棧由邏輯堆棧幀組成. 當(dāng)調(diào)用函數(shù)時邏輯堆棧幀被壓入棧中, 當(dāng)函數(shù)返回時邏輯
堆棧幀被從棧中彈出. 堆棧幀包括函數(shù)的參數(shù), 函數(shù)地局部變量, 以及恢復(fù)前一個堆棧
幀所需要的數(shù)據(jù), 其中包括在函數(shù)調(diào)用時指令指針(IP)的值.
堆棧既可以向下增長(向內(nèi)存低地址)也可以向上增長, 這依賴于具體的實現(xiàn). 在我
們的例子中, 堆棧是向下增長的. 這是很多計算機的實現(xiàn)方式, 包括Intel, Motorola,
SPARC和MIPS處理器. 堆棧指針(SP)也是依賴于具體實現(xiàn)的. 它可以指向堆棧的最后地址,
或者指向堆棧之后的下一個空閑可用地址. 在我們的討論當(dāng)中, SP指向堆棧的最后地址.
除了堆棧指針(SP指向堆棧頂部的的低地址)之外, 為了使用方便還有指向幀內(nèi)固定
地址的指針叫做幀指針(FP). 有些文章把它叫做局部基指針(LB-local base pointer).
從理論上來說, 局部變量可以用SP加偏移量來引用. 然而, 當(dāng)有字被壓棧和出棧后, 這
些偏移量就變了. 盡管在某些情況下編譯器能夠跟蹤棧中的字操作, 由此可以修正偏移
量, 但是在某些情況下不能. 而且在所有情況下, 要引入可觀的管理開銷. 而且在有些
機器上, 比如Intel處理器, 由SP加偏移量訪問一個變量需要多條指令才能實現(xiàn).
因此, 許多編譯器使用第二個寄存器, FP, 對于局部變量和函數(shù)參數(shù)都可以引用,
因為它們到FP的距離不會受到PUSH和POP操作的影響. 在Intel CPU中, BP(EBP)用于這
個目的. 在Motorola CPU中, 除了A7(堆棧指針SP)之外的任何地址寄存器都可以做FP.
考慮到我們堆棧的增長方向, 從FP的位置開始計算, 函數(shù)參數(shù)的偏移量是正值, 而局部
變量的偏移量是負值.
當(dāng)一個例程被調(diào)用時所必須做的第一件事是保存前一個FP(這樣當(dāng)例程退出時就可以
恢復(fù)). 然后它把SP復(fù)制到FP, 創(chuàng)建新的FP, 把SP向前移動為局部變量保留空間. 這稱為
例程的序幕(prolog)工作. 當(dāng)例程退出時, 堆棧必須被清除干凈, 這稱為例程的收尾
(epilog)工作. Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于
有效地序幕和收尾工作.
下面我們用一個簡單的例子來展示堆棧的模樣:
example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
------------------------------------------------------------------------------
為了理解程序在調(diào)用function()時都做了哪些事情, 我們使用gcc的-S選項編譯, 以產(chǎn)
生匯編代碼輸出:
$ gcc -S -o example1.s example1.c
通過查看匯編語言輸出, 我們看到對function()的調(diào)用被翻譯成:
pushl $3
pushl $2
pushl $1
call function
以從后往前的順序?qū)unction的三個參數(shù)壓入棧中, 然后調(diào)用function(). 指令call
會把指令指針(IP)也壓入棧中. 我們把這被保存的IP稱為返回地址(RET). 在函數(shù)中所做
的第一件事情是例程的序幕工作:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
將幀指針EBP壓入棧中. 然后把當(dāng)前的SP復(fù)制到EBP, 使其成為新的幀指針. 我們把這
個被保存的FP叫做SFP. 接下來將SP的值減小, 為局部變量保留空間.
我們必須牢記:內(nèi)存只能以字為單位尋址. 在這里一個字是4個字節(jié), 32位. 因此5字節(jié)
的緩沖區(qū)會占用8個字節(jié)(2個字)的內(nèi)存空間, 而10個字節(jié)的緩沖區(qū)會占用12個字節(jié)(3個字)
的內(nèi)存空間. 這就是為什么SP要減掉20的原因. 這樣我們就可以想象function()被調(diào)用時
堆棧的模樣(每個空格代表一個字節(jié)):
內(nèi)存低地址 內(nèi)存高地址
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
堆棧頂部 堆棧底部
緩沖區(qū)溢出
~~~~~~~~~~~~
緩沖區(qū)溢出是向一個緩沖區(qū)填充超過它處理能力的數(shù)據(jù)所造成的結(jié)果. 如何利用這個
經(jīng)常出現(xiàn)的編程錯誤來執(zhí)行任意代碼呢? 讓我們來看看另一個例子:
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);
}
------------------------------------------------------------------------------
這個程序的函數(shù)含有一個典型的內(nèi)存緩沖區(qū)編碼錯誤. 該函數(shù)沒有進行邊界檢查就復(fù)
制提供的字符串, 錯誤地使用了strcpy()而沒有使用strncpy(). 如果你運行這個程序就
會產(chǎn)生段錯誤. 讓我們看看在調(diào)用函數(shù)時堆棧的模樣:
內(nèi)存低地址 內(nèi)存高地址
buffer sfp ret *str
<------ [ ][ ][ ][ ]
堆棧頂部 堆棧底部
這里發(fā)生了什么事? 為什么我們得到一個段錯誤? 答案很簡單: strcpy()將*str的
內(nèi)容(larger_string[])復(fù)制到buffer[]里, 直到在字符串中碰到一個空字符. 顯然,
buffer[]比*str小很多. buffer[]只有16個字節(jié)長, 而我們卻試圖向里面填入256個字節(jié)
的內(nèi)容. 這意味著在buffer之后, 堆棧中250個字節(jié)全被覆蓋. 包括SFP, RET, 甚至*str!
我們已經(jīng)把large_string全都填成了A. A的十六進制值為0x41. 這意味著現(xiàn)在的返回地
址是0x41414141. 這已經(jīng)在進程的地址空間之外了. 當(dāng)函數(shù)返回時, 程序試圖讀取返回
地址的下一個指令, 此時我們就得到一個段錯誤.
因此緩沖區(qū)溢出允許我們更改函數(shù)的返回地址. 這樣我們就可以改變程序的執(zhí)行流程.
現(xiàn)在回到第一個例子, 回憶當(dāng)時堆棧的模樣:
內(nèi)存低地址 內(nèi)存高地址
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
堆棧頂部 堆棧底部
現(xiàn)在試著修改我們第一個例子, 讓它可以覆蓋返回地址, 而且使它可以執(zhí)行任意代碼.
堆棧中在buffer1[]之前的是SFP, SFP之前是返回地址. ret從buffer1[]的結(jié)尾算起是4個
字節(jié).應(yīng)該記住的是buffer1[]實際上是2個字即8個字節(jié)長. 因此返回地址從buffer1[]的開
頭算起是12個字節(jié). 我們會使用這種方法修改返回地址, 跳過函數(shù)調(diào)用后面的賦值語句
'x=1;', 為了做到這一點我們把返回地址加上8個字節(jié). 代碼看起來是這樣的:
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -