One of the oldest forms of memory safety exploitation is that of stack corruption vulnerabilities, with several early high-profile exploits being of this type. It seems fitting therefore to kick off this Software Defense series by looking at the status of software defense today with respect to this age-old problem.
Mitigating stack-based corruption vulnerabilities
The most common form of stack corruption – overrun of a buffer beyond the amount of stack space that was allocated for it – has been mitigated to some degree since Windows XP via the compiler-based /GS feature. A copy of a per-module random value, the /GS cookie, is placed between the stack local variables and stack metadata including the return address. Before using the return address, the program verifies the integrity of this local copy of the /GS cookie: if its value does not match the master per-process value then an overrun is assumed to have taken place and the program is terminated.
The limitations of this defensive device have driven a number of refinements over the years; the main ones are summarized in the following table:
Scope of GS protection
For performance reasons only a subset of functions are protected with a GS cookie. The scope of this protection was initially aimed primarily at character arrays, where attackers would supply malicious strings, often over the network that the program did not handle correctly. GS enhancements in Visual Studio 2010 extended the scope of GS protection to functions with a far more general range of data structures. Visual Studio 2013 builds on this further and also protects arrays of pointers.
Protecting a function with /GS has a codesize cost – the prolog code to set up the GS cookie on the stack and the epilog to verify its value - and the runtime cost of actually executing these extra instructions. The cost of extending the scope of GS protection has been partially offset by a new compiler optimization: if usage of the buffers that have led to the GS cookie can be proven safe by the optimizer – i.e. all writes to the memory associated with these variables is within the bounds of their allocated stack space – then the GS cookie is eliminated.
Enabling /GS still typically only inserts a GS check on less than 10% of functions – though clearly this varies considerably depending on usage of local variables in each individual application.
Evading the GS check through exception abuse
An important aspect touched on above deserves further discussion – the cookie check only occurs at the end of the function. This means that there is a window between the stack corruption and the GS check in which an attacker can seek to gain control. One favoured approach has been to trigger an exception and abuse the resulting exception handling process.
On x86, exception metadata associated with SEH is stored on the stack: this includes pointers to the handler code that should be invoked. If the attacker can use the initial stack corruption to modify this SEH metadata – replacing the function pointer with an address of his choosing – then when the exception dispatching code runs, it will transfer control to the attacker-controlled address.
Mitigating exception metadata abuse on x86 platforms
Again compile-time techniques can help - /SAFESEH effectively creates a whitelist of exception handler addresses. In the corrupted SEH metadata scenario above, the attacker’s modified ‘pointer to exception handler’ address is not on the whitelist thus defeating the simple form of this exploitation technique. It is however costly: all code needs to be recompiled to benefit – as any non-SAFESEH code in the program introduces a “code with unknown SAFESEH whitelist” module – which for application compatibility purposes means that any address inside that module is assumed to be on the whitelist.
Windows XP SP2 included a recompile of most OS binaries with /SAFESEH – but even that is not sufficient. Any 3rd party browser plugin (not compiled with /SAFESEH) provided a potential means to evade the /SAFESEH validation.
SEHOP, supported initially in Windows Vista SP1 (off-by-default) and Windows Server 2008 (on-by-default), provides a more robust solution. We previously described SEHOP in detail, but a summary of the basic idea is as follows. When an exception occurs, SEHOP walks the entire list of SEH metadata structures on the stack and verifies that it terminates at a special “known good handler address”. Any attacker corruption of one of these structures overwrites the forward pointer to the next SEH metadata structure and so breaks the integrity of the list, which is therefore detected by the SEHOP check.
Windows 7 added support for per-process opt-in to SEHOP.
SEHOP in Windows 8 and Windows 8.1
Windows 8 raises the bar in that SEHOP is enabled by default for applications that built to run on Windows 8 and above (more precisely for any application built with subsystem version greater or equal to 6.2 – see /SUBSYSTEM for details).
In Windows 8.1 SEHOP is further improved in a couple of ways.
First SEHOP now has a range of 64 possible distinguished FinalExceptionHandler values that may be chosen, instead of just the one handler address within the system DLL that was used previously. The actual distinguished FinalExceptionHandler value used to validate the exception handling frame chain is selected at random during process startup, and differs on a per-process basis. The advantage of this is that SEHOP no longer has a system-wide shared secret, but a per-process secret, such that a local attacker can no longer assume that they know the distinguished value required to pass the frame validation check in a separate process on the local machine. All of the possible FinalExceptionHandler values are also valid SafeSEH handlers.
Secondly, Windows 8.1 adds support for SEHOP in kernel mode and enables this by default. Like the enhanced version of SEHOP for user mode, kernel mode SEHOP has 64 possible FinalExceptionHandler addresses, so just disclosing the kernel base address is not enough to defeat kernel SEHOP; one has to be able to read arbitrary kernel memory to do that.
GS limitations and future work
The effectiveness of the GS design described thus far – even if it were applied to every stack frame and one assumed that the cookie check were always reached after stack memory corruption occurred – is limited to cases where the cookie value is altered. This is what is detected. Important classes of stack-based vulnerability therefore remain that are not mitigated today for example targeted attacker-controlled writes to a stack address, or stack underruns – i.e. the direction of the writes goes towards the start address of the allocation.
Visual Studio 2012 updates /GS to emit range checks to mitigate one of the most common types of targeted write scenario that we were seeing through MSRC.
Recently published trend data shows that the number of successful exploits of stack-based vulnerabilities represents but a handful of the set of issues faced by our customers.
The obstacles posed by GS, SafeSEH and SEHOP and opt-in to these measures by increasing numbers of developers and IT professionals are likely part of the reason behind this.
Investment in static analysis tooling has also likely played a part, some of which is available to developers through compiler warnings such as C4789 and CodeAnalysis warnings such as C6200. The lifetime of a stack-based buffer tends to be shorter than its heap counterpart: it is limited to execution of one function – albeit potentially with many callees – making it more tractable to completely analyse its use across the entire program. By contrast pointers to heap allocations are frequently stored in multiple objects with a more complex usage pattern by the program, making it harder for analysis to track usage of a heap buffer with the same level of precision.
Although some unmitigated stack-based vulnerability classes do occasionally arise in practice – for example MS08-067 which was an underrun of a stack-based array, these are relatively isolated examples. As attacker focus has shifted to the heap, so the priority accorded to improving defensive measures there has increased.
Exploitation of stack-based corruption vulnerabilities is one of the oldest forms of memory safety exploit. History shows a succession of mitigation refinements developed to counter to attacker innovations in this area. We note a series of evolutions:
- Mitigation robustness and completeness has improved over time, including the protection of arrays of pointers by /GS in Visual Studio 2013 and a move to a per-process rather than a system-wide secret for SEHOP in Windows 8.1.
- Counter-measures to thwart exception handler abuse have evolved from expensive (from an engineering perspective) measures such as /SAFESEH that requires recompilation to OS-based SEHOP that can be applied on a per-process basis to existing applications.
- Default settings have evolved over time; e.g. user-mode SEHOP is now on-by-default for applications designed for Windows 8 and Windows 8.1, and kernel mode SEHOP is both new and enabled in Windows 8.1.
A state of relative maturity has been reached with fewer stack-based vulnerabilities being reported or exploited. Does this mean we are “done”? – by no means! Rather attacker attention appears to have turned to other less mature areas. And as attacker focus has shifted, so has defense. The next article takes a closer look at some of the advances related to memory corruption on the heap.
Tim Burrell, MSEC Security Science