03 · ROP 链 —— 用 ret 串起来的积木
03 · ROP 链 —— 用 ret 串起来的积木ROP (Return-Oriented Programming, 返回导向编程):NX 开启后栈不能执行代码, 但我们可以拼接已有代码的片段(gadget)来实现任意逻辑。这是 DEP/NX 绕过的基础, 也是 64 位传参的标准解法。1. 为什么需要 ROP问题解法NX: 栈不可执行不执行自己的代码, 执行 libc/程序里的函数64位: 参数要进寄存器gadget:pop rdi; ret等一个函数不够多个 gadget 串成链, ret 一个个衔接ROP 链 栈上的地址序列, 每个地址指向一个 gadget, ret 依次执行。2. Gadget 是什么gadget 已有代码里以 ret 结尾的短指令序列:原代码: ... 5f c3 ... ; 字节 5f pop rdi, c3 ret gadget: pop rdi; ret ; 从任意对齐开始解码, 这就是个 gadget!链: [pop rdi; ret][/bin/sh][system][...] 执行: ret → pop rdi; ret → rdi /bin/sh → ret → system(/bin/sh)3. 找 gadget 的工具本库自带纯 Python 工具 (tools/find_gadgets.py), 两种模式:# 模式1: 反汇编对齐扫描 (找函数内完整指令序列)python3 tools/find_gadgets.py victim/lab2_rop/vuln# 模式2: 字节级搜索 (找非对齐 gadget: 5f c3 pop rdi; ret)# —— 字节级才是本质! CPU 从任意地址解码python3 tools/find_gadgets.py /lib/x86_64-linux-gnu/libc-2.27.so# 输出示例 (libc 2.27):# pop rdi; ret (5f c3) - 0x2164f# pop rsi; ret (5e c3) - 0x23a6a# pop rdx; ret (5a c3) - 0x130516专业工具: ROPgadget / ropper (功能全, 但字节级原理相同)4. ret2csu —— 编译器送你的万能 gadget__libc_csu_init是每个 gcc 编译的程序都有的初始化函数, 里面藏着两个万能 gadget:g1 0x4007fa: pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret g2 0x4007e0: mov rdx,r15; mov rsi,r14; mov edi,r13d; call [r12rbx*8]; add rbx,1; cmp rbx,rbp; jne; add rsp,8; ret用 g1g2 可以调用任意函数, 传 3 个参数(r13→edi, r14→rsi, r15→rdx)。链布局(关键! 这是最容易翻车的地方)[g1][rbx][rbp][r12][r13][r14][r15][g2][8字节垃圾][48字节垃圾][返回地址] └───── 6个pop值 ─────┘ └r12函数指针表┘ └add rsp,8跳过┘ └g2的ret┘ └─ 48字节: g2尾的add rsp,8之后 会落入g1的6个pop序列!血泪教训: g2 尾部add rsp,8之后不是 ret, 而是与 g1 重叠的pop 序列 (0x4007f6 add rsp,8 → 0x4007fa pop rbx…)。所以链必须补848 字节垃圾, 最后才是真正的返回地址。少 48 字节 ret 弹出垃圾 崩。参数规则call [r12rbx*8] 调用r12 指向的内存里存的函数地址想调 puts? r12 putsgot (GOT 里存着 puts 真实地址)想调自定义地址? 先用 gets 写 .bss, 再 r12 .bss只能传 32 位参数 (mov r13d截断) → 栈地址(0x7fff…)会被截断!高地址参数要用真pop rdi; ret(字节级 gadget)5. lab2 完整攻击链(两阶段, ASLRNX 全开)阶段1 (泄漏): ret2csu → puts(putsgot) 打印 libc 地址 → 回 vulnerable 阶段2 (写): ret2csu → gets(bss_buf) 写入 system 地址 /bin/sh 阶段3 (执行): ret2csu → call [bss] system(/bin/sh) → shell!真实运行 (evidence/lab2_rop.log):[] 泄漏 putslibc 0x7cc8d9280970 [] libc base 0x7cc8d9200000 system 0x7cc8d924f420 ROP-CHAIN-PWNED uid0(root) hello world!6. 栈对齐 —— 64 位的隐藏杀手规则: call 之前 rsp%16 0, 函数入口 rsp%16 8 (ABI) libc 内部用 movaps (SSE) 要求严格对齐 → 对齐错 SIGSEGV修法: 链里插retgadget (一个 ret 栈顶-8, 改 1 位对齐)。本库实测规律 (lab2): 阶段1/2 链起点 rsp%168 → 不加 ret (函数入口恰好 8) 阶段3 链起点 rsp%160 → 加一个 ret (对齐回 8)代数: 链起点 X → call 时 X-8(无ret)/X-16(有ret)。把 X 实测出来再算, 别猜。7. 与 JOP 对比(详见 04)ROPJOP串联指令retjmp *寄存器(间接跳转)链在哪栈上地址序列栈上函数指针序列心脏ret 指令调度器 (dispatcher)共同点不执行自己代码, 拼已有代码同左8. 记忆卡ROP 用 ret 串 gadget 的链式攻击gadget 以 ret 结尾的已有代码片段字节级:5f c3 pop rdi; ret —— 非对齐也有效!ret2csu: 编译器白送的万能 3 参数调用器g2 尾 add rsp,8 后落入 g1 的 pop → 链要补 848 垃圾字节64位参数: rdi/rsi/rdx; 栈地址 4GB 时 r13d 截断 → 用真 pop rdi对齐: call 前 rsp%160; 错位插 ret gadgetASLR 绕过 泄漏 → 减偏移 → 得基址