PWN堆利用学习笔记

本文阅读指南

本文主要使用尽可能新的版本的源码进行分析

对于内存示意图,符合以下标准:

  • 红色 内容被删除的内存区域
  • 黄色 内容被修改的内存区域
  • 蓝色 内容需要被关注的内存区域
  • 绿色 内容被写入的内存区域

基础知识

关键结构名称

arena

arena本意竞技场,是glibc堆管理器ptmalloc2用于管理堆内存的核心数据结构。它本质上是堆管理器向操作系统申请来的一大块连续内存(内存池),程序中的动态内存分配和释放请求最终都会在这块区域中进行划分和管理。

特性 main arena (主分配区) thread arena (线程分配区)
所属线程 主线程 子线程
创建方式 通过 brk()系统调用 通过 mmap()系统调用
malloc_state存储位置 存储在 glibc 的全局变量中 存储在该 arena 本身的内存区域中
数量限制 (举例) 唯一一个 有限 (例如 64 位系统上限一般为 CPU 核数的 8 倍)
堆内存扩展 通过 brk()扩展 通过 mmap()创建新的 sub-heap
struct malloc_state
{
    __libc_lock_define(, mutex);
    int flags;
    int have_fastchunks;
    mfastbinptr fastbinsY[NFASTBINS];
    mchunkptr top;
    mchunkptr last_remainder;
    mchunkptr bins[NBINS * 2 - 2];
    unsigned int binmap[BINMAPSIZE];
    struct malloc_state *next;
    struct malloc_state *next_free;
    INTERNAL_SIZE_T attached_threads;
    INTERNAL_SIZE_T system_mem;
    INTERNAL_SIZE_T max_system_mem;
};

bins

管理结构 管理的内存大小范围 (字节) 管理算法 链表
tcache 0x20 - 0x410 LIFO 单链表
fastbin 0x20 - 0x80 LIFO 单链表
small bin 0x20 - 0x3f0 FIFO 双链表
large bin ≥ 0x400 (通常 ≥ 0x410) - 双链表(四链表)
unsorted bin - FIFO 双链表
  • largebin
    • 横向链表:通过fd和bk指针连接。

    • 纵向链表:通过fd_nextsize和bk_nextsize连接。

    • 插入规则:

      未命名绘图-第 2 页.drawio.png

利用方法

基础攻击方式

decrypt safe linking

在glibc的高版本中,glibc会通过safe_linking保护机制对堆块中的指针进行加密。但是仔细观察可以发现,此加密是可逆的。因为key的最高的12位(一个半字节)全是0,所以plaintext的最高12位就是cipher最高12位。这样已知的key就延长为最高的24位了,同理,我们一共只需要5轮解密就可以解出七个半个字节。至于最后的半个字节,由于内存对齐,一定为0,加密和解密脚本如下:

def encrypt(plaintext):
    key = plaintext >> 12
    cipher = plaintext ^ key
    return cipher

def decrypt(cipher):
    key = 0
    plaintext = 0
    for i in range(1, 6):
        bits = 64 - 12 * i
        plaintext = ((cipher ^ key) >> bits) << bits
        key = plaintext >> 12
    return plaintext

触发unsafe_unlink时需要提前做好如下布局(主要是绿色部分prev_size和prev_inuse要对应)

unsafe_unlink-Page-1.drawio.png

此时如果将chunk1 free,由于chunk0为free状态,会触发堆块合并,因此在合并前需要将chunk0脱链。当执行完FD→bk=BK, BK→fd=FD 的时候,原本指向chuck0的指针就会指向其向前0x18字节的位置。此时再向新的chunk0写入东西就可能会覆盖到指向chunk0的指针,那么我们就可以通过覆盖,把指针改到任意位置,实现任意地址写。

unsafe_unlink-Page-2.drawio.png


tcache attack


fastbin attack


unsorted bin attack


large bin attack

  • glibc < 2.30

  • glibc ≥ 2.30

    自 glibc 2.30 版本起,在对 large bin chunk 进行插入操作时,强制实施了两项新的检查,不过这两项检查都在一个分支里。

    //检查1
    if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
    	malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
    //检查2
    if (bck->fd != fwd)
    	malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
    

    malloc/malloc.c_int_malloc函数中存在将unsorted bin放入large bin的相关代码如下:

    if (in_smallbin_range(size))
    {
        ...
    }
    else // in_largebin_range(size)
    {
        victim_index = largebin_index(size);
        bck = bin_at(av, victim_index);
        fwd = bck->fd;
        if (fwd != bck)
        {
            ...
            if ((unsigned long)(size) < (unsigned long)chunksize_nomask(bck->bk)) // size小于最小的堆块的大小
            {
                fwd = bck;
                bck = bck->bk;
                victim->fd_nextsize = fwd->fd;
                victim->bk_nextsize = fwd->fd->bk_nextsize;
                fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
            }
            else
            {
                //检查在这里面!!
            }
        }
        else
            victim->fd_nextsize = victim->bk_nextsize = victim;
    }
    
    ...
    victim->bk = bck;
    victim->fd = fwd;
    fwd->bk = victim;
    bck->fd = victim;
    

    利用条件

    • 目标largebin中只有一个可控的Chunk A
    • victim的size略小于Chunk A的size

    large_bin_attack-第 2 页.drawio.png

