Visual C++: Protecting Against Buffer Overruns with the /GS Switch

Wednesday Oct 6th 2004 by Nick Wienholt

Visual C++.NET supports the automatic detection of stack-based buffer overruns through the use of the /GS compiler switch. Learn why stack-based buffer overruns are so serious, and how /GS and other Visual C++ settings can combat them.

Buffer overruns represent one of the most common security vulnerabilities that exist in software today. By allowing a hacker to modify the execution flow of a process by providing malicious input, buffer overruns can allow an entire process, machine, or domain to be compromised. If the identity that the process is running under is a highly trusted account, such as an administrator or the Local System account, the damage that the hackers can cause is severe and potentially widespread. Some of the more famous virus outbreaks in recent times, such as the Code Red and Blaster worms, were the result of buffer overruns in C/C++ code.

Buffer overruns are a simple programming mistake—they involve copying the contents of a region of memory into another region of memory that is too small to accommodate the source block. The code below demonstrates a simple example:

char* source = "A reasonably long string";
char dest[10];
::strcpy(dest, source);

In this case, the source string is 25 characters long (including the null terminator) and will be too large for the destination block of memory, which is declared on the stack. When this code executes, the stack will become corrupted, and the program typically will crash with an access violation. A security vulnerability exists if the source block of memory is provided by an external party because this allows a block of memory that modifies the stack in a specific way to be passed into the function.

When a function is called in C/C++, the return address of the calling function is placed on the stack so that execution can return to this point once the callee has completed. By calling a function that contains a potential buffer overrun, the return address can be changed and execution will jump to the location nominated by the data in the buffer. By changing the return address of the function, an attacker can get code at an arbitrary location in the process to execute. This is commonly exploited in two main ways:

  1. If the application with the vulnerability is known and widely available, the attacker can look for the address of a function that will be located at a fixed address in all process instances and modify the stack so this function is called.
  2. The instructions to execute can be passed into the process address space as part of the buffer, and an attacker can exploit this to carry out an attack.

Defending Against Buffer Overruns

The simplest defense against a buffer overrun is limiting the size of the data copied so that it is not greater than the size of the destination buffer. While applying this defense seems trivial—and, in fact, it is in contrived situations like the earlier example—experience has shown that totally eliminating the potential for buffer overruns in large C/C++ code bases is an extremely difficult undertaking. Using managed technologies such as .NET and Java can significantly reduce the potential for buffer overruns, but moving large code bases to these technologies is often impossible or inappropriate.

The reason that stack-based buffer overruns are so easily exploitable is that the return address for a function is stored on the stack by instructions that the compiler generates. Recognizing that the compiler plays a small part in causing the problem, the Visual C++ team took the approach with the release of Visual C++.NET (7.0) that the compiler could play a part in alleviating the problem. They inserted a generated cookie with a known value in the stack below the data that held the return address of a function. By using this technique, a buffer overrun that changes the value of the function's return address will also overwrite the cookie, which can be detected when the function returns. When a modified cookie is detected, a security exception is raised, and if the exception is not handled, the process that is running the code will exit. The code below shows a skeleton program with a security exception handler present:

void _cdecl sec_handler( int code, void *)
   if ( code == _SECERR_BUFFER_OVERRUN )
      printf("You had a buffer overrun\n");

int main()
   _set_security_error_handler( sec_handler );
   //main application code here

Visual C++.NET 2003 (7.1) enhances the protection against buffer overruns by moving vulnerable data structures, such as the address of exception handlers, to a position in the call stack below the area where buffers are located. In the 7.0 release of the compiler, bypassing the protection offered by the security cookie could be accomplished by corrupting sensitive data between the buffers and the cookie. However, by moving this data to a region below the buffers in the new compiler version, modifying this data with buffer overruns is no longer possible.

Figure 1 shows the conceptual layouts of a stack in versions 6, 7.0, and 7.1 of the C++ compiler. The stacks are shown growing down from higher to lower address spaces, which is the way execution stacks grow when executing. Downward growth of the stack is the reason that buffer overruns work so well, because an overrun will write to memory addresses higher than that owned by the buffer, which is the location of the vulnerable data structures.

Figure 1: Logical Stack Layout

In addition to moving the exception handler information to a position in the stack below the data buffers, the linker in Visual C++.NET 2003 also emits the address of all structured exception handlers into the header of the executable file. When an exception occurs, the operating system can check that the address nominated in the exception information stored on the stack corresponds to an exception handler recorded in the executable's header information. If this is not the case, the exception handler will not execute. Windows Server 2003 shipped with the ability to check the structured exception information, and this technology has recently been back-ported to Windows XP in Service Pack 2.

Using Buffer Protection

Activating buffer protection is a simple matter of turning on the /GS compiler switch. Using Visual Studio, the switch can be activated from the Code Generation option page on the C/C++ tab (as shown in Figure 2). By default, the setting will be disabled for the Debug configuration and enabled for the Release configuration.

Figure 2: Setting the /GS Switch

Safe structured exception handling will be enabled by default if all the inputs to the linker, where compiled with the most recent version of the compiler and producing the structured exception information, makes sense for the target subsystem. The Windows CE operating system does not make use of the safe structured exception handling information, and it will not be included if this operating system is targeted. The /SAFESEH:NO command line switch can be used to disable safe structured exception handling. There is no Visual Studio project-setting item to disable safe structured exception handling, but it can be done for a Visual Studio project using the command line options for the linker.

/GS and the Wider Security Picture

Flipping a single compiler switch will not make an application secure. It can help make an application more secure, but security vulnerabilities come in many different forms. Overruns of stack-based buffers are an important class of security vulnerability, but they are far from the end of the story when it comes to the techniques that hackers will use to attack an application. Despite initially appearing relatively harmless, heap-based buffer overruns are now known to be an exploitable vulnerability, and other attacks like SQL injection, cross-site scripting, and Denial of Service can all happily succeed against an application that relies on /GS for security.

To use the formal terminology that has been adopted by Microsoft, /GS and SAFESEH are examples of software-enforced data execution prevention (DEP). Software-enforced DEP also can be complemented by hardware-enforced DEP, in which the processor will refuse to execute instructions if they are located within a memory page that has been marked as non-executable. Windows XP SP2 and Windows Server 2003 currently support these technologies, but few shipping processors have No Execute (NX) support. AMD and Intel have high-end offerings in the 64-bit world with NX technology, and future 32-bit processors may also ship with these security enhancements.

Any good security system has multiple layers of defense to defeat the bad guys. Compiler assistance, which can defeat or reduce the severity of common coding mistakes, is yet another layer that can be deployed in this ongoing battle, and given the ease and low cost of this defense, it is one worth deploying in your applications.

Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved