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