Stack Canaries

Two protections that use a secret unpredictable value to reduce exploitability in memory corruption. Learn how to bypass them in certain scenarios

Stack Canaries

Stack buffer overflows where you overwrite the return pointer were such a big problem, that a mitigation called "Stack Canaries" was invented.

A stack canary is a reference to canaries in a coal mine. When a canary got sick, the miners would know it is unsafe here and stop mining. For a binary, it is the same idea: Right before the return address on the stack, a random value is placed. Then before actually returning at the ret (return) instruction, it checks if this random value is still the same. If it was overwritten by a buffer overflow, the value would change, and then the program would notice and exit before anything malicious can happen.

In more technical detail, every function has a prologue (start) and an epilogue (end). At the start, it does some common stuff like saving the rbp register, and making space on the stack by moving the rsp register. After that setup, if canaries are enabled, a random value from the kernel is taken and also put on the stack. That means in total, the process of calling a function will put 3 things on the stack:

  1. Stack Canary - eg. 0xba3173dcf7fb7c00

  2. Base Pointer (rbp) - eg. 0x7fffffffd620

  3. Return Address - eg. 0x5555555552bf

If we are overflowing the stack entire stack, we will first hit the Stack Canary, then the Base Pointer, and lastly the Return Address. This means that if we want to overwrite the return address we would also have to overwrite the canary with something.

Where this matters is at the end of the function, right before it calls ret. Here the program checks if the canary is still intact by comparing it with the kernel-provided value again. It does so by xor'ing the saved canary from the stack, with the actual value. If they match fully, the result should be 0 and it will successfully return. If it is not zero, however, the builtin __stack_chk_fail() function which immediately exits the program with a message

*** stack smashing detected ***: terminated
Aborted (core dumped)

The reason this works is that when overflowing everything up until the return point, you also have to overwrite the canary with something. As the attacker you don't know the value beforehand so you cannot write the correct value in this place again, meaning the stack check will fail and exit the program before reaching your ret to execute your exploit.

Tip: To at any point view the canary of the current process in GDB GEF, simply use the canary command:

gef➤  canary
The canary of process 3139 is at 0x7ffff7dc4768, value is 0x3f4d8b4481a2c200

The ideas and examples explained here were taken from the pwn.college - Stack Canaries video. Give their whole site a look if you're interested in learning practical and advanced Binary Exploitation while you're at it.

Leaking the Canary

Now that you understand how the canary is supposed to protect against stack overflow, let's learn how to break it. Note however that a stack canary is a decently strong protection that often requires a whole another vulnerability in the program that is able to leak it.

Format String (printf())

It all comes down to reading the canary through various methods. One simple way is using another arbitrary read vulnerability like a Format String exploit using printf(). If you have control over the first argument in this function, you can provide an arbitrary format string that prints more values than are provided in the arguments after. This causes more variables to be read from the stack and will eventually leak everything on there.

In this case, try providing an input like %p %p %p %p %p %p %p %p %p %p %p %p... to see a list of many hex values on the stack, for example:

0x7fff4f6f5b40 0xc8 0x7f5ea553b0ed (nil) 0x7f5ea56456a0 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0xa (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) (nil) 0xee5bc5192e9ec700 0x7fff4f6f5c20 0x55e60b36c2b8

Here we see all the values on the stack, we just have to find which one is the canary. Luckily, they are pretty recognizable. Look for 7 random bytes ending in a null byte. In this case, 0xee5bc5192e9ec700 is definitely the stack canary because it looks very random, with a null-byte at the end (note that the null-byte is not actually at the end, but rather stored at the start because of the little-endian representation).

If you can exploit this vulnerability to read the canary first, and then dynamically prepare your stack overflow, you should carefully overwrite the location of this canary with your leaked value, and then also overwrite the return address with whatever your exploit needs. This makes the stack check at the end pass because it only notices a changed canary, not a rewritten one with the correct value.

Null-terminated Strings

