Secure Coding in C++/CLI

C++/CLI is an extension to C++ that adds properties, events, garbage collection, and generics for all types including existing Standard C++ classes.


May 16, 2006
URL:http://www.drdobbs.com/security/secure-coding-in-ccli/security/secure-coding-in-ccli/187203693

Robert C. Seacord is Senior Vulnerability Analyst for CERT/CC. He can be reached at [email protected].


Visual C++ 2005 expands support for developing components and applications that run on a virtual machine with garbage collection using C++/CLI (the Common Language Infrastructure). C++/CLI is an extension of the C++ programming language that adds features such as properties, events, garbage collection, and generics for all types including existing Standard C++ classes.

Visual C++ 2005 supports the .NET Framework Common Language Runtime (CLR), Microsoft's implementation of a garbage-collected virtual machine. The C++ syntax supported in Visual C++ 2005 for .NET programming has evolved significantly from the managed extensions for C++ introduced in Microsoft Visual C++ .NET 2003. Managed extensions for C++ are still supported but have been deprecated in favor of the new syntax. Visual C++ 2005 also adds new features for native programming, including targeting 64-bit processor architectures and new library functions with improved security,

In this article, I examine C++ programs that are minimally ported to this new environment using managed extensions. This involves porting an existing legacy system to this environment [1] with minimal effort (a common approach by understaffed and overworked software maintenance teams). The purpose is to determine whether these programs are still susceptible to the buffer overflow vulnerabilities that have plagued C and C++ programs [2].

Listing One prompts users for a username and password. Regardless of the username, the program only accepts "NCC-1701" as a valid password. If the user fails to enter the proper password, the program exits. (This program is intended as a test for vulnerabilities in C++/CLI code and not as an exemplar of how to handle passwords.)

 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. }
Listing One

Execution starts on line 21 in main(). The program prompts for a username on lines 25 and 26 using a combination of puts() and gets(), resulting in an unbounded string copy from standard input into the buff character array declared on line 4. This is one of two locations in the program where a buffer overflow vulnerability can occur. The checkpassword() function is called from line 27 of main(). The checkpassword() function prompts users for a password on lines 12 and 13, also using a combination of puts()/gets(). This second call to gets() can also result in a buffer overflow in the password character array defined on the stack.

The program is compiled using Microsoft Visual C++ 2005 with the buffer security check option disabled (/GS-) and managed extensions enabled (/clr). The buffer security check is enabled by default, and it is generally a bad idea to disable it (as this example demonstrates). The /clr setting allows mixed assemblies consisting of managed and unmanaged code.

There are several warnings during the build process that are ignored; for example, "warning C4996: 'gets' was declared deprecated" and "warning C4996: 'strcpy' was declared deprecated." The compiler recommends using gets_s() instead of gets() and strcpy_s() instead of strcpy(). If these replacement functions are used properly, the possibility of buffer overflow would be eliminated [3]. However, these are only warnings and can be ignored (or even disabled). Ignoring these warnings is consistent with the scenario of minimally porting an existing legacy system.

When using managed extensions, the compiler generates Microsoft Intermediate Language (MSIL) (renamed the Common Intermediate Language or CIL as part of the standardization process) for main() and for the checkpassword() functions. The CIL bytecodes are packaged into an executable that invokes the just-in-time compiler (JIT) to translate these bytecodes into native assembly instructions and then transfers control to main().

When executed, the program first prompts for a username:

Enter user name:
rcs

The program prompts users for a password, which is read into the password variable declared on line 11 as an automatic array of 10 characters. In Example 1, if you examine the data starting at the address of the array on the stack (0x002DF3D4, in this example) before the password is read from standard input, you can see the storage allocated for password (shown in bold) and the return address on the stack (shown in red). The return address is Little Endian.

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].

Example 1: The data starting at the address of the array on the stack.

By providing more characters than can fit in the storage allocated for the password character array, an attacker can overflow the buffer and overwrite the return address with the address of shellcode (arbitrary code inserted into memory by the attacker). For the purpose of this example, we'll assume this code has already been injected in a separate operation (that does not need to involve a buffer overflow or other vulnerability) and is located at 0x00408130. To execute arbitrary code, the attacker need only enter the following string as the password:


Enter 8 character password:
123456789012345678900|@

This input string is copied into the password character array, overflowing the buffer and overwriting memory up to and including the return address. The three characters in the 0|@ string overwrite the first three bytes of the return address. The last byte of the return address is overwritten by a null-termination character as a result of the gets() function. Notice that if the null byte were located anywhere but in the last byte, it would be impossible to copy the entire string because the gets() function interprets the null character as the end of the string. So why these three characters? The hexadecimal representation of these characters provides the values required to represent the address in memory. The ASCII hexadecimal code for "0" is 0x30, "|" is 0x81, and "@" is 0x40. By concatenating these three characters in the order { '0', '|', '@' }, you can write the Little Endian representation of the address of the shellcode (0x00408130) into memory. Again, the final null byte is supplied by the null termination byte for the string (see Example 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].

Example 2: The final null byte is supplied by the null termination byte for the string.

When the checkpassword() function returns, control is passed to the shellcode instead of returning to the original return address in the main() function.

To simplify the exploit, buffer security checks using the /GS option were disabled. If this option had not been disabled, the compiler would have inserted a canary after any array (buffer) declared on the stack; see Figure 1.

Figure 1: Canary-based buffer overflow protection.

To overwrite the return address (EIP), base pointer (EBP), or other values in a stack frame protected by a canary using an unbounded string copy operation such as gets() or strcpy(), an attacker would need to first overwrite the canary. If the canary is modified, an error is generated when the function returns and the attack will fail (unless the goal of the attack is a denial of service). It is possible to defeat the canary by brute force, predicting the value, or other means. However, the difficulty of launching a successful exploit increases.

