This challenge introduced a game, where you could change cell values, while a monster is chasing you around the gamefield :)
For “winning” this, you’d have to match all values with some random target values, but well, winning this game would lead us nowhere near a flag, so we have to find a way to do something out of the ordinary.
So first, let’s take a look at the source code, hxp provided…
The game will hold the information about the current position (and other stuff) in this struct. Should be noted, that uint8_t is used for both x and y values.
Whenever the player executes an action, like moving or changing cell values, the monster will also update its action. It has a cooldown, so it always pauses for three steps and then moves towards the player direction. If it hits you, you will be knocked back on cell and lose one life.
Well, we have a game field on the stack… A monster knocking us back one cell… There should be a way to abuse this, to get outside of the game field.
On a first glance, everything seems fine. It will check, if a knockback would put us outside of X_MIN, X_MAX, Y_MIN, Y_MAX and only move the player, if its new position will still be in boundaries.
Well, except that the player position x and y values are stored as uint_8, which means they are susceptible for an underflow.
If the player is on y = 0 and the monster would hit us from below, p->pos_y - 1 will evaluate to 0 - 1, which will wrap around uint_8 and become 255, which is bigger than Y_MIN. So, this will lead to an y position of 255 and we’ll be out of the gamefield. By this, we’ll get put way behind the game field and can walk through the stack (between the current gamefield and gamefield+(255*10)).
With the ability to walk around on the stack, still seeing CURRENT CELL values and the possibility to modify current cell values, we can for one leak arbitrary stack values and also modify them.
Though… The monster will still be chasing us, which makes the whole procedere a little bit troublesome. But first, let’s get a starter script to get into the initial exploit stage by walking outside of the game field.
In the original exploit, I obviously didn’t track current monster position, but just tried to get it right. For the writeup, I decided to clean this up a bit and use proper move functions, which will also update all game informations, so we always know, where the monster is currently located and more important its distance to the player (this will come in handy later on).
So, by now, we’ll just walk to 9 / 0 and walk into the wall until the monster catches up (from below) and kicks us out of the gamefield.
From here we can walk left, right and up. Since y is now > Y_MAX, no down movement is possible anymore, but we won’t need that anyways. Since we deal with PIE and ASLR, it’s time now to leak some addresses.
For the sake of simplicity, let’s just think about non ASLR addresses here, since everything in the binary will be calculated in offsets to the game field anyways.
On my machine, the real game field started at 0x7fffffffe440, so we can calculate the needed x, y offsets based on the distance from this address.
Now, let’s take a look at the stack, to get an idea, where we want to move and leak
So, there’s the return address of main_loop at 0x7fffffffe528, which we can use for leaking PIE and calculating the real base address. And we have a nice (stable) libc address at 0x7fffffffe4a8 (_IO_file_jumps), with which we can calculate libc base.
One catch though, we can only move upwards… Thus after reading those two addresses, we’d ultimately fail, since we only have 1 life left and it will be game over, if the monster hits us another time.
Easy way out… While leaking the main_loop return address, modify it also and let it point back to call main_loop :
Since the call is extremely near to the current return address, we can just fix it by reducing the cell value at 0x7fffffffe528 5 times. If we quit the game after that, it will just call main_loop again, giving us another try :)
So, let’s do the leaking part
Just moving the player to the appropriate offset and then read byte per byte by parsing the game screen and reading current cell value.
While we’re at that address, we can fix up the return address, by just decrementing the LSB of the return address 5 times. I’ll skip that part for now, since the following writes will be more complicated, but we can reuse that functionality also to do this overwrite.
By now, we have the PIE and LIBC leak and have overwritten main_loop return address to just call main_loop again. Since we cannot do anything useful anymore in the current game state, we’ll just quit the current game, triggering main_loop again with a clean game state.
Ok, we’re back in our initial oob position, but this time, we know some leaks. Though we can loop the game by making small modifications to the main_loop return address, we won’t be able to do a complete libc address write. The monster will just catch up, while we’re at it, resulting in a game over or segfault…
But since we can always do the loop overwrite to continue playing after quitting, we can target the return address of main instead and keep replaying, until we have successfully written a complete arbitrary address to it, and then quit the game completely.
The end of main is also a good target since rax will be 0x0 at that point, which will fulfill this one_gadget:
At this point, modifying the libc address will get a little bite more complicated, since we need to do so many increases/decreases, that the monster will definitely catch up.
It was around 02:00 AM, when I went haywire because of this and started a huge copy/paste mess, modifying a byte, overwriting main loop, running back to the current address on and on and on. It looked really awful (though it ultimately worked and spit out the flag).
Let’s try to improve it a bit this time:
Since we have the leaks, we know the current value of the return address, and we also know the value, we would like to have it instead. Thus it’s just a matter of walking to main return address, modifying its bytes, until the monster gets too close.
We’ll then run away to main_loop return address, modify it back to call main_loop and quit, which will save us from getting caught :)
Get back into initial exploit stage… Rinse and repeat until main return address matches our desired address.
And there we are, main return address should now nicely point to one_gadget and all that’s left to do for us, is to quit without modifying main_loop return address, so it will jump back into main again, which will finally trigger a shell.