程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 緩沖區溢出還是問題嗎?C++/CLI安全編碼

緩沖區溢出還是問題嗎?C++/CLI安全編碼

編輯:關於C++

C++/CLI是對C++的一個擴展,其對所有類型,包括標准C++類,都添加了對屬性、事件、垃圾回收、及泛型的支持。

Visual C++ 2005擴展了對使用C++/CLI(通用語言基礎結構)開發運行於帶有垃圾回收的虛擬機上的控件及應用程序的支持,而C++/CLI是對C++編程語言的一個擴展,其對所有類型,包括標准C++類,都添加了如屬性、事件、垃圾回收、及泛型等特性。

Visual C++ 2005支持.NET Framework通用語言運行時庫(CLR),其是垃圾回收虛擬機Microsoft的實現。Visual C++ 2005對.NET編程的C++語法支持是從Visual C++ .NET 2003中引入的托管擴展C++演化而來的,托管擴展C++仍然被支持,但在傾向於新語法的情況下已不贊成使用。Visual C++ 2005同時也對本地編程添加了新的特性,包括64位處理器架構支持,及提高了安全性的新庫函數。

在本文中,將主要講解在以最小代價把現有老系統移植到使用CLR的新環境中來時,所要面臨的問題,目的是為了確定這些程序是否仍然易受折磨C/C++程序多年的緩沖區溢出的影響。

例1會要求用戶輸入用戶名及密碼,除去用戶名之外,程序只接受"NCC-1701"為有效的密碼。如果用戶輸入了錯誤的密碼,程序將退出。(這個程序只是作為C++/CLI代碼的漏洞測試,而不是演示如何處理密碼。) 例1:

1. #include <stdlib.h>
2. #include <stdio.h>
3. #include <windows.h>
4. char buff[1028];
5. struct user {
6. char *name;
7. size_t len;
8. int uid;
9. };
10. bool checkpassword() {
11.  char password[10];
12.  puts("Enter 8 character password:");
13.  gets(password);
14.  if (strcmp(password, "NCC-1701") == 0) {
15.   return true;
16.  }
17.  else {
18.   return false;
19.  }
20. }
21. int main(int argc, char *argv[]) {
22.  struct user *userP = (struct user *)0xcdcdcdcd;
23.  size_t userNameLen = 0xdeadbeef;
24.  userP = (struct user *)malloc(sizeof(user));
25.  puts("Enter user name:");
26.  gets(buff);
27.  if (!checkpassword()) {
28.   userNameLen = strlen(buff) + 1;
29.   userP->len = userNameLen;
30.   userP->name = (char *)malloc(userNameLen);
31.   strcpy(userP->name, buff); // log failed login attempt
32.   exit(-1);
33.  }
34. }

程序從21行的main()開始執行,在25及26行使用了一對puts()和gets()來提示輸入用戶名,導致了一個從標准輸入到緩沖區字符數組(聲明在第4行)的不受控制的字符串復制,程序中的這兩處地方都有可能會導致一個緩沖區溢出的漏洞。checkpassword()函數由main()中的27行調用,並在12及13行中提示用戶輸入密碼,這也是使用了一對puts()/gets()。對gets()的第二次調用也會導致一個定義在堆棧上的密碼字符數組緩沖區溢出。

程序使用Microsoft Visual C++ 2005編譯,並關閉了緩沖區安全檢查選項(/GS-),打開了托管擴展(/clr)。默認情況下,緩沖區安全檢查是打開的,把它關閉並不是個好做法(如本例所示),而/clr選項可允許由托管及非托管代碼生成混合的程序集。

程序生成過程中產生的幾個警告信息都可以忽略掉,例如,"warning C4996: 'gets' was declared deprecated"和"warning C4996: 'strcpy' was declared deprecated",編譯器推薦使用gets_s()來代替gets(),用strcpy_s()來代替strcpy()。如果完全使用這些替代函數,那麼就可消除緩沖區溢出潛在的可能性。然而,這些只是警告信息,可以忽略甚至關閉,忽略這些警告信息是符合用最小的代價移植現有老系統這個前提的。

當使用托管擴展時,編譯器會為main()及checkpassword()函數生成Microsoft媒介語言(MSIL或稱為通用媒介語言CIL),CIL字節碼會被打包進一個可執行文件,在調用即時編譯器(JIT)將其翻譯為本地程序集指令後,接著把控制權交給main()。

程序運行時,提示用戶輸入用戶名:

