首发于安擎靶场
二进制安全之堆溢出(系列)——堆基础 & 结构(二)

二进制安全之堆溢出(系列)——堆基础 & 结构(二)

哈喽啊

这里是二进制安全之堆溢出(系列)第二期“堆基础 & 结构”第二节!!

话不多说,直接上干货!

微观结构

函数执行流程

void *malloc (size_t bytes)
void *__libc_malloc (size_t bytes) //对于_int_malloc做简单封装
  __malloc_hook  //类似于虚函数,派生接口,指定一个malloc的方式
_int_malloc(mstate av, size_t bytes) //申请内存块的核心

main_arena

  • 集中管理bins链的结构体,使用含fd和bk的bin头一对一管理各个free的chunk
  • 分释放配堆块是基于main_arena来寻址的,首先找的是fastbin,其次再找bins
  • main_arena存储在libc上,用以管理所有bins的头和尾,每个bins链头的fd和尾的b与之连接
struct malloc_state
{
 /* Serialize access. */
 __libc_lock_define (, mutex);//定义了一个0x4字节的lock

 /* Flags (formerly in max_fast). */
 int flags;//0x4

 /* Set if the fastbin chunks contain recently inserted free blocks. */
 /* Note this is a bool but not all targets support atomics on booleans. */
 int have_fastchunks;//0x4

 /* Fastbins */
 mfastbinptr fastbinsY[NFASTBINS]; //fastbin链的管理头,总共10个, 每个0x10字节

 /* Base of the topmost chunk -- not otherwise kept in a bin */
 mchunkptr top;//0x4 到此为止总共0x96字节

 /* The remainder from the most recent split of a small request */
 mchunkptr last_remainder;        //切割后剩下的chunk链接到last_remainder

 /* Normal bins packed as described above */
 mchunkptr bins[NBINS * 2 - 2];   // 每个bin头有fd和bk两个指针

 /* Bitmap of bins */
 unsigned int binmap[BINMAPSIZE];   //位图,用32bit来分别表示当前bin哪个链上有chunk,通过按位与的方式

 /* Linked list */
 struct malloc_state *next;

 /* Linked list for free arenas. Access to this field is serialized
    by free_list_lock in arena.c. */
 struct malloc_state *next_free;

 /* Number of threads attached to this arena. 0 if the arena is on
    the free list. Access to this field is serialized by
    free_list_lock in arena.c. */
 INTERNAL_SIZE_T attached_threads;

 /* Memory allocated from the system in this arena. */
 INTERNAL_SIZE_T system_mem;
 INTERNAL_SIZE_T max_system_mem;
}
  • main_arena:用来管理整个bin链的结构体,总共128个bin,10个fastbin
  • 每个bin头可以简化为fd和bk两个前后项指针
  • glibc ---> main_arena ---> 对应的bins头的fd和bk ---> 遍历找到对应free的chunk
  • main_arena存放在libc中,其中存放的是每一个bin链的头尾
tips:如果我们能打印一个非fastbin链中的fd,bk,那我们就可以计算出libc的基地址libc.addr = libc_on - libc.sysbols["main_arena"] - 88

main_chunk

  • 在程序的执行过程中,我们称malloc申请的内存为chunk。这块内存在ptmalloc内部用malloc_chunk结构体来表示。
  • 当程序申请的chunk被free后,会被加入到相应的空闲管理列表中。
  • 无论一个chunk的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构。但根据是否被释放,结构会有所更改。
struct malloc_chunk {

 INTERNAL_SIZE_T      mchunk_prev_size;  // 如果前面一个堆块是空闲的则表示前一个堆块的大小,否则无意义
 INTERNAL_SIZE_T      mchunk_size;       //当前chunk的大小,由于对齐的原因所以低三位作为flag,意义如下:
 /*
 A:倒数第三位表示当前chunk属于主分配区(0)还是非主分配区(1)
 M:倒数第二位表示当前chunk是从mmap(1)[多线程]分配的,还是从brk(0)[子线程]分配的
 P:最低为表示前一块是否在使用中
 */

