Heap Exploitation
Last updated
Last updated
笔者最近跟学委吴一起接了软件安全测试中的 Heap Exploitation Project,借此 blog 梳理学习思路。
借 stackoverflow & geeksforgeeks 的回答,先大致了解一下堆是个什么东西,与我们所熟悉的栈又有怎样的区别和联系。
通过以上两张图片,相信读者已经能建立起对堆的初步印象。接下来就可以愉快地讨论堆的实现方式与利用。再次声明,以下讨论的堆,均指运行内存中的堆区域,该区域由程序员手动申请、手动释放,被同一个进程中的所有线程全局共享。
从古到今,笔者了解到的堆实现方式有以下七种:
堆多种的实现方式,也间接提示我们 heap exploitation 不是件轻松活。那么 heap exploitation 究竟难在什么地方呢?
Heap-based vulnerabilities can be very dependent on how the internal implementation of the heap allocator actually works.
因此为了“不耍流氓”,定义讨论的范围为采用 ptmalloc2 的 glibc,接下来让我们去一睹 glibc 堆管理的实现吧!
堆的最小单元为 chunk,由 metadata 与 userdata 组成。metadata 指下图中的 PREV_SIZE & CHUNK SIZE,用于记录 userdata 的元信息,方便 glibc 进行内存管理。
metadatas 中 PREV_SIZE 的作用:
给前一块 chunk 作为 userdata 存储数据使用(见 Fig.2);
在 bin 中表示前项 chunk 的大小。
metadata 中 CHUNK SIZE 记录了整个 chunk 的大小,其计算方式如下图所示。需要注意的是,glibc 中规定了 alignment 是 double word,即在 32bits 机器中要求 8bytes 地址对齐,64bits 机器中要求 16bytes 地址对齐。
glibc 对于被 free 掉的 chunk 都丢给了“小弟”——bin去管理。bin 要考虑的问题是:该把这块 chunk 改造成什么样,丢到哪里去,才方便后续分配嘞?
bin 根据用途分为了五类:small bin、large bin、unsorted bin、fast bin、tcache bin。其中发挥基础作用的是 small bin 和 large bin,small bin 实现对 512bytes 以下的 chunk 管理,large bin 实现对 512bytes 及以上的 chunk 管理。unsorted bin、fast bin 和 tcache bin 充当 cache layer 的作用。具体可参考下图以及联想存储器多级缓存的架构理解:
先从高空俯瞰一下 chunk 释放的策略:
if pointer is NULL “do nothing” else convert the pointer back to a chunk
→ Perform sanity checks on the chunk, and abort when fail
→ chunk fits into a tcache bin, store it there.
→ chunk has the M bit set, give it back to the os via munmap.
→ Otherwise we obtain the arena heap lock and then:
fits into a fastbin, put it on the corresponding fastbin, and we’re done.
size > 64KB, consolidate the fastbins, put the resulting merged chunks → unsorted bin.
Merge the chunk with neighboring freed chunks in the small, large, and unsorted bins.
Merge the result chunk if neighboring the top chunk.
Otherwise store it in the unsorted bin
第一次看到这些策略,肯定是一头雾水,这时候就需要结合细节去理解。
sanity checks:
A check that the allocation is aligned on an 8-byte (or 16-byte on 64-bit) boundary, since malloc ensures all allocations are aligned.
A check that the chunk’s size field isn’t impossible–either because it is too small, too large, not an aligned size, or would overlap the end of the process’ address space.
A check the chunk lies within the boundaries of the arena.
A check that the chunk is not already marked as free by checking the corresponding “P” bit that lies in the metadata at the start of the next chunk.
五类 bin 的具体实现:
bin 的整体管理是将各类 bin 放到一个数组中,包括 62 small bins, 63 large bins, 1 unsorted bin, 10 fast bins and 64 tcache bins per thread
small bin
Every chunk less than 512 bytes on 32-bit systems (or than 1024 bytes on 64-bit systems) has a corresponding small bin. (the size of allocated memory is stable)
large bin
基于用户分配的空间趋向于小块的事实,large bin 倾向于作为 small bin 的补充。注意下图中 BETWEEN 是表 AMONG 的意思。
unsorted bin
cluster free & malloc 聚集在某一个时间段时,将其放入 unsorted bin,即拿即用。
fast bin
为了减少内存碎片对系统的影响,fast bin 用于管理程序释放掉的较小的内存块(小于 88bytes)。
tcache bin
因为堆是全局共享的,对于多线程的情况,则不可避免会遇上需要等待别的线程释放锁,才能申请资源的情况。为了避免此类开销,设计了 tcache。
Each bin contains a maximum of 7 same-size chunks ranging from 24 to 1032 bytes on 64-bit systems and 12 to 516 bytes on 32-bit systems.
为了避免频繁的系统调用,在程序第一次 malloc 过后(即便申请很小的内存,如 malloc(0)),os 会分配一块 0x21000 大小的内存给 glibc,并拍了拍 glibc 的肩膀说,“小老弟,拿去耍吧。这些应该足够你应付来申请内存的小鬼了,如果实在不够再来找我”。glibc 捧着这块 0x21000 大小的内存,屁颠屁颠得开始了它的打工生活——替 os 处理内存申请与释放的工作。
老规矩,先瞅一眼 chunk 分配的策略:
tcache
→ mmap
→ obtain arena heap lock → fast bin → small bin
→ unsorted bin,if suitable stop,otherwise put into small / large bin
→ large bin
→ merge unsorted bin、fast bin、small bin、large bin
→ sbrk
→ mmap
→ return NULL
想必你初见觉得晦涩难懂。没事,咱们 step by step 来瞅瞅看这究竟是怎么回事。
chunk 分配机制
If all these strategies fail, the allocation can’t be serviced, and malloc returns NULL.
有了以上的基础,就可以愉快尝试一下 heap exploitation。笔者近期了解到的 heap exploitation 如下:
Heap Overflow(堆溢出): 攻击者通过向动态分配的堆内存写入超出分配的内存空间的数据,覆盖相邻内存的内容,可能导致程序崩溃或执行恶意代码。
Use-After-Free(释放后使用): 攻击者利用程序中已经释放的堆内存块,尝试重新使用这些内存块来执行恶意操作。
Double Free(重复释放): 这种攻击涉及重复释放相同的堆内存块,从而可能导致内存管理数据结构被破坏,以及潜在的漏洞利用机会。
Off-by-One Error(溢出一个字节): 攻击者试图溢出一个字节的内存,通常用于更细微的堆利用攻击。
Fastbin Attack(快速分配块攻击): 这种攻击针对 glibc 中的 fastbins,攻击者试图伪造 fastbin 链表,以便获取堆上的未分配内存块。
Tcache Attack(Tcache攻击): 这种攻击利用glibc 的 tcache 机制,攻击者试图伪造 tcache 链表,以获取未分配内存块。
Unlink Attack(取消链接攻击): 这种攻击涉及伪造一个被取消链接的堆块,以触发漏洞利用机会。
Poison Null Byte Attack(毒零字节攻击): 这种攻击利用零字节终止字符串,攻击者试图修改内存块的大小字段,以实现漏洞利用。
以下借 ctf-wiki 的示例介绍 Use-After-Free(UAF)。
UAF 的实现机理是观察堆的分配与释放,构造合法的内存写入,利用 dangling pointer 访问原先分配的地址,通过这种未定义行为发起恶意攻击(如劫持控制流),其本质是如下 L4 所述的 “arbitrary effects”。
示例源码可通过链接获取,注意将 Makefile
修改成如下所示,将 PIE 保护关闭。温馨提示: exp.py
中的 magic 变量值是通过反汇编(如,将可执行文件拖入 IDA) ,查看函数的起始位置得到的。其它具体的实现过程参照示例即可。
示例代码架构与利用步骤如下:
UAF 在此题的体现:首先申请两个 node,然后删除两个 node。两个 node 共计四块 chunk 被加入到 tcache 中管理。此时再申请一个新的 node,如果申请的 size 得当,glibc 就会将 node0 与 node1 的两块 printnote 分配给新的 node 使用,注意到 printnote 是一个函数指针!此时 content2 是从标准输入读入的,即可以被用户所控制的。那么先往 content2 写入 magic 函数地址,再利用 node0(此时为 dangling pointer)访问 content2 这块区域,RIP 就会访问 magic,成功实现控制流的劫持。
该文内容较杂,因此采用分点记录的方式。
该文总结了 allocator 的实现机制对堆利用的影响;
kernel 中两种常见的动态分配机制如下:
General-purpose allocations performed using kmalloc/kzalloc/...
API
Special-purpose allocations via kmem_cache_create/kmem_cache_alloc
Slab 是 Linux 内核中的一种内存分配机制,用于高效地管理内核数据结构和对象的内存分配和释放。Slab 分配器的主要目标是减少内存分配和释放的开销,以提高系统性能。Slab 分配器通过预分配一定大小的内存块并将它们组织成 slabs,然后在需要时分配这些内存块,从而减少了内存碎片和频繁的内存分配操作。Slab 分配器通常管理多个 slab 缓存,每个缓存用于不同大小的内存块。这些缓存通常包括三种类型:
Slab缓存:用于保存已分配的内存块。
Slab未分配缓存:用于保存已分配但尚未使用的内存块,以便后续分配。
Slab空闲缓存:用于保存完全未使用的内存块。
对齐问题的潜在利用:攻击者可以利用对齐问题来修改数据结构的大小,进而改变其行为。例如,他们可能故意构造一个对象,以便它与某个通用缓存别名化,从而可以操纵通用缓存并导致不安全操作。但安全机制近年来也在不断地更新:
cred_jar
在 kernel version > 4.4 的情况下加入 SLAB_ACCOUNT 标签,使得其 standalone 免受其攻击。
安全的函数(copy_to_user
/ copy_from_user
)被逐步引入,更加严格的检测机制使漏洞利用难以下手。
A freelist pointer randomisation CONFIG_SLAB_FREELIST_RANDOM
was introduced in 4.8 and is now enabled by default on most modern distributions.
If there is a previously-freed chunk of memory, and that chunk is big enough to service the request, the heap manager will use that freed chunk for the new allocation.
Otherwise, if there is available space at the top of the heap, the heap manager will allocate a new chunk out of that available space and use that.
Otherwise, the heap manager will ask the kernel to add new memory to the end of the heap, and then allocates a new chunk from this newly allocated space.
mmap
provides greater flexibility, can map files or anonymous memory, and can map memory to non-contiguous addresses. This helps overcome some limitations of brk
, such as heap fragmentation.