Strings are very simple structures in C. No length is saved anywhere, it is stored completely raw in memory. The only way it knows where the string ends is by the trailing null byte (\x00). Printing any string in C will keep reading and printing that string until it reaches a null byte, and considers that the end of the string. This often works nicely when user input is put into a buffer and then a null byte is added to the end to delimit it. In some cases, however, this trailing null byte can be removed meaning it does not know where the string ends and just keeps reading.

One example is when you have a vulnerability that you overwrite a buffer that gets printed later. If it isn't null-terminated, you can write characters right up until the value on the stack you want to leak. If the string is then ever printed, it won't find a null byte at the end of your input and will keep reading, also including that secret value on the stack you place your input in front of. Let's look at an example:

# Imagine the "13 37" bytes are the secret data we want to leak

# If we store "AAAA" before it, it might be terminated by leftover null bytes:
41 41 41 41 00 00 00 00 13 37
-> "AAAA"
# If we instead write data that overwrites all null bytes up until the secret:
41 41 41 41 41 41 41 41 13 37
-> "AAAAAAAA\x13\x37"

Here you can see how we were able to leak the secret bytes in memory. This idea works the same for stack canaries, we just have to overwrite all null bytes that come before it to make sure it is printed. One caveat is that the canary itself starts with a null byte for precisely this exploit idea. The creators gave up a whole byte of randomness to protect against this trick!

Luckily, however, not all hope is lost. Of course, we can also overwrite its null byte and just assume it was a null byte when we recreate it from the leak. This way the 7 random bytes are still printed, and we have leaked the canary. One slight problem with this is that while doing we are overwriting the canary, meaning the program would exit when this is checked at the end of a function. If we however can perform the leak and the stack overflow without returning we would exploit it just in time so the program never checks the canary.

In practice, the PwnTools code for doing this could look like this:

# Overwrite until and including null byte
payload = b"A"*(size_until_canary+1)
p.sendline(payload)

p.recvuntil(b"You said: ")
p.recvuntil(payload)  # Receive the payload itself, we don't care about it
# The next coming bytes are the canary leak

# Receive them and add the null byte back
canary = u64(b"\x00" + p.recv(7))
success(f"Canary leak: {hex(canary)}")

payload = flat({
    # Rewrite the canary at the right place, now that we know it
    size_until_canary: canary,
    # 16 bytes after, comes regular the return address
    size_until_canary+16: WIN_FUNCTION,
})

Tip: Leaks like these come in all shapes and sizes. If you can find any way to read more than you are supposed to, or read at an unexpected location, try reading and leaking the canary with it to bypass this protection.

The ideas mentioned here are very similar to everything you can do to leak an ASLR, PIE, or Stack address, which you may also need to leak in order to fully exploit a binary using ROP or shellcode

(Smartly) Brute-Forcing the Canary

Brute-Forcing the canary sounds like a hard task, as there are way too many bytes in 64-bit machines to guess. There is however a smarter way you can guess if the canary stays the same for every guess in a certain situation.

A canary is unique per process. This means a parent process spawning a child (like execve()) will have a different canary value. A fork() however, is considered the same process! We can abuse this by allowing a forked version to crash, while another process with the same canary keeps running. The information for if it crashed or not can help us determine if a guess for the canary was correct, and when we know it is completely correct, we can exploit it fully without the canary changing. This idea of forking a process is common in a multithreaded socket server, which we can abuse as all threads will have the same canary.

int main() {
    char buf[16];  // Buffer is 16 long
    
    while (1) {
        if (fork()) { wait(0); }  // Fork spawns new thread, with same canary
        else { read(0, buf, 128); return; }  // We can overflow the buffer (128 > 16)
    }
}

We know the first byte of the canary is always \x00. The idea here is to try to write all 256 possible bytes in the place of the second byte of the canary until one doesn't crash with *** stack smashing detected ***. Once we find it, we know the second byte of the canary and can include it in the write, then try all 256 bytes for the third position until one doesn't crash. We can keep going writing more and more of the canary until we have found all 7 secret bytes and we have leaked the real canary. Then we simply use this newfound canary in the stack overflow in order to overwrite it with the correct value and overwrite the return address.

The attempts will look something like this:

