什么是格式化字符串攻擊?
格式化字符串漏洞同其他許多安全漏洞一樣是由于程序員的懶惰造成的。當你正在閱讀本文的時候,也許有個程序員正在編寫代碼,他的任務是:打印輸出一個字符串或者把這個串拷貝到某緩沖區內。他可以寫出如下的代碼:
|
但是為了節約時間和提高效率,并在源碼中少輸入6個字節,他會這樣寫:
|
為什么不呢?干嘛要和多余的printf參數打交道,干嘛要花時間分解那些愚蠢的格式?printf的第一個參數無論如何都會輸出的!程序員在不知不覺中打開了一個安全漏洞,可以讓攻擊者控制程序的執行,這就是不能偷懶的原因所在。
為什么程序員寫的是錯誤的呢?他傳入了一個他想要逐字打印的字符串。實際上該字符串被printf函數解釋為一個格式化字符串(format string)。函數在其中尋找特殊的格式字符比如"%d"。如果碰到格式字符,一個變量的參數值就從堆棧中取出。很明顯,攻擊者至少可以通過打印出堆棧中的這些值來偷看程序的內存。但是有些事情就不那么明顯了,這個簡單的錯誤允許向運行中程序的內存里寫入任意值。
Printf中被忽略的東西
在說明如何為了自己的目的濫用printf之前,我們應該深入領會printf提供的特性。假定讀者以前用過printf函數并且知道普通的格式化特性,比如如何打印整型和字符串,如何指定最大和最小字符串寬度等。除了這些普通的特性之外,還有一些深奧和鮮為人知的特性。在這些特性當中,下面介紹的對我們比較有用:
* 在格式化字符串中任何位置都可以得到輸出字符的個數。當在格式化字符串中碰到"%n"的時候,在%n域之前輸出的字符個數會保存到下一個參數里。例如,為了獲取在兩個格式化的數字之間空間的偏量:
|
* %n格式返回應該被輸出的字符數目,而不是實際輸出的字符數目。當把一個字符串格式化輸出到一個定長緩沖區內時,輸出字符串可能被截短。不考慮截短的影響,%n格式表示如果不被截短的偏量值(輸出字符數目)。為了說明這一點,下面的代碼會輸出100而不是20:
|
簡單的例子
除了討論抽象和復雜的理論,我們將會使用一個具體的例子來說明我們剛才討論的原理。下面這個簡單的程序能滿足這個要求:
|
對這個程序有幾點說明:第一,目的很簡單:將一個通過命令行傳遞值格式化輸出到一個定長的緩沖區里。并確保緩沖區的大小限制不被突破。在緩沖區格式化后,把它輸出。除了把參數格式化,還設置了一個整型值隨后輸出。這個變量是隨后我們攻擊的目標。現在值得我們注意的是這個值應該始終為1。
本文中所有的例子都是在x86 BSD/OS 4.1機器上完成。如果你到莫桑比克執行任務超過20年時間可能會對x86不熟悉,這是一個little-endian機器。這決定在例子中多精度數字的表示方法。在這里使用的具體數值會因為系統的差異而不同,這些差異表現在不同體系結構、操作系統、環境甚至是命令行長度。經過簡單調整,這些例子可以在其他x86平臺上工作。通過努力也可以在其他體系結構的平臺上工作。
用Format攻擊
現在是我們戴上黑帽子開始以攻擊者方式思考問題的時候了。我們現在手頭有一個測試程序。知道這個程序有一個漏洞并且了解程序員是在哪里犯錯誤的(直接把用戶輸入的命令行參數作為snprintf的格式化參數)。我們還擁有關于printf函數深入的知識,知道如何運用這些知識。讓我們開始修補我們的程序吧。
從簡單的開始,我們通過簡單的參數調用程序。看這兒:
|
現在這兒還沒有什么特別的事情發生。程序把我們輸入的字符串格式化輸出到緩沖區里,然后打印出它的長度和數值。程序還告訴我們變量x的值是1(以十進制和十六進制分別顯示),x的存儲地址是0x804745c。接下來我們試著使用一些格式指令。在下面的例子中我們打印出在格式化字符串之上棧堆中的整型數值:
|
對這個程序的快速分析可以揭示在調用snprintf函數時程序堆棧的規劃:
|