正文
2. 简单虚拟机
这个程序的主体部分是我自己实现的一个非常简单的基于栈的虚拟机。整个虚拟机只有17条指令。除了 push_d 指令之外的所有指令的操作数都是存放在一个栈中的。类似于x86上浮点指令的语法。当然和普通vm指令不同的一点是这个虚拟机是区分数据与索引的。对于纯粹的数据与索引有两套不同的字节码运算指令。我将64bit数据的最高位作为flag来区分数据与索引。最高位为1的数据将会被解释为索引类型,对这种类型的加减运算将会检查索引边界是否超过栈的范围(可以认为一个固定大小的栈是这个这个虚拟机的所有内存,不能索引超过栈内存的数据)。当然,程序的漏洞其实也就存于索引类型边界没有严格检查的地方。
下面是 17 个 opcode 的定义。行为根据名字应该就能猜到。
3. des 解密与求解 8 元一次方程组
最后是程序的输入部分,为了不让程序的输入部分太过直白:)
而且也为了满足后续漏洞利用的一个重要条件。程序需要用户输入des加密过opcode和密钥。我写了一个8元一次方程组,输入的密钥必须是此方程组的解才能进行解密。当然,des不用静态编译肯定是不够意思的:)
破解思路
1. 输入数据
为了能够输入数据,让虚拟机执行。首先要做的就是解 8 元一次方程组来获取密钥。方法和工具有很多z3,angr,matlab,python 的数学库。即使是手算我想也不是很困难:-)
然后就是 des 加密和生成字节码的工作。des加密很简单,我用的是标准的 DES-CBC。用 python 的 Crypto 库可以轻松加密。至于生成字节码,因为只有 17 个字节码而且几乎没有后继的立即数。所以直接写一个 python 字典来替换就可以了。
下面就是生成一个完整 payload 的代码
2. 漏洞位置以及利用
首先,这题我开启 linux 上的全部6种二进制漏洞保护:)所以普通的 shellcode,栈溢出等是没法用的,而且 mmap,mprotect 等系统调用都被禁用了。
虚拟机中提供了 load 和 store 可以存取索引类型所引用的内存的功能。而且在load和 store 中其实是不检查索引类型是否越界的。对索引越界的检查全部都是放在索引类型写到栈上的时候。具体来说就是 change_to_point,p_add,p_add 这 3 个 opcode 中会检查。这样可以保证所有写入到栈上的索引类型不越界。程序的漏洞就在于,虚拟机使用的栈是存放在系统栈上的,而且在使用之前没有被初始化过。这样我们可以通过编排数据,来在未初始化的虚拟机栈上构造出一个越界的指针。这样使用load和store来索引的时候就会产生一个越界的读写。从而控制返回值。
3. 具体利用细节
首先是如何在虚拟中栈上构造越界的索引。在 getCode 函数中,输入的加密数据也是储存在栈上的。所以只要在加密数据之后构造就可以了。而且 opcode 中有 stop 这个指令,所以不用担心解密出来的错误code。这样就可以在 runCode 的虚拟机栈上构造出一个越界的索引了。当然,具体的偏移需要好好算一算
然后是 pie/aslr 绕过。因为 aslr 和 pie 的关系。内存中的地址都是随机的。因为没有输出,所以没有办法 leak 地址。不过只要将返回值读取进来,直接写字节码来对返回地址加减偏移,再将算好的地址写回去。这样就可以在不 leak 的情况下调用任意的 libc 函数以及 rop 了。
下面是读取并计算 libc 以及程序加载基地址,并且储存在栈最低位的代码
side attack
虽然通过漏洞能够成功的执行rop。通过open,read就可以将flag读取到内存中。但是还是没有办法将flag输出出来。所以只能用side attack。我在libc里找到了这样一条rop
当cl等于[rsi]的时候则跳转。否则返回。我们看一下跳转后的代码。
如果我们将rdi设置为0 。就能够让程序崩溃退出。
只要将rsi指向读取到的flag,再通过rop设置cl。这样就可以让flag和我们指定的字节做比较。如果正确程序就崩溃退出。如果错误的话,下一个rop链可以跳转一个getchar来block停住程序。这样就可以根据程序是否崩溃来爆破flag的值了。通过反复的连接测试就可以得到完整的flag。
1. 这题又是一个 pwn,促使我觉得自身的水平实在不够(第四题就用完了),就去学习各路 dalao 的 writeup,
附赠一篇
。
2. 体会到几个工具的便利:
-
checksec,替换我之前用命令行查看程序安全特征
-
pwntools,替换我之前用的 python 自带库 subprocess/telnetlib
-
ROPgadget,别人都在用,用的人都说叼,我还是手动靠谱
3. 学到几个 pwn 关键词