heap pwn 入门大全 - 2:glibc heap机制与源码阅读(下)

本文对glibc堆管理器的各项主要内存操作,以及glibc 2.26后引入的tcache机制进行源码级分析,可作为查找使用。

glibc memory operations

  • 第一次malloc,会初始分配一个0x290的chunk,top chunk split返回给user后,剩余部分继续作为top chunk
  • 通常heap的第一个chunk,prev_inuse都为1,防止非法内存访问
  • 将双向链表中的一个chunk取出,也就是链表中的删除元素
  • 维护fd, bk链表,若为large bin,则同时维护nextsize链表
  • 可能的泄漏点 :不会修改两个链表的指针,可能会造成chunk地址泄漏
    • libc地址(通过main_arena

      • Chunk 为head chunk,bk泄漏bins地址
      • chunk 为tail chunk,fd泄漏bins地址
    • 泄漏堆地址(中间的chunk都可)

c 复制代码
static void
unlink_chunk (mstate av, mchunkptr p)
{
  // check chunk size in 2 positions
  if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr ("corrupted size vs. prev_size");

  mchunkptr fd = p->fd;
  mchunkptr bk = p->bk;

  // check double-linked list
  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");

  // unlink
  fd->bk = bk;
  bk->fd = fd;
  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
    {		// 维护nextsize双向链表,仅有large bin使用
      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)	
	{			// 下一个块不在nextsize链表中,即size与p相同
	  if (p->fd_nextsize == p) // nextsize链表仅有一个元素
	    fd->fd_nextsize = fd->bk_nextsize = fd;	// 重新构建,将下一个块作为nextsize链表
	  else
	    {		// nextsize链表不只一个元素,将fd插入nextsize链表(代替当前size)
	      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;
	}
    }
}

malloc

handler entry: __libc_malloc, finally invokes _int_malloc

__libc_malloc

  1. 检查memory hook,若有,则直接执行
    • 初始化时,将memory_hook设为malloc_hook_ini,执行ptmalloc_init
  2. 若配置tcache,使用tache
  3. 若程序为单线程,则直接调用_int_malloc(main_arena, size)进行内存分配,并直接返回
  4. arena_get获取可用arena,[inpage ref](#inpage ref)
  5. 调用_int_malloc进行实际malloc调用

_int_mallloc

  • 调整request size到nb(+SIZE_SZ,只需要一个,因为数据可以存储在下一个chunk的top SIZE_SZ bytes)

    • 如果没有可用的arena,fallback到sysmalloc,使用mmap来分配chunk
  • 如果size满足fast bin,则直接到fast bin进行分配,有结果就直接返回

  • 若在small bin 范围内,则尝试small bin,有结果就直接返回

  • 否则调用large bin,只计算index,不直接搜索结果

  • 在unsorted bin中遍历,从后(bk)往前 (av)

    • 对于每一个unsorted bin中的chunk,检查其next块的属性(size)等是否与其匹配,检查fd和bk双向链表是否损坏
    • 如果请求的是一个small bin,并且当前unsorted bin中只有一个last remainder chunk,就直接使用它。这将有助于提升在频繁malloc 小块时的效率 。这是唯一没有使用best fit策略的地方,并且只有在没有合适的small chunk能够匹配当前请求时才会使用。
    • 将chunk移出 unsorted bin,即使没有被直接使用,也会被放回对应的bin中
    • 如果大小匹配,则直接使用该块,不放回
    • 否则将该块放回bin
      • 若为small bin,直接找到位置
      • 若为large bin,则还需要保持其有序,还需要另行处理nextsize双链表
  • 遍历结束,所有unsorted bin被清空,若没有刚好匹配上的chunk,则进行下一步

  • 若request为large bin

    • 若在当前bin 中找到第一个合适大小的chunk,将其移出队列(若有多个符合,取第二个,否则会破坏nextsize链表)
    • 剩余部分不足MIN_SIZE,则使用整块
    • 否则进行split,剩余部分组成一个remainder,加入unsorted bin头部,并设置chunk 内容
    • 否则基于bitmap寻找下一个非空bin
      • 注意bitmap延迟响应,偏否的蒙特卡洛,只有为0时才一定为空
      • 所以检验得到false alarm后,需要更新bit
  • 否则最后一步,使用 top chunk use_top

    • 若top chunk不足,则会进行fast bin碎片整理consolidate
    • 若没有fast bin,则调用sysmalloc进行堆扩容
    • 以上两种例外情况结束后,都将返回上面重新检查bin等
  • 通用

    • 当一个chunk被选中使用,会执行以下操作
      • 对应位置位(inuse,main_arena)
      • 操作tcache,优先fill tcache,若已满则返回给用户
      • check_malloced_chunk 为debug预留接口,默认不开启
      • chunk2mem将chunk地址转换为user data首地址(加上header偏移0x20

ptmalloc_init

  • 第一次运行malloc时,作为memory hook内容(malloc_hook_ini)被调用,所有线程和起来只用一次
  • 所有线程的main_arena相同,都指向主线程的那个
  • 初始化main_arenamalloc_init_state,主要初始化bins链表头,设置top为unsorted bin(假top)

arena_get

获取一个可用arena,并上锁。如果没有找到可用的arena,则新建一个arena_get2

c 复制代码
/* arena_get() acquires an arena and locks the corresponding mutex.
   First, try the one last locked successfully by this thread.  (This
   is the common case and handled with a macro for speed.)  Then, loop
   once over the circularly linked list of arenas.  If no arena is
   readily available, create a new one.  In this latter case, `size'
   is just a hint as to how much memory will be required immediately
   in the new arena. */

#define arena_get(ptr, size) do { \
      ptr = thread_arena;						      \
      arena_lock (ptr, size);						      \
  } while (0)

#define arena_lock(ptr, size) do {					      \
      if (ptr)								      \
        __libc_lock_lock (ptr->mutex);					      \
      else								      \
        ptr = arena_get2 ((size), NULL);				      \
  } while (0)

Arena_get2:

  • 若当前存在空闲arena in free_list,则直接返回
  • 若当前无空闲,并且当前arena数量没有超过限制,则新建一个arena - _int_new_arena
  • 否则复用一个现有的arena

_int_new_arena

  • new_heap 调用mmap来分配arena空间,top_pad由multi thread配置,默认0x20000,用于指定top chunk的padding
    • 前部size包含heap info以及arena 结构体(mstate),经过align up后对其到页(后面需要进行mmap操作),共0x21000
    • mmap内存区间,映射size 为HEAP_MAX_SIZE,为DEFAULT_MMAP_THRESHOLD_MAX*2 = 64MB,初始不赋予权限
      • 需要对齐到HEAP_MAX_SIZE,所以一开始mmap 2倍空间,并使用中间经过对齐的一段(类似align up),如果一开始mmap的2倍空间已经对齐,在下次申请新arena时,会使用原来剩余的部分,减少heap对内存空间的碎片化
    • 对申请部分(包括align_top)进行mprotect,赋予读写权限
    • 初始化heap_info其他信息
  • 将arena放置在heap_info之后,并设置参数
  • 将top chunk 放置在arena之后,并设置参数
Offset Name Type Size
0x0 Heap info Heap_info 0x20
0x20 arena Malloc_state 0x898
0x8c0 (aligned) Top chunk malloc_chunk

sysmalloc

  1. 若request size过大(>= mp_.mmap_threshold (0x20000)),并且当前mmap块数量少于上限,则直接使用mmap为request分配,而不是扩展top
    • mmap分配的空间对齐到page size
    • 由于不能使用下一个chunk的prev_size位置,所以当前chunk存在size_sz的overhead
    • 同样将mmap内容作为chunk,需要初始化prev,以及size
  2. 正常拓展top
    • 首先记录初始top信息,并检查top情况
    • 若为main_arena
      • 为top留出足够空间(top_pad
      • 调用MORECORE来获取更多内存,系统中使用_sbrk
        • (若存在)调用hook
        • 若brk失败,则尝试mmap
          • 使用mmap后,sbrk region为非连续段
      • 若sbrk为拓展已有的top,简单拓展top即可
      • 否则对堆做出调整
        • 若为contiguous,且已有旧堆(比较奇怪),则需要增加旧堆的大小(增加到old_sz + size,即拓展新需求的空间),且记录新的brk位置到snd_brk
        • 若非contiguous,需要建立fencepost
        • 建立新的top chunk,并将原top chunk free掉
    • 若非 main_arena
      • 尝试使用mmap来拓展已有堆grow_heap,heap最大不超过HEAP_MAX_SIZE
      • 若失败,建立新堆 new_heap
      • 将旧有的top chunk free掉,设置fence post
      • 将heap的arena等属性设置好,将新开辟的空间初始化为top chunk
  3. 完成拓展,将top chunk split,并返回给user

其他封装函数

  • calloc返回n个元素和指定大小的内存,全部初始化为0

free

entry: __libc_free

libc_free

  1. 检查hook (通常没有)
  2. 将pointer转换为chunk ptr
  3. 若为mmap chunk (检查mmaped bit - chunk_is_mmaped)
    1. 调整mmap threshold,后续大小更小的(不超过最大阈值)的request不会触发mmap
    2. __munmap解除块映射
  4. 调用_int_free

_int_free

  1. 检查chunk各项参数
  2. 处理tcache
  3. 检查fast bin
    • 将chunk 插入bin头部,若非单线程,则需要使用原子操作
  4. 检查非mmapped chunk
    1. 尝试合并前面的chunk(如果!prev_inuse)
      • 若可以合并,则unlink前面的chunk
    2. 尝试合并后面的chunk
      • 若后面为top chunk,则直接修改当前chunk参数,将其合并,直接结束
        • 否则,若后面chunk正在使用,则将其prev_inuse清空,否则同样unlink
    3. 将可能合并后的chunk重新设置头,并将其插入unsorted bin头部(仅需要维护fd/bk链表即可,若为large chunk,还需要清空next_size位置)
  5. 如果目前free的size(合并后)大于FASTBIN_CONSOLIDATION_THRESHOLD,则进行consolidate
    • 首先调用malloc_consolidate进行整理
    • 若为main arena,则使用systrim缩小arena体积,缩小量为top pad
    • 否则使用heap_trim缩小mmap堆

malloc_consolidate

  • 对所有fast bin中的bin进行遍历
    • 对每一个bin中的chunk进行遍历
      • 合并前后chunk,与_int_free中第4步(检查非mmapped chunk)完全相同

systrim

  • 只在当前top chunk末尾与current_brk相同时使用,避免外部sbrk对外部数据的影响
  • 调用MORECORE(-extra)将多余部分裁剪
  • update top chunk and heap

heap_trim

  • 如果当前heap仅剩top chunk,说明整个heap都可以被删除
    • 清除上一个heap的fencepost,检查后将上一个heap的大小拓宽fencepost大小
    • 删除当前heap
    • consolidate previous chunk
    • 更新 top
  • 尝试收缩当前heap
    • shrink_heap将多余部分重新映射为不可访问区域,用于节约内存

Other Operations

realloc

重新分配该内存块,提供两个参数oldmem和size,分别制定原内存块指针地址和需要修改的大小

  1. 若oldmem为空,直接转malloc,若size为0并且oldmem不为空,直接转free
  2. 进行相关安全检查,如wrap around,size等
  3. 若为mmaped chunk
    • 尝试mremap (linux syscall),成功即返回mremap_chunk
    • 否则malloc,并进行memcpy,复制内存内容
  4. 否则
    • 若为单线程,则直接调用_int_realloc
    • 若为多线程,则尝试在该arena上进行_int_realloc,失败则在其他arena上尝试malloc

_int_realloc

  • check size
  • if new_size < old_size, 直接选择该块待用
  • 若下一个chunk是top chunk,则缩减top chunk 直接使用,直接return
  • 若下一个chunk 空闲且空间足够,直接使用下一个chunk,先unlink
  • 否则,allocate, copy, free
  • 若当前选择的chunk(该块,或与下一个合并块)较大,则需要split

tcache

tcache是glibc 2.26后引入技术,用于提升堆小块管理性能,但是缺乏检查,存在较多漏洞

structure

tcache引入两个新结构体tcache_entry, tcache_perthread_struct

c 复制代码
/* We overlay this structure on the user-data portion of a chunk when
   the chunk is stored in the per-thread cache.  */
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

/* There is one of these for each thread, which contains the
   per-thread cache (hence "tcache_perthread_struct").  Keeping
   overall size low is mildly important.  Note that COUNTS and ENTRIES
   are redundant (we could have just counted the linked list each
   time), this is for performance reasons.  */
typedef struct tcache_perthread_struct
{
  uint16_t counts[TCACHE_MAX_BINS];		// count for entries
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
  • tcache_entry用于指向下一个chunk的user data部分
  • tcache_entry放在chunk的user data部分,并且会复用该部分(tcache_entry结构体)

工作原理

  • 第一次 malloc 时,会先 malloc 一块内存用来存放 tcache_perthread_struct - MAYBE_INIT_TCACHE
  • free 内存,且 size 小于 small bin maxsize 时
    • tcache 之前会放到 fastbin 或者 unsorted bin 中
    • 在tcache出现后:
      • 先放到对应的 tcache 中,直到 tcache 被填满(mp_.tcache_count, 默认是 7 个)
      • tcache 被填满之后,再次 free 的内存和之前一样被放到 fastbin 或者 unsorted bin 中
      • tcache 中的 chunk 不会合并(不取消 inuse bit,fastbin也不会
  • malloc 内存,且 size 在 tcache 范围内
  • 先从 tcache 取 chunk,直到 tcache 为空
  • tcache 为空后,从 bin 中找
  • tcache 为空时,如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来

Source Code

MAYBE_INIT_TCACHE

tcache_init()初始化tcache

  1. 获取一个可用arena,并malloc tcache_perthread_struct
  2. 设置tcache变量,并memset清空区域

free

in _int_free

  1. 在user data部分建立一个entry
  2. 检查tcache double free
  3. tcache_put 将chunk 放入tcache

tcache_put

  • 将chunk插入tcache单链表
  • 设置entry的key为tcache,以便检查double free

malloc

  • 若对应位置(tcidx)count不为0,则可以使用tcache (可在bins命令中查看)
  • 使用FIFO机制,同样从头部取一个块返还给user
  • 由于entry直接为user data头部指针,所以直接返还给用户空间
相关推荐
内核程序员kevin2 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
朝九晚五ฺ7 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream7 小时前
Linux的桌面
linux
xiaozhiwise7 小时前
Makefile 之 自动化变量
linux
意疏10 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu10 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿10 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
我的K840911 小时前
Flink整合Hudi及使用
linux·服务器·flink
19004311 小时前
linux6:常见命令介绍
linux·运维·服务器