pwn——vsyscall滑动绕过以及爆破
vsyscall滑动绕过以及爆破
2020-09-02 10:11:36 hawk
这道题目是DASCTF8月月赛的题目magic_number,原始wp链接在这里,这里我对于里面的一些知识点、坑进行总结一下。
这里是我的wp以及文件
概括总览
这里主要介绍一下vsyscall技术,方便下面的利用。
首先介绍一下背景——对于一般的系统调用,例如sys_read、sys_write等,其需要想内核传递一些参数,从而实现一些特定的功能。当我们调用这个系统调用时,而由于这里会进行一些参数的传递,为了保证用户态和内核态的数据隔离,往往需要把当前的上下文(寄存器状态)保存好,然后切换到内核态;然后执行完调用后,将结果放置到对应的寄存器和内存中,再恢复上下文,切换回用户态。这中间会产生大量的系统开销。因此,为了解决这个问题,Linux系统会将仅仅从内核里请求读取数据的系统调用单独罗列出来进行优化——包括gettimeofday、time以及getcpu这几个系统调用。其地址实际上是固定的,在源码中如下所示
#define VSYSCALL_ADDR (-10UL << 20)
实际上,通过这段代码,已经可以确定这部分是固定地址,为0xffffffffff600000。我们实际上可以将这部分代码dump出来,这里我们直接引用其他博主的图片https://www.cnblogs.com/ichunqiu/p/11350476.html,如下所示
可以看到,这里面由syscall。但是实际上这并不能简单的当作syscall进行使用——这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错。因此我们仅仅可以使用0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800这三个地址,而这三个地址处对应的函数,我们可以简单地将其看作一个retn指令——也就是说,我们可以通过覆盖返回地址为上面分分析到的三个地址,从而改变栈的布局。
往往这个技术的思路是——在栈中寻找之前遗留的信息,通过溢出技术修改,并通过vsyscall将返回地址滑动到该信息处,从而完成攻击。
需要特别说明的是,这个技术实际上在发布后的版本就被裁剪掉了,因此就无法再使用了,目前是只能在Ubuntu 16.04版本上进行使用,需要特别注意一下。
例题
这里的例题采用的是DASCTF 8月赛的magic_number题目,首先按照管理,查看一下开启的保护措施,如下所示
可以看到,RELRO保护全开,栈不可执行,并且PIE保护开启。实际上看到PIE保护开启,往往就需要进行PIE保护绕过了。下面我们观察一下他的主要逻辑,如下所示
可以看到,实际上这里的逻辑非常简单——如果v5的值等于0x12345678,则执行system("/bin/sh")命令。最后进行读取操作。明显的,该程序存在严重的栈溢出漏洞,最后读取的输入明显超过了栈的大小,因此我们可以控制返回地址,从而控制程序的执行流程。而考虑到代码中存在system("/bin/sh")指令,因此如果我们将返回地址修改为该命令对应的地址,我们也就成功获取了shell。
但问题在于程序开启了PIE保护,我们并不知道该指令对应的具体地址。因此我们考虑通过vsyscall技术进行绕过——如果栈上存在指令段的相关地址信息,我们通过栈溢出进行覆盖后几位,从而可以在栈上构造system("/bin/sh")指令对应的地址,然后我们通过上面介绍的vsyscall滑动,让返回地址滑动到该地址,则我们既可以成功获取shell。我们在gdb中首先查看一下栈上信息,如图所示
实际上,我们根据rip可以注意到,代码段的地址大概位于0x55831b454adb。而在栈上,$rbp + 0x8是main的返回地址,$rbp + 0x28处的值仅仅最后一字节和该地址不一样。因此实际上我们将$rbp + 0x8到$rbp + 0x28中间的值用上面分析的vsyscall地址覆盖掉,而后溢出最后一个字节,使该地址为system("/bin/sh")指令的地址即可,这样子即可完成shell的获取。
但是一般栈上的地址信息和我们想要的指令地址信息差距会比较大,这里我们选择$rbp + 0x40处的地址,可以看到,其与rip对应的地址后12个比特不同。但我们每一次至少改变8个比特,因此我们同样进行覆盖,但是需要覆盖8 * 2 = 16个比特,因此需要对4比特的值进行爆破,概率为1 / 16,还是比较大的。
这里特别说明一下爆破——我们可以使用recv(timeout = 1)来进行爆破,其模板如下
def exp(): global r ''' 获取shell ''' r.recv(timeout = 1) if __name__ == '__main__': while True: try: exp() r.interactive() break except KeyboardInterrupt: break except: continue
这个我简单说一下我的理解,实际上这个模板仅使用这道题——核心思想是区分获取shell的程序和没有获取shell的程序的区别。这道题中,没有获取shell的话,程序结束,那么我们调用r.recv(timeout=1)的话,会返回EOFError错误(这里timeout不能设置太小,否则不会报错)。而获取shell的程序不会崩溃,因此调用r.recv(timeout=1)的话返回空值,因此我们即可通过这个模板进行爆破。
如果程序是一个无限循环的(菜单题),则我们需要根据r.recv(timeout=1)的返回值进行判断来爆破。
最后贴出这道题目的wp,如下所示
#coding:utf-8 from pwn import * #context.log_level = 'debug' debug = 1 def exp(debug): global r if debug == 1: r = process('./magic_number') #gdb.attach(r, 'b *$rebase(0xadb)') r.recvuntil('Your Input :\n') vsyscall = 0xffffffffff600000 r.send('a' * 0x38 + p64(vsyscall) * 7 + '\xa8\x4a') r.recv(timeout = 1) if __name__ == '__main__': time = 1 while True: try: log.info("No.%d try"%(time)) exp(debug) r.interactive() break except KeyboardInterrupt: break except: r.close() time = time + 1 continue