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