Enter user name:
rcs

接著程序要求用戶輸入密碼,其被讀入到聲明在11行上的10個字符數組這個變量中,在插1中,如果在密碼從標准輸入讀取之前,查看堆棧上的數組地址起始處的數據(本例中為0x002DF3D4),將會看到分配給密碼的存儲空間(以黑體字標出)及堆棧上的返回地址(以紅色字標出)。返回地址在此為小尾字節序(Little Endian)。

代碼段1:堆棧上數組地址起始處的數據

002DF3D4 00 00 00 00 04 f4 2d 00 a0 1b e7 79 80 63 54 00 ......-....y.cT.
002DF3E4 04 f4 2d 00 f9 0f 0a 02 01 00 00 00 79 3a 4e 00 ..-.........y:N.
002DF3F4 a8 2b 2f 00 38 f4 2d 00 da c4 fc 79 78 f4 2d 00 .+/.8.-....yx.-.
002DF404 48 f4 2d 00 60 13 40 00 01 00 00 00 50 53 54 00 H.-.`[email protected].

倘若輸入了更多的字符,以致密碼字符數組存儲空間無法容納,一個攻擊者就可以溢出此緩沖區,並以shellcode(可為任意的代碼)地址覆蓋掉返回地址。出於演示的目的,在此假定shellcode已被注入,且定位於0x00408130,為執行此代碼,攻擊者只需把下列字符串作為密碼輸入:

Enter 8 character password:
123456789012345678900|@

這個輸入的字符串被復制到密碼字符數組,溢出了此緩沖區並覆蓋相應的內存包括返回地址。字符串中的三個字符0|@覆蓋了返回地址的前三個字節,而返回地址的最後一個字節被一個由gets()函數產生的null結尾字符所覆蓋。注意,如果這個null不在最後一個字節上,那麼不可能復制整個字符串,因為gets()函數會把這個null字符解釋為字符串的結尾。那為什麼要以上這三個字符呢?因為,這些字符的十六進制形式提供了內存中表示地址所需的值,"0"的ASCII十六進制碼為0x30,"|"為0x81,而"@"為0x40。如果把這三個字符以順序{ '0', '|', '@' }連接起來,就可將shellcode(0x00408130)地址的小尾字節序表示形式寫入到內存中。最後一個null字節 由字符串的null字符提供。(見代碼段2。)

代碼段2:

002DF3D4 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 1234567890123456
002DF3E4 37 38 39 30 30 81 40 00 01 00 00 00 79 3a 4e 00 [email protected]:N.
002DF3F4 a8 2b 2f 00 38 f4 2d 00 da c4 fc 79 78 f4 2d 00 .+/.8.-....yx.-.
002DF404 48 f4 2d 00 60 13 40 00 01 00 00 00 50 53 54 00 H.-.`[email protected].

當checkpassword()函數返回時,控制權就傳到shellcode而不是main()函數中的原始返回地址上。

為了簡化這個攻擊過程,在此,關閉了緩沖區安全檢查選項/GS。如果這個選項沒有關閉,編譯器將會在聲明在堆棧上的任何數組(緩沖區)之後插入一個"密探"--實際上為一個Cookie,見圖1。

  

圖1:基於"密探"的緩沖區溢出保護

如果要使用那些不受控制的字符串復制操作,如gets()或strcpy(),來覆蓋掉由"密探"保護的返回地址(EIP)、基指針(EBP)、或堆棧上的其他值,一個攻擊者將首先要覆蓋掉這個"密探"。如果"密探"被修改了,當函數返回時,將會產生一個錯誤,導致攻擊失敗--除非是為了進行"拒絕服務攻擊"。通過暴力枚舉猜測這個值,或其他方法,還是有可能挫敗這個"密探"的,但是,進行一次成功攻擊的難度增加了。

打開/GS選項不會讓程序對緩沖區溢出漏洞徹底免疫,堆棧中的緩沖區溢出仍會使程序崩潰,攻擊者利用基於堆棧的溢出來執行任意代碼的可能性,即使在打開/GS的情況下仍然存在。更重要的是,/GS選項不會檢測堆中或數據段中的緩沖區溢出。

為舉例說明,例2使用Win32 GUI重寫了前面那個示例程序,這個程序提供一個帶有一些簡單選項的菜單欄--File菜單下有兩個菜單項:"Login"和"Exit"。Login會用一個對話框來提示用戶輸入密碼,一旦輸入了密碼,在用戶點擊"OK"按鈕之後,將把輸入的密碼與之前記錄的密碼相比較。

