熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

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

2013-11-13 10:06:03  來源: .NET編程 
C++/CLI是對C++的一個擴展其對所有類型包括標准C++類都添加了對屬性事件垃圾回收及泛型的支持

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

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

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

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

#include <stdlibh>
#include <stdioh>
#include <windowsh>
char buff[];
struct user {
char *name;
size_t len;
int uid;
};
bool checkpassword() {
 char password[];
 puts(Enter character password:);
 gets(password);
 if (strcmp(password NCC) == ) {
  return true;
 }
 else {
  return false;
 }
}
int main(int argc char *argv[]) {
 struct user *userP = (struct user *)xcdcdcdcd;
 size_t userNameLen = xdeadbeef;
 userP = (struct user *)malloc(sizeof(user));
 puts(Enter user name:);
 gets(buff);
 if (!checkpassword()) {
  userNameLen = strlen(buff) + ;
  userP>len = userNameLen;
  userP>name = (char *)malloc(userNameLen);
  strcpy(userP>name buff); // log failed login attempt
  exit();
 }
}

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

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

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

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

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

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

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

DFD f d a b e ycT
DFE f d f f a a e y:N
DFF a b f f d da c fc f d +/yx
DF f d H`@PST
  倘若輸入了更多的字符以致密碼字符數組存儲空間無法容納一個攻擊者就可以溢出此緩沖區並以shellcode(可為任意的代碼)地址覆蓋掉返回地址出於演示的目的在此假定shellcode已被注入且定位於x為執行此代碼攻擊者只需把下列字符串作為密碼輸入

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

  代碼段

DFD
DFE a e @y:N
DFF a b f f d da c fc f d +/yx
DF f d H`@PST
  當checkpassword()函數返回時控制權就傳到shellcode而不是main()函數中的原始返回地址上

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



基於密探的緩沖區溢出保護

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

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

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

  例

   #include stdafxh
#include TestItDanh
#include <stdlibh>
#include <stdioh>
#include <windowsh>
#define MAX_LOADSTRING
struct user {
 wchar_t *name;
 size_t len;
 int uid;
};
HINSTANCE hInst;
TCHAR szTitle[MAX_LOADSTRING];
TCHAR szWindowClass[MAX_LOADSTRING];
TCHAR lpszUserName[] = Lguest;
TCHAR lpszPassword[] = Labcde;
struct user *userP = (struct user *)xcdcdcdcdcdcdcdcd;
size_t userNameLen = ;
size_t userPasswordLen = xffffffff;
int APIENTRY _tWinMain(HINSTANCE hInstance
HINSTANCE hPrevInstance
LPTSTR lpCmdLine
int nCmdShow) {
 UNREFERENCED_PARAMETER(hPrevInstance);
 UNREFERENCED_PARAMETER(lpCmdLine);
 MSG msg;
 HACCEL hAccelTable;
 LoadString(hInstance IDS_APP_TITLE szTitle MAX_LOADSTRING);
 LoadString(hInstance IDC_TESTITDAN szWindowClass MAX_LOADSTRING);
 MyRegisterClass(hInstance);
 userP = (struct user *)malloc(sizeof(user));
 if (!InitInstance (hInstance nCmdShow)) {
  return FALSE;
 }
 hAccelTable =LoadAccelerators(hInstance MAKEINTRESOURCE(IDC_TESTITDAN));
 while (GetMessage(&msg NULL )) {
  if (!TranslateAccelerator(msghwnd hAccelTable &msg)) {
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }
 }
 return (int) msgwParam;
}

INT_PTR CALLBACK GetPassword(HWND hDlg UINT message WPARAM wParam LPARAM lParam) {
 TCHAR lpszGuestPassword[] = LNCC;
 UNREFERENCED_PARAMETER(lParam);
 switch (message) {
  case WM_INITDIALOG:
   return (INT_PTR)TRUE;
  case WM_COMMAND:
   if (LOWORD(wParam) == IDOK) {
    EndDialog(hDlg LOWORD(wParam));
    SendDlgItemMessage(hDlg
    IDC_EDIT
    EM_GETLINE
    (WPARAM) // line
    (LPARAM) lpszPassword
    );
    userP>len = userNameLen;
    if (wcscmp(lpszPassword lpszGuestPassword) == ) {
     return true;
    }
    else {
     MessageBox(hDlg
      (LPCWSTR)LInvalid Password
      (LPCWSTR)LLogin Failed
      MB_OK
     );
    }
    return (INT_PTR)TRUE;
   }
   break;
  }
  return (INT_PTR)FALSE;
 }

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

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

   &lpszPassword = xC
&userP = xC
&userNameLen = x
&userPasswordLen = x

  userP的值為xDuserNameLen的值為xuserPasswordLen的值為xffffffff如果我們查看lpszPassword地址的起始處內存可以非常清楚地看到這些變量的初始值(見插

  代碼段

   C
C
C d ff ff ff ff a
C c

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

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

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

  代碼段

DEBC e d
DEBAC
DEBBC e df b bd ec d
DEBCC ec eb d a f
DEBDC b f ec d da c fc f d
  假定shellcode已被注入到程序中的x那麼接下來攻擊者可在Login對話框的密碼輸入欄中輸入以下字符串

\xebcc\xd\x\x
  在緩沖區溢出之後數據段的內存顯示見插

  代碼段

C
C
C cc eb d ff ff a
C c
  棕色的字節表示userP的值在何處被堆棧上的返回代碼地址所覆蓋(負綠色的字節表示userNameLen的值在何處被shellcode的地址所覆蓋行的內存任意寫入執行之後堆棧現在如插所示

  代碼段

DEBC e d
DEBAC
DEBBC e df b bd ec d
DEBCC ec eb d e
DEBDC b f ec d da c fc f d
  紅色表示的字節標出了堆棧上的返回值在何處被地址值所覆蓋在這並沒有修改堆棧上的其他任何字節(包括密探使得運行時的系統很難發現這次攻擊結果控制權在GetPassword()函數返回時傳到了shellcode中

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

  例

LRESULT Retval;
*((WORD *)(&lpszPassword)) = (sizeof(lpszPassword)/sizeof(TCHAR));
Retval = SendDlgItemMessage(hDlg IDC_EDIT EM_GETLINE
(WPARAM) // line
(LPARAM) lpszPassword
);
lpszPassword[Retval]=\;


From:http://tw.wingwit.com/Article/program/net/201311/12555.html
    推薦文章
    Copyright © 2005-2013 電腦知識網 Computer Knowledge   All rights reserved.