缓冲区溢出还是问题吗?C++/CLI安全编码
Visual C++ 2005擴(kuò)展了對(duì)使用C++/CLI(通用語(yǔ)言基礎(chǔ)結(jié)構(gòu))開(kāi)發(fā)運(yùn)行于帶有垃圾回收的虛擬機(jī)上的控件及應(yīng)用程序的支持,而C++/CLI是對(duì)C++編程語(yǔ)言的一個(gè)擴(kuò)展,其對(duì)所有類(lèi)型,包括標(biāo)準(zhǔn)C++類(lèi),都添加了如屬性、事件、垃圾回收、及泛型等特性。
Visual C++ 2005支持.NET Framework通用語(yǔ)言運(yùn)行時(shí)庫(kù)(CLR),其是垃圾回收虛擬機(jī)Microsoft的實(shí)現(xiàn)。Visual C++ 2005對(duì).NET編程的C++語(yǔ)法支持是從Visual C++ .NET 2003中引入的托管擴(kuò)展C++演化而來(lái)的,托管擴(kuò)展C++仍然被支持,但在傾向于新語(yǔ)法的情況下已不贊成使用。Visual C++ 2005同時(shí)也對(duì)本地編程添加了新的特性,包括64位處理器架構(gòu)支持,及提高了安全性的新庫(kù)函數(shù)。
在本文中,將主要講解在以最小代價(jià)把現(xiàn)有老系統(tǒng)移植到使用CLR的新環(huán)境中來(lái)時(shí),所要面臨的問(wèn)題,目的是為了確定這些程序是否仍然易受折磨C/C++程序多年的緩沖區(qū)溢出的影響。
例1會(huì)要求用戶輸入用戶名及密碼,除去用戶名之外,程序只接受"NCC-1701"為有效的密碼。如果用戶輸入了錯(cuò)誤的密碼,程序?qū)⑼顺觥?#xff08;這個(gè)程序只是作為C++/CLI代碼的漏洞測(cè)試,而不是演示如何處理密碼。) 例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()開(kāi)始執(zhí)行,在25及26行使用了一對(duì)puts()和gets()來(lái)提示輸入用戶名,導(dǎo)致了一個(gè)從標(biāo)準(zhǔn)輸入到緩沖區(qū)字符數(shù)組(聲明在第4行)的不受控制的字符串復(fù)制,程序中的這兩處地方都有可能會(huì)導(dǎo)致一個(gè)緩沖區(qū)溢出的漏洞。checkpassword()函數(shù)由main()中的27行調(diào)用,并在12及13行中提示用戶輸入密碼,這也是使用了一對(duì)puts()/gets()。對(duì)gets()的第二次調(diào)用也會(huì)導(dǎo)致一個(gè)定義在堆棧上的密碼字符數(shù)組緩沖區(qū)溢出。
程序使用Microsoft Visual C++ 2005編譯,并關(guān)閉了緩沖區(qū)安全檢查選項(xiàng)(/GS-),打開(kāi)了托管擴(kuò)展(/clr)。默認(rèn)情況下,緩沖區(qū)安全檢查是打開(kāi)的,把它關(guān)閉并不是個(gè)好做法(如本例所示),而/clr選項(xiàng)可允許由托管及非托管代碼生成混合的程序集。
程序生成過(guò)程中產(chǎn)生的幾個(gè)警告信息都可以忽略掉,例如,"warning C4996: 'gets' was declared deprecated"和"warning C4996: 'strcpy' was declared deprecated",編譯器推薦使用gets_s()來(lái)代替gets(),用strcpy_s()來(lái)代替strcpy()。如果完全使用這些替代函數(shù),那么就可消除緩沖區(qū)溢出潛在的可能性。然而,這些只是警告信息,可以忽略甚至關(guān)閉,忽略這些警告信息是符合用最小的代價(jià)移植現(xiàn)有老系統(tǒng)這個(gè)前提的。
當(dāng)使用托管擴(kuò)展時(shí),編譯器會(huì)為main()及checkpassword()函數(shù)生成Microsoft媒介語(yǔ)言(MSIL或稱(chēng)為通用媒介語(yǔ)言CIL),CIL字節(jié)碼會(huì)被打包進(jìn)一個(gè)可執(zhí)行文件,在調(diào)用即時(shí)編譯器(JIT)將其翻譯為本地程序集指令后,接著把控制權(quán)交給main()。
程序運(yùn)行時(shí),提示用戶輸入用戶名:
| Enter user name: rcs |
接著程序要求用戶輸入密碼,其被讀入到聲明在11行上的10個(gè)字符數(shù)組這個(gè)變量中,在插1中,如果在密碼從標(biāo)準(zhǔn)輸入讀取之前,查看堆棧上的數(shù)組地址起始處的數(shù)據(jù)(本例中為0x002DF3D4),將會(huì)看到分配給密碼的存儲(chǔ)空間(以黑體字標(biāo)出)及堆棧上的返回地址(以紅色字標(biāo)出)。返回地址在此為小尾字節(jié)序(Little Endian)。
代碼段1:堆棧上數(shù)組地址起始處的數(shù)據(jù)
| 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.-.`.@.....PST. |
倘若輸入了更多的字符,以致密碼字符數(shù)組存儲(chǔ)空間無(wú)法容納,一個(gè)攻擊者就可以溢出此緩沖區(qū),并以shellcode(可為任意的代碼)地址覆蓋掉返回地址。出于演示的目的,在此假定shellcode已被注入,且定位于0x00408130,為執(zhí)行此代碼,攻擊者只需把下列字符串作為密碼輸入:
| Enter 8 character password: 123456789012345678900|@ |
這個(gè)輸入的字符串被復(fù)制到密碼字符數(shù)組,溢出了此緩沖區(qū)并覆蓋相應(yīng)的內(nèi)存包括返回地址。字符串中的三個(gè)字符0|@覆蓋了返回地址的前三個(gè)字節(jié),而返回地址的最后一個(gè)字節(jié)被一個(gè)由gets()函數(shù)產(chǎn)生的null結(jié)尾字符所覆蓋。注意,如果這個(gè)null不在最后一個(gè)字節(jié)上,那么不可能復(fù)制整個(gè)字符串,因?yàn)間ets()函數(shù)會(huì)把這個(gè)null字符解釋為字符串的結(jié)尾。那為什么要以上這三個(gè)字符呢?因?yàn)?#xff0c;這些字符的十六進(jìn)制形式提供了內(nèi)存中表示地址所需的值,"0"的ASCII十六進(jìn)制碼為0x30,"|"為0x81,而"@"為0x40。如果把這三個(gè)字符以順序{ '0', '|', '@' }連接起來(lái),就可將shellcode(0x00408130)地址的小尾字節(jié)序表示形式寫(xiě)入到內(nèi)存中。最后一個(gè)null字節(jié) 由字符串的null字符提供。(見(jiàn)代碼段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 78900.@.....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.-.`.@.....PST. |
當(dāng)checkpassword()函數(shù)返回時(shí),控制權(quán)就傳到shellcode而不是main()函數(shù)中的原始返回地址上。
為了簡(jiǎn)化這個(gè)攻擊過(guò)程,在此,關(guān)閉了緩沖區(qū)安全檢查選項(xiàng)/GS。如果這個(gè)選項(xiàng)沒(méi)有關(guān)閉,編譯器將會(huì)在聲明在堆棧上的任何數(shù)組(緩沖區(qū))之后插入一個(gè)"密探"--實(shí)際上為一個(gè)Cookie,見(jiàn)圖1。
| 圖1:基于"密探"的緩沖區(qū)溢出保護(hù) |
如果要使用那些不受控制的字符串復(fù)制操作,如gets()或strcpy(),來(lái)覆蓋掉由"密探"保護(hù)的返回地址(EIP)、基指針(EBP)、或堆棧上的其他值,一個(gè)攻擊者將首先要覆蓋掉這個(gè)"密探"。如果"密探"被修改了,當(dāng)函數(shù)返回時(shí),將會(huì)產(chǎn)生一個(gè)錯(cuò)誤,導(dǎo)致攻擊失敗--除非是為了進(jìn)行"拒絕服務(wù)攻擊"。通過(guò)暴力枚舉猜測(cè)這個(gè)值,或其他方法,還是有可能挫敗這個(gè)"密探"的,但是,進(jìn)行一次成功攻擊的難度增加了。
打開(kāi)/GS選項(xiàng)不會(huì)讓程序?qū)彌_區(qū)溢出漏洞徹底免疫,堆棧中的緩沖區(qū)溢出仍會(huì)使程序崩潰,攻擊者利用基于堆棧的溢出來(lái)執(zhí)行任意代碼的可能性,即使在打開(kāi)/GS的情況下仍然存在。更重要的是,/GS選項(xiàng)不會(huì)檢測(cè)堆中或數(shù)據(jù)段中的緩沖區(qū)溢出。
為舉例說(shuō)明,例2使用Win32 GUI重寫(xiě)了前面那個(gè)示例程序,這個(gè)程序提供一個(gè)帶有一些簡(jiǎn)單選項(xiàng)的菜單欄--File菜單下有兩個(gè)菜單項(xiàng):"Login"和"Exit"。Login會(huì)用一個(gè)對(duì)話框來(lái)提示用戶輸入密碼,一旦輸入了密碼,在用戶點(diǎn)擊"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. } |
程序編譯及測(cè)試的環(huán)境均與前例相同,除了在此使用了Unicode字符集及打開(kāi)了緩沖區(qū)安全檢查選項(xiàng)(/GS),我們?cè)诖死^續(xù)使用托管擴(kuò)展(CLR)。
這是一個(gè)非常簡(jiǎn)單的程序,盡管為了支持Windows GUI,它顯得稍微有點(diǎn)長(zhǎng)。在17至20行,有幾個(gè)有意思的變量,lpszPassword是一個(gè)由16個(gè)寬字符(32字節(jié))組成的已初始化的靜態(tài)變量,緊跟其后的是userP指針及兩個(gè)無(wú)符號(hào)整形:userNameLen和userPasswordLen,之后,userP在33行初始化。這些變量的地址如下:
| &lpszPassword = 0x0040911C &userP = 0x0040913C &userNameLen = 0x00409140 &userPasswordLen = 0x00409144 |
userP的值為0x00554D30,userNameLen的值為0x00000010,userPasswordLen的值為0xffffffff。如果我們查看lpszPassword地址的起始處內(nèi)存,可以非常清楚地看到這些變量的初始值(見(jiàn)插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行中對(duì)SendDlgItemMessage的調(diào)用,EM_GETLINE消息指定了從編輯控件IDC_EDIT1獲取一行文本--編輯控件在Login對(duì)話框中,并把它復(fù)制到定長(zhǎng)緩沖區(qū)lpszPassword中。這個(gè)緩沖區(qū)只能容納15個(gè)Unicode字符及一個(gè)結(jié)尾的null,如果輸入了多于15個(gè)字符,就會(huì)發(fā)生緩沖區(qū)溢出;在此假設(shè)輸入了20個(gè)字符,第17及18個(gè)字符將會(huì)覆蓋掉userP,第19及20個(gè)字符將會(huì)覆蓋掉userNameLen,結(jié)尾的null將會(huì)覆蓋掉userPasswordLen。
假定userP與userNameLen兩者都被覆蓋,當(dāng)userNameLen被賦給存儲(chǔ)在userP+4(user結(jié)構(gòu)內(nèi)len的偏移地址)的地址時(shí),在124行就會(huì)導(dǎo)致對(duì)內(nèi)存的任意寫(xiě)入。通過(guò)把一個(gè)地址覆蓋為控制權(quán)最終要傳遞到的地址,攻擊者就能利用內(nèi)存的任意寫(xiě)入,把控制權(quán)傳給任意的代碼。而在本例中,堆棧上的返回地址被覆蓋了。
因?yàn)閘pszGuestPassword變量是一個(gè)聲明在GetPassword函數(shù)中的自動(dòng)變量,我們也可以查看這個(gè)變量地址起始處的內(nèi)存。假定lpszGuestPassword定位在0x002DEB9C,那么可在這個(gè)位置查看堆棧的內(nèi)容。經(jīng)由程序調(diào)試,可以確定0x004f3a99的返回碼位于堆棧上的0x002DEBD0處(見(jiàn)插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,那么接下來(lái),攻擊者可在Login對(duì)話框的密碼輸入欄中輸入以下字符串:
| "1234567812345678/xebcc/x002d/x9028/x0040" |
在緩沖區(qū)溢出之后,數(shù)據(jù)段的內(nèi)存顯示見(jiàn)插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 |
棕色的字節(jié)表示userP的值在何處被堆棧上的返回代碼地址所覆蓋(負(fù)4),綠色的字節(jié)表示userNameLen的值在何處被shellcode的地址所覆蓋。當(dāng)124行的內(nèi)存任意寫(xiě)入執(zhí)行之后,堆?,F(xiàn)在如插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 |
紅色表示的字節(jié)標(biāo)出了堆棧上的返回值在何處被地址值所覆蓋,在這,并沒(méi)有修改堆棧上的其他任何字節(jié)(包括"密探"),使得運(yùn)行時(shí)的系統(tǒng)很難發(fā)現(xiàn)這次攻擊。結(jié)果,控制權(quán)在GetPassword()函數(shù)返回時(shí),傳到了shellcode中。
讓我們?cè)賮?lái)回顧一下,首先,它演示了堆棧上的返回地址仍可被覆蓋--甚至在打開(kāi)緩沖區(qū)安全檢查(/GS)的情況下,這些安全檢查只會(huì)減輕聲明在堆棧上的自動(dòng)變量緩沖區(qū)溢出;其次,它也說(shuō)明了一個(gè)在Visual Studio 2005環(huán)境中編譯時(shí)毫無(wú)警告信息的程序并不是沒(méi)有漏洞可言。例3就消除了這個(gè)緩沖區(qū)溢出,在發(fā)送消息之前,lpszPassword的第一個(gè)字設(shè)為以TCHAR表示的緩沖區(qū)大小,對(duì)Unicode文本而言,這表示字符數(shù)。第一個(gè)字中的大小被復(fù)制進(jìn)來(lái)的字符數(shù)所覆蓋,同樣,對(duì)編輯控件來(lái)說(shuō),復(fù)制進(jìn)來(lái)的字符串并不包含一個(gè)null結(jié)尾字符,返回值(所復(fù)制的TCHAR數(shù))必須再設(shè)為以null結(jié)尾的字符串。
例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'; |
總結(jié)
以上是生活随笔為你收集整理的缓冲区溢出还是问题吗?C++/CLI安全编码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: CMake4:安装与测试
- 下一篇: 3DSlicer12:风格准则