This is the first post in a series covering the tasks of the competition.
The task
The code
The program consists of a loop calling do_stuff().
In case do_stuff returns a truthy value, we also call win().
Examining win() we can spot a buffer overflow:
We get prompted to enter a name (which can be up to 360 characters including a null character). This name gets copied into a buffer that has a size of 100 characters.This results in a buffer overflow if we enter a name longer than 99 characters.
To exploit the buffer overflow, we need to win the game.
But how do we win the game?
Sadly there seems to be no way to cheat, we just have to luckily guess a number between 1 and 100. However, computers being fast that should not be an issue.
So before diving into exploiting the bufferoverflow we should be able to reliably win the game.
This code is setup so we can develop our exploit locally (with debugger etc.) and then switch to remote after we are confident that it works.
All this code does is repeatedly guess 1 and send it to the process until we’ve won.
Now we can try to win the game:
And remotely so we can see our fancy progress bars and feel prouder:
Now that we can reliably reach the bufferoverflow after an amount of time we’re ready to exploit it.
The exploit
The stack stores the current return address in close proximity to the buffer we are overflowing.
Our first step is to exactly find out at which buffer position that address sits.
For that we could manually inspect the program in a debugger, but pwnlib supplies us a handy utility.
This is a sequence that is unique for every substring of length 8.
So [0..8] != [1..9], [1..9] != [2..10] etc.. This implies if we can a random slice of the sequence [x..(x+8)] we can figure out x by just looking at the content.
We override the return address with an unknown part of the sequence, and the program tries to jump to that address.
This results in a SIGSEGV, a result of accessing invalid memory, showing us exactly what address was accessed.
This information is enough to figure out at which offset from the buffer the return address is stored, 120 in our case.
At this point we are able to continue executing from an arbitrary address. Combined with our ability to write abitrary data into memory e.g. a program,
this used to be enough to run shellcode.
Nowadays, we usually can not execute from memory in the stack because of Executable space protection. We are able to write a program into the buffer, but trying to execute it would result in a crash.
Pwnlib exposes tools to find the necessary gadgets. The syntax for that is not documented too well, so I needed to use an external tool to find one of the gadgets.
The shellcode
Our goal is to gain shell access, the easiest way to accomplish this is to call the execve() system call with /bin/sh.
Finding a tailored data copy routine or writing one ourself within the constraints is fairly difficult. Luckily we are on a 64 bit system and our payload /bin/sh fits entirly within one reigster.