 /*
 1.真正的内存从这里开始分配
 2.malloc之后这些指针没有用,这时存放的是数据
 3.只有在free之后才有效。
 */
 
 struct malloc_chunk* fd;       //当chunk空闲时才有意义,记录后一个空闲chunk的地址
 struct malloc_chunk* bk;   //同上,记录前一个空闲chunk的地址
 
 /* Only used for large blocks: pointer to next larger size. */
 struct malloc_chunk* fd_nextsize; //当前chunk为largebin时才有意义,指向比当前chunk大的第一个空闲chunk
 struct malloc_chunk* bk_nextsize; //指向比当前chunk小的第一个空闲堆块
};
  • prev_size
    • malloc(0x18)会分配0x20的内存
    • malloc(0x19)分会配0x30的内存
    • 如果该chunk的物理相邻的前一地址chunk(两个指针的地址差值为前一个chunk大小)是空闲的话,那该字段记录的是前一个chunk的大小
    • 否则用来存储物理相邻的前一个chunk的数据,这里前一个chunk指的是较低地址的chunk。
    • prev_size位可以被共享,当前的chunk, 如果不够用就会占用下一块chunk的prev_size
  • size
    • chunk1的数据有效区域覆盖到chunk2的prev_size位,并且chunk2的size位的prev_inuse被覆盖为0。系统认为chunk2之前的chunk1已经未在使用了。
    • 当free(chunk2)的时候,系统会将chunk2与chunk2中prev_size大小的空间合并到bins。
    • 我们可以通过改变chunk2的prev_size的内容,操纵向前合并的大小。
    • 造成的问题:overlap(堆块重叠),chunk1被释放了,但是我们可以操纵修改它(堆利用的核心思想),从而修改bins链的内容,泄露其中的地址。
    • 形成的攻击:fastbin ---> fd ---> main_arena ---> 分配新的堆块,我们通过修改chunk1的fd内容,达到分配任意内存的目的,造成fastbin attack。
    • 记录前一个chunk是否被分配。
    • 一般来说,堆中第一个被分配的内存块的size字段的P位都会被设置为1,以便于防止访问前面的非法内存。
    • 当一个chunk的size位的P位为0时,我们能通过prev_size获取上一个chunk的大小及地址,方便进行空闲堆块的合并。
    • 对于fastbin的堆块,不管前面还有没有被分配的chunk,PREV_INUSE都为1。
    • 64位chunk的size必须是16字节对齐
    • 32位chunk的size必须是8 字节对齐
    • 64位 低4位没用 11110000
    • 32位 低3位没用 11111000
    • define chunksize(p) (chunk_nomask (p) & ~(SIZE_BITS))
    • define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
    • NON_MAIN_ARENA记录当前chunk是否是main_arena管理的堆块,1表示不属于,0表示属于
    • IS_MAPPED记录当前的chunk是否是由mmap分配的。
    • PREV_INUSE
    • 最小堆原则 : malloc(0)会分配0x20的空间,prev_size + size + 数据对齐的0x10字节
    • prev_inuse 漏洞利用
  • fd / bk
    • 释放到bins链有效
    • fd指向下一个(非物理相邻)空闲的chunk
    • bk指向上一个(非物理相邻)空闲的chunk
    • 通过fd和bk可以将空闲的chunk块加入到空闲的chunk链表进行统一管理。
  • fd_nextsize / bk_nextsize
    • 释放到bins链有效,不过其用于较大的chunk(large chunk)
    • fd_nextsize指向前一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针
    • bk_nextsize指向后一个与当前chunk大小不同的第一个空闲块,不包含bin的头指针
    • 一般空闲的largechunk在fd的遍历顺序中,按照从大到小的顺序排列,可以避免在寻找合适的chunk时挨个遍历。

__libc_malloc

