花式栈溢出之stack pivoting
/0x01 stack pivoting
stack pivoting,即劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行ROP。
什么情况下需要利用stack pivot?
- 栈溢出的字节比较少,无法直接利用溢出字节进行ROP;
- 开启了 PIE 保护,栈地址未知并且无法泄露,但是利用某些利用技术时必须要知道栈地址,就可以通过stack pivot将栈劫持到相应的区域;
- stack pivot能够使得一些非栈溢出的漏洞变成为栈溢出漏洞从而进行攻击,典型:可以将程序劫持到heap空间中;
stack pivot有什么利用条件?
1、存在内容可控的内存,位置已知,拥有读写的权限,有几个典型的位置可供选择:
(1)一个是bss段末有较大的空间,因为进程内存按页分配,分配给bss段的内存大小至少一个页(4k,x1000)大小,一般bss段的内容是用不了这么大的空间的,并且bss段分配的内存页拥有读写权限,是stack pivot的好目标;
(2)另一个是heap空间,这个不用赘述了,但是需要注意泄露堆地址;
2、控制rsp(esp)。一般来说,控制栈指针会使用 ROP,需要相应的Gadgets,常见的控制栈指针的Gadgets一般是:
1 | pop rsp/esp |
其中有一个最典型,在x64的libc_csu_init通过Godgets中,做一个适当偏移能够得到这样一个Gadgets:
1 | mov rdx,r13 |
可见其实就是ret2csu的经典Gadget,是一个pop rsp ret,如果将ret的地址改成leave ,ret,那么我们正常通过pop rbp;pop r12;pop r13;pop r14执行到返回时,将rbp中的值相应设置好也可以劫持rsp。
或者其他诸如add rsp,0x100等能够劫持rsp寄存器值的Gadget。
stack pivoting适用场景:
我们控制了橙色部分区域,但是中间有一段不可控制的内存,这时,我们需要控制rsp跳转到橙色部分,继续执行我们的Rop指令,这就是stack pivot,如下图是最简单的一种,通过add esp, 0x40c;ret的gadget来实现劫持栈指针:
以下的一些Gadgets都是可以通过对esp的操作来实现劫持栈指针:
0x02 X-CTF Quals 2016 - b0verfl0w
运行程序,询问名字并输入内容,再输出出来;查看程序是个32位的动态链接文件;查看安全编译选项,发现啥都没开:
GDB计算溢出至ret处的偏移量为36:
打开IDA分析:
这里看到是通过fgets()来获取用户输入内容,存在明显的栈溢出漏洞,限定了只能输入50个字节;同时看到变量s相对ebp的偏移量为20h=16*2d=32d,再加上ebp的4个字节就和前面计算的溢出偏移量是一致的。
由此可以算出能够溢出的字节数为50-36=14。
shellcode选择
这里因为没有开启NX,所以我们可以直接在栈上写shellcode。
我们来看下pwntools的shellcode长度是多少:
1 | ski12@ubuntu:~/ctf/pwn/stack$ python -c "from pwn import *;print len(asm(shellcraft.sh()))" |
可以看到是44个字节。而我们知道变量s处到ret处的偏移量为36个字节,是塞不下这个shellcode的。
那就换一个更简短的shellcode如下:
1 | \x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80 |
看下该shellcode的长度:
1 | ski12@ubuntu:~/ctf/pwn/stack$ python -c "from pwn import *;print disasm('\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80')" |
13h+2=15h=21d,即shellcode长度为21个字节,满足条件。
寻找Gadget
shellcode的问题搞定了,接着是ret处应该覆盖为shellcode的起始地址即ret2shellcode,但是这里有个问题,系统开启了ASLR,因此栈地址是随机的,我们无法预测。解决办法是利用相对地址即可,如上一节最后提到的几个对esp进行偏移量操作的Gadgets。
这里就用到经典的方法:jmp esp。
因为在函数ret的时候,esp刚好指向ret地址的下一个地址;而当我们找到如jmp esp的gadget并覆盖到ret地址时,就可以跳到下一个地址去执行这个gadget地址后面的指令。
搜索到了一个jmp esp的Gadget:
找到目标Gadget为0x08048504。剩下的后面的指令就是需要ret2shellcode执行了。
那么可以知道我们构造的payload其结构如下:
1 | shellcode|padding|fake ebp|jmp esp|set esp point to shellcode and jmp esp |
参考前面小节提到的几个Gadgets,我们可以通过sub esp, 0xXX;jmp esp这个来实现ret2shellcode,因为我们没有办法直接ret到指定的shellcode代码处(原因是ASLR),只能通过相对地址的方式实现跳转;这里就用sub esp, 0xXX来实现相对地址的跳转,因为当前esp指向本地址,而我们可以算出shellcode起始地址里该地址的相对偏移量为20h(shellcode+padding)+4(fake ebp)+4(jmp esp)=28h,当使用sub esp, 0x28时可以使esp指向shellcode起始地址处 ;最后在jmp esp跳转至修改后的esp指向的地址即shellcode起始地址。
简单地说,就是将修改esp指向shellcode起始地址,然后再跳到esp指向地址去执行从而执行shellcode。
现在我们再算下整个payload的长度,我们已知shellcode起始地址到最后sub esp, 0x28;jmp esp这段Gadget处的偏移量为28h=40d,而这段Gadget的长度如下:
1 | ski12@ubuntu:~/ctf/pwn/stack$ python -c "from pwn import *;print len(asm('sub esp, 0x28;jmp esp'))" |
整个长度为40+5=45<50,满足只能输入50个字节以内内容的限制。
整个payload结构如下图所示:
编写payload:
1 | from pwn import * |