界: Input Validation and Representation

輸入驗證和表示法問題是由中繼字元、替代編碼和數值表示法引起的。信任輸入會導致安全問題。問題包括:「Buffer Overflows」、「Cross-Site Scripting」攻擊、「SQL Injection」及其他許多問題。

Buffer Overflow

Abstract
在分派的記憶體區塊界線外寫入資料會導致資料毀損、程式當機或惡意程式碼的執行。
Explanation
Buffer overflow 可能是軟體安全性弱點最為人知的一種形式。大部分軟體開發人員都知道什麼是 Buffer overflow 弱點,但傳統和新開發的應用程式仍常遭到 Buffer overflow 攻擊。此問題的部份原因是,發生 Buffer overflow 的方式很多,部分原因是人們常常用不恰當的方式來防範 Buffer overflow。

在典型的 Buffer overflow 攻擊中,攻擊者會將資料傳送到程式,而程式會將其儲存在一個較小的堆疊緩衝區內。結果就是呼叫這個堆疊的資訊超出了它的邊界,其中包括函數的回傳指標。該資料設定了回傳指標的值,當函數回傳時,會將控制傳送到攻擊者資料所包含的惡意程式碼。

這類型的堆疊 Buffer overflow 在一些平台和開發社群中仍然很常見,但卻還有多種其他類型的 Buffer overflow,包括堆積 Buffer overflow 和差一錯誤 (off-by-one-error) 等等。有許多優秀的著作提供了關於堆疊 Buffer overflow 如何攻擊的具體資訊,包括 Building Secure Software[1]、Writing Secure Code[2]和 The Shellcoder's Handbook[3]。

在程式碼層級,會發生 Buffer overflow 弱點通常是因為程式設計師的假設被推翻。許多在 C 和 C++ 的記憶體操作函數不執行範圍檢查,且可以輕易地覆寫所操作的緩衝區已分配的範圍。甚至如 strncpy() 的範圍函數,使用不正確時也會引起弱點。大多數 Buffer overflow 弱點的根本原因,都是緩衝區的處理,加上對資料的大小或組成假設錯誤。

Buffer overflow 弱點通常出現在這樣的程式碼中:

- 依靠外部資料來控制運作方式者。

- 仰賴被強制位於程式碼臨接範圍外的資料特性者。

- 程式碼複雜到程式設計師不能準確預測到其運作方式者。



以下範例顯示了這三種情況。

範例 1.a:接下來的程式碼範例展示了通常由第一種情況導致的簡單 Buffer overflow,即靠外部資料控制運作方式者。程式碼使用 gets() 函數,將一個任意長度的資料讀取到堆疊緩衝區。因為沒有什麼方法可以限制這個函數讀取的資料量,所以使用者輸入的字元數必須少於 BUFSIZE,以確保程式碼的安全性。


...
char buf[BUFSIZE];
gets(buf);
...
範例 1.b:此範例顯示只要使用 >> 運算子將輸入讀取到 char[] 字串中,即可輕易模仿 C++ 中 gets() 函數不安全的運作方式。


...
char buf[BUFSIZE];
cin >> (buf);
...
範例 2:雖然這個範例中的程式碼也是依賴於使用者的輸入來控制它的運作方式,但是它會藉由使用邊界記憶體複製函數 memcpy() 增加一個間接的層面。此函數接受目的緩衝區、來源緩衝區,以及要複製的位元組數量。輸入緩衝區由 read() 的範圍呼叫所填滿,但使用者指定 memcpy() 要複製的位元組數量。


...
char buf[64], in[MAX_SIZE];
printf("Enter buffer contents:\n");
read(0, in, MAX_SIZE-1);
printf("Bytes to copy:\n");
scanf("%d", &bytes);
memcpy(buf, in, bytes);
...


注意:這個類型的 Buffer overflow 弱點 (程式讀取資料,然後信賴其他資料上後續記憶體操作中的一個數值) 在圖形、音頻和其他的檔案處理庫中發生次數頻繁。