void *__libc_malloc (size_t bytes)
{
 mstate ar_ptr;
 void *victim;

 void *(*hook) (size_t, const void *)
   = atomic_forced_read (__malloc_hook);
 if (__builtin_expect (hook != NULL, 0))
   return (*hook)(bytes, RETURN_ADDRESS (0));
#if USE_TCACHE
 /* int_free also calls request2size, be careful to not pad twice. */
 size_t tbytes;
 checked_request2size (bytes, tbytes);   //注意:用户申请的字节一旦进入申请内存函数被转化为了无符号整数
 size_t tc_idx = csize2tidx (tbytes);

 MAYBE_INIT_TCACHE ();

 DIAG_PUSH_NEEDS_COMMENT;
 if (tc_idx < mp_.tcache_bins
     /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
     && tcache
     && tcache->entries[tc_idx] != NULL)
  {
     return tcache_get (tc_idx);
  }
 DIAG_POP_NEEDS_COMMENT;
#endif


 if (SINGLE_THREAD_P)
  {
     victim = _int_malloc (&main_arena, bytes);
     assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
      &main_arena == arena_for_chunk (mem2chunk (victim)));
     return victim;
  }

 arena_get (ar_ptr, bytes);

 victim = _int_malloc (ar_ptr, bytes);
 /* Retry with another arena only if we were able to find a usable arena
    before. */
 if (!victim && ar_ptr != NULL)
  {
     LIBC_PROBE (memory_malloc_retry, 1, bytes);
     ar_ptr = arena_get_retry (ar_ptr, bytes);
     victim = _int_malloc (ar_ptr, bytes);
  }

 if (ar_ptr != NULL)
   __libc_lock_unlock (ar_ptr->mutex);

 assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
         ar_ptr == arena_for_chunk (mem2chunk (victim)));
 return victim;
}
  1. 该函数会首先检查是否有内存分配函数的钩子函数(__malloc_hook),这个主要用于用户自定义的堆分配函数。
这就造成了一个利用点:将__malloc_hook指针指向的内容改为one_gadget的地址,再次malloc的时候就会直接启动shell。__malloc_hook -> one_gadget(直接起shell的地址)这时不能将其修改为system的地址,因为system的参数为字符型,而malloc_hook的参数为无符号整数。
  1. 接着会寻找一个arena来试图分配内存,然后调用__int_malloc函数去申请对应的内存
