During last weekend’s Nuit du Hack CTF, I did not manage to solve the “Secure File Reader” challenge. I had the binary already downloaded, so I went back and did it to sharpen my skills. I started by investigating what the binary did – fortunately, this binary was relatively straightforward (click to see full size):
Spot the vulnerability yet?
This challenge is a combination of two separate vulnerabilities – a time-of-check-time-of-use vulnerability:
- safe_save checks the filesize at 0x8048F15, passing because the filesize is small
- we change the file (well, we re-symlink the file)
- save_in_buffer calls open at 0x8048E8B, against our new (and much larger) file
The second vulnerability is a textbook stack overflow (strncat at 0x08048ED9 in save_in_buffer, saving to safe_save’s stack).
I decided to tackle the problems in reverse order: first, I’d make the stack overflow work and return a shell, and then I’d put together a script to win the race condition. To do this, I first patched the “pwn” binary, by reversing the jump at 0x8048F1C (so instead of rejecting files over 0xFFF bytes, it would reject files under 0xFFF bytes – but our overflow is over 0xFFF anyway).
From the disassembly / application logic, we know the “save buffer” is 0x1000 in length. In order to find out how many bytes I needed to put on the stack to gain initial control of EIP via return address, I took the lazy approach and hacked together a Python script, which printed 0x1000 * “A”, then print incrementing bytes from 1 (i.e. \x01\x02\x03\x04 and so on). I then ran the application in GDB and waited – the application crashed at 0x201F1E1D, so I knew how many bytes I needed to overflow (0x1000+0x20-4) and confirmed by setting EIP to 0xCCCCCCCC:At this point, the observant reader will note that this application has a non-executable stack (“readelf -l pwn” and look for GNU_STACK or if you’re using gdb-peda, use “checksec” to do the same).
The hard counter to non-executable stack is the ROP chain: our goal is to eventually call execve to /bin/sh (int 0x80). The excellent “ropper” tool can get us halfway there with rop chain auto-generation, but we need to fiddle with the resulting addresses:
- We need to remove null bytes (remember – ./pwn copies input using strncat, a null byte makes strncat terminate).
- The ropper-generated chain contains a broken link, one of the ROP gadgets has extra instructions which break the stack layout, so we found an alternative gadget. I can’t remember which one, I don’t have original notes =(
(Edit: you can use gdb-peda’s “ropsearch” tool to quickly do this, if you don’t want to go through a ton of ROP gadgets. The “ropgadget” command seems to miss even the int 0x80 gadget for some reason).
We tack this to the end of our buffer overflow padding, then we run the patched “pwn” executable and watch the magic:
Running exit returns us to our “parent shell” confirming we’re in a spawned shell. All we need to do is now feed this payload into the executable by winning the above race condition. This is trivial:
1$ while true; do ln -fs payload.bin out.bin; ln -fs decoy.bin out.bin; done 2$ while true; do ./pwn out.bin; done
This constantly flips out.bin between a small file (which passes the stat check) and a larger payload file (which hopefully is the file we open()). Race conditions are unreliable by nature, so we run the two loops side by side and wait (a short time) for our shell.
You can find the exploit code here: it’s got a bit of extra debugging to help verify that everything’s working before a final int 0x80 call, so you’ll need to remove that if you want to try it yourself.
Unfortunately, I wasn’t able to solve this in the time allocated (in truth – I completely missed the race condition on the weekend), but this was a fun exercise in exploitation nonetheless.