Featured image of post PWN-3

PWN-3

二进制漏洞与利用——堆利用

[TOC]

前文:以下是我入门PWN的记录,欢迎各位前来观看,小弟领教!

在前面的章节中,我们已经粗略的了解了栈相关的知识点: ROP、shellcode、栈转移等。

内存中还有一块区域叫做堆(heap)。

接下来的一整章的内容,我们就来讲堆。

堆章节比较抽象,我还刻意加了很多图,但依旧比栈章节要更加抽象…,以至于作者也学的迷迷糊糊,看不懂的欢迎来评论区讨论。

Linux动态内存管理机制

什么是堆?+

堆是程序用于分配动态内存的一段内存区域。 他独立的存在于内存中,介于程序内存 基地址和libc地址之间,从低地址向高地址生长,与用户打交道最多。

带你举个例子理解动态分配内存:

开一个统计表格,每人最多输入的最大内存长度是4096个字节,按照以往惯性思维去分配的话,你是不是应该每人都在之前填表时就已经分好了内存给他们了,但不是每个人都能用完这4096个字节的,有些人只输入一个字节,那就意味着就有4095个字节的内存区域被浪费,如果每个人都这样做或输入少于最大字节长度的内存的数据,是不是浪费更多?

对于这种现象的下一步应该是回收,怎么回收?

那么,有没有一种方法,能让程序根据用户所需要的内存长度大小来分配内存呢?并且不需要我们管理内存呢?有的,在libc中,我们可以通过malloc(size)来给用户分配一段长度为size的内存,通过free(ptr)来释放这段内存区域。 这些数据,被统一的存放在了堆中,维护这些数据的运行机制在glibc中,称之为ptmalloc

堆的内存管理机制

堆是真无敌恶心….堆的管理机制相比于栈十分复杂,可以算是对我这样0基础的萌新杀手。

但是堆的漏洞比栈有更多的形式和利用方式,而且堆漏洞所需要的条件比栈更 少。 一般情况下栈溢出起码需要16个字节,也就是至少溢出到返回地址才能利用,但是堆的话只需要一个字节就可完成利用,甚至这个字节可以是个\x00,也就是空字节,nullbyte。

栈的话基本都会关闭一两个保护机制,堆的话一般全开。

CTFpwn的主流是堆利用。

堆块介绍

chunk

在了解ptmalloc的内存管理机制前,我们先了解一下堆块在内存中的存储形式。在内存中,堆是以一个个堆块构成的,这些堆块称之为chunk。

在64位系统中,堆块的大小是8字节对齐的,也就是说,我们申请一个15字节长度的堆块,实际到我们手中的用户可控的数据区域大小为16字节。

但是在管理中,一个堆块除了用户数据区外,还有头部字段,头部字段的长度为16字节。同时在64位系统中,一个堆块最小长度为32字节(包括头部),也就是说,我们分配一个1字节的堆块,他的实际长度是32字节。

堆块结构图

prev_size和size字段分别代表一个chunk对的大小,大小都是8字节,两个一共16字节,称之为chunk的头部字段。后面的user data区域是用户可以输入数据的地方。

chunk的大小8字节对齐,所以说对于分配器来说,0x80、0x81、0x82大小的堆块都是一样的,都为0x80大小。

可能你举得有点懵,但这是一种简化表述,核心是堆块大小必须满足 8 字节对齐,所以分配器会把不满足的大小调整到最近的 8 字节对齐的大小,这样就会出现多个请求大小对应同一个实际分配大小的情况。

为了节省空间,将size的最低三个bit设置为三个标志位。

从高到低分别为non_main_arenais_mmapprev_inuse,这里分别解释一下:

non_main_arena用来记录当前chunk是否不属于主线程,1表示不属于,0表示属于。

is_mmap表示当前chunk是否由mmap分配的,1表示属于,0表示不属于。

prev_inuse用来表示前面紧邻的那个chunk是否正在使用,0表示前面的chunk已经被释放,1表示正在被用户使用。

prevsize记录前面一个chunk的大小。这里注意,prevsize只有在前面的chunk被free掉的时候才生效,也就是说,只有在prev_inuse为0的时候,系统才把prev_size字段当作prevsize。

那么其他时候这个字段有用吗?没用的话不就浪费了八个字节?

有用的!如果chunk正在被使用,那么他会把下一个chunk的prevsize字段当作userdata。充分利用空间。

也就是说,如果我们申请一个0x80长度大小的区域,系统实际给我们0×90大小(0x10头部),如果我们申请0x88大小的区域,系统同样也会给我们0x90大小的区域(算头部),剩下的8字节,使用nextchunk的prevsize区域。因为,只有当一个chunk被释放的时候,nextchunk的prevsize才真正代表前一个chunk的大小,所以就这么设计了。


