Search code examples
cstackheap-memorybuffer-overflow

How do you properly implement a robust stack in C


Prior Knowledge

I've read and understand a bit about stacks and data structures, but couldn't find the answer to this specific question. I know that any programmer worth anything will implement security exception handling beyond the default in their programs.

Situation

I would like to understand how a program could be written in C that establishes a relatively robust stack with exception handling for some arbitrary case S. My goal is to discern from this very specific information why (from what I understand) it is always possible to exploit the SEH in a program and execute arbitrary code.

The issue is not that I do not understand the concept of overflowing a buffer - I do not understand why (in very specific, CPU architecture related reasons) the security implemented on the stack (canaries, etc) cannot sufficiently address these issues (like heap overflows cannot be stopped?).

Reference Information

"There is no sane way to alter the layout of data within a structure; structures are expected to be the same between modules, especially with shared libraries. Any data in a structure after a buffer is impossible to protect with canaries; thus, programmers must be very careful about how they organize their variables and use their structures. In C and C++, structures with buffers should either be malloc()ed or obtained with new." - via http://en.wikipedia.org/wiki/Buffer_overflow_protection#Implementations

Also:

http://blogs.msdn.com/b/michael_howard/archive/2006/08/16/702707.aspx

If anyone knows a good resource for understanding this material or can provide a code snippet, I would be very grateful.


Solution

  • The security of the stack can be made secure enough to cope with stack overflows:

    http://msdn.microsoft.com/en-us/library/9a89h429(VS.80).aspx

    The question as to why you need registered exception handlers (safe-SEH) and why normal exception handlers won't cut it is because of the case where you get very large stack overflows.

    Let's suppose I have the function which begins

    try {
       char buffer[N];
       strcpy(&buffer, &attacker);
    } __except(...) { }
    

    This might translate into the assembly code

    push ebp
    mov ebp, esp
    ; GS if you want to here
    
    ; install the exception handler:
    push lbl_Exceptionhandler
    push dword ptr [fs:0]
    mov dword ptr[fs:0], esp
    
    ; setup the locals inside the stack
    sub esp, LOCALS
    ; GS if you want to here
    
    ; call strcpy
    lea ecx [ebp + offset_to_buffer];
    push ecx
    lea edx, [ebp + offset_to_attacker]
    push edx
    call _strcpy
    add esp, 8
    
    ; uninstall the locals
    mov esp, ebp
    
    ; uninstall the exception handler
    pop dword ptr [fs:0]
    
    ; return
    pop ebp
    
    ; optionally check GS cookies that we might have also inserted at any point in this function.
    call _checksecuritycookie
    ret 
    

    Or in other words, the stack looks like this:

    RET PTR 
    /GS1
    SAVED EBP
    /GS2
    SAVED FS:0
    /GS3
    LOCAL char buffer[N]
    

    GS1, GS2 and GS3 are locations where stack canaries might choose to write the stack cookie. Note that the cookie will only be checked at the end of the function (This is important in computer security. When you introduce a check you need to think not only whether the check will detect the overflow, but whether it will detect it before it is already too late; and that requires thinking where the check will take place. For stack cookies, the cookie is only inspected on function exits, because stack cookies are generally only there to protect the return address, not to protect local variables).

    The problem with normal exception handlers is what happens if the attacker buffer is really huge. Let's suppose it's so huge it destroys the entire stack, writes onto the guard page for the thread and triggers a fault?

    Well, the kernel calls back into ntdll and tells it to sort it's process out, and ntdll's first port of call is to see if there are any registered exception handlers. Now how does it find what exception handler to call? Well, it looks at fs:0, which points to the exception handler on the stack, and calls the exception handler pointer. Except that exception handler's on the stack that the attacker just destroyed.

    Oops. Now the attacker has control of EIP and you lose.

    Safe-SEH solves this problem by noting that the list of exception handlers that you might ever want to call is in fact a finite list entirely determined at compile time. By burning this list into the PE file itself, ntdll has a chance to double check that the exception handler that it should jump to is, in fact, a real exception handler and not the cause of some evil attacker's plot to take over your EIP.

    There's a cost to Safe-SEH (hence why it is opt in), but that cost is that it becomes more expensive to catch an exception, since ntdll will now do more work before your exception handler takes over.

    Despite this, my advice is that SafeSEH should always be on. Making it easier to lose your customer's credit card details because your app is critically performance dependent on the speed of throwing exceptions suggests a mentality so horrendously broken in the developers mind that they should be immediately put into a cannon and fired into the sun to avoid their awful code from damaging society.