如果分配失败的话,ptmalloc会试图再去寻找一个可用的arena,并分配内存如果申请到了arena,那么在退出之前还得解锁(__libc_lock_lock)
  1. 判断目前的状态是否满足以下条件
  • 要么没有申请到内存
  • 要么是mmap的内存
  • 要么申请的的内存必须在其所分配的arena中
  • assert
  • 最后返回内存,进入__int_malloc
  • __int_malloc

    __int_malloc是内存分配的核心函数,其核心思路为:

    它根据用户申请的内存块大小以及相应大小chunk通常使用的频度,依次实现了不同的分配方法它由小大到大依次检查不同的bin中是否有相应的空闲块可以满足用户请求的内存当所有空闲的chunk都无法满足时,他会考虑top_chunk当top_chunk也无法满足时,堆分配器才会进行内存块申请

    1. 定义变量


    2. 判断有没有可用的arena

    如果没有可用的arena,则返回系统调用mmap去申请一块内存


    3. 判断是否在fastbin范围

    如果申请的chunk的大小正好位于fastbin的范围,则从fastbin的头节点开始取chunk。需要注意的是,这里比较的是无符号整数调用remove_fb取出,并返回得到的fastbin的头

    4. 判断是否在smallbin

    如果获取的内存块的范围为smallbin的范围,执行以下流程找到其大小对应的下标,判断其链表是否为空,不为空则取最后一个

    注意,为了防止一个堆块能够正常free且不前向后并,需要修改当前堆块的物理相邻的紧接着的2个堆块的inuse位为1。

    5.调用consolidate

    当fastbin,small bin中的chunk都不能满足要求时,就会考虑是不是largebin,在此之前先调用malloc_consolidate处理fastbin中的chunk

    将有可能合并的chunk先进行合并后放到unsorted bin中,不能合并的就直接放到unsorted bin中,然后再进入大循环,以减少堆中的碎片。只有在分配一个size在largebin范围内的堆块,才能触发malloc_consolidate

    6. 小总结

    在fastbin范围内,先判断对应链表是否为空,不为空则取刚放入的chunk在smallbin范围内,先判断对应链表是否为空,不为空则取第一个放入的chunk这两者都无法匹配用户申请的chunk时,就会进入大循环

    7. 进入大循环

    a. 尝试从unsorted bin中分配用户需要的内存b. 尝试从large bin中分配用户需要的内存b. 尝试从top_chunk中分配用户需要的内存

    8. 从unsorted bin中分配nb

    如果申请的size小于unsorted bin中符合要求的chunk的size,会对其进行切割,剩下的进入last_remainder(由unsorted bin管理)如果unsorted bin中没有满足要求的chunk时,会先place in order整理,然后再去large bin中寻找

    9. 从large bin中分配nb

    注意,large bin中的堆块不会split,不满足的话就从top_chunk中切割

    10. 大循环之对于unsorted bin的check

    对于size的check:检查当前size是否满足对齐的 要求对于fd和bk的check:bck -> fd != victim对于double free的check:next->prev_inuse = 0

    11. 大循环之切割unsorted bin

    如果用户请求为small bin chunk,那么我们首先考虑last_remainder如果last_remainder分割后还够可以作为一个chunk,则使用set_head,set_foot设置标志位,将last_remainder放入原来unsorted bin的位置

    12. 大循环之取出unsorted bin

    首先将unsorted bin取出,如果其size和我们的nb(need bytes)一样则直接放回这个unsorted bin

    13. 大循环之放入对应的bin

    根据取出的size来判断应该放入哪个bin,放入small bin的时候则双向链表插入在else if中处理large bin的逻辑,包括大小排序以及fd_nextsize和bk_nextsize

    14. 大循环总结

    整个过程迭代了10000次

    __int_malloc的大循环主要用来处理unsorted bin如果整个循环没有找到合适的bin,说明所有的unsorted bin的大小都不满足要求如果经过了10000次的循环,所有的unsorted bin中的bin都被放入了对应的bin中,即small bin放入对应的index中,large bin排好序后放入对应的index中

    15. 大循环之large bin

    如果请求的chunk在large bin范围内,就在对应的bin中从小到大依次扫描,直到找到第一个合适的,并不一定精确


    切割后的remainder会被放入到unsorted bin中,同时设置标志位等信息

    16. 寻找较大的chunk

    如果走到了这里,说明对于用户所需的chunk,不能直接从其对应的合适的bin中获取chunk,需要扫描所有的bin,查找比当前bin更大的fast bin或small bin 以及large bin

    17. 找到一个合适的map

    18. 取出chunk

    切割之后还是一样,放入到unsorted bin

    19. 使用top_chunk

    如果所有的bin中的chunk都没有办法直接满足要求(即不合并),或者没有空闲的chunk时,就只能使用top_chunk了

    20. top_chunk不够用

    > 如果top_chunk不够用的时候并不是直接申请内存,而是先调用consolidate合并空闲的fastbin
    >
    > 然后等待下次循环再去判断是否够用,不够用才会调用sysmalloc申请内存
    >
    > ![](https://ws1.sinaimg.cn/large/006nFhrCly1g47vckz6b7j30gn0cmgnb.jpg)

    _int_malloc总结

    • malloc寻找堆块的顺序
    1. 在fastbin中寻找有没有对应的chunk
    2. 请求大小为small bin范围,在small bin中寻找有没有对应的chunk
    3. 请求大小为large bin范围,仅调用malloc_consolidate合并fastbin
    4. 在unsorted bin中寻找有没有合适的chunk
    5. 在large bin中寻找有没有合适的chunk
    6. 寻找较大的bin链中有没有合适的chunk
    7. 寻找top_chunk
    8. top_chunk不够用,调用malloc_consolidate合并fastbin
    9. top_chunk不够用,系统调用再次申请内存

    发布于 2019-08-09 10:16