ROP¶
ROP(返回导向编程)是一种利用程序已有代码片段(称为「gadget」)来执行任意操作的技术,常用于绕过现代操作系统中的安全机制,如数据执行保护(DEP)和地址空间布局随机化(ASLR)。在CTF竞赛中,ROP技术常被用来利用缓冲区溢出等漏洞,实现代码执行。
随着NX (Non-eXecutable)保护(栈和堆不可执行)的启用,传统的直接注入代码并执行的攻击方式不再奏效。返回导向编程(ROP)成为一种广泛用于绕过此保护的技术。
基本ROP¶
- Gadget:通常是以
ret(返回指令)结尾的一系列机器指令。通过这些指令序列,攻击者可以改变某些寄存器或变量的值。 - 工作原理:攻击者利用栈溢出覆盖返回地址,将其指向第一个gadget的地址。当函数返回时,
ret指令执行,CPU跳转到第一个gadget。第一个gadget执行完毕后,其结尾的ret指令从栈上取出下一个地址(即第二个gadget的地址)作为新的返回地址,从而形成一个指令链,实现对程序控制流的多次劫持和运行特定指令序列的目的。
基础攻击载荷结构:
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字符串。这需要利用以下知识点:
- libc.so库中的函数之间(如
system和其他函数的地址)的相对偏移是固定的。要确定程序当前使用的libc版本,可以泄漏已知函数的地址后在https://libc.blukat.me/查询,或者用stringsgrep 'GNU'打出版本信息。 - 通过GOT表泄露输出libc中某个已执行函数的绝对地址。
- 一旦确定了libc版本和某个函数的真实地址,就可以计算出libc基址,进而计算出
system函数的地址和/bin/sh字符串在libc中的地址。 - 第一次溢出 (泄露): 构造ROP链,泄露函数地址,并让程序返回到
main函数或其他可再次溢出的位置。 - 第二次溢出 (利用): 确定
system和/bin/sh地址后,构造最终的 ROP 链。