1473 字
7 分钟
Stack-2-1 Return Oriented Programming

%%本节前置:Stack-1%%

在Stack-1中,我们学习了PWN的第一个十分重要的缓冲区溢出漏洞——栈溢出,并学习了十分重要的覆写思想。在上一节中,我们主要讲解栈溢出对于栈上变量的覆写操作,而在后续的学习中,我们主要考虑对返回地址的覆写操作,通过控制返回地址来控制程序的执行流。马上我们将学习一个对栈溢出极其重要的利用手法——返回导向编程Return Oriented Programming

技术背景#

栈溢出漏洞其实是一个十分远古的漏洞。传统的利用方式是直接向栈上写入shellcode然后覆盖返回地址跳转至shellcode执行。随着Non-eXecutable技术的开启,传统的执行方式已经不允许被利用,于是返回导向编程(简称ROP)应运而生。

ROP是一种高级的堆栈溢出攻击。这类攻击往往利用操作堆栈调用时的程序漏洞,通常是缓冲区溢出。在缓冲区溢出中,在将数据存入内存前未能正确检查适当范围的函数会收到多于正常承受范围的数据,如果数据将写入栈,多余的数据会溢出为函数变量分配的空间并覆盖替换返回地址(return address)。在原本用以重定向控制流并返回给调用者的地址被覆盖替换后,控制流将改写到新分配的地址。

标准的缓冲区溢出攻击,攻击者只需要写出针对堆栈部分的代码(有效载荷)。直到1990年代后期,主流操作系统没有为该类攻击作出任何防范,微软直到2004年才提供了缓冲区溢出保护。操作系统最终使用数据执行保护技术来修补这个漏洞,该技术标记内存数据不可执行。 启用数据执行保护,机器将拒绝执行任何内存中user级别可写区域的代码。该技术的硬件支持不久用以加强该防范。

技术详情#

在二进制漏洞利用中,我们一般向“如何控制程序执行流”的方向思考并寻找漏洞,这中间不免要过渡到“构造任意读写”,“越界覆写变量”这些过程。当然,在ROP攻击中,我们主要在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

gadgets 通常是以 ret 结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。

根据gadgets的来源,我们对ROP有如下分类

  • ret2text
  • ret2shellcode
  • ret2syscall
  • ret2libc
  • ret2csu
  • ret2reg
  • ret2dl_resolve
  • ret2VDSO
  • SROP
  • BROP

例如在程序中有这样一个gadgets

pop rdi
ret

在经过bk-2的学习我们可以知道,这个gadgets的意义是

  • pop rdi:将栈顶的数据弹入rdi寄存器中
  • ret : 将栈顶的数据弹入rip寄存器中 第一条指令的作用是传递参数,第二条指令的作用是控制程序执行流

举个例子,我们有这样一个函数

void vuln()
{
	char buf[0x10];
	read(0, buf, 0x100);
}

在这个函数中,很明显存在0x90的栈溢出,那么理应有以下的栈结构

那么我们很容易就能通过溢出覆写到返回地址。回顾Bk-2的内容,返回地址实际上是函数在被调用之前push到栈上的地址,那么我们通过覆写这里的内容,就可以将函数的执行流控制到我们想要的地方。譬如说,我想要泄露程序的libc_base,那么我要执行这么一个函数puts(puts@got)那么我们就可以使用刚刚的攻击手法。

我们需要了解这三个信息

  • puts@plt函数在程序中的地址
  • puts@got在程序中的地址
  • pop rdi;ret在程序中的地址

前两个信息我们很容易就能使用IDA找到,或者使用pwntools库中的symbols查找。最后一个gadget的信息我们使用ROPgadget工具去查找。

ROPgadget --binary ./pwn --only "pop|ret"
  • --binary后指定程序的位置
  • --only用来筛选指令

那么接下来,我们就可以通过read(0, buf, 0x100)这个溢出点布置栈结构,使得我们可以正常控制程序流程。如下:

如图所示

  • deadbeed[0x18]用于填充缓冲区
  • pop_rdi_address返回地址覆写为gadget的地址,即pop rdi;ret

此时我们要开始执行pop rdi了,我们不妨看一下栈结构,栈顶应当正好在puts@got的位置

  • pop rdi弹出puts@got放入rdi
  • ret弹出puts@pit放入rip中,程序现在前去执行puts,而我们rdi中的puts@got正好作为参数传入puts

至此,我们完成了一次简单的ROP攻击。我们可以回顾一下,刚刚的ROP攻击我们实际上使用了2次ret,第一次是vuln函数本身的ret,第二次是gadgets中自带的ret。实际上我们调用puts函数仍然还有一次ret,来自puts函数本身,我们仍然可以通过继续向下布置栈来控制程序的执行流。这正是返回导向编程所提示的一个重要思想——劫持hijack。所以,栈溢出漏洞的基本思想就是覆写+劫持,以至于许多二进制的漏洞利用最终都是要发展于这两个基本思想。

接下来,我们要学习Bk-3 ELF 文件结构基础,为我们学习的第一种典型ROP-ret2text打下基础。

Stack-2-1 Return Oriented Programming
https://k4per-blog.xyz/posts/stack-2-1-return-oriented-programming/
作者
K4per
发布于
2025-03-30
许可协议
CC BY-NC-SA 4.0