stack pivoting
原理
该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后在相应位置进行ROP。一般来说,我们可能在下述情况使用劫持栈指针。
- 可以控制栈溢出的字节数较少,难以构造较长的ROP链。
- 开启了PIE保护,栈地址未知,我们可以将栈劫持到已知的区域。
- 其他漏洞难以利用,需要进行转换,比如将栈劫持到推空间,从而在堆上写rop及进行堆漏洞利用。
此外,栈指针劫持有以下几个要求:
- 可以控制程序执行流.
- 可以控制sp指针,一般来说控制栈指针会使用rop,常见的控制栈指针的gadgets一般为:
1 | pop rsp/esp |
例子
checksec查看保护:
1 | bi0x@ubuntu:~/桌面/pwn/otherROP/pivoting$ checksec b0verfl0w |
32位文件,无NX保护,无PIE。
IDA打开:
1 | signed int vul() |
fgets 函数存在栈溢出漏洞,但能溢出的字符只有 50 - 0x20 - 4 = 14字节,可以控制栈溢出的字节数较少,难以构造较长的ROP链。这里我们就考虑 stack pivoting 。由于程序本身并没有开启堆栈保护,所以我们可以在栈上布置shellcode 并执行。基本利用思路如下
- 利用栈溢出布置 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 | bi0x@ubuntu:~/桌面/pwn/otherROP/pivoting$ ROPgadget --binary b0verfl0w --only 'jmp|ret' |
sub esp offset;offset的确定
- 0x20的 shellcode + padding
- 0x4的 ebp
- 0x4的 ret
- 加起来为0x28
脚本如下:
1 | from pwn import * |
还有一种泄露libc_main_start地址,确定libc版本,再使用system地址也可打通。
- 通过puts函数泄露libc_main_start地址
- 确定libc版本
- 计算system地址与/bin/sh地址
- 最长的rop链仅需要12个字节,小于14个字节,可以打通。
1 | from pwn import * |
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 | fake ebp |
这里我们的 fake ebp 指向 ebp2,即它为 ebp2 所在的地址。通常来说,这里都是我们能够控制的可读的内容。
下面的汇编语法是 intel 语法。
在我们介绍基本的控制过程之前,我们还是有必要说一下,函数的入口点与出口点的基本操作
入口点
1 | push ebp # 将ebp压栈 |
出口点
1 | leave |
其中 leave 指令相当于
1 | mov esp, ebp # 将ebp的值赋给esp |
下面我们来仔细说一下基本的控制过程。
在有栈溢出的程序执行 leave 时,其分为两个步骤:
- mov esp, ebp ,这会将 esp 也指向当前栈溢出漏洞的 ebp 基地址处。
- pop ebp, 这会将栈中存放的 fake ebp 的值赋给 ebp。即执行完指令之后,ebp便指向了ebp2,也就是保存了 ebp2 所在的地址。
执行 ret 指令,会再次执行 leave ret 指令。
执行 leave 指令,其分为两个步骤
- mov esp, ebp ,这会将 esp 指向 ebp2。
- pop ebp,此时,会将 ebp 的内容设置为 ebp2 的值,同时 esp 会指向 target function。
执行 ret 指令,这时候程序就会执行 target function,当其进行程序的时候会执行
push ebp,会将 ebp2 值压入栈中,
mov ebp, esp,将 ebp 指向当前基地址。
此时的栈结构如下
1 | ebp |
当程序执行时,其会正常申请空间,同时我们在栈上也安排了该函数对应的参数,所以程序会正常执行。
程序结束后,其又会执行两次 leave ret addr,所以如果我们在 ebp2 处布置好了对应的内容,那么我们就可以一直控制程序的执行流程。
可以看出在 fake frame 中,我们有一个需求就是,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址,这一点与 stack pivoting 相似。