This is a binary exploitation challenge from the USC CTF 2024. It involves an arbitrary stack overflow that allows for control flow hijacking.
Reader
Running the normal checks, we can see that we’re working with a x86-64 ELF. The protections on the binary are partial relro, a canary, non-executable stack, and no pie. Let’s decompile the binary and see what we can find.
1 | void main(void) |
Pulling out the main function here from Ghidra, we can see some interesting functionality here. The program is forking before continuing. This kind of simulates interaction with some like a web server, where on connecting you are spawned into your own little world while the web server still persists even when nobody is connected. This is interesting behavior for a binary because it allows for certain exploitation like we’ll see later. Let’s check out the vuln function:
1 | void vuln(void) |
So pretty obvious stack overflow in this function. We have a 72 byte buffer in which we are reading 0x80 bytes into. Given that there is no pie and theres actually a ‘win’ function inside of this binary, we can hijack control flow and return to win. The only problem here is the stack canary protection. But as discussed before, using a forking process for this introduces some attack surface. Inside of a binary, the stack canary is generated and doesn’t change while the binary lives. It is also inherited between all the functions that use a canary inside of the binary. So everytime we fork and get into this function, we will always have the same canary.
Exploiting this becomes trivial as we have a persisting connection and can brute force the stack canary. Now from general knowledge, I know that the trailing byte of a canary is always ‘\x00’, so realistically we only need to brute force the 7 other bytes of the canary. This is feasible in this scenario as we can go byte by byte by triggering the stack smashing response from the binary when we guess the incorrect byte.
The actual bruteforce calculation comes down to 1792 (7 bytes of canary * 256 possible values for a single byte) maximum iterations. While in reality we probably wont get too close to this number as the bytes aren’t always going to be 256 or close to it. Either way this number is very feasible.
Solve Script
1 | from pwn import * |
One last important thing you should note about this script is that I am utilizing p.send()
instead of any form of p.sendline()
. This is extremely important because using p.sendline()
appends the \n
byte to your payload, which will always (well almost always) return false for the canary, even if you guessed the correct byte before the \n
was appended.