缓冲区溢出照旧问题吗?C++/CLI安详编码
副标题#e#
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()的第二次挪用也会导致一个界说在仓库上的暗码字符数组缓冲区溢出。
#p#副标题#e#
措施利用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]
#p#分页标题#e#
倘若输入了更多的字符,乃至暗码字符数组存储空间无法容纳,一个进攻者就可以溢出此缓冲区,并以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)。
#p#分页标题#e#
这是一个很是简朴的措施,尽量为了支持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:
#p#分页标题#e#
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';