CTF pwn heap — 什么是堆以及malloc_chunk

发布于 2023-04-18  384 次阅读


关于堆那些年的那些事

什么是堆

引用CTF-wiki的解释,个人觉得解释的非常好:

在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。

堆管理器

其次,堆跟栈不同的是,在汇编层面并没有对堆的操作和实现,因此堆可以说是人为写出来的一个管理系统,我们一般称管理堆的那部分程序为堆管理器,比如C语言里面的malloc函数和free函数堆管理器的一部分程序源码。以下是一些不同的堆管理器

dlmalloc  – General purpose allocator
ptmalloc2 – glibc
jemalloc  – FreeBSD and Firefox
tcmalloc  – Google
libumem   – Solaris

除去以上这些以外,还有目前比较主流的musl堆管理器,大多用于IOT的场景,也是需要攻克的一个难题。

堆管理器是干什么的

堆管理器处于用户程序与内核中间,主要做以下工作 1.响应用户的申请内存请求,向操作系统申请内存,然后将其返回给用户程序。同时,为了保持内存管理的高效性,内核一般都会预先分配很大的一块连续的内存,然后让堆管理器通过某种算法管理这块内存。只有当出现了堆空间不足的情况,堆管理器才会再次与操作系统进行交互。 2.管理用户所释放的内存。一般来说,用户释放的内存并不是直接返还给操作系统的,而是由堆管理器进行管理。这些释放的内存可以来响应用户新申请的 内存的请求。

ptmalloc2 – glibc

当前主流pwn题所使用的都是该堆管理器,因此往后我将会分不同版本编写其利用技巧

堆的基本结构体

malloc_chunk

该结构体在源码中定义如下:

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的大小 size:显示这个chunk的大小 以上这两个字段我们成为chunk header字段,后面部分则成为user_data字段,每次malloc申请得到的内存指针,其实指向user_data的起始处。 fd 指向下一个(非物理相邻)空闲的 chunk bk 指向上一个(非物理相邻)空闲的 chunk fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk 一个正在使用的chunk结构如下图:

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)                      .
next    .                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             (size of chunk, but used for application data)    |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|1|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

一个释放的chunk结构如下图

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)                .
        .                                                               .
 next   .                                                               |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' |             Size of chunk, in bytes                           |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of next chunk, in bytes                |A|0|0|
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

chunk结构的一些特殊处理

chunk对齐与堆的对齐

首先关于堆的基地址,其是需要符合内存页对齐的,即最后的三个16进制位为0 chunk的最小大小为prev_size+size+fd+bk,即最小为4SIZE_SZ(size_t),在64位下最小为0x20的大小。 其次每个chunk的大小需要是2SIZE_SZ的整数倍,表现出来的形式就是最后一位16进制位为0,这也能解释为什么下面所说的AMP位对chunk大小没有影响 在源码中其实有不少函数对其是否对齐进行了检查,但这不是我们分析的重点(而且写的有点难看懂QAQ),因此我们只需要知道它干了什么即可,下面是一个例子更好的理解堆对齐与chunk对齐:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char* a = malloc(0x18);
    char* b = malloc(0x20);
    char* c = malloc(0x28);
    free(a);
    free(b);
    free(c);
    return 0;
}</stdlib.h></stdio.h>

对于以上这个源码,我们可以用gdb进行调试,只需要观察其堆的基地址和各个chunk的地址和大小即可 可以看到每一个chunk地址的最后一个16进制位为0,堆的基地址后面的三个16进制位为0,对于小于0x20的chunk大小会自动成为0x20,大于或者等于0x20但是小于0x30的chunk大小会自动对齐成为0x30大小的chunk

AMP位

在上面的结构可以看到,size处最低的3bit对size大小不起作用(自动对齐),从而可以用于表示一些信息 NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。 IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。 PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。

chunk中的空间复用

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用,以下是一个展示的例子

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
    char* a = malloc(0x10);
    char* b = malloc(0x10);
    char* c = malloc(0x10);
    strcpy(a, aaaaaaaaaaaaaaaabbbbbbbb); //16a 8b
    free(a);
    free(b);
    free(c);
    return 0;
}

用gdb调试可以看到如下的结果: 可以看到前两行是chunk a的结构,这里为了模拟写到了chunk b的prev_size字段,这里可以发现一个strcpy的特性,会在复制完的结尾加一个\'\x00\',因此覆盖了chunk b的size字段,正常来说的堆题是会限制输入的长度的,但是如果用了这个函数就有一个off by null的漏洞可以使用,这个漏洞的利用在后面也会经常的使用,但是在这个例子中,只是为了理解chunk空间复用是什么样的。

总结

认识chunk结构体是学习堆的第一步,需要彻底理解其结构和各个字段的作用以及特殊处理,才能在后面进行更加深入的学习

届ける言葉を今は育ててる
最后更新于 2023-04-18