题目分析
逆向虚拟机结构
这道题目的虚拟机还算规整,从main函数开始看,从对虚拟机初始化的函数入手并结合代码执行的函数可以看出,整体的vm结构放在了bss,包括32个通用寄存器,rip,代码指针和代码大小。
1 | struct vm *__fastcall initvm(struct vm *mem, __int64 code, unsigned __int64 size) |
虚拟机结构体如下:
1 | 00000000 struct vm // sizeof=0x118 |
逆向指令集
这里复现的时候也头晕的不行,我们先看执行函数
1 | unsigned __int64 __fastcall execute(struct vm *vm) |
可以看到,虚拟机指令最终的执行是借助一个vtable实现的。逐一分析vtable里的函数就能还原指令的功能,包括add,sub等常见运算与两个关键的访存指令指令包含三个操作数和一个操作码。最高4位是操作数,限制为0-10,用于下标直接从vtable中索引操作。而其余三个操作数对于运算指令来说,分为最低5位,次5位和高字的低5位(16-20)。用5位数0-31来在32个通用寄存器中索引操作,第一操作数为5-9,第二操作数位16-20,第三操作数(结果寄存器)为0-4.
ps: 这里绕了我半天,每次看着看着就不记得哪个减哪个了,也不确定自己封装的对不对…
1 | struct vm *__fastcall sub(struct vm *a1) |
而两个访存指令则比较特别,对于load,其可以从一个指定地址加载8字节到一个通用寄存器。虚拟机运行指令时会在调用指令前的一个函数内分配一段空间并作为参数传入。这个指令也是三个操作数,不同的是会用一个寄存器做基地址,高字的低12位做偏移来针对a2做索引,然后结果放入结果寄存器。store则与其相反,参数规则一样,是将结果寄存器内容存入指定地址。
1 | void *__fastcall load(struct vm *vm, char *a2) |
漏洞分析
显然,访存指令的权限太大了。这个指令里的偏移能索引0xfff,也就是说我们能够在一个相当大范围的栈上任意读写。构造ROP或者直接ret2og都是可以的。不过这道题的难点是,他没有泄露,我们需要自己去凑libc地址,所以从栈上想办法找一个“好”的地址就成为了本题的关键。但比较恶心的是,可能由于aslr的存在,栈内部的偏移许多都是不固定的,因此这道题让我掌握了一个很宝贵的经验,也就是合理使用search+distance指令组合拳。
另外有一个简单的点,由于0xfff实在太大了,我们可以load和store我们输入的code内容,也就是说基本是直球的栈上任意地址读写。
pwndbg好用捏
pwndbg为我们提供了轮椅为什么不用?调试过程中会以返回地址形式显示函数调用堆栈,我们可以直接找最近的libc内地址,直接search -t pointer
,然后distance $rsi
,就能看到需要多少的偏移。找一个不变的即可,比stack之后肉眼观察强太多了。
onegadget
这里og选取不是很容易,改完返回地址程序退出后rbp是0x1导致很多条件简单的og用不了,这个是直接从网上的wp copy的,实战过程中可以一个个试一试,指定-l 1
之后其实也没有太多。
exp
1 | from pwn import * |