- 論壇徽章:
- 0
|
格式化字符串攻擊(Format String Attacks)
Format String Attacks
Tim Newsham
Guardent, Inc.
September 2000
Copyright (c) 2000. All Rights Reserved
翻譯: xuzq@chinasafer.com
2000/9/13
內(nèi)容:
介紹
什么是格式化字符串攻擊?
Printf-學(xué)校忘記教給你的東西
簡單的例子
來格式化吧。‵ormat Me!)
X MARKS THE SPOT(X是本文示例程序中我們試圖重寫的一個變量)
怎么著(So what)?
摘要
本文討論格式化字符串漏洞的成因和含義,并給出實際的例子來解釋原理。
介紹
我知道在某些時候?qū)τ谀阄液臀覀兇蠹叶,下面這種情況總會發(fā)生。在一個時下流行的
晚餐會上,夾雜在同事們大呼小叫的聲音里,你聽到了"格式化字符串攻擊"這只言片語。
"格式化字符串攻擊?什么是格式化字符串攻擊?"你心說。由于害怕在同事們面前顯露出
自己的無知,你決定停止不自然的微笑,而頻頻點頭以示自己對這玩藝了如指掌。如果一
切順利,大家會共飲雞尾酒,談話仍將繼續(xù),但是沒人明白這究竟是怎么回事,F(xiàn)在不用
再害怕什么了,本文會提供你想知道而又不好意思問的所有內(nèi)容。
什么是格式化字符串攻擊?
格式化字符串漏洞同其他許多安全漏洞一樣是由于程序員的懶惰造成的。當(dāng)你正在閱讀本
文的時候,也許有個程序員正在編寫代碼,他的任務(wù)是:打印輸出一個字符串或者把這個
串拷貝到某緩沖區(qū)內(nèi)。他可以寫出如下的代碼:
printf("%s", str);
但是為了節(jié)約時間和提高效率,并在源碼中少輸入6個字節(jié),他會這樣寫:
printf(str);
為什么不呢?干嘛要和多余的printf參數(shù)打交道,干嘛要花時間分解那些愚蠢的格式?
printf的第一個參數(shù)無論如何都會輸出的!程序員在不知不覺中打開了一個安全漏洞,可
以讓攻擊者控制程序的執(zhí)行,這就是不能偷懶的原因所在。
為什么程序員寫的是錯誤的呢?他傳入了一個他想要逐字打印的字符串。實際上該字符串
被printf函數(shù)解釋為一個格式化字符串(format
string)。函數(shù)在其中尋找特殊的格式字符比如"%d"。如果碰到格式字符,一個變量的參
數(shù)值就從堆棧中取出。很明顯,攻擊者至少可以通過打印出堆棧中的這些值來偷看程序的
內(nèi)存。但是有些事情就不那么明顯了,這個簡單的錯誤允許向運行中程序的內(nèi)存里寫入任
意值。
Printf-學(xué)校忘記教給你的東西
在說明如何為了自己的目的濫用printf之前,我們應(yīng)該深入領(lǐng)會printf提供的特性。假定
讀者以前用過printf函數(shù)并且知道普通的格式化特性,比如如何打印整型和字符串,如何
指定最大和最小字符串寬度等。除了這些普通的特性之外,還有一些深奧和鮮為人知的特
性。在這些特性當(dāng)中,下面介紹的對我們比較有用:
*在格式化字符串中任何位置都可以得到輸出字符的個數(shù)。當(dāng)在格式化字符串中
碰到"%n"的時候,在%n域之前輸出的字符個數(shù)會保存到下一個參數(shù)里。例如,為了獲取在
兩個格式化的數(shù)字之間空間的偏量:
int pos, x = 235, y = 93;
printf("%d %n%d\n", x, &pos, y);
printf("The offset was %d\n", pos);
* %n格式返回應(yīng)該被輸出的字符數(shù)目,而不是實際輸出的字符數(shù)目。當(dāng)把一個字符串格式
化輸出到一個定長緩沖區(qū)內(nèi)時,輸出字符串可能被截短。不考慮截短的影響,%n格式表示
如果不被截短的偏量值(輸出字符數(shù)目)。為了說明這一點,下面的代碼會輸出100而不
是20:
char buf[20];
int pos, x = 0;
snprintf(buf, sizeof buf, "%.100d%n", x, &pos);
printf("position: %d\n", pos);
簡單的例子
除了討論抽象和復(fù)雜的理論,我們將會使用一個具體的例子來說明我們剛才討論的原理。
下面這個簡單的程序能滿足這個要求:
/*
* fmtme.c
* Format a value into a fixed-size buffer
*/
#include <stdio.h>;
int
main(int argc, char **argv)
{
char buf[100];
int x;
if(argc != 2)
exit(1);
x = 1;
snprintf(buf, sizeof buf, argv[1]);
buf[sizeof buf - 1] = 0;
printf("buffer (%d): %s\n", strlen(buf), buf);
printf("x is %d/%#x (@ %p)\n", x, x, &x);
return 0;
}
對這個程序有幾點說明:第一,目的很簡單:將一個通過命令行傳遞值格式化輸出到一個
定長的緩沖區(qū)里。并確保緩沖區(qū)的大小限制不被突破。在緩沖區(qū)格式化后,把它輸出。除
了把參數(shù)格式化,還設(shè)置了一個整型值隨后輸出。這個變量是隨后我們攻擊的目標(biāo),F(xiàn)在
值得我們注意的是這個
值應(yīng)該始終為1。
本文中所有的例子都是在x86 BSD/OS
4.1機器上完成。如果你到莫桑比克執(zhí)行任務(wù)超過20年時間可能會對x86不熟悉,這是一個
little-endian機器。這決定在例子中多精度數(shù)字的表示方法。在這里使用的具體數(shù)值會
因為系統(tǒng)的差異而不同,這些差異表現(xiàn)在不同體系結(jié)構(gòu)、操作系統(tǒng)、環(huán)境甚至是命令行長
度。經(jīng)過簡單調(diào)整,這
些例子可以在其他x86平臺上工作。通過努力也可以在其他體系結(jié)構(gòu)的平臺上工作。
來格式化吧。‵ormat Me!)
現(xiàn)在是我們戴上黑帽子開始以攻擊者方式思考問題的時候了。我們現(xiàn)在手頭有一個測試程
序。知道這個程序有一個漏洞并且了解程序員是在哪里犯錯誤的(直接把用戶輸入的命令
行參數(shù)作為snprintf的格式化參數(shù))。我們還擁有關(guān)于printf函數(shù)深入的知識,知道如何
運用這些知識。讓我們開始修補我們的程序吧。
從簡單的開始,我們通過簡單的參數(shù)調(diào)用程序。看這兒:
% ./fmtme "hello world"
buffer (11): hello world
x is 1/0x1 (@ 0x804745c)
現(xiàn)在這兒還沒有什么特別的事情發(fā)生。程序把我們輸入的字符串格式化輸出到緩沖區(qū)里,
然后打印出它的長度和數(shù)值。程序還告訴我們變量x的值是1(以十進(jìn)制和十六進(jìn)制分別顯
示),x的存儲地址是0x804745c。
接下來我們試著使用一些格式指令。在下面的例子中我們打印出在格式化字符串之上棧堆
中的整型數(shù)值:
% ./fmtme "%x %x %x %x"
buffer (15): 1 f31 1031 3133
x is 1/0x1 (@ 0x804745c)
對這個程序的快速分析可以揭示在調(diào)用snprintf函數(shù)時程序堆棧的規(guī)劃:
Address Contents Description
fp+8 Buffer pointer 4-byte address
fp+12 Buffer length 4-byte integer
fp+16 Format string 4-byte address
fp+20 Variable x 4-byte integer
fp+24 Variable buf 100 characters
(補充:我參考了"緩沖區(qū)溢出機理分析"一文,才看明白上面的內(nèi)容。簡單介紹一下:當(dāng)
程序中發(fā)生函數(shù)調(diào)用時,計算機做如下操作:首先把參數(shù)壓入堆棧;然后保存指令寄存器
(IP)中的內(nèi)容做為返回地址(RET);第三個放入堆棧的是基址寄存器(FP);然后把當(dāng)前的
棧指針(SP)拷貝到FP,
做為新的基地址;最后為本地變量留出一定空間,把SP減去適當(dāng)?shù)臄?shù)值。
----------------------------------------------------------------------
當(dāng)調(diào)用函數(shù)snprintf ()時,堆棧如下:
低內(nèi)存端 高內(nèi)存端
函數(shù)局部變量 sfp ret buf sizeof(buf) argv[1] x和buf
<- [ ] [ ] [ ] [ ] [ ] [ ] 數(shù)據(jù)區(qū)
棧頂 棧底
)
前一個測試運行結(jié)果的四個輸出值(1 f31 1031 3133)是在格式化字符串后面堆棧中接
下來的四個參數(shù):變量x和3個4字節(jié)整型(未經(jīng)初始化)。
現(xiàn)在該主角出場了。作為一個攻擊者,我們要控制儲存在緩沖區(qū)中的變量。這些值也是傳
遞給snprintf調(diào)用的參數(shù)!讓我們看看這個測試:
% ./fmtme "aaaa %x %x"
buffer (15): aaaa 1 61616161
x is 1/0x1 (@ 0x804745c)
耶!我們提供的這四個'a'字符被拷貝到buffer的起始處,然后被snprintf作為整型參數(shù)
解釋成0x61616161 ('a' is 0x61 in ASCII)。
X MARKS THE SPOT
所有的工作準(zhǔn)備就緒了,是時候把我們的攻擊從被動探測轉(zhuǎn)為主動改變程序的狀態(tài)了。還
記得變量"x"嗎?讓我們試著改變它的值。為了完成這個任務(wù),我們必須跳過snprintf的
第一個參數(shù),它就是變量x,最后使用%n格式寫入我們指定的地址。這聽起來比實際情況
復(fù)雜。用一個例子可以解釋清楚。
【注意:我們在這里使用PERL來執(zhí)行程序,這可以讓我們方便地在命令行參數(shù)中
放置任意字符】:
% perl -e 'system "./fmtme", "\x58\x74\x04\x08%d%n"'
buffer (5): X1
x is 5/x05 (@ 0x804745
x的值被改變了,但是究竟發(fā)生了什么?傳給snprintf的參數(shù)看起來如下所示:
snprintf(buf, sizeof buf, "\x58\x74\x04\x08%d%n", x, 4 bytes from buf)
起先snprintf把頭四個字節(jié)拷入buf。接下來掃描%d格式并打印出x的值。最后遇到%n指令
。這個指令從棧堆中取出下一個值,該值來自buf的頭四個字節(jié)。這四個字節(jié)是剛才填入
的"\x58\x74\x04\x08",或者解釋成一個整型0x08047458。Snprintf然后寫入到目前為止
輸出的字節(jié)數(shù)目,5,到
這個地址(0x08047458)。這個地址就是變量x的地址。這不是巧合。我們通過先前對程
序的檢查仔細(xì)選擇了數(shù)值0x08047458。在這里,程序打印出我們感興趣的地址是十分有幫
助的。更普遍的情況是這個值要通過debugger的幫助來獲取
好棒耶!我們可以選取任意地址(幾乎是任意地址;長度和不帶NULL字符的地址一樣長)
并且可以寫入一個值。但是我們能寫入一個有用的值嗎?snprintf僅能寫入到目前為止輸
出的字符數(shù)目。如果我們想要寫入一個比四大的小值,解決方法很簡單:按照實際需要的
數(shù)值填充格式化字符串
直到我們得到正確的值。但是如果是大數(shù)值怎么辦?這里我們可以利用一個事實:%n會計
數(shù)不考慮截短情況應(yīng)該輸出的字符個數(shù):
% perl -e 'system "./fmtme", "\x54\x74\x04\x08%.500d%n"
buffer (99): %0000000 ... 0000
x is 504/x1f8 (@ 0x8047454)
%n寫入x的值為504,比buf的長度限制99要長多了。我們可以通過指定一個大的域?qū)捴礫1]
(field
width)提供任意大的值。但是對于小值怎么辦呢?我們可以通過多次寫入的組合來構(gòu)造
任意數(shù)值(甚至是0)。如果我們每次以一個字節(jié)的偏量寫出四個數(shù)字,我們可以構(gòu)造任
意整數(shù)而不僅限于至少四個字節(jié)(地址通常用四字節(jié)表示)。為了說明這一點,考慮下面
的四次寫操作:
Address A A+1 A+2 A+3 A+4 A+5 A+6
Write to A: 0x11 0x11 0x11 0x11
Write to A+1: 0x22 0x22 0x22 0x22
Write to A+2: 0x33 0x33 0x33 0x33
Write to A+3: 0x44 0x44 0x44 0x44
Memory: 0x11 0x22 0x33 0x44 0x44 0x44 0x44
在四次寫操作完成后,整型值0x44332211留在地址為A的內(nèi)存中。由四次寫入操作的有效
字節(jié)構(gòu)成。這個技術(shù)使得我們更靈活地選擇數(shù)值寫入,但是這種方法是有缺點的:賦一個
值要用四次寫操作。而且會覆蓋目標(biāo)地址臨近的三個字節(jié)。它還要進(jìn)行三次非對齊的寫操
作,這項技術(shù)并不是通
用的。
怎么著(So what)?
So what? So what!? SO WHAT!#@?? 你可以向內(nèi)存中的任意地址寫入任意值(幾乎是任意
的)。!你肯定可以想出利用這一點的好方法。讓我們看看
* 覆蓋一個程序儲存的UID值,以降低和提升特權(quán)
* 覆蓋一個執(zhí)行命令
* 覆蓋一個返回地址,將其重定向到包含shell code的緩沖區(qū)中
更通俗地講:你擁有這個程序(為所欲為)
今天我們都學(xué)到了什么?
* printf 比你以前想象的功能更強大
* 抄近路從來都是沒有回報的(raphaelzl(小飛熊))
* 一個看起來很微小的錯誤會給攻擊者一個有力的杠桿用來毀掉你的生活(raphaelzl
(小飛熊))
* 擁有足夠的時間、努力和一個復(fù)雜的輸入字符串,你可以把某人的簡單錯誤變成全國性
的新聞事件
[1] 在某些版本的glibc中printf的實現(xiàn)有缺陷。當(dāng)指定一個大的域?qū)挄r,printf會導(dǎo)致
一個內(nèi)部緩沖區(qū)的下溢出(?underflow)并且導(dǎo)致程序崩潰。因此,在某些版本的linux
下不可能使用大于幾千的域?qū)捴祦砉舫绦。例如:下面的代碼會在有這個缺陷的系統(tǒng)上
導(dǎo)致segmentation
fault:
printf("%.9999d", 1); |
|