Enabling the /GS option does not make programs completely immune to buffer overflow vulnerabilities. Buffer overflows in the stack can still crash the program, and the possibility still exists for attackers to exploit a stack-based overflow to execute arbitrary code even with the /GS flag enabled. More importantly, the /GS flag cannot detect buffer overflows in either the heap or data segments.

To illustrate, Listing Two presents the previous example program rewritten to use the Win32 GUI. This program provides a menu bar with some simple options, including a File menu with two menu options: "Login" and "Exit." The Login command prompts the user for a password using a dialog window. Once the password is entered, the user presses the "OK" button and the password is checked against the recorded password.

 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.  }
Listing Two

The program is compiled and tested in the same environment except that the Unicode character set is specified and the buffer security check option (/GS) is enabled. We continue to use managed extensions (that is, Common Language Runtime support). The program builds cleanly without error.

This is a relatively simple program, although it is somewhat longer because of the code required to support the Windows GUI. There are several variables of interest from lines 17-20. The lpszPassword variable is an initialized static variable consisting of 16 wide characters (32 bytes). Following this variable is the userP pointer and two unsigned integers: userNameLen and userPasswordLen. After userP is initialized on line 33, these variables have the following addresses:

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

userP is 0x00554D30. userNameLen is 0x00000010. userPasswordLen is 0xffffffff. If we examine memory starting at the address of lpszPassword, we can clearly see the initial values for these variables (Example 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

Example 3: Memory starting at the address of lpszPassword.

The vulnerability in this program is in the call to SendDlgItemMessage on lines 118-123. The EM_GETLINE message specifies that a line of text from the edit control IDC_EDIT1 (the text edit box in the Login dialog) is copied to the fixed-length buffer lpszPassword. This buffer has sufficient space for 15 Unicode characters and a trailing null word. If more than 15 characters are input, a buffer overflow occurs. If 20 characters are entered, the 17th and 18th characters overwrite userP. The 19th and 20th characters overwrite userNameLen, and the trailing null word overwrites userPasswordLen.

Assuming that both userP and userNameLen are overwritten, an arbitrary memory write occurs on line 124 when userNameLen is assigned to the address stored in userP + 4 (the offset of len within struct user). By overwriting an address to which control is eventually transferred, attackers can take advantage of an arbitrary memory write to transfer control to arbitrary code. In this example, the return address on the stack is overwritten. Because the lpszGuestPassword variable is declared as an automatic variable in the GetPassword function, you can examine memory starting from the address of this variable.

Assuming that lpszGuestPassword is located at 0x002DEB9C, you can examine the contents of the stack by examining memory at this location. Through debugging the example program or by trial and error, it is possible to determine that the return code of 0x004f3a99 is located at the address 0x002DEBD0 in the stack (see Example 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

Example 4: Examining memory from 0x002DEB9C.

Assuming that the shellcode has been injected into the program at 0x00409028, an attacker can enter this string at the password prompt in the Login dialog box:

"1234567812345678\xebcc\
                x002d\x9028\x0040"

Methods for reading hex codes as input for Unicode characters can be found at www.fileformat.info/tip/ microsoft/enter_unicode.htm. The contents of memory in the data segment after the buffer overflow is shown in Example 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

Example 5: The contents of memory in the data segment after the buffer overflow.

The brown bytes show where the value of userP has been overwritten with the address of the return code on the stack (minus four), and the green bytes show where the value of userNameLen has been overwritten with the address of the shellcode. After the arbitrary write on line 124 is executed, the stack now appears as in Example 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

Example 6: After the arbitrary write on line 124 is executed.

The bytes shown in red illustrate where the return code on the stack has been overwritten with the value of the address code. No other bytes on the stack (including the canary) are modified, making this attack undetectable by the runtime system. As a result, control is passed to the shellcode when the GetPassword() function returns.

This second case is interesting for a variety of reasons. First, it demonstrates that the return address on the stack can still be overwritten—even with buffer security checks (/GS flag) enabled, as these checks only mitigate overflows for automatic buffers declared on the stack. Second, it shows that a program can compile cleanly without warning in the Visual Studio 2005 environment and still be vulnerable. Listing Three eliminates the buffer overflow. Before sending the message, the first word of lpszPassword must be set to the size, in TCHARs, of the buffer. For Unicode text, this is the number of characters. The size in the first word is overwritten by the copied line. Also, for edit controls the copied line does not contain a terminating null character. The return value (the number of TCHARs copied) must be used to null-terminate the string.

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';
Listing Three

Acknowledgments

I would like to acknowledge Dan Plakosh and Hal Burch and Andrew M. for their help in developing the programming examples and Tim Shimeall, Louis Lafreniere, and Pamela Curtis for reviewing the article.

References

  1. [1] Seacord, Robert C., Daniel Plakosh, and Grace A. Lewis. Modernizing Legacy Systems: Software Technologies, Engineering Processes, and Business Practices. Addison-Wesley, February 2003.
  2. [2] Seacord, Robert C. Secure Coding in C and C++. Addison-Wesley, 2005 (ISBN 0321335724).
  3. [3] Meyers, Randy. "Specification for Safer, More Secure C Library Functions," ISO/IEC TR 24731, June 6, 2004.

All examples presented here were compiled using Microsoft Visual Studio 2005 Version 8.0 and the Microsoft .NET Framework Version 2.0 and tested on an Intel Xeon machine running Microsoft Windows XP Professional x64 Edition Version 2003, Service Pack 1.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.