topchunk

topchunk是一个特殊的chunk,类似于bss字段未被分配的内存一样,比较原始的。

最开始时,程序的堆还未被使用,整个的堆区域属于一个很大的堆块叫做topchunk。当已经被使用的空间不够时,程序就会从topchunk中分割一块出来个程序使用。

堆块的管理

为了保证程序的快速运行,而且方便系统内存管理,所以ptmalloc将释放后的堆块根据其大小分成不同的bin。

fastbin:大小范围从0x20-0×80

smallbin:大小范围:0×90-0x400

Large bin:大小范围:0x410以上

unsortedbin:未被归类的bin,临时存储用,存放的堆块大小不一定多大,后续详细介绍。

chunk被free之后如图:

堆释放示意图

由于chunk被free了,所以按常理说用户不应该能够访问到这个chunk。于是乎在userdata区域存放一些用于管理内存的指针信息。

fastbin:单链表结构,只会用到fd这个指针;

small &unsortedbin:双向链表结构,fd和bk都用;

largebin:双向链表,fd、bk都用,同时还会用fd nextsize和bk nextsize。

堆块的合并操作

如果我们free掉一个堆块,(可能)会触发向前合并和向后合并。

堆

向前合并:检查当前chunk的prev inuse位,如果为0,则根据当前chunk的prev size找到prev chunk的头,两个堆块合并;

向后合并:检查当前chunk的next next chunk的prev inuse位(因为一个堆块的状态由他后面chunk的prev inuse位决定,所以确定next chunk的状态需要检查next next chunk的prev inuse位,怎么找? size就行),然后根据next chunk的状态决定是否合并。

变化示意图:




