Download the binary here
My go-to process for binary exploitation (pwn) challenges is:
- Check Security (checksec or the
filecommand) - Dogbolt / Ghidra
- pwntools (see above link)
Step 1: Security
The output of checksec was:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'$ORIGIN'
SHSTK: Enabled
IBT: Enabled
Stripped: No
The critical values we’re looking for are:
- “Stack” (no canary is good)
- “NX” (NX enabled means we can’t execute instructions on the stack)
- PIE (no PIE means the binary’s functions will be at at the same memory addresses every time).
The good news is that we don’t have to worry about stack canaries and if we can find a buffer overflow, then we should be able to return to any function in the program we want.
Most simple pwn challenges should be immediately obvious after some testing what vulnerability you’re targeting, but to save us the headache when this isn’t the case, we’ll load it into Ghidra to get a better feel for what this program does before we run it.
Step 2: Reverse Engineering
I am using Ghidra 11.4.2 at the time of this article and after the default “Auto Analyze” it will automatically find main(), which jumps directly into vuln(), the decompilation of which is shown below:
| |
Clearly there is some weirdness going on, but the code is overall not very complicated. We can see a strcmp() between a variable “local_68” (+6) and the string “Jaguars”, if the strcmp() fails, then it will call exit(). Because a buffer overflow “ret2” attack relies on our stack address to be pop’d off the stack, calling exit() aptly exits the program without ever technically leaving main(), which means we will have to pass the strcmp() check.
There is also a gets(local_68), so we can safely assume that local_68 is input and tidy up the Ghidra code since the previous lines simply zero-out the input buffer.
Before we get too far ahead, there is a win() function that gives us shell access (which we can get the flag pretty quickly afterwards), which is why this is a ret2win challenge. If we can get the program to call win(), then we’ll get the flag.
btw, the gets() is a dead-giveaway that this is a buffer overflow vulnerability, go read the man page for it.
Looks like we can buffer overflow so long as the 6th position onward has the string “Jaguars” in it. Knowing that, we can start testing the binary live.
$ ./jacksonville
What's the best Florida football team?
> FSU
WRONG ANSWER!!
$ ./jacksonville
What's the best Florida football team?
> Jaguars
WRONG ANSWER!!
$ ./jacksonville
What's the best Florida football team?
> ------Jaguars
$ [Return code was 0]
Now we load it into GDB, put a breakpoint on the JE instruction after the strcmp() call (vuln + 0xad) and load a De Bruijn sequence to find our offset (I use pwntools’ cyclic but there’s also a neat website). A length of 128 should work since Ghidra tells us our buffer is 96-wide.
Once you hit the breakpoint, jump to the address the instruction points to and the pattern should be in the $RSP register since this a 64-bit program. I got the offset 104, which means we can make our solve script.
Step 3: Pwntools
| |
This was a very simple pwn challenge, but it’s a good example why you can’t immediately jump into black-box testing when you’re given the binary (and even more so if you were given the source). There are challenges where those skills are required, but they’re harder for a reason :)