格式化字符串漏洞由於目前編譯器的默認禁止敏感格式控制符,而且容易通過代碼審計中發現,所以此類漏洞極少出現,一直沒有筆者本人的引起重視。最近搗鼓pwn題,遇上了不少,決定好好總結了一下。
格式化字符串漏洞最早被Tymm Twillman在1999年發現,當時並未引起重視。在tf8的一份針對wu-ftpd格式化字符串漏洞實現任意代碼執行的漏洞的報告之后(詳情可參閱 《黑客攻防技術寶典-系統實戰篇》),才讓人們意識到它的危害,至此而發現了大量的相關漏洞。
格式化字符串漏洞的產生根源主要源於對用 戶輸入未進行過濾,這些輸入數據都作為數據傳遞給某些執行格式化操作的函數,如printf,sprintf,vprintf,vprintf。惡意用戶 可以使用"%s","%x"來得到堆棧的數據,甚至可以通過"%n"來對任意地址進行讀寫,導致任意代碼讀寫。
//正確的使用方式test1 main() { printf("%x",1); } //錯誤的使用方式test2 main() { printf("%x") } //導致漏洞test3 main(int argc,char * argv[]) { printf(argv[1]) }
如果可以對printf(如test3)內進行任意輸入,將會是一件很糟糕的事情。將會導致徹底的系統威脅。
在說明原理之前,先介紹幾個格式控制符:
%d 用於讀取10進制數值
%x 用於讀取16進制數值
%s 用於讀取字符串值
%n 用於講當前字符串的長度打印到var中,例 printf("test %hn",&var[其中var為兩個字節]) printf("test %n",&var[其中var為一個字節])
具體原理:當printf在輸出格式化字符串的時候,會維護一個內部指針,當printf逐步將格式化字符串的字符打印到屏幕,當遇到%的時候,printf會期望它后面跟着一個格式字符串,因此會遞增內部字符串以抓取格式控制符的輸入值。這就是問題所在,printf無法知道棧上是否放置了正確數量的變量供它操作,如果沒有足夠的變量可供操作,而指針按正常情況下遞增,就會產生越界訪問。甚至由於%n的問題,可導致任意地址讀寫。
talk is simple,Let's show by code:
#include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int target; void printbuffer(char *string) { printf(string); } void vuln() { char buffer[512]; fgets(buffer, sizeof(buffer), stdin); printbuffer(buffer); if(target == 0x01025544) { printf("you have modified the target :)\n"); } else { printf("target is %08x :(\n", target); } } int main(int argc, char **argv) { vuln(); }
這份代碼來自Protostar format3,編譯取消所有保護措施。
在試圖利用格式化字符串漏洞之前,你需要知道格式化字符串會在調用printf之前先壓入堆棧中。所以當發現一個格式化字符串漏洞時,首先你需要找到格式化字符串距離當前位置的偏移
可以利用%08x進行查找,如圖
#關於偏移值,可能會由於環境和程序名稱的不同而不同,甚至會產生不處於4的倍數的位置,面對這種情況,可以通過在最前方加入墊子進行取整。
可以得到偏移值為12,然后嘗試通過利用%12$x(這種使用方法可以直接省去前面11個%08x的使用)進行直接讀取,如圖
至此,可以通過使用%Nx(N為任意長度的十進制數字)來控制字符串長度,字符串長度為len('AAAA')+N,通過將長度寫入到偏移地址中來進行對任意地址進行任意讀寫。其中若使用%hn,只需要進行兩次寫操作,可以節省時間,但會消耗極大的空間。而若使用%n,則需要進行4次讀寫,但可以節省空間,對此,由於部分環境下,由於可能對緩沖區長度有所限制,導致exploit失敗,所以更加偏向於使用%n。
很多地址均為不可見字符串,所以利用python和管道講之前代碼進行轉化。如圖
現在,嘗試target低位進行寫操作,即0x80496f4進行讀寫(使用IDA或者gdb進行查看target地址),如圖:
此時,對0x080496f5進行寫操作,此時只需要在后面添加長度為0x55-0x44的字符串即可,同時,需要對%.64進行-4操作,對12的偏移值+1即如圖:
對0x080496f6,0x080496f6進行操作,這個時候發現0x04-0x55為負數,無法繼續下去,可以從上一位借1.如圖
成功對任意地址進行了任意讀寫。當然,順序進行讀寫由於借位可能對更高位產生影響,本題為0x080496f8。若想解決這個問題,可以嘗試從最小的開始進行寫,本體0x080496f7,0x080496f6,0x080494f4,0x080496f3。對於,%hn的使用,可以直接參照魔術公式(雖然,%n也可以總結出來魔術公式,但是就顯的比直接寫更加復雜了)。詳情參照《灰帽黑客》第四版。
格式化字符串最近出現的頻率極為稀少,較近的可能為CVE-2012-0809 sudo_debug格式化字符串漏洞,和CVE-2012-3569 VMware OVF Tool格式化字符串漏洞。分別處於windows和linux環境。
順便講一下選擇那些地址進行讀寫:
主流的讀寫位置如下:
FINI_ARRAY區:程序初始化和結束需要經過這里,可以寫這里的析構函數。
全局偏移表:
全局函數指針:
atexit處理函數:
堆棧值(主要指返回地址):
虛表指針:
理論上只要程序在后面會調用的,可進行寫操作的位置均可進行寫,從而改變程序執行,進行成功的exploit