例2:

1. #include "stdafx.h"
2. #include "TestItDan.h"
3. #include <stdlib.h>
4. #include <stdio.h>
5. #include <windows.h>
6. #define MAX_LOADSTRING 100
7. struct user {
8.  wchar_t *name;
9.  size_t len;
10. int uid;
11. };
13. HINSTANCE hInst;
14. TCHAR szTitle[MAX_LOADSTRING];
15. TCHAR szWindowClass[MAX_LOADSTRING];
16. TCHAR lpszUserName[16] = L"guest";
17. TCHAR lpszPassword[16] = L"0123456789abcde";
18. struct user *userP = (struct user *)0xcdcdcdcdcdcdcdcd;
19. size_t userNameLen = 16;
20. size_t userPasswordLen = 0xffffffff;
25. int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow) {
26.  UNREFERENCED_PARAMETER(hPrevInstance);
27.  UNREFERENCED_PARAMETER(lpCmdLine);
28.  MSG msg;
29.  HACCEL hAccelTable;
30.  LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
31.  LoadString(hInstance, IDC_TESTITDAN, szWindowClass, MAX_LOADSTRING);
32.  MyRegisterClass(hInstance);
33.  userP = (struct user *)malloc(sizeof(user));
34.  if (!InitInstance (hInstance, nCmdShow)) {
35.   return FALSE;
36.  }
37.  hAccelTable =LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TESTITDAN));
38.  while (GetMessage(&msg, NULL, 0, 0)) {
39.   if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
40.    TranslateMessage(&msg);
41.    DispatchMessage(&msg);
42.   }
43.  }
44.  return (int) msg.wParam;
45. }
109. INT_PTR CALLBACK GetPassword(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
110.  TCHAR lpszGuestPassword[16] = L"NCC-1701";
111.  UNREFERENCED_PARAMETER(lParam);
112.  switch (message) {
113.   case WM_INITDIALOG:
114.    return (INT_PTR)TRUE;
115.   case WM_COMMAND:
116.    if (LOWORD(wParam) == IDOK) {
117.     EndDialog(hDlg, LOWORD(wParam));
118.     SendDlgItemMessage(hDlg,
119.     IDC_EDIT1,
120.     EM_GETLINE,
121.     (WPARAM) 0, // line 0
122.     (LPARAM) lpszPassword
123.     );
124.     userP->len = userNameLen;
125.     if (wcscmp(lpszPassword, lpszGuestPassword) == 0) {
126.      return true;
127.     }
128.     else {
129.      MessageBox(hDlg,
130.       (LPCWSTR)L"Invalid Password",
131.       (LPCWSTR)L"Login Failed",
132.       MB_OK
133.      );
134.     }
135.     return (INT_PTR)TRUE;
136.    }
137.    break;
138.   }
139.   return (INT_PTR)FALSE;
140.  }

程序編譯及測試的環境均與前例相同,除了在此使用了Unicode字符集及打開了緩沖區安全檢查選項(/GS),我們在此繼續使用托管擴展(CLR)。

這是一個非常簡單的程序,盡管為了支持Windows GUI,它顯得稍微有點長。在17至20行,有幾個有意思的變量,lpszPassword是一個由16個寬字符(32字節)組成的已初始化的靜態變量,緊跟其後的是userP指針及兩個無符號整形:userNameLen和userPasswordLen,之後,userP在33行初始化。這些變量的地址如下:

&lpszPassword = 0x0040911C
&userP = 0x0040913C
&userNameLen = 0x00409140
&userPasswordLen = 0x00409144

userP的值為0x00554D30,userNameLen的值為0x00000010,userPasswordLen的值為0xffffffff。如果我們查看lpszPassword地址的起始處內存,可以非常清楚地看到這些變量的初始值(見插3)。

代碼段3:

0040911C 30 00 31 00 32 00 33 00 34 00 35 00 36 00 37 00
0040912C 38 00 39 00 61 00 62 00 63 00 64 00 65 00 00 00
0040913C 30 4d 55 00 10 00 00 00 ff ff ff ff 8a 00 07 02
0040914C c6 00 07 02 02 01 07 02 00 00 00 00 01 00 00 00

