实验平台:
x86_64, Ubuntu 18.04.6 LTS, Kernel 4.15.0-167-generic
GLIBC 2.27-3ubuntu1.4
实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/VSyscall/vul64
这道题的来源是Chao Zhang, Tsinghua University。真诚的感谢Chao老师!
ELF信息:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
amd64架构,保护全开。
下面给出反汇编及反编译后的代码:(IDA原本反编译出的代码有一定问题,我做了修改)
这道题目,一看完源码就明白,应该想办法用第15行的jmp跳到sub_A2C()
再进行后续的进一步操作。但是这道题目开了PIE,我们是无法知道sub_A2C()
的具体地址的。那我们应该怎么办呢?答案是利用栈上已有的信息。
利用gdb+pwndbg进行调试:
在反编译后的第13行下断。
观察栈上的内容:
可以看到,栈上第0x1e
个也就是第30个的数据的位置,非低12位基址就是程序的代码段基址,第8~11位也恰好是0xa
,和sub_A2C()
中的0xa2c
也是恰好吻合的。又因为x86机器是小端法。所以我们只需要把0x2c
写上去,再让rsp指向这个位置,再执行ret的gadget,就可以成功的让机器执行到我们想要的地方。
通过反编译的代码可以看到,rsp正好指向rbp+buf。因此我们需要在buf上填充30个ret的地址,再覆盖1字节的地址低位。就可以达到我们想要的效果了。
可关键问题是,程序开了PIE,我上哪里去找ret的gadget呢?这个时候我们要请出Linux的一个Pwn高级玩法——VSyscall[1]。
简单来说,VSyscall是为了方便用户执行一些非常简单,而无需参数的系统调用的设置的一个段。Loader会默认的把这个段加载进来。这些系统调用分别是gettimeofday、time以及getcpu。他们的地址都是固定的,分别为0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800。我们可以直接拿它当作一个ret的gadget进行使用。
更正参考资料[1]中的一个错误,经实验,在Ubuntu 18.04上,VSyscall段还是存在的。Pwn仍然能够正常运行。
因此,我们最基础的代码部分,就是:
可以使控制流重定向到sub_A2C()
。
接下来是一波常规操作,程序主动泄露write的GOT表内容,我们可以拿这个找到libc基址:
程序第29\~37行的逻辑是,一个一个字符的读入。写到栈上的buf中。并且可以缓冲区溢出。而且还可以在写入的过程中控制字符的写入位置。
因此下面的代码是:
前0x33
个0用于填充buf,\x47
用于写入length,使其跳过canary直接写入返回地址,后面直接接system("/bin/sh")
的one_gadget即可完成整套流程。
因此整套exp代码如下: