pwn之堆学习笔记

有关堆的基础知识:

Linux堆内存管理深入分析(上)

关于heap overflow的一些笔记:

https://etenal.me/archives/1121#top_chunk

heap堆结构及相关漏洞类型:

https://joyceqiqi.wordpress.com/2017/05/30/heap%e5%a0%86-%e5%9f%ba%e7%a1%80%e7%9f%a5%e8%af%86/

PWN之堆内存管理:

https://paper.seebug.org/255/

Dance In Heap(一)浅析堆的申请释放及相应保护机制:

http://www.freebuf.com/articles/system/151372.html

堆操作可视化工具:

https://github.com/wapiflapi/villoc

有关堆的实践练习题

how2heap总结-上:

https://www.anquanke.com/post/id/86808

PWN学习之house of系列(一):

https://paper.seebug.org/521/

how2heap-01 first fit实践笔记:

https://vancir.com/2017/08/04/how2heap-01-first-fit/

image.png

** 各种流程图(来自http://www.cnblogs.com/shangye/):

glibc-2.23_large_bin_链接方式浅析

glibc-2.23_int_free_流程浅析

glibc-2.23_int_malloc_流程浅析

利用知识点:

malloc_chunk 的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /*记录前一个chunk的大小*/
INTERNAL_SIZE_T size; /* 记录本chunk的大小(包括头部) */

struct malloc_chunk* fd;
struct malloc_chunk* bk;

struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
};

一般来说,size_t 在 64 位中是 64 位无符号整数,32 位中是 32 位无符号整数。

  • prev_size, 如果该 chunk 的物理相邻的前一地址chunk(两个指针的地址差值为前一chunk大小)是空闲的话,那该字段记录的是前一个 chunk 的大小(包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk
  • size,该 chunk 的大小,大小必须是 2 SIZE_SZ 的整数倍。如果申请的内存大小不是 2 SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk 的大小没有影响,它们从高到低分别表示
    • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1表示不属于,0表示属于。
    • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的P位都会被设置为1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲chunk之间的合并。
  • fd,bk, chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适chunk 时挨个遍历。

相关机制规则

一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前chunk使用。这就是chunk中的空间复用

最小申请的堆内存大小

用户最小申请的内存大小必须是 2 * SIZE_SZ 的最小整数倍,也就是每个chunk至少得包含fd和bk

global_max_fast

ptmalloc 默认情况下会调用 set_max_fast(s) 将全局变量 global_max_fast 设置为 DEFAULT_MXFAST,也就是设置 fast bins 中 chunk 的最大值。当 MAX_FAST_SIZE 被设置为 0 时,系统就不会支持 fastbin 。

因此有可能通过操作这个全局变量来实现将fastbin的范围改大,从而实现骚操作

unsorted bin的来源

unsorted bin 处于我们之前所说的 bin 数组下标 1 处。故而 unsorted bin 只有一个链表。unsorted bin 中的空闲 chunk 处于乱序状态,主要有两个来源

  • 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
  • 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中

初始情况下,我们可以将 unsorted chunk 作为 top chunk,初始状态下unsorted chunk存的是top chunk的地址

只有不是 fast bin 的情况下才会触发unlink

为了避免heap中有太多零零碎碎的内存块,合并之后可以用来应对更大的内存块请求。合并的主要顺序为

  • 先考虑物理低地址空闲块
  • 后考虑物理高地址空闲块

合并后的 chunk 指向合并的 chunk 的低地址。

分配空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                      Heap
high address +-------------------+
| |
| Unusable Region |
| |
+-------------------+ <--- rlimit
| |
| |
| Unmapped Region |
| |
| |
+-------------------+ <--- break
| |
| Mapped Region |
| |
low address +-------------------+ <--- start of heap

若请求的空间小于 mmap 分配阈值(mmap threshold,默认值为 128KB)时,主分配区会调用 sbrk()增加一块大小为 (128 KB + chunk_size) align 4KB 的空间作为 heap。

非主分配区会调用 mmap 映射一块大小为 HEAP_MAX_SIZE,32位系统上默认为1MB(0x100000字节),64位系统上默认为64MB(0x4000000字节)的空间作为sub-heap。

注意: 当用户请求的内存大于128KB时,并且没有任何arena有足够的空间时,那么系统就会执行mmap函数来分配相应的内存空间。这与这个请求来自于主线程还是从线程无关。


malloc的流程

计算真正的chunk大小(用户请求+堆头 –> 对齐)

  • 是否在fastbin的范围内?

    • 是,检查fastbin中有没有对应的bin可以用

      • 有,返回该bin,完事

不在fastbin的范围或者没有找到bin,继续下一步判断

  • 是否在small bin范围内?
    • 是,检查small bin中有没有对应bin可以用?
      • 有,返回该bin,完事

不在small bin的范围或者没有找到bin,继续下一步判断

判断是否有堆块在unsorted bin中?

  • 有bin存在,从尾部取出第一个bin,检查是否满足需求
    • 满足大小,判断切割后的chunk是否大于最小chunk所需大小?
      • 是,进行切割返回所需大小,剩余部分留在unsorted bin
      • 否,直接返回这块bin
    • 不满足,将此bin放入对应的small/large bin中,继续遍历下一块bin

遍历完后,首先会造成unsorted bin中不满足的bin被放入其他链表,这里通过有意构造,可以使得一个fastbin被double free

unsorted bin中没有bin或者找不到满足需求的chunk,继续下一步判断

判断是否在large bin范围内?

  • 是,检查large bin中有没有对应bin可以用?
    • 有,找到满足需求的最小chunk,切割后返回给用户,把剩余的部分放入unsorted bin

这里会造成被切割剩余的fastbin/smallbin被放入unsorted bin中

large bin中没有bin或者找不到满足需求的chunk,继续下一步判断

这时,再次浏览所有的small bin和large bin,检查是否有满足的堆块

为什么要这样做?通过前面的流程分析,(以及下面free的过程)其实可以发现,bins最开始都是在fastbin和unsorted bin中的,通过在malloc的过程中被链接到small 、 large bin中,因此这里可能存在某些漏网之鱼,需要再次遍历检查

这时实在没有办法了

就通过切割 top chunk 来取得chunk,一般在程序第一次执行malloc的时候就是直接切割top chunk

如果Top chunk也不能满足需求,就调用系统调用来扩充堆的空间

free的流程

  • 首先判断大小
    • 若是fastbin范围内的,放入fastbin中,不改变p标志位
    • 大小大于fastbin
      • 该chunk是通过mmap分配的吗?
        • 是,则用mmap回收
      • 相邻的前一个chunk是free状态吗?
        • 是,和前一个chunk合并
      • 相邻的后一个chunk是top chunk吗?
        • 是,和top chunk合并成为新的top chunk
      • 相邻的后一个chunk是free状态吗?
        • 是,和后一个chunk合并
      • 链入unsorted bin,完事

fastbin

单链表结构,FILO,共有10个bins,每个bin的大小是一样的

32位中相邻的两个bin大小差距8个字节;在64位系统中,则是差距16个字节。

1
2
3
4
5
6
#define DEFAULT_MXFAST (64 * SIZE_SZ / 4)
#endif
//默认的最大fastbin是64字节(32位),128字节(64位)

/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)

ptmalloc 默认情况下会调用 set_max_fast(s) 将全局变量 global_max_fast 设置为 DEFAULT_MXFAST,也就是设置 fastbins 中 chunk 的最大值。当 MAX_FAST_SIZE 被设置为 0 时,系统就不会支持 fastbin

范围:0x10~0x40(32位),0x20~0x80(64位)

以32位为例子,0~9下标的bin对应的大小为:0x10,0x18,0x20,0x28,0x30,0x38,0x40,[0x48,0x50,0x58]

以64位为例子,0~9下标的bin对应的大小为:0x20,0x30,0x40,0x50,0x60,0x70,0x80,[0x90,0xa0,0xb0]

所以默认情况下,fastbin数组的最后3个bin是不会存储数据的

因此,可以想到的是,如果有办法修改global_max_fast,那么fastbin的范围会大大增加,使得大的chunk也能按照fastbin的处理方法进行malloc和free

fastbin是单链表结构,且属于FILO的链接方式,相对应一个栈的结构,可以利用该特点,来构造分配chunk 的位置

在链入表和取出的时候都是在表头进行,同一个bin中每个chunk靠fd链接

这里的fd表示前一个被free的chunk而不是物理地址的前一个chunk


unsorted bin

双向链表结构,只有一个bin(实际上占bins[0]和bin[1],作为表头),Unsorted bin中的chunk大小不一样

FIFO,在链入unsorted bin时,是加入到头部,在取出的时候是从尾部取出


smallbin

堆漏洞的系列操作

first fit

在分配内存时,malloc 会先到 unsorted bin(或fastbins) 中查找适合的被 free 的 chunk,如果没有,就会把 unsorted bin 中的所有 chunk 分别放入到所属的 bins(small_bins和large_bins)中,然后再去这些 bins 里去找合适的 chunk。

fastbin attack

double free

libc-2.23 中对 double-free 的检查过程如下:

1
2
3
4
5
6
7
/* Check that the top of the bin is not the record we are going to add
(i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}

它在检查 fast bin 的 double-free 时只是检查了第一个块。所以其实是存在缺陷的。

例子:

三个堆块abc,free完a后free b再free a

这时:fastbin –> a –> b –> a 箭头为fd所指方向

malloc:a,malloc:b后,修改a的fd

此时:fastbin –> a –> fd
接着
malloc:a
malloc:fd ,就能将内容写到fd所指的地址中去,这里只要保证,fd[1] (size位)是属于fastbin的就行了

除了上面的double free方式以外,还有这种:

在free 掉一个fastbin的chunk1后,首先放入fastbin

再次申请一个大于chunk1时,会在查找合适大小的chunk的时候被分配到small bin里面去

这时,chunk1在small bin就不会受到fastbin的检测,还能在free一次到fastbin中

PS:在 libc-2.26 中,即使两次 free,也并没有触发 double-free 的异常检测,这与 tcache 机制有关

hosue of Einherjar

原理:

在free(p)的时候,首先是通过p的size位判断,前一个chunk是否空闲

发现标志位p为0,说明空闲,那么向前合并,通过pre_size位找到fake chunk,如果pre_size很大或者很小,就会寻找到非堆空间的区域去

接着进行合并,由于与top chunk相邻,于是又和top chunk合并一次,导致top chunk被搞到 非堆空间中

这时再次malloc的时候,就会把chunk分配到栈,bss,got等地址了

关键点,在于首先改好fake chunk的size ,通过off by one ,修改chunk b 的pre_size为fake_size

通过检查:size(P) == prev_size(next_chunk(P))

利用条件:

1、可以改chunk的pre_size

2、在可控空间构造fake chunk并保证size对应一致

house 0f forces

原理:

利用改写top chunk的size ,使得无论分配多大的chunk,都是由top chunk进行切割

这时候如果申请一个超大的chunk,就会使得下一次分配的地址往高低地址或者地址的空间写

原因是,size在malloc里面是无符号的数,容易造成溢出

利用条件是:

1、能改top chunk的size

2任意大小分配堆块

House of Lore

利用small bin的缺陷,在取出bin时,只检查了bck->fd != victim)

导致可以构造smallbin的bk,使得任意地址分配chunk

如果在栈里面分配,还能使得修改ret进行跳转

利用条件是:

1、可改位于链表尾部的small bin的bk

2、需要构造两处bck->fd= victim来绕过检查

overlap chunks

溢出覆盖chunk是堆风水排布的关键所在,巧妙利用这一操作,对解题有很大的帮助

example 1

假设有三个chunk: a,b,c,大小均为0x100,属于small bins范围

free b,这时b先进入unsorted bin

此时如果能有办法修改b的size,将其改为0x200

同时malloc 0x200,那么这时新的chunk d 的空间就会包含chunk b,c

从而使得chunk d可以任意修改和泄漏chunk c的所有内容

example 2

假设5个chunk:a ,b,c,d,e,大小均为0x100

free d,这时d先进入unsorted bin

修改b的size为0x201,再free掉

这时,ptmalloc会认为b和d是相邻的,因此在free掉b的时候,就让b d合并了

这时再malloc(0x300)的时候,新的chunk f的空间实际上就是chunk b+c+d

这样一来,chunk f 可以任意修改和泄漏chunk b、c、d的所有内容

unsorted bin attack

这里主要有两个用处:

一、泄漏libc,在将一个 不属于fastbin的chunk free掉的时候他会首先进入到unsorted bin

并且在fd和bk的字段中写入arean的地址,从而方便泄漏出libc

二、任意地址改数值

先举个例子吧

如果此时有两个chunk a,b,大小均为0x100

free掉a后,a进入unsorted bin,如果这时有办法修改a的bk使其指向一个地址target

那么再进行一次malloc(0x100)的时候,由于unsorted bin中有满足条件的chunk

会执行以下代码,把a取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
bck = victim->bk;
if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
size = chunksize(victim);

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
/* 显然,bck被修改,并不符合这里的要求*/
if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
....
}

/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);

简单总结就是执行了:

  • victim = unsorted_chunks(av)->bk = p //取unsorted bin中的最后一个chunk,也就是我们的chunk a
  • bck = victim->bk=p->bk = target addr-16 // 设置bkc=a->bk
  • unsorted_chunks(av)->bk = bck=target addr-16 // 把unsorted bin的bk设置为bck
  • bck->fd = *(target addr -16+16) = unsorted_chunks(av) //把bck->fd 设置为 unsorted bin的地址

如果我们一开始就设置chunk a的bk是target addr -16,那么最终会导致target addr被赋值为一个很大的数(也就是unsorted bin的地址)

那有啥用呢?

  • 我们通过修改循环的次数来使得程序可以执行多次循环。
  • 我们可以修改 heap 中的 global_max_fast 来使得更大的 chunk 可以被视为 fast bin,这样我们就可以去执行一些 fast bin attack 了

但就需要注意,这个改的值只能保证是一个很大的值,却不能受我们控制

img

同时也可以用这种方式把chunk分配到栈里面

off_by_one

也就是溢出一个字节的各种操作,大概分以下几种:

  • 扩展被释放块:当溢出块的下一块为被释放块且处于 unsorted bin 中,则通过溢出一个字节来将其大小扩大,下次取得次块时就意味着其后的块将被覆盖而造成进一步的溢出
  • 扩展已分配块:当溢出块的下一块为使用中的块,则需要合理控制溢出的字节,使其被释放时的合并操作能够顺利进行,例如直接加上下一块的大小使其完全被覆盖。下一次分配对应大小时,即可取得已经被扩大的块,并造成进一步溢出
  • 收缩被释放块:此情况针对溢出的字节只能为 0 的时候,也就是本节所说的 poison-null-byte,此时将下一个被释放的块大小缩小,如此一来在之后分裂此块时将无法正确更新后一块的 prev_size 字段,导致释放时出现重叠的堆块
  • house of einherjar:也是溢出字节只能为 0 的情况,当它是更新溢出块下一块的 prev_size 字段,使其在被释放时能够找到之前一个合法的被释放块并与其合并,造成堆块重叠

house-of-spirit

house-of-spirit是一种 fastbins 攻击方法,通过构造 fake chunk,然后将其 free 掉,就可以在下一次 malloc 时返回 fake chunk 的地址,即任意我们可控的区域。

house-of-spirit 是一种通过堆的 fast bin 机制来辅助栈溢出的方法,一般的栈溢出漏洞的利用都希望能够覆盖函数的返回地址以控制 EIP 来劫持控制流,但如果栈溢出的长度无法覆盖返回地址,同时却可以覆盖栈上的一个即将被 free 的堆指针,此时可以将这个指针改写为栈上的地址并在相应位置构造一个 fast bin 块的元数据,接着在 free 操作时,这个栈上的堆块被放到 fast bin 中,下一次 malloc 对应的大小时,由于 fast bin 的先进后出机制,这个栈上的堆块被返回给用户,再次写入时就可能造成返回地址的改写。

当然也不一定是仅用于改写栈的内容,可根据题目的具体灵活运用,改bss,函数got表等等

所以利用的第一步不是去控制一个 chunk,而是控制传给 free 函数的指针,将其指向一个 fake chunk。所以 fake chunk 的伪造是关键。

另外需要注意的是:

伪造 chunk 时需要绕过一些检查,首先是标志位,PREV_INUSE 位并不影响 free 的过程,但 IS_MMAPPED 位和 NON_MAIN_ARENA位都要为零。其次,在 64 位系统中 fast chunk 的大小要在 32~128 字节之间。最后,是 next chunk 的大小,必须大于 2*SIZE_SZ(即大于16),小于 av->system_mem(即小于128kb),才能绕过对 next chunk 大小的检查。

首先主要是绕过一个检查机制:

1
(FD->bk != P || BK->fd != P

然后通过构造可控制的chunk的内容,伪造一个fake chunk 并且该chunk被伪造为free状态,如何伪造?通过修改下一个chunk的size的p位为0,即可,构造fakechunk 的fd和bk绕过检查

接着就可以达到这样的效果:

1
2
FD->bk = BK
BK->fd = FD

这时如果

就能改写 P 指针自身的地址,从而造成任意内存写入。若允许堆 P 进行读取,则会造成信息泄漏

tcache

tcache 全名 thread local caching,它为每个线程创建一个缓存(cache),从而实现无锁的分配算法,有不错的性能提升。libc-2.26 正式提供了该机制,并默认开启

值得注意的比如每个线程默认使用 64 个单链表结构的 bins,每个 bins 最多存放 7 个 chunk。

chunk 的大小在 64 位机器上以 16 字节递增,从 24 到 1032 字节。

32 位机器上则是以 8 字节递增,从 12 到 512 字节。

所以 tcache bin 只用于存放 non-large 的 chunk。

然后引入了两个新的数据结构,tcache_entrytcache_perthread_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 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;
} 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
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread tcache_perthread_struct *tcache = NULL;

tcache_perthread_struct 包含一个数组 entries,用于放置 64 个 bins,数组 counts 存放每个 bins 中的 chunk 数量。每个被放入相应 bins 中的 chunk 都会在其用户数据中包含一个 tcache_entry(FD指针),指向同 bins 中的下一个 chunk,构成单链表。

大概是这样的

img

将chunk放入tcache的情况:

一、free时,在 fastbin 的操作之前进行,如果 chunk size 符合要求,并且对应的 bins 还未装满,则将其放进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);//取bins的下标

if (tcache
&& tc_idx < mp_.tcache_bins//检查size是否在bin的范围内
&& tcache->counts[tc_idx] < mp_.tcache_count)//检查数量是否超过7个
{
tcache_put (p, tc_idx);//满足条件,进入tcache 中
return;
}
}
#endif

二、

堆tips

  • 检查是否有UAF漏洞,也就是看malloc的时候和free的时候对应的堆指针有没有可能重复使用
  • 利用fastbin中的double_free来泄漏一个地址的内容,或者实现往该地址中写入内容
  • 一般进行堆的各种操作的时候都需要先建立一个堆来防止后面free操作的时候与top_chunk合并
  • 利用unsorted bin的fd或者bk进行泄漏libc或者堆地址
  • 利用unlink漏洞进行任意地址写,前提是非fastbin,需要知道heap地址和可利用的uaf指针
  • 修改top chunk的大小,进行下一次分配时可造成任意地址写

堆的题目一般如果要getshll的话,基本上通过改got表进行操作,如果是改函数的got表为system,则还需要参数“/bin/sh”,或者将某些程序中调用程序的真正地址改为one_gadget(比如malloc_hook)

这里有一篇神仙的极其详细的总结,看了以后自愧不如:https://xz.aliyun.com/t/2307

持续更新ing。。。。。。。