input                   | canary
-------------------------------------------------
"AAAAAAAA"                00 c7 9e 2e 19 c5 5b ee
41 41 41 41 41 41 41 41 | 00 c7 9e 2e 19 c5 5b ee

"AAAAAAAA\x00\x00"        00 c7 9e 2e 19 c5 5b ee
41 41 41 41 41 41 41 41 | 00 00 9e 2e 19 c5 5b ee -> fail
"AAAAAAAA\x00\x01"        00 c7 9e 2e 19 c5 5b ee
41 41 41 41 41 41 41 41 | 00 01 9e 2e 19 c5 5b ee -> fail
...
"AAAAAAAA\x00\xc7"        00 c7 9e 2e 19 c5 5b ee
41 41 41 41 41 41 41 41 | 00 c7 9e 2e 19 c5 5b ee -> success!

"AAAAAAAA\x00\xc7\x00"    00 c7 9e 2e 19 c5 5b ee
41 41 41 41 41 41 41 41 | 00 c7 00 2e 19 c5 5b ee -> fail
"AAAAAAAA\x00\xc7\x01"    00 c7 9e 2e 19 c5 5b ee
41 41 41 41 41 41 41 41 | 00 c7 01 2e 19 c5 5b ee -> fail
...
"AAAAAAAA\x00\xc7\x9e"    00 c7 9e 2e 19 c5 5b ee
41 41 41 41 41 41 41 41 | 00 c7 9e 2e 19 c5 5b ee -> success!

...

"AAAAAAAA\x00\xc7\x9e\x2e\x19\xc5\x5b\xee"
41 41 41 41 41 41 41 41 | 00 c7 9e 2e 19 c5 5b ee -> success!

Jumping over the Canary

The whole reason a canary is protecting the return address is that it comes before it in memory. Often in a stack buffer overflow, you have to overwrite all data that comes before the return address, including the canary. But in some specific cases, it may be possible to write the saved return address directly, skipping the canary and keeping it intact. This is very situational, but in various circumstances, you may find yourself able to perform an arbitrary write, or even simply skip a small part of memory that includes the canary, but not the return address.

One such example looks like this:

int main() {
    char buf[16];
    int i;    

    // Write 128 separate characters into too big of a buffer
    for (i = 0; i < 128; i++) {
        read(0, buf+i, 1);  // Address is pointed to by `i`
    }
}

This case might seem unexploitable until you realize that while overflowing the buf with your 17th character, it will end up overflowing into the local i variable. This variable will decide in the next iteration at what offset the rest of the data will be written, and will let you decide how much to jump by setting that 17th character.

In this case, you should find how far the return address is, and set i so that it directly jumps to it instead of writing over the canary. Then continue in the loop to write the desired return address your exploit needs.

Using scanf() - "."

The scanf() function allows a program to read data into a specific format, like strings, floats, or integers. You might be able to perform a stack overflow if the location scanf() is writing to is out of bounds, which can happen in a loop like this that goes too far:

double buffer[20];
int n = 0;
double sum = 0;

printf("How many numbers do you want to add? ");
scanf("%d", &n);

for(int i = 0; i < n; i++) {
    printf("Number[%d]: ", i);
    scanf("%lf", &buffer[i]);
    sum += buffer[i];
}
printf("Your sum: %lf\n", s);

Adding more than 20 numbers overflows the buffer, but if a Stack Canary is enabled, you would normally first overwrite it instead of the return pointer. This is where the trick comes in, taken from "scanf and the hateful dot". When your input to this %lf (double) variable is a single . dot, the variable is not overwritten and the execution continues like normal. Doing this right as you would overwrite the canary will skip it, and then you can overwrite the return pointer again with another double value.

How many numbers do you want to add? 22
Number[0]: 1
Number[1]: 1
...
Number[20]: .  # Stack Canary (skipped)
Number[21]: .  # RBP (skipped)
Number[22]: 12345678  # Return address

When researching the reason this trick works, they found the following results:

Format
Skipped

%d (integer)

. and .5

%f (float), %lf (double), %Lf (long double)

only .

%x (hex)

only .

%s (string)

Last updated