跳转至

ROP

ROP(返回导向编程)是一种利用程序已有代码片段(称为「gadget」)来执行任意操作的技术,常用于绕过现代操作系统中的安全机制,如数据执行保护(DEP)和地址空间布局随机化(ASLR)。在CTF竞赛中,ROP技术常被用来利用缓冲区溢出等漏洞,实现代码执行。

随着NX (Non-eXecutable)保护(栈和堆不可执行)的启用,传统的直接注入代码并执行的攻击方式不再奏效。返回导向编程(ROP)成为一种广泛用于绕过此保护的技术。

基本ROP

基本ROP - CTF Wiki

  • Gadget:通常是以 ret(返回指令)结尾的一系列机器指令。通过这些指令序列,攻击者可以改变某些寄存器或变量的值。
  • 工作原理:攻击者利用栈溢出覆盖返回地址,将其指向第一个gadget的地址。当函数返回时,ret指令执行,CPU跳转到第一个gadget。第一个gadget执行完毕后,其结尾的ret指令从栈上取出下一个地址(即第二个gadget的地址)作为新的返回地址,从而形成一个指令链,实现对程序控制流的多次劫持和运行特定指令序列的目的。

基础攻击载荷结构:

[ Padding ] + [ Gadget 1 Address ] + [ Gadget 2 Address ] + ... + [ Gadget N Address ]

1. ret2text(返回到程序代码段)

text段中通常包含程序的可执行代码。此种方法适用于程序中已经存在可以直接获取Shell的函数。

部分x64程序需要注意堆栈平衡问题,在函数调用后要再调用一个ret指令。可以在system处下断点,观察rsp寄存器的值,如果16进制表示的地址末尾不是0,则需要添加一个ret指令来平衡堆栈。

2. ret2shellcode

遇到题目提供了可读可写可执行的内存区域时,可以将shellcode写入该区域,然后通过ROP链跳转到该区域执行shellcode。

Pwntools中的shellcraft.sh()可以生成用于打开shell的shellcode。

3. ret2syscall

通过构建ROP链,实现类似execve("/bin/sh", NULL, NULL)这样的系统调用。

参考:CPU寄存器

示例(32位):

\[\underbrace{\mathrm{b'A'*112}}_{\mathrm{填充}}+\underbrace{gadget}_{\mathrm{pop~eax;~ret;}}+\underbrace{\mathrm{0xb}}_{\mathrm{execve\, 的调用号}}+\underbrace{gadget}_{\mathrm{pop~edx;~pop~ecx;~pop~ebx;~ret;}}+\underbrace{\mathrm{NULL}}_{\mathrm{edx~(环境变量)}}+\underbrace{\mathrm{NULL}}_{\mathrm{ecx~(参数)}}+\underbrace{addr}_{\mathrm{ebx~("/bin/sh")}}+\underbrace{\mathrm{int~0x80}}_{\mathrm{触发系统调用}}\]
payload = b'A' * 112 + p32(POP_EAX_RET_ADDR) + p32(0xb) + p32(POP_EDCBX_RET_ADDR) + p32(0) + p32(0) + p32(BIN_SH_ADDR) + p32(INT_0x80_ADDR)

4. ret2libc

在大多数情况下,程序不会直接提供system函数的地址和/bin/sh字符串。这需要利用以下知识点:

  1. libc.so库中的函数之间(如system和其他函数的地址)的相对偏移是固定的。要确定程序当前使用的libc版本,可以泄漏已知函数的地址后在https://libc.blukat.me/查询,或者用strings grep 'GNU'打出版本信息。
  2. 通过GOT表泄露输出libc中某个已执行函数的绝对地址。
  3. 一旦确定了libc版本和某个函数的真实地址,就可以计算出libc基址,进而计算出system函数的地址和/bin/sh字符串在libc中的地址。
  4. 第一次溢出 (泄露): 构造ROP链,泄露函数地址,并让程序返回到main函数或其他可再次溢出的位置。
  5. 第二次溢出 (利用): 确定system/bin/sh地址后,构造最终的 ROP 链。