範例 3:這是第二種情況的範例,其中的程式碼根據非本地驗證的資料特性而異。在這個範例中,一個名為 lccopy() 的函數將一個字串做為引數讀取,然後將這個字串中所有的大寫字母轉化成小寫字母回傳一個堆疊分配的字串副本。這個函數不會對其輸入執行範圍檢查,因為程式預期 str 始終小於 BUFSIZE。如果攻擊者避開對呼叫 lccopy() 的程式碼的測試,或者,如果程式碼有什麼變化,使得關於 str 長度的假設失真,那麼 lccopy() 就會在 strcpy() 超出邊界的呼叫過程中溢位 buf


char *lccopy(const char *str) {
char buf[BUFSIZE];
char *p;

strcpy(buf, str);
for (p = buf; *p; p++) {
if (isupper(*p)) {
*p = tolower(*p);
}
}
return strdup(buf);
}
範例 4:下列程式碼示範了第三種情況,其中程式碼太過複雜以致無法輕易預測其行為。這段程式碼來自於主流的 libPNG 圖像解碼器,此解碼器廣泛用於多種應用程式。

該程式碼似乎能安全執行界限檢查,因為它檢查了變數長度的大小,之後這會被用來控制 png_crc_read() 複製的資料量。不過,在測試長度之前,程式碼會立即對 png_ptr->mode 執行檢查。檢查失敗時,系統會發出警告,而處理會繼續執行下去。由於 length 會在一個 else if 區塊中進行測試,所以當第一個檢查失敗時,就不會測試 length;且在呼叫 png_crc_read() 期間對其的盲目使用易引發堆疊 Buffer overflow。

雖然這個範例中的程式碼不是我們所見到的當中最複雜的一個,但是它解釋了為什麼執行緩衝區操作的程式碼的複雜度應該減到最低。


if (!(png_ptr->mode & PNG_HAVE_PLTE)) {
/* Should be an error, but we can cope with it */
png_warning(png_ptr, "Missing PLTE before tRNS");
}
else if (length > (png_uint_32)png_ptr->num_palette) {
png_warning(png_ptr, "Incorrect tRNS chunk length");
png_crc_finish(png_ptr, length);
return;
}
...
png_crc_read(png_ptr, readbuf, (png_size_t)length);
範例 5:此範例同樣證明了第三種情況,程式的複雜性將 Buffer overflow 的問題暴露出來。在此案例中,弱點歸咎於函數某個不明確的介面,而不是程式碼的結構 (就如同前面一個範例中所描述的)。

getUserInfo() 函數採用一個定義為多位元組字元串的使用者名和一個指標來組成使用者資訊的結構,並將使用者資訊填入這個結構。因為 Windows 對使用者名稱的 authentication 是使用統一的字元編碼標準,所以 username 是第一個從多位元組字元串轉換成統一字元編碼標準的字串的參數。函數接著會不正確地以位元組而非字元傳送 unicodeUser 的大小。呼叫 MultiByteToWideChar() 可能會因此而將最多 (UNLEN+1)*sizeof(WCHAR) 個寬字元,或
(UNLEN+1)*sizeof(WCHAR)*sizeof(WCHAR) 個位元組寫入 unicodeUser 陣列,而僅為該陣列分配了 (UNLEN+1)*sizeof(WCHAR) 個位元組。如果 username 字串包含了多於 UNLEN 的字元,那麼呼叫 MultiByteToWideChar() 將會溢出 unicodeUser 緩衝區。


void getUserInfo(char *username, struct _USER_INFO_2 info){
WCHAR unicodeUser[UNLEN+1];
MultiByteToWideChar(CP_ACP, 0, username, -1,
unicodeUser, sizeof(unicodeUser));
NetUserGetInfo(NULL, unicodeUser, 2, (LPBYTE *)&info);
}
References
[1] J. Viega, G. McGraw Building Secure Software Addison-Wesley
[2] M. Howard, D. LeBlanc Writing Secure Code, Second Edition Microsoft Press
[3] J. Koziol et al. The Shellcoder's Handbook: Discovering and Exploiting Security Holes John Wiley & Sons
[4] About Strsafe.h Microsoft
desc.dataflow.cpp.buffer_overflow