此程序中的漏洞是在118至123行中對SendDlgItemMessage的調用,EM_GETLINE消息指定了從編輯控件IDC_EDIT1獲取一行文本--編輯控件在Login對話框中,並把它復制到定長緩沖區lpszPassword中。這個緩沖區只能容納15個Unicode字符及一個結尾的null,如果輸入了多於15個字符,就會發生緩沖區溢出;在此假設輸入了20個字符,第17及18個字符將會覆蓋掉userP,第19及20個字符將會覆蓋掉userNameLen,結尾的null將會覆蓋掉userPasswordLen。

假定userP與userNameLen兩者都被覆蓋,當userNameLen被賦給存儲在userP+4(user結構內len的偏移地址)的地址時,在124行就會導致對內存的任意寫入。通過把一個地址覆蓋為控制權最終要傳遞到的地址,攻擊者就能利用內存的任意寫入,把控制權傳給任意的代碼。而在本例中,堆棧上的返回地址被覆蓋了。

因為lpszGuestPassword變量是一個聲明在GetPassword函數中的自動變量,我們也可以查看這個變量地址起始處的內存。假定lpszGuestPassword定位在0x002DEB9C,那麼可在這個位置查看堆棧的內容。經由程序調試,可以確定0x004f3a99的返回碼位於堆棧上的0x002DEBD0處(見插4)。

代碼段4:

002DEB9C 4e 00 43 00 43 00 2d 00 31 00 37 00 30 00 31 00
002DEBAC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
002DEBBC 1e df b4 bd 00 00 00 00 50 15 40 00 64 ec 2d 00
002DEBCC ec eb 2d 00 99 3a 4f 00 05 27 00 01 00 00 00
002DEBDC b0 32 2f 00 84 ec 2d 00 da c4 fc 79 58 f1 2d 00

假定shellcode已被注入到程序中的0x00409028,那麼接下來,攻擊者可在Login對話框的密碼輸入欄中輸入以下字符串:

"1234567812345678\xebcc\x002d\x9028\x0040"

在緩沖區溢出之後,數據段的內存顯示見插5:

代碼段5:

0040911C 31 00 32 00 33 00 34 00 35 00 36 00 37 00 38 00
0040912C 31 00 32 00 33 00 34 00 35 00 36 00 37 00 38 00
0040913C cc eb 2d 00 28 90 40 00 00 00 ff ff 8a 00 07 00
0040914C c6 00 07 02 02 01 07 02 00 00 00 00 01 00 00 00

棕色的字節表示userP的值在何處被堆棧上的返回代碼地址所覆蓋(負4),綠色的字節表示userNameLen的值在何處被shellcode的地址所覆蓋。當124行的內存任意寫入執行之後,堆棧現在如插6所示。

代碼段6:

002DEB9C 4e 00 43 00 43 00 2d 00 31 00 37 00 30 00 31 00
002DEBAC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
002DEBBC 1e df b4 bd 00 00 00 00 50 15 40 00 64 ec 2d 00
002DEBCC ec eb 2d 00 28 90 40 00 0e 05 27 00 01 00 00 00
002DEBDC b0 32 2f 00 84 ec 2d 00 da c4 fc 79 58 f1 2d 00

紅色表示的字節標出了堆棧上的返回值在何處被地址值所覆蓋,在這,並沒有修改堆棧上的其他任何字節(包括"密探"),使得運行時的系統很難發現這次攻擊。結果,控制權在GetPassword()函數返回時,傳到了shellcode中。

讓我們再來回顧一下,首先,它演示了堆棧上的返回地址仍可被覆蓋--甚至在打開緩沖區安全檢查(/GS)的情況下,這些安全檢查只會減輕聲明在堆棧上的自動變量緩沖區溢出;其次,它也說明了一個在Visual Studio 2005環境中編譯時毫無警告信息的程序並不是沒有漏洞可言。例3就消除了這個緩沖區溢出,在發送消息之前,lpszPassword的第一個字設為以TCHAR表示的緩沖區大小,對Unicode文本而言,這表示字符數。第一個字中的大小被復制進來的字符數所覆蓋,同樣,對編輯控件來說,復制進來的字符串並不包含一個null結尾字符,返回值(所復制的TCHAR數)必須再設為以null結尾的字符串。

例3:

LRESULT Retval;
*((WORD *)(&lpszPassword)) = (sizeof(lpszPassword)/sizeof(TCHAR))-1;
Retval = SendDlgItemMessage(hDlg, IDC_EDIT1, EM_GETLINE,
(WPARAM) 0, // line 0
(LPARAM) lpszPassword
);
lpszPassword[Retval]='\0';

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved