萬盛學電腦網

 萬盛學電腦網 >> 健康知識 >> 格式化字符串攻擊(Format String Attacks)

格式化字符串攻擊(Format String Attacks)

  內容:介紹什麼是格式化字符串攻擊?Printf-學校忘記教給你的東西簡單的例子來格式化吧!(Format Me!)X MARKS THE SPOT(X是本文示例程序中我們試圖重寫的一個變量)怎麼著(So what)?摘要本文討論格式化字符串漏洞的成因和含義,並給出實際的例子來解釋原理。介紹我知道在某些時候對於你我和我們大家而言,下面這種情況總會發生。在一個時下流行的晚餐會上,夾雜在同事們大呼小叫的聲音裡,你聽到了"格式化字符串攻擊"這只言片語。 "格式化字符串攻擊?什麼是格式化字符串攻擊?"你心說。由於害怕在同事們面前顯露出自己的無知,你決定停止不自然的微笑,而頻頻點頭以示自己對這玩藝了如指掌。如果一切順利,大家會共飲雞尾酒,談話仍將繼續,但是沒人明白這究竟是怎麼回事。現在不用再害怕什麼了,本文會提供你想知道而又不好意思問的所有內容。什麼是格式化字符串攻擊?格式化字符串漏洞同其他許多安全漏洞一樣是由於程序員的懶惰造成的。當你正在閱讀本文的時候,也許有個程序員正在編寫代碼,他的任務是:打印輸出一個字符串或者把這個串拷貝到某緩沖區內。他可以寫出如下的代碼:printf("%s", str);但是為了節約時間和提高效率,並在源碼中少輸入6個字節,5自學網,他會這樣寫:printf(str);為什麼不呢?干嘛要和多余的printf參數打交道,干嘛要花時間分解那些愚蠢的格式?printf的第一個參數無論如何都會輸出的!程序員在不知不覺中打開了一個安全漏洞,可以讓攻擊者控制程序的執行,這就是不能偷懶的原因所在。為什麼程序員寫的是錯誤的呢?他傳入了一個他想要逐字打印的字符串。實際上該字符串被printf函數解釋為一個格式化字符串(formatstring)。函數在其中尋找特殊的格式字符比如"%d"。如果碰到格式字符,一個變量的參數值就從堆棧中取出。很明顯,攻擊者至少可以通過打印出堆棧中的這些值來偷看程序的內存。但是有些事情就不那麼明顯了,這個簡單的錯誤允許向運行中程序的內存裡寫入任意值。Printf-學校忘記教給你的東西在說明如何為了自己的目的濫用printf之前,我們應該深入領會printf提供的特性。假定讀者以前用過printf函數並且知道普通的格式化特性,比如如何打印整型和字符串,如何指定最大和最小字符串寬度等。除了這些普通的特性之外,還有一些深奧和鮮為人知的特性。在這些特性當中,下面介紹的對我們比較有用:*在格式化字符串中任何位置都可以得到輸出字符的個數。當在格式化字符串中碰到"%n"的時候,在%n域之前輸出的字符個數會保存到下一個參數裡。例如,為了獲取在兩個格式化的數字之間空間的偏量:int pos, x = 235, y = 93;printf("%d %n%d\n", x, &pos, y);printf("The offset was %d\n", pos);* %n格式返回應該被輸出的字符數目,而不是實際輸出的字符數目。當把一個字符串格式化輸出到一個定長緩沖區內時,輸出字符串可能被截短。不考慮截短的影響,%n格式表示如果不被截短的偏量值(輸出字符數目)。為了說明這一點,下面的代碼會輸出100而不是20:char buf[20];int pos, x = 0;snprintf(buf, sizeof buf, "%.100d%n", x, &pos);printf("position: %d\n", pos);簡單的例子除了討論抽象和復雜的理論,我們將會使用一個具體的例子來說明我們剛才討論的原理。下面這個簡單的程序能滿足這個要求:/** fmtme.c* Format a value into a fixed-size buffer*/#includeintmain(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;}對這個程序有幾點說明:第一,目的很簡單:將一個通過命令行傳遞值格式化輸出到一個定長的緩沖區裡。並確保緩沖區的大小限制不被突破。在緩沖區格式化後,把它輸出。除了把參數格式化,還設置了一個整型值隨後輸出。這個變量是隨後我們攻擊的目標。現在值得我們注意的是這個值應該始終為1。本文中所有的例子都是在x86 BSD/OS4.1機器上完成。如果你到莫桑比克執行任務超過20年時間可能會對x86不熟悉,這是一個little-endian機器。這決定在例子中多精度數字的表示方法。在這裡使用的具體數值會因為系統的差異而不同,這些差異表現在不同體系結構、操作系統、環境甚至是命令行長度。經過簡單調整,這些例子可以在其他x86平台上工作。通過努力也可以在其他體系結構的平台上工作。來格式化吧!(Format Me!)現在是我們戴上黑帽子開始以攻擊者方式思考問題的時候了。我們現在手頭有一個測試程序。知道這個程序有一個漏洞並且了解程序員是在哪裡犯錯誤的(直接把用戶輸入的命令行參數作為snprintf的格式化參數)。我們還擁有關於printf函數深入的知識,知道如何運用這些知識。讓我們開始修補我們的程序吧。從簡單的開始,我們通過簡單的參數調用程序。看這兒:% ./fmtme "hello world"buffer (11): hello worldx is 1/0x1 (@ 0x804745c)現在這兒還沒有什麼特別的事情發生。程序把我們輸入的字符串格式化輸出到緩沖區裡,然後打印出它的長度和數值。程序還告訴我們變量x的值是1(以十進制和十六進制分別顯示),x的存儲地址是0x804745c。接下來我們試著使用一些格式指令。在下面的例子中我們打印出在格式化字符串之上棧堆中的整型數值:% ./fmtme "%x %x %x %x"buffer (15): 1 f31 1031 3133x is 1/0x1 (@ 0x804745c)對這個程序的快速分析可以揭示在調用snprintf函數時程序堆棧的規劃:Address Contents Descriptionfp 8 Buffer pointer 4-byte addressfp 12 Buffer length 4-byte integerfp 16 Format string 4-byte addressfp 20 Variable x 4-byte integer fp 24 Variable buf 100 characters(補充:我參考了"緩沖區溢出機理分析"一文,才看明白上面的內容。簡單介紹一下:當程序中發生函數調用時,計算機做如下操作:首先把參數壓入堆棧;然後保存指令寄存器(IP)中的內容做為返回地址(RET);第三個放入堆棧的是基址寄存器(FP);然後把當前的棧指針(SP)拷貝到FP,做為新的基地址;最後為本地變量留出一定空間,把SP減去適當的數值。----------------------------------------------------------------------當調用函數snprintf ()時,堆棧如下:低內存端 高內存端函數局部變量 sfp ret buf sizeof(buf) argv[1] x和buf<- [ ] [ ] [ ] [ ] [ ] [ ] 數據區棧頂 棧底)前一個測試運行結果的四個輸出值(1 f31 1031 3133)是在格式化字符串後面堆棧中接下來的四個參數:變量x和3個4字節整型(未經初始化)。現在該主角出場了。作為一個攻擊者,我們要控制儲存在緩沖區中的變量。這些值也是傳遞給snprintf調用的參數!讓我們看看這個測試:% ./fmtme "aaaa %x %x"buffer (15): aaaa 1 61616161x is 1/0x1 (@ 0x804745c)耶!我們提供的這四個'a'字符被拷貝到buffer的起始處,然後被snprintf作為整型參數解釋成0x61616161 ('a' is 0x61 in ASCII)。X MARKS THE SPOT所有的工作准備就緒了,是時候把我們的攻擊從被動探測轉為主動改變程序的狀態了。還記得變量"x"嗎?讓我們試著改變它的值。為了完成這個任務,我們必須跳過snprintf的第一個參數,它就是變量x,最後使用%n格式寫入我們指定的地址。這聽起來比實際情況復雜。用一個例子可以解釋清楚。【注意:我們在這裡使用PERL來執行程序,這可以讓我們方便地在命令行參數中放置任意字符】:% perl -e 'system "./fmtme", "\x58\x74\x04\x08%d%n"'buffer (5): X1x is 5/x05 (@ 0x8047458)x的值被改變了,但是究竟發生了什麼?傳給snprintf的參數看起來如下所示:snprintf(buf, sizeof buf, "\x58\x74\x04\x08%d%n", x, 4 bytes from buf)起先snprintf把頭四個字節拷入buf。接下來掃描%d格式並打印出x的值。最後遇到%n指令。這個指令從棧堆中取出下一個值,該值來自buf的頭四個字節。這四個字節是剛才填入的"\x58\x74\x04\x08",或者解釋成一個整型0x08047458。Snprintf然後寫入到目前為止輸出的字節數目,5,自學教程,到這個地址(0x08047458)。這個地址就是變量x的地址。這不是巧合。我們通過先前對程序的檢查仔細選擇了數值0x08047458。在這裡,程序打印出我們感興趣的地址是十分有幫助的。更普遍的情況是這個值要通過debugger的幫助來獲取好棒耶!我們可以選取任意地址(幾乎是任意地址;長度和不帶NULL字符的地址一樣長)並且可以寫入一個值。但是我們能寫入一個有用的值嗎?snprintf僅能寫入到目前為止輸出的字符數目。如果我們想要寫入一個比四大的小值,解決方法很簡單:按照實際需要的數值填充格式化字符串直到我們得到正確的值。但是如果是大數值怎麼辦?這裡我們可以利用一個事實:%n會計數不考慮截短情況應該輸出的字符個數:% perl -e 'system "./fmtme", "\x54\x74\x04\x08%.500d%n"buffer (99):
copyright © 萬盛學電腦網 all rights reserved