Stack canaries or security cookies are tell-tale values added to binaries during compilation to protect critical stack values like the Return Pointer against buffer overflow attacks. If an incorrect canary is detected during certain stages of the execution flow, such as right before a return (RET), the program will be terminated. Their presence makes exploitation of such vulnerabilities more difficult. But not impossible.
In this blog post, we will be discussing:
- What stack canaries look like
- What kinds of stack canaries can be found
- When compilers add them
- How they can be circumvented
We will be looking at 32 and 64 bit binaries, assembly (though no fluency is expected), /GS. For this article, we will be using a simple C program on a 32 bit Linux system.
Prelim – buffer overflows
Before we discuss stack canaries, we must first introduce buffer overflows. This class of attacks makes use of unsafe functions (usually in C or C++) that allow writing of arbitrary content outside a designated area of memory.
Consider the following snippets of code. The functionality of the program is not important, we are mainly interested in the execution flow in memory.# |
C |
Assembly |
1 2 3 4 5 6 7 8 9 |
int main(void) { askUser(); <rest of main function> } void askUser(void) { char name[100]; gets(name); … } |
<function prologue main> 0x0804850c call 0x0804851f <askUser> 0x08048511 <rest of main function> … 0x0804851f <function prologue askUser> 0x08048524 <allocate room for name[100]> … 0x0804859b call 0x080483a0 <gets@plt> |
Table 1. Example program
This is a simple C program that has a main() function and an askUser() function. The main() function calls askUser(), which in turn has a local variable called name[] of size 100 into which a user input is being read through gets().
When a function is called in a compiled binary (see line 2), the address of the next instruction inside main() will first be pushed onto the stack. This will serve as the Return Pointer. When the return (RET) instruction is called at the end of askUser(), the return pointer will be popped off the stack and placed into the instruction pointer (EIP in 32 bit architecture). If an attacker can overwrite this Return Pointer, they can redirect the execution flow of the program, often to a location the attacker desires.
Such overwrites are possible when a library function called inside askUser() does not perform correct bounds checking, often in string operations. Functions such as gets() and strcpy() do not perform any bounds checking during their operation. The read() function takes a size as argument, but does not check if this size corresponds to the size of the buffer where the data is written to.
If a user inputs more characters than the buffer can contain (in this case 100), the gets() function will keep on writing outside name[]’s memory space. Figure 1 and 2 display this in the Gnu Debugger (GDB). For the purposes of this article, we will not discuss the Saved Frame Pointer (SFP), which serves to restore the Base Pointer (EBP) to the calling function’s stack frame. Overwriting only the Saved Frame Pointer can also lead to an exploitable condition. In the example of Figure 1 and 2, a buffer of 28 bytes is foreseen for the second input from the user. As this input is read through the vulnerable gets() function, a large input can overwrite the Return Pointer on the stack.
The double arrows in Figure 2 indicate the position of the Return Pointer on the stack. Before the overwrite (green arrows), it is pointing into the main() function just after the call to askUser(). The backtrace command (bt) shows the call chain pointing into an address in main().
After the overwrite, the Return Pointer was overwritten with our arbitrary B’s (ASCII hex value 42, as shown on Figure 2). The call chain shows 42’s as the return pointer. This overwrite is possible because of a call to the gets() function, which does not perform bounds checking on the input from the user.
Stack canaries - intro
One way to prevent the stack-based buffer overflow above from being successful, is introducing a stack canary just before the SFP and the RP. This token value will be added by the compiler and serve as a warning that the SFP and RP may be overwritten. Figure 3 displays this schematically.
In the debugger, this would look like Figure 4. The orange boxes indicates the canary, the yellow boxes show the Return Pointer. The canary will change at every run of the program, making for a difficult to predict value. The NULL byte at the beginning of the canary (Intel processors use little endian, so the last byte shown will be the first written) would present a problem for many string operations to print.
Canaries in 64 bit programs follow the same principles, but will use the extra 32 bits for entropy. In Figure 5, we see the canary in orange and the Return Pointer in yellow.
Stack canaries – their check
Stack canaries will be checked for their value just before the return to the calling function, which is the moment at which the attacker will gain control over the instruction pointer as their overwritten value for the return pointer is loaded into the instruction pointer.
Programs compiled with canaries will look different inside debugger, with added instructions just before the function epilogue and the subsequent return.
# |
Assembly – no canary |
Assembly – with canary |
1 2 3 4 5 6 7 8 |
<inside askUser function> 0x0804856e leave 0x0804856f ret |
<inside askUser function> 0x080485d9 mov eax, <canary> 0x080485dc xor eax, <right canary val> 0x080485e3 je 0x080485ea 0x080485e5 call <__stack_chk_fail@plt> 0x080485ea leave 0x080485eb ret |
Table 2. Program assembly without and with canary introduced.
As we can see in Table 2, the compiled version of the askUser function contains an extra check in lines 6 and 7. This check will copy the current canary value from the stack to the EAX register (line 2), XOR it against the stored correct value (line 3), and jump to the function epilogue if the values are equal (line 4). If the check fails, the stack_chk_fail function will be called (line 5). This will terminate the program, without the attacker ever gaining control of the Return Pointer and subsequently the Instruction Pointer.
Stack canaries – types
There are different types of stack canaries that a compiler can add to a program. Table 3 contains an overview of the most common types and how they offer protection.
Type |
Example |
Protection |
Null canary |
0x00000000 |
0x00 |
Terminator canary |
0x00000aff |
0x00, 0x0a, 0xff |
Random canary |
<any 4-byte value> |
Usually starts with 0x00 |
Random XOR canary |
Usually starts with 0x00 |
|
64-bit canary |
<8 bytes> |
|
Custom canary |
Table 3. Types of canaries.
Many buffer overflow vulnerabilities are caused by string operations such as gets(), strcpy(), read(). Strings in C are commonly terminated using a single NULL byte (0x00). An attacker would not be able to use such a byte in their payload through a string operation to reconstruct the canary. The 0x0a byte represents a line feed, commonly also terminating string operations. 0xff corresponds to an End Of File (EOF), terminating certain string operations as well.
The null canary would be the simplest for the compiler programmer to implement. It places 4 NULL bytes just before the SFP and RP. As this is a predictable value, an attacker may still be able to bypass the canary. The read() function, which is vulnerable to buffer overflows, does allow NULL bytes to be written.
The terminator canary introduces two more hex values that attempt to terminate string operations, 0x0a and 0xff. These values are again predictable, and can be bypassed with relative ease under the right conditions.
A random canary will offer better protection. It usually consists of a NULL byte followed by 3 random bytes. The NULL byte would attempt to terminate string operations, while the 3 random bytes will make the canary less predictable to the attacker.
The random XOR canary will be like the random canary, except it will be XOR’ed against a non-static value in the program (usually the Base Pointer EBP). As operating systems nowadays run with Address Space Layout Randomization (ASLR) activated, EBP will not be static across runs of the program. This adds an extra layer of randomization to the cookie, making it hard to predict this value.
Application of stack canaries
The Linux C compiler gcc currently contains the Stack Smashing protector, which will introduce a random canary if /dev/urandom is available. In the absence of that source of random data, it will revert to a terminator canary. gcc only introduces canaries in specific cases. Functions with buffers over 8 bytes and calls to alloca(), the function that allows for allocation of memory space on the stack, will be protected by a canary. The programmer can introduce canaries for all functions with the –fstack-protector-all compiler flag, but this will likely hinder performance of the program.
The cookie value is generated at the start of the process and will remain the same during the running of the program. Any forks of the process will contain the same cookie value. This offers some bypass opportunities, as we will discuss later.
Microsoft Visual Studio has the /GS flag that introduces canaries – named Security Cookies by Microsoft[1]- to vulnerable functions. This option became available as of Visual Studio 2003, and is switched on by default since Visual Studio 2005. It will add a cookie for buffers over 4 bytes and that are not a pointer, and structures over 8 bytes that do not contain a pointer.
[1] https://docs.microsoft.com/en-...
Stack canary bypasses
There are multiple ways in which stack canaries can be bypassed. The first technique involves leaking out the cookie value through a memory leak vulnerability. Format string vulnerabilities are excellent for this purpose. This can work against all types of canaries, with the possible exception of the Random XOR type. Furthermore, as the standard random canary added by the Gnu C compiler gcc does not XOR against EBP, it remains static during the execution of the program. See Figure 6. The format string payload is in the yellow box, the canary in the orange box.
If the canary is of the null or terminator kind, it may not be effective in preventing certain writes. As discussed earlier, the read() function will allow us to write null bytes to a buffer, effectively disabling the security the null bytes should add. Furthermore, if multiple buffers can be written sequentially, the attacker may take use of null termination of strings in C to write the required NULL bytes into the canary position.
In some occasions, brute forcing the canary may be successful. When using a random canary on a 32 bit system, the canary will have 24 bits of entropy. This is due to the 8 bits (1 byte) being used for the NULL byte. 2^24 possibilities of randomisation makes 16.777.216 possible canary values. In a local privilege escalation exploit, 16 million guesses could well be within the bounds of a brute force attack.
On 64 bit systems, that entropy increases to 2^56 or 7.20 * 10^16 possibilities. This would be less feasible.
However, our guessing can be steered a bit. If we guess the canary byte by byte, we will be able to discern when we have guessed the right value. This is possible because an incorrect guess for a byte will generate a stack smashing error, where a correct byte guess will yield no such error. The maximum number (worst case) of guesses will remain 2^24, but the average number will decrease.
The canary can also be avoided if the attacker can overwrite a buffer and then cause an exception to occur within the same function. On Windows systems, this would trigger the structured exception handler (SEH) to intervene to solve the exception. If the SEH pointer is also overwritten, this can also lead to control over the instruction pointer. The SEH routine will not check canary values before handing execution to the handler.
Protection against stack canary bypasses
There is no silver bullet solution to protecting against stack canary bypasses. If a program has a memory leak vulnerability, the attacker could leak out the cookie value. Only the random XOR’ed canary would offer protection against this. The Furthermore, if an attacker can trigger an exception before the cookie is checked, code execution can still be obtained. The writeability of certain bytes (NULL, LF, EOF) will depend on the vulnerable function.
Before we move into the most significant protection against stack canary bypasses, it should be considered whether a program needs to be written in a language that expects the programmer to do memory management. For some applications, this will certainly be the case. C and C++ offer higher computing performance compared to higher level languages. However, for some applications, these conditions are not fulfilled. Using languages that take care of memory management will greatly decrease the likelihood of a canary being bypassed.
By far the greatest protection against canary bypassing lies in the very reason of the canary’s existence. Stack canaries were invented to prevent buffer overflow (BOF) vulnerabilities from being exploited. This BOF is the root problem that needs to be addressed. To make a small analogy, an umbrella can protect against getting wet, but simply not walking in the rain will do that much more efficiently.
Buffer overflow vulnerabilities occur when no bounds checking is being done on buffer operations. Functions such as gets() and strcpy() do no such bounds checking. This is why they have been deprecated and replaced by fgets() and strlcpy(). These functions conduct correct bounds checking and, if correctly used, remove the buffer overflow vulnerability. Googling will effortlessly yield a list of insecure C functions and their safer variants.
When inspecting import address tables of Windows executables or Procedure Linkage Tables on Linux executables, one can easily find many programs still importing these unsafe functions rather than their replacement variants. While there may be good technical reasons to use these unsafe variants rather than their replacement variant, the ubiquity of these unsafe functions raises the question of necessity.
This issue is not unlike the continuing need to repeat that patching systems is vital, yet still many environments remain insufficiently patched. The use of unsafe functions has been deprecated a long time ago, and this advice should definitely be adhered to.
Michiel Lemmens
Standing on the shoulders of giants:
http://www.s3.eurecom.fr/docs/ifip18_bierbaumer.pdf
https://www.emb-team.com/stack-canary-and-aslr-bypassing-on-x86_32/
https://docs.microsoft.com/en-us/cpp/build/reference/gs-buffer-security-check?view=msvc-160#gs-buffers
https://bananamafia.dev/post/binary-canary-bruteforce/
https://inst.eecs.berkeley.edu/~cs161/fa08/papers/stack_smashing.pdf
Erickson, J (2008). Hacking, The Art of Exploitation 2nd edition. No Starch Press.