这四张图看得有点懵?说人话就是prev_inuse=0说明前面chunk被释放了,呃身体都没了你要头有什么用?我的话过重了…身体(chunk)被释放free没了应该头也跟着一起灰飞烟灭!!一起消失掉,和其它堆合并。(先向前合并再考虑向后合并

“合并” 的关系

向前合并(关键关联 prev_inuse = 0

若当前堆块的 prev_inuse = 0 时,说明前一个堆块是空闲的。此时,当前堆块可以:

1、读取前一个堆块的头部信息(通过当前堆块的 prev_size 字段,获取前一个堆块的大小)。

2、将 “当前堆块” 与 “前一个空闲堆块” 合并,形成一个更大的空闲堆块,减少内存碎片。

向后合并(与 prev_inuse 无直接关联)

1、“向后合并” 是指当前空闲堆块后一个相邻的空闲堆块合并,其判断条件是:后一个堆块是否为空闲(通过检查后一个堆块的 prev_inuse 标志)。

For instace,当前堆块是空闲的,若后一个堆块的 prev_inuse = 0(说明后一个堆块认为 “前一个堆块(即当前堆块)是空闲的”),则可以进行向后合并。

2、可见,“向后合并” 的核心是后一个堆块的 prev_inuse 标志,而非当前堆块的 prev_inuse

考虑到相对关系,简单来说对于当前你要讨论的堆头部来说,它的prev_inuse=0时,它本身就应该和前面的chunk合并。

而“向后合并” 的触发条件是后一个堆块的 prev_inuse = 0(表示后一个堆块允许与前一个堆块 —— 即当前堆块 —— 合并)。

结构体

aren

是一块结构体,用于管理bins。主线程创建的arena称之为main_arena,其他的叫threadarena。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct malloc state
{
	/* Serialize access.*/
	mutex_t mutex;
	int flags;
    /*Fastbins*/
	mfastbinptr fastbinsY[NFASTBINS];
	/* Base of the topmost chunk -- not otherwise kept in a bin*/
	mchunkptr top;
	/* The remainder from the most recent split of a small request */
	mchunkptr last remainder;
    /*Nromal bins packed as described above*/
	mchunkptr bins[NBINS * 2 - 2];
    /*Bitmap of bins*/
	unsigned int binmap[BINMAPSIZE];
    /*Linked list*/
	struct malloc state *next;
    /*Linked list for free arenas.*/
	struct malloc state *next_free;
	/* Memory allocated from the system in this arena.*/
	INTERNAL_SIZE_T_system_mem;
	INTERNAL_SIZE_T_max_system_mem;
}

各种内存块介绍

各种bins:

Fastbin

管理fastbin free chunk,单链表结构,FILO(最后一个进入fastbin链表的,会被放在头部)总共有十个fastbin链表,每个链表中fastbin的size一样,0x10递增。

大小属于fastbin的chunk被free掉时,不会改变next chunk的prev inuse位,也就是说不会被合并。

Fastbin

Unsortedbin

管理unsorted chunk,只有一个双向链 表。所有大小大于fastbin的chunk都会先被暂 时放入unsortedbin中,链表中的chunk大小不一样。

*注意:这里的指向箭头比较多

Smallbin

管理small chunk,由62个双向链表组成, 每个链表中的chunk大小一样,大小以0x10递 增。长得和unsortedbin差不多的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
+-------------------+      +-------------------+      +-------------------+
|     smallbin      | <--> |   free chunk 1    | <--> |   free chunk 2    | <--> ...
|  (size = 0x20)    |      |                   |      |                   |
+-------------------+      +-------------------+      +-------------------+
         |
         v
+-------------------+      +-------------------+      +-------------------+
|     smallbin      | <--> |   free chunk 1    | <--> |   free chunk 2    | <--> ...
|  (size = 0x30)    |      |                   |      |                   |
+-------------------+      +-------------------+      +-------------------+
         |
         v
       ... (更多 smallbin,大小递增)
         |
         v
+-------------------+      +-------------------+      +-------------------+
|     smallbin      | <--> |   free chunk 1    | <--> |   free chunk 2    | <--> ...
|  (size = 0x3F0)   |      |                   |      |                   |
+-------------------+      +-------------------+      +-------------------+

Largebin

管理large chunk,63个双向链表,FIFO。同一个双线链表中chunk大小可以不一样,但是在一定范围内,bins大小从小到大排列。

在此我们先不学习larbin attack的相关内容,所以看看就行,了解一下。比较少遇见Largebin的相关题。

Malloc运行流程

了解完各种bin之后,现在来看看这:

一、当我们调用malloc时,程序都干了些什么?

1、计算真正的堆块的大小(加上堆头部长度、对齐):

判断是否在fastbin范围内:

  • 确定在,检查对应大小的bin链表中有无chunk。

    • 有,那就分配给用户,至此完成。
  • 如果不在fastbin范围内,或者没用chunk可用。(两者满足一个或者都满足的话)

    • 继续判断是否在smallbin范围内:

      • 在smallbin范围内,检查对应大小的bin链表中有无chunk。
        • 有chunk,那就取出来给程序,至此完成。
      • 不在smallbin范围内,或者smallbin里面也没有chunk。这时候跳到unsortedbin的检查。
    • unsortedbin中有无chunk?

      • 有,从尾部取出第一个chunk,看看大小是否满足需求。

        • 满足,切分后大小是否大于minsize
          • 大于,再切分块,返回给用户,剩下的块放进unsortedbin。
          • 小于或等于minsize,直接返回给用户,完成。
        • 不满足大小需求,把这个块放入smallbin / largebin对应的链表中,继续遍历下一个块。
      • 没。unsortedbin的所有块都不满足,那此时就判断是否在largebin范围。

        • 是,检查对应的bin链表中有无符合的chunk。
          • 有符合的,找到满足需求最小的chunk,切分块返回,剩下的放进unsortedbin中。
        • 不在,那就再次遍历smallbin / largebin找best fit的chunk。
        • 我去?还是没用,那就从topchunk中切割。
        • ??搞什么鬼??topchunk也不够?那就mmap系统调用

二、当我们调用了free时,程序都干了些什么?

free的chunk大小属于fastbin吗?

  • 是,放进fastbin,至此完成。

  • 不属于,那就接着判断这个free的chunk是否是mmap分配的。

    • 是,那就调用munmap回收,完成。

    • 不是,那就接着判断前一个chunk是否是空闲的。

      • 是,那就向前合并

      • 不是,接着判断:后一个chunk是topchunk吗?

        • 是,那就和topchunk合并,至此完成。

        • 不是topchunk,那就判断:后一个chunk是free的吗?

          • 是,那就向后合并,然后放进unsortedbin,终于完成了。

堆动态保护机制的小总结

我的妈啊,难死我了,堆的内存管理机制比较复杂,多刷刷题吧,受不了了,刚开始就上强度了😫。

我这里有一个发自内心的疑问:堆的本质是指针指来指去吗?O(∩_∩)O

堆的本质可以理解为通过指针操作管理动态内存的机制,但 “指针指来指去” 只是其表现形式之一,更深层的本质是内存块的分配、释放和复用规则,以及攻击者通过破坏规则实现漏洞利用的过程。理解堆的核心在于掌握内存块的组织结构、堆管理器的行为逻辑,而非单纯的指针操作。

堆的调试方法

最后更新于 2025-10-04