Trail of Bits CTF 3/5 Brute Cookie
Trail of Bits released a number of CTF challeleges on Github.
This post is about the brute_cookie
binary exploitation challenge.
The brute_cookie
challenge is found in the ctf/exploits/binary2_workshop/brute_cookie/
directory. It contains only a single binary (bc
) and no source code.
Analysing the disassembly in r2 we see this binary listens on port 12345
and forks a new child to handle each client. The children print out the file descriptor of the accepted socket (in the guise of fprintf(socket_fd, "Welcome client #%u\n", socket_fd);
). This information leak is important for later. The child process then reads input from the socket inside the firstFunc
function. Unfortunately it tries to read 0x2000
bytes into a 0x1000
byte buffer. This is more than enough to overflow the return address in the stack frame.
The return address is protected by a stack cookie, however we can see from running the binary and connecting with multiple clients that the cookie is the same for each child. This problem makes it possible for us to simply brute force the cookie.
† ./bc &
[1] 19904
† nc localhost 12345
nc: using stream socket
Greetings client #4
cookie: 16604500
^C
† nc localhost 12345
nc: using stream socket
cookie: 16604500
Greetings client #4
^C
† nc localhost 12345
nc: using stream socket
Greetings client #4
cookie: 16604500
^C
But what do we want to change the return address to? The binary does not have all symbols stripped so looking through function names we can see a readKey
function. Disassembling shows us that it reads 0x29 bytes from a file called key
in the current working directory and is not called referenced by any other code in the binary so I guess redirecting execution flow to this function is the goal of this particular CTF. Note that the CTF does not come with a key file so I guess we need to make one ourselves. If you forget and plough ahead with exploitation you will run into some annoying segfaults as the process tries to read a non existent flag file!
† ruby -e 'puts "bunny" * 8' > key
So we want to overflow the buffer, and replace the return address with the address of readKey
while keeping the stack cookie intact. There is one more caveat. The readKey
function takes an argument which is the file descriptor to write the key out to. This is where the information leak from before comes in.
The way I decided to approach this was to write a Ruby script to run the binary under r2 and use the debugger to read out the real cookie and develop an exploit from there. At the end when everything is working we can take away the debugger and add the brute force logic.
The binary helpfully prints out the stack cookie whenever a client connects however it would be nice to also have a nice print out of the stack before and after the overflow to help us write the exploit, so lets start with that.
Because r2pipe blocks while the debugged application is running, we will run it in a separate thread. We will use Ruby’s native sockets implementation to play the part of the client and we will use condition variables to coordinate when each thread should do what. Here is an outline without the details of the r2 thread.
The m.synchronize { c.wait(m) }
will wait until the r2 thread has started, so there won’t be a race condition between starting the bc
binary and trying to connect as a client.
For now, all we want is the r2 thread to start the bc
binary, signal to the client part of the script that is it ready to handle connections, break on the vulnerable stack overflow function and then print out the stack including the cookie.
Example output of running what we have so far:
[*] Client is waiting for r2 to start
[+] r2 is waiting for connection on port 12345
[*] Client is connecting
[*] Client is waiting for r2 to finish.
[+] r2 stack before overflow
+-----------------------------------------+
| ebp + 0xc | 0xffa9763c |
| ebp + 0x8 | 0x00000004 |
| ebp + 0x4 (return address) | 0x08048879 |
| ebp + 0x0 | 0xffa98648 |
| ebp - 0x4 | 0xf764a223 |
| ebp - 0x8 | 0xffa9763c |
| ebp - 0xc (cookie) | 0x72e99300 |
+-----------------------------------------+
At this point the r2
thread could just pass the cookie to the client
thread so we can create a working exploit before brute forcing the cookie. We should be able to calculate the correct offsets to construct our exploit payload from the disassembly.
[0x00000000]> pdf @ sym.firstFunc
| sym.firstFunc (int arg_8h);
| ; var int local_1010h @ ebp-0x1010
| ; var int local_100ch @ ebp-0x100c
| ; var int local_ch @ ebp-0xc
| ; arg int arg_8h @ ebp+0x8
...
| 0x08048790 81ec28100000 sub esp, 0x1028
...
| 0x08048796 65a114000000 mov eax, dword gs:[0x14] ; [0x14:4]=1
| 0x0804879c 8945f4 mov dword [local_ch], eax
...
| 0x080487ca c74424080020. mov dword [local_8h], 0x2000 ; [0x2000:4]=-1
| 0x080487d2 8d85f4efffff lea eax, [local_100ch]
| 0x080487d8 89442404 mov dword [local_4h], eax
| 0x080487dc 8b4508 mov eax, dword [arg_8h] ; [0x8:4]=0
| 0x080487df 890424 mov dword [esp], eax
| 0x080487e2 e849fdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
We can see here the buffer starts at ebp-0x100c
and ends at ebp-0xc
, which is where the stack cookie is. Then for some reason there is 8 bytes of stuff between the cookie (ebp-0xc
) and the saved base pointer (ebp
) and after that (ebp+4
) is the return address we want to overwrite. And after that (ebp+8
) is the file descriptor argument and (ebp+0xc
) is the saved base pointer from the previous function call.
+-------------------------------+
| ebp + 0xc | previous ebp |
| ebp + 0x8 | socket fd arg |
| ebp + 0x4 | return address |
| ebp | saved ebp |
| ebp - 0x4 | dunno |
| ebp - 0x8 | dunno |
| ebp - 0xc | stack cookie |
| ebp - 0xd | buffer end |
| ... | ... |
| ebp - 0x100c | buffer start |
+-------------------------------+
After executing the flag function we don’t necessarily want the binary to crash when it tries to return to an invalid address like 0xaaaaaaaa
so we may as well give it somewhere sensible to return back to.
So our payload looks like this, where cookie_g
will be the correct cookie extracted from the r2 debugger thread.
And from the r2
thread, we can simply pass the cookie through the global variable cookie_g
.
From the client thread there is nothing left to do but read the flag from the socket and cleanup.
Example output looks like this
[*] Client is waiting for r2 to start
[+] r2 is waiting for connection on port 12345
[*] Client is connecting
[*] Main got banner and fd = 4.
[*] Client is waiting for r2 thread to send cookie
[+] r2 stack before overflow
+-----------------------------------------+
| ebp + 0xc | 0xffc5763c |
| ebp + 0x8 | 0x00000004 |
| ebp + 0x4 (return address) | 0x08048879 |
| ebp + 0x0 | 0xffc58648 |
| ebp - 0x4 | 0xf7604223 |
| ebp - 0x8 | 0xffc5763c |
| ebp - 0xc (cookie) | 0x7a35c100 |
+-----------------------------------------+
[*] Client got cookie 0x7a35c100
[+] r2 continue
[+] r2 stack after overflow
+-----------------------------------------+
| ebp + 0xc | 0x00000004 |
| ebp + 0x8 | 0x08048a2c |
| ebp + 0x4 (return address) | 0x08048714 |
| ebp + 0x0 | 0x42424242 |
| ebp - 0x4 | 0x42424242 |
| ebp - 0x8 | 0x42424242 |
| ebp - 0xc (cookie) | 0x7a35c100 |
+-----------------------------------------+
[*] Client got flag bunnybunnybunnybunnybunnybunnybunnybunny
[*] Client is waiting for r2 to finish.
[+] r2 is done
If for some reason the exploit didn’t work, having r2 print out the stack before and after the overflow like this is invaluable.
Now that we have a working exploit, all we need to do is replace cookie_g
with our brute force values. We should also ditch the debugger and add multithreading to the client so that it won’t take 3 years to exhaust the search space.
The first step is to wrap the exploit code into a function that takes a cookie to try as an argument.
We can use this function like so
Example output
[*] Main is waiting for r2 to start
[*] Main is connecting
[*] Main got banner and fd = 4.
[*] main sending response
""
We got nil
instead of the flag, I guess 0x88888800 wasn’t the cookie this time.
We can attempt multiple cookies at the same time by spawning threads like this. Note that the bottom 8 bits of the cookies have always been zero so far, so this function will take a 24 bit number of multiply by 0x100 to turn it into a 32 bit number with the bottom 8 bits set to zero.
Since the code exits right away when it finds the flag, all we need to do is call the function like this.
Example output
[+] Trying 0x88888000..0x88888800
88888000
88888800
88888600
88888700
88888500
88888400
88888300
88888200
88888100
[!] No flag, sorry
At this point it may be temping to wack the full 24 bit range into the exploit_range
function. You might find that spawning 2^24 threads within the same Ruby script to be a disappointing experience. We need to do a few at a time.
Now we can leave the brute force code running and be confident that the exploit will work when the correct cookie value is finally guessed.