picoCTF

Babygame01 [100 pts]

Get the flag and reach the exit. Welcome to BabyGame! Navigate around the map and see what you can find! The game is available to download here. There is no source available, so you’ll have to figure your way around the map. You can connect with it using nc saturn.picoctf.net [port #].


We’re given a 32-bit ELF binary. checksec shows the following:

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

I decompiled the binary with Ghidra. After a bit of renaming, I got this for main():


/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */

undefined4 main(void)

{
  int move;
  undefined4 uVar1;
  int in_GS_OFFSET;
  int player_x;
  int player_y;
  char win_condition;
  undefined map [2700];
  int local_14;
  undefined *local_10;
  
  local_10 = &stack0x00000004;
  local_14 = *(int *)(in_GS_OFFSET + 0x14);
  init_player(&player_x);
  init_map(map,&player_x);
  print_map(map,&player_x);
  signal(2,sigint_handler);
  do {
    do {
      move = getchar();
      move_player(&player_x,(int)(char)move,map);
      print_map(map,&player_x);
    } while (player_x != 0x1d);
  } while (player_y != 0x59);
  puts("You win!");
  if (win_condition != '\0') {
    puts("flage");
    win();
    fflush(_stdout);
  }
  uVar1 = 0;
  if (local_14 != *(int *)(in_GS_OFFSET + 0x14)) {
    uVar1 = __stack_chk_fail_local();
  }
  return uVar1;
}


First, let’s break down what’s happening in this program. Essentially, upon starting, we’re provided a map of where we can travel and given a certain set of moves that can be found in the program’s decompilation:

There is a win() function that prints out the flag, and it is called in main() once the following conditions are satisfied:

Notably, in the player_init() function, the win_condition variable appears to have been set to a null byte (since it’s at an offset of 2 on the stack compared to the player variable itself). Additionally, it is located on the stack right after the map, which can be found in Ghidra at the start of main():

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined main(undefined1 param_1)
             undefined         AL:1           <RETURN>                                XREF[1]:     080497e1(W)  
             undefined1        Stack[0x4]:1   param_1                                 XREF[1]:     08049764(*)  
             undefined4        EAX:4          move                                    XREF[1]:     080497e1(W)  
             undefined4        Stack[0x0]:4   local_res0                              XREF[2]:     0804976b(R), 
                                                                                                   080498a1(*)  
             undefined1        Stack[-0x10]:1 local_10                                XREF[1]:     0804989b(*)  
             undefined4        Stack[-0x14]:4 local_14                                XREF[2]:     0804978a(W), 
                                                                                                   0804988a(R)  
             undefined1[270    Stack[-0xaa0   map                                     XREF[4]:     080497a5(*), 
                                                                                                   080497be(*), 
                                                                                                   080497f6(*), 
                                                                                                   08049817(*)  
             undefined1        Stack[-0xaa4   win_condition                           XREF[1]:     0804984e(R)  
             undefined4        Stack[-0xaa8   player_y                                XREF[1]:     08049831(R)  
             undefined4        Stack[-0xaac   player_x                                XREF[6]:     0804978f(*), 
                                                                                                   0804979e(*), 
                                                                                                   080497b7(*), 
                                                                                                   080497fe(*), 
                                                                                                   08049810(*), 
                                                                                                   08049826(R)  
             undefined1        Stack[-0xaad   local_aad                               XREF[2]:     080497e6(W), 
                                                                                                   080497ec(R)  

So, essentially, we need to find a way to overwrite the win_condition variable.

Now, the move_player() function turns out to be crucial for this exact purpose:


/* WARNING: Function: __x86.get_pc_thunk.bx replaced with injection: get_pc_thunk_bx */

void move_player(int *player,char move,int map)

{
  int new_tile_char;
  
  if (move == 'l') {
    new_tile_char = getchar();
    player_tile = (undefined)new_tile_char;
  }
  if (move == 'p') {
    solve_round(map,player);
  }
  *(undefined *)(*player * 0x5a + map + player[1]) = 0x2e;
  if (move == 'w') {
    *player = *player + -1;
  }
  else if (move == 's') {
    *player = *player + 1;
  }
  else if (move == 'a') {
    player[1] = player[1] + -1;
  }
  else if (move == 'd') {
    player[1] = player[1] + 1;
  }
  *(undefined *)(*player * 0x5a + map + player[1]) = player_tile;
  return;
}

Because the map is basically stored as a 1-d array, with its rows and columns calculated with a bit of math, finding a specific tile’s location on the stack is calculated with this:

*player * 0x5a + map + player[1]

However, notice how, in all of the move options, none of them actually set bounds on player movement! Meaning, we can go beyond the supposed “limit” of the player’s position of (89, 29), as well as go to the negatives. This can be confirmed with a bit of testing of going out of bounds of the map in the program.

Importantly, the calculated tile location is set to a non-null byte, either the player_tile variable or just 0x2e. So, with the move_player function, if we can set the calculated tile location to the location of the win_condition variable, we can overwrite the null byte with some other byte and then get to the X (i.e. input “p”) to get the flag!

Conveniently, win_condition, as aforementioned, is located directly after map on the stack. Essentially, it is before the stack in the memory by an offset of 0x4 bytes, meaning that, if we set player[1] to -4, we can get to its location!

So, since the player starts at (4, 4), the sequence wwwwaaaaaaaap should get our flag!

Connect to the service and send the sequence for the flag:

picoCTF{gamer_m0d3_enabled_8985ce0e}