曾听过某大佬讲,pwn的等级大致分为三种,栈,堆,内核。

这篇文章总结了我目前对堆的理解,肯定不够深入,不过我将更注重于偏抽象和本质的东西,希望各位看完能有些不同的收获。

堆题小结

就我目前的理解来说,堆和栈有个很不同的地方。

栈的漏洞经常是可以栈溢出直接改变函数返回地址,通过pop|ret做到连续的劫持程序流以及修改寄存器的值。

堆的漏洞经常会导致任意已知地址写或泄露(这一点具体原因等会说) ,对于程序流的劫持经常是通过改got表实现(如果relro保护开了那则经常是改写mallocrealloc函数中的hook来做到劫持程序流,通常只能劫持程序流一次。onegadget在这种情况就有用武之地了。

以及堆的题目经常都是通过破坏堆结构,让你能使用到已被释放的堆,用到本来你不应该用的的堆。

总体来说,堆题劫持程序流比起栈题更加不自由,难度更大,且劫持之后收益也更小(堆的已知地址任意写一般都是用来改gothook,,因为很少能泄漏到栈地址。)

当然,这些都是我目前的理解,实际上我做过的堆题也不多,只是感觉堆题似乎是这样的。

堆概述

首先,堆只分两种,binchunkchunk是使用中的堆,bin是释放后的堆。

chunk管理

这里有一个特殊机制说明一下,任何的申请的堆size都会被调整成整SIZE_SZ*2的整数(SIZE_SZ是一个宏定义,在32位中是4,在64位中是8),这就导致size的最后三位bit必定为0,这三位都用来做一个标记位,前两个标记位我不记得是用来干嘛的了。但是最后一位很重要,它标记着物理地址的前一个chunk是否被使用)

chunk的管理方式很简单,基本只根据物理地址及size和prev_size和标记位管理。

图中两个chunk,黄色标记部分是表示这个chunk的大小size.蓝色部分标记的是前一个chunksize,也就是prev_size。但是图中两个chunk都在使用(堆管理器认为这个chunk在使用的根据是前面说的最后一bit的标记位。这时候每一个chunk都会借用下一个chunkprev_size,原因很简单,chunk只有在需要合并的时候才需要知道前一个chunk有多大,这个prev_size才能让堆管理器最快的找到前一个chunk判断是否能合并。而在使用时就没必要了。 (这个界面是vis指令调出来的,今天才发现我哥们不知道这个指令,所以特意说一嘴)

CTFpwn进阶之路从栈到堆---初步理解_CTFpwn

还是放出最经典的图吧(转自CSDN的Morphy_Amo)

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |Size of previous chunk 				|        	Size of chunk, in bytes|                     |A|M|P|
   mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         |             User data starts here...                          .
         .                                                               .
         .             (malloc_usable_size() bytes)                      .
         .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
         |Size of previous chunk 				|        	Size of chunk, in bytes|                     |A|M|P|
   mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

bin

bin那就可有的说了,不过我也不是很懂

首先,binchunk最大的差别就是bin通过链表来管理。而chunk不是。

所有的bin都是链表。通过fd和bk指针堆管理器可以快速索引到任何bin

fastbin

范围大致为0x20-0x80字节(64位)0x10-0x40字节(32位).这个大小包含size及prev_size

之所以说是大致,是因为不同libc版本似乎范围不一样。(不过这个影响倒不大,大概知道就行了)

fastbin采用单链表结构,这也导致了fastbin必然是先进后出

图中这个例子就能看出来,除了最后一个fastbin,其他fastbin都有指向下一个fastbin的指针.

CTFpwn进阶之路从栈到堆---初步理解_堆初步_02

这样操作就是为了方便管理。

举个很怪的例子。比如你班主任(堆管理器)为了能尽快联系到班上(fastbin)所有人(每一个bin),把班上所有人根据身高,分为fastbin[0]fastbin[9],比如身高160全部进入fastbin[0]。(因为前面说堆的大小分配只支持整的,对应到这个例子就是假设所有人身高都是整数,所以这里说的身高160其实是严格160)

而其中每一个进入fastbin[0]的人拥有下一个进入的人的联系方式(fd指针),直到最后一个人什么都没有,如果班主任(堆管理器)需要用到身高160(特定size的bin)的人做某件事(分配给用户内存),那么他就找第一个人,第一个人去做事之前,会把第二个人的联系方式给班主任(堆管理器)。

此外班主任(堆管理器)还拥有一个花名册(数组),上面记录着fastbin[0]到fastbin[9]的每个bin的大小,记录着每个bin的第一个人的名字。

这样,堆管理器就能再需要用到特定大小的fastbin时很轻松找到了。

small&largebin

之所以放在一起讲,是因为这两个玩意基本就尺寸的差异。

其实只要懂了fastbin基本上就能理解这两个了。

1.fastbin是单链表模式管理,采取先进后出。

small&largebin采取双链表管理,比fastbin多了一个bk指针,采取先进先出原则。

2.其次就是largebin再多了一个fdnextsize和bknextsize这样对管理器就不需要像fastbin那样,专门为其维护保存一个储存大小的数组。而是直接通过该指针就能找到不同大小的largebin

unsortedbin

各种乱七八糟的bin都有可能进入这个bin

具体待会在堆管理流程再说

tcachebin

这个是在比较新的libc版本才有的,有点像fastbin。并且当同样大小(符合fastbin的大小)的tcachebin有了七个之后,接下来的释放操作就会放进fastbin

不过讲真,tcachebin我真不懂,留个坑,以后再填吧?

堆管理机制

我也不太懂,留个坑,下次填。可以看看博客园的一篇文章。那里有张图很不错,但是我复制上来就变成压缩阉割版了。如果有兴趣建议去看看他的。

CTFpwn进阶之路从栈到堆---初步理解_堆初步_03