house of系列

(glibc 2.39)house of apple

  • house of apple1

  • house of apple2

    _IO_flush_all_lockp() -> _IO_wfile_overflow() -> _IO_wdoallocbuf() -> _IO_WDOALLOCATE() -> __doallocate() 利用链:

    1. 利用largebin attack向_IO_list_all写入 一个可控内存的地址

    2. 在可控内存伪造下下结构体:

      结构体 要求
      _IO_FILE 1. vtable指针应指向合法的虚表,避免影响后续的漏洞利用
      1. _wide_data指针指向下面要伪造的_IO_wide_data结构体 |
        | _IO_wide_data | _wide_vtable指针应指向下面要伪造的虚表地址,这个虚表的位置不会被检查 |
        | 虚表 | 将关键函数指针(如 __doallocate或 __overflow)设置为目标函数地址(如 system函数或 one_gadget的地址) |
    3. 触发清理IO流,触发_IO_flush_all_lockp()

  • house of apple3


house of banana


(glibc>2.28)house of botcake

直接说利用流程:

  1. 先将tcache填满(大小要大于0x80,避免进入fastbin影响第二步堆块的合并)
  2. 再连续free连个连着的堆块Chunk A和Chunk B(Chunk A不能进入fastbin,且Chunk B的大小与第一步一致,A在B上面),确保二者会发生合并,进入unsortedbin
  3. 从刚刚的tcache中取出一个堆块,空出一个位置
  4. 再次释放Chunk B进入tcache,再申请回Chunk A&B(两个堆块合并后的堆块),完成利用。

此时我们通过修改Chunk A&B中的内容,修改Chunk B的fd指针,具体情形如下图所示

house_of_botcake.drawio.png


(glibc 2.23)house of orange

当我们申请了一个chunk后会检测top chunk的大小是否满足我们所需要的大小,若不满足则会将符合条件(如下)的old top chunk放入unsorted bin中,再重新映射一块top chunk。因此我们获得了unsorted bin。

  • 保证old top chunk的size > MINSIZE
  • 保证old top chunk的size < MINSIZE + 申请的大小
  • 保证old top chunk的prev_inuse为1
  • 保证old top chunk的结尾部分0x1000页对齐

(当对申请的chunk大小有限制的时候,可以通过修改top chunk的size完成house of orange)


(glibc 2.35)house of kiwi

观察__malloc_assert 函数:

#define __assert_fail(assertion, file, line, function) __malloc_assert(assertion, file, line, function)
extern const char *__progname;
static void __malloc_assert(const char *assertion, const char *file, unsigned int line, const char *function)
{
    (void)__fxprintf(NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n", __progname, __progname[0] ? ": " : "", file, line, function ? function : "", function ? ": " : "", assertion);
    fflush(stderr);
    abort();
}

我们发现了我们熟悉的fflush函数,这个函数会调用_IO_file_jumps中的sync指针。

__assert_fail被分三种情况宏定义为了assert ,具体如下:

/* When possible, define assert so that it does not add extra
   parentheses around EXPR.  Otherwise, those added parentheses would
   suppress warnings we'd expect to be detected by gcc's -Wparentheses.  */
#if defined __cplusplus
#define assert(expr)         \
    (static_cast<bool>(expr) \
         ? void(0)           \
         : __assert_fail(#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))
#elif !defined __GNUC__ || defined __STRICT_ANSI__
#define assert(expr)             \
    ((expr)                      \
         ? __ASSERT_VOID_CAST(0) \
         : __assert_fail(#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))
#else
/* The first occurrence of EXPR is not evaluated due to the sizeof,
   but will trigger any pedantic warnings masked by the __extension__
   for the second occurrence.  The ternary operator is required to
   support function pointers and bit fields in this context, and to
   suppress the evaluation of variable length arrays.  */
#define assert(expr)                                                      \
    ((void)sizeof((expr) ? 1 : 0), __extension__({                        \
         if (expr)                                                        \
             ; /* empty */                                                \
         else                                                             \
             __assert_fail(#expr, __FILE__, __LINE__, __ASSERT_FUNCTION); \
     }))
#endif

所以我们只需要触发assert函数即可。assert函数的简便的触发姿势:

  1. _int_malloc从large bin分配空间的时候,存在语句assert (chunk_main_arena (bck->bk)); 。该assert成立的条件是:让bck->bk指针指向一个伪造的堆块(fake chunk),并且这个伪造堆块的size字段的NON_MAIN_ARENA位被置位(即设置为1)
  2. _int_malloc中,当top chunk大小不足时,存在语句sysmalloc (nb, av); 。该assert成立的条件参考house of orange。

在glibc 2.36,__malloc_assert修改

在glibc 2.37,__malloc_assert完全删除,house_of_kiwi失效。


资料

https://github.com/shellphish/how2heap

https://elixir.bootlin.com/glibc/

posted @ 2025-11-08 19:50  Mistyovoovoovo  阅读(13)  评论(0)    收藏  举报