The /GS flag has been around since the release of Visual Studio 7.0. In the original approach, it was simply a randomly generated cookie placed between the return address and the local variables on the stack. If the EBP register was pushed onto the stack, the cookie would guard this as well. It would stop some of the simplest attacks against simple functions, and, although it was better than nothing, no developer should ever believe that they’re protected against attacks merely because of /GS. Let’s take a look at a sample application:
#define _CRT_SECURE_NO_DEPRECATE #include <stdio.h> #include <string.h> void VulnerableFunc( const char* input, char* out ) { char* pTmp; char buf[256]; strcpy( buf, "Prefix:" ); strcat( buf, input ); // Transform the input, and write it to the output buffer for( pTmp = buf; *pTmp != '\0'; pTmp++ ) { *out++ = *pTmp + 0x20; } } int main( int argc, char* argv[] ) { char buf2[256]; VulnerableFunc( argv[1], buf2 ); printf( "%s\n", buf2 ); return 0; }
Starting just after we enter main, here’s the commented disassembly (my comments above the instructions):
int main( int argc, char* argv[] ) { // Save the previous value of the frame pointer on the stack 00411300 push ebp // Put the current stack pointer into the ebp register 00411301 mov ebp,esp // Create room for buf2 00411303 sub esp,140h // Save three more registers on the stack 00411309 push ebx 0041130A push esi 0041130B push edi char buf2[256]; VulnerableFunc( argv[1], buf2 ); // Get the address of buf2, and put it on the stack 0041130C lea eax,[buf2] 00411312 push eax // Find the value of argv[1], also place it on the stack 00411313 mov ecx,dword ptr [argv] 00411316 mov edx,dword ptr [ecx+4] 00411319 push edx // Call VulnerableFunc, and on return, adjust the stack pointer 0041131A call VulnerableFunc (41102Dh) 0041131F add esp,8
Once inside VulnerableFunc, we have this:
void VulnerableFunc( const char* input, char* out ) { // Save the previous value of the frame pointer on the stack 00411260 push ebp // Put the current stack pointer into the ebp register 00411261 mov ebp,esp // Create room for buf and pTmp 00411263 sub esp,144h 00411269 push ebx 0041126A push esi 0041126B push edi char* pTmp; char buf[256];
In this example, all optimizations and stack checking are disabled so that you can see what typical applications used to do. In this application, the stack is shown by the diagram in Figure 3-2.
Figure 3-2: Stack diagram for a typical application.
Now let’s look at what happens, step by step, as buf overflows. The first value to get mangled is pTmp, but that won’t get us anywhere–it will just be initialized to zero and incremented as we process the buf character array. The next value to get overwritten is the previous frame pointer. This is interesting because on return from the second function (in this case, main), code execution will jump into the address located here. If we had the classic off-by-one attack, this is where the fun starts.
Next, things start to get really fun: we can now overwrite the return address and set the input and out values to anything we like. Setting input isn’t especially interesting–by the time the overrun has happened, we’ve already used that variable and won’t write it again. The out parameter is a real problem, because once that is overwritten the attacker can write nearly anywhere in application memory that they’d like once we fall into the for loop toward the end of VulnerableFunc.
In the original /GS implementation, a cookie was just placed between the local variables and the stored registers (if any) and the return address. This would leave pTmp open to attack, and although in this case, it is uninteresting, that may not always be true, especially if this were a function pointer. In the case of a limited overwrite, it isn’t possible to tamper with the previous frame pointer without detection, so /GS in its simplest incarnation would stop a simple off-by-one (or even off-by-a-few) overflow from being exploitable. In the case of an unchecked overrun, the attacker has the problem of having to fix up the cookie, but recall that overwriting the out pointer allows the attacker to put nearly any value he or she would like into any memory location (although in this example, a location with any 0 bytes would be a problem). Let’s take a look at what the most recent version of /GS will do with the same function:
void VulnerableFunc( const char* input, char* out ) { // Make room for the local variables 00401000 sub esp,104h // Copy the security cookie into eax 00401006 mov eax,dword ptr [___security_cookie (403000h)] // XOR the cookie with the stack pointer 0040100B xor eax,esp // Put the resulting cookie at the end of the buffer 0040100D mov dword ptr [esp+100h],eax char* pTmp; char buf[256]; strcpy( buf, "Prefix:" ); 00401014 mov ecx,dword ptr [string "Prefix:" (4020DCh)] // Now copy the function arguments into registers before anything can // tamper with them. 0040101A mov eax,dword ptr [esp+108h] 00401021 mov edx,dword ptr [esp+10Ch]
As you can see, this implementation is much safer. The arguments to the function are either copied into registers or are put on the stack above the buffer. Additionally, as we discuss in the next section, if an exception were thrown inside this function, the cookie would be checked prior to executing the exception handler.
Note | While you should always use /GS, you still need to write solid code! |