So this “game” consisted of a loader and several object files, which were dynamically loaded at startup and mmapped with a custom aslr functionality.
At startup, it fetches a random state from urandom, initializes a stack guard canary and then loads the provided object files and maps them to “randomized” memory regions.
In aslr_get_addr we can see, that it will effectly only randomize 12 bits and shifts them resulting in memory regions like this:
The functionality needed for the challenge is separated in different object files
basic.o - Provides basic functionality like strcmp, strcpy
syscalls.o - Provides access to syscall interface and provide simple syscalls like write, read, …
guard.o - Provides simple _stack_chk_fail handler
res.o - Resource file (containing game banner)
debug.o - Just contains all the nice gadget, you’d need for ropchains
main.o - Entry point, show banner and enter game
game.o - Contains all the game logic
To get started, let’s check the game logic, since this is our main interface to the challenge.
We don’t have too many options in the game
Play the game
See full scoreboard
See score for place
Checking the full scoreboard isn’t too useful, since it will just show the existing scores and doesn’t seem to be vulnerable.
Play the game will always create two random numbers and asks for the result when adding those two values. Nothing too interesting here.
But if we win the game and have a score, which would let us enter the scoreboard, a simple buffer overflow waits for us.
Though it seems to check for a valid size, it doesn’t return and thus only prints an error message but happily continues reading into buf resulting in an overflow. But the function is guarded with a canary, so just overflowing it, will get us nowhere without knowing the correct canary.
Let’s put that aside and try to find some leaks first.
The option See score for place gives us a somewhat arbitrary read, since it doesn’t do proper boundary checks.
With this, we can now read any qword relative to the start of the scoreboard.
By now, we don’t know any address, since all object file regions are randomized, but using this, we can search for addresses in the region, the game_scoreboard is located in.
At first, I only searched for leaks and stored them randomly. But it later turned out, that it would be a good idea to know exactly to which object file which address belongs to, so lets do it right this time from the beginning.
game_scoreboard is a reference to scoreboard in main.o, so the scoreboard itself is stored in the data region of main.
Let’s see, if we can find something useful behind the scoreboard
So, we have a reference to names at offset 0x1000 from the scoreboard, which we can read and thus know the base address of main.
Since main.o is calling the show_banner function from game.o at startup, it needs to know its address and we can find it at the start of main region.
By reading that, we can derive the base address of game.o. But since we now want to read a value, which is located “before” the scoreboard and we cannot enter negative values, we have to pass such a big index, that it will overflow and thus “wrap” around at 0xffffffffffffffff
Since we now know the base address of game.o we can also start leaking addresses from there, since we can exactly calculate the needed offset from scoreboard to game now.
game.o will contain a reference to print from basic.o and _stack_chk_fail from guard.o
We can also find a reference in game.o to game_banner, which comes from res.o
In basic.o we can find a reference to sys_read at offset 0x28
With this, we now have found all discoverable leaks (debug.o cannot be found, since none of the other object files reference it)
Still, we don’t know the canary to exploit the buffer overflow in the enter score function.
But checking the randomize functionality from the challenge again, it seemed to be quite possible to reverse it, to get the inital random_state to be able to calculate the canary ourself.
The loader initially fills rand_state by reading it from urandom and then initializes the stack guard with rand(64).
rand will call rand_get_bit for every bit and shift it to the left, thus calculating the resulting random number.
rand_get_bit will calculate a random bit based on bits in rand_state, then shift rand_state by 1 bit to the left and append the resulting bit at the end of rand_state.
Soooo, rand is also used for calculating the aslr addresses of the object regions, and we know the addresses for those and we also know the order, in which they were calculated (main, syscalls, guard, basic, game, res, debug).
Though, while being quite confident, that this process should definitely be reversable, I’m not that good at rng reverse magic, so I would have been quite stuck at that point.
But what do we have team colleagues for? :)
So, I cleaned up the current script, ordered the leaks in the way, they would have been produced by the random number generator and summarized my idea in discord and asked for help from our crypto/math geniuses.
And thankfully, rkm0959 picked it up and quickly came up with a sage algo to calculate back the random_state, providing the stack guard canary and also the address of the debug region (which can be derived by calculating the next random value after res region).
Now, we got everything we need to exploit the buffer overflow and write a simple execve("/bin/sh", 0, 0) ropchain.
We just need to play the game and get a score, that qualifies us for the scoreboard.
Since we have access to debug.o now, which contains all the gadgets you could wish for, we can easily craft a ropchain and finish this.
When triggering our ropchain, rdi will already point to the start of our name, so we can just start it with /bin/sh\x00 and omit pop rdi.