堆是程序用于分配动态内存的一段内存区域。
独立的存在于内存中,介于程序内存基地址和 bc 地址之间。
从低地址向高地址生长。
1.堆内存管理机制
一般情况下栈溢出起码需要 16 个字节,也就是至少溢出到返回地址才能利用,但是堆的话只需要一个字节就可完成利用,甚至这个字节可以是个 x00,也就是空字节,nullbyte。
栈的话基本都会关闭一两个保护机制,堆的话一般全开。
1.堆块(堆在内存中的存储形式)
在内存中,堆是以一个个堆块构成的,这些堆块称之为 chunk。
在 64 位系统中,堆块的大小是 16 字节对齐的,也就是说,我们申请一个 15 字节长度的堆块,实际到我们手中的用户可控的数据区域大小为 16 字节。
但是在管理中,一个堆块除了用户数据区外,还有头部字段,头部字段的长度为 16 字节。
同时在 64 位系统中,一个堆块最小长度为 32 字节(包括头部) ,也就是说,我们分配一个 1 字节的堆块,他的实际长度是 32 字节。
prev size 和 size 字段分别代表前一个 chunk 的大小以及当前 chunk 的大小,
大小都是 8 字节,两个一共 16 字节,称之为 chunk 的头部字段.
后面的 user data 区域是用户可以输入数据的地方
chunk 的大小 8 字节对齐,所以说对于分配器来说,
0x80、0x81、0x82 大小的堆块都是一样的,都是 0x80 大小。
为了节省空间,将 size 的最低三个 bit 设置为三个标志位。
从高到低分别为 non_main_arena、is_mmap、prev_inuse.
non main arena 用来记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于
is mmap 表示当前 chunk 是否由 mmap 分配的,1 表示属于,0 表示不属于。
prev_inuse.用来表示前面紧邻的那个 chunk 是否正在使用,O 表示前面的 chunk 已经被释放,1 表示正在被用户使用。
prevsize 记录前面一个 chunk 的大小。这里注意,prevsize 只有在前面的 chunk 被 free 掉的时候才生效,也就是说,只有在 prev_inuse 为 O 的时候,系统才把 prev_size 字段当作 prevsize。
在其他时间时如果 chunk 正在被使用则这个字段会被当做 userdata
也就是说,如果我们申请一个 0x80 长度大小的区域,系统实际给我们 0x90 大小(0x10 头部),如果我们申请 0x88 大小的区域,系统同样也会给我们 0x90 大小的区域(算头部),剩下的 8 字节,使用 nextchunk 的 prevsize 区域。因为,只有当一个 chunk 被释放的时候,nextchunk 的 prevsize 才真正代表前一个 chunk 的大小,所以就这么设计了。
2.特殊的堆块(chunk):topchunk
最开始时,程序的堆还未被使用,整个的堆区域属于一个很大的堆块叫做 topchunk。
当已经被使用的空间不够时,程序就会从 topchunk 中分割一块出来个程序使用。
3.堆块的管理
为了保证程序的快速运行,而且方便系统内存管理,所以 ptmalloc 将释放后的堆块根据其大小分成不同的 bin(本质是链表)。
-
fastbin:大小范围从 0x20-0x80
-
smallbin:大小范围:0x90-0x400
-
Large bin:大小范围:0x410 以上
-
unsortedbin:未被归类的 bin,临时存储用,存放的堆块大小不一定多大。
-
fastbin:单链表结构,只有 fd
-
small&unsortedbin:双向链表结构,fd 和 bk 都有
-
largebin:双向链表,fd bk 都有,同时还有 fd nextsize 和 bknextsize
4.堆块的合并
如果我们 free 掉一个堆块,(可能)会触发向前合并和向后合并。
向前合并: 检查当前 chunk 的 previnuse 位,如果为 0,则根据当前 chunk 的 prev size 找到 prev chunk 的头,两个堆块合并
向后合并: 检查当前 chunk 的 next nextchunk 的 prev inuse 位(因为一个堆块的状态由他后面 chunk 的 previnuse 位决定,所以确定 nextchunk 的状态需要检查 next next chunkl 的 previnuse 位,怎么找?size 就行),然后根据 nextchunk 的状态决定是否合并。
5.arena--堆块的管理结构体
本质是 结构体,用于管理 bins。
主线程创建的 arena 称之为 main arena,其他的叫 threadarena。
Fastbin
管理 fastbin free chunk,单链表结构 FILO(最后一个进入 fastbin 链表的,会被放在头部,即头插法)
总共有十个 fastbin 链表,每个链表中 fastbin 的 size 一样,0x10 递增
大小属于 fastbin 的 chunk 被 free 掉时,不会改变 nextchunk 的 orevinuse 位,也就是说不会被合并。
unsortedbin
管理 unsorted chunk,只有一个双向链表
所有大小大于 fastbin 的 chunk 都会先被暂时放入 unsortedbin 中,链表中的 chunk 大小不一
Smallbin
管理 small chunk,由 62 个双向链表组成每个链表中的 chunk 大小一样,大小以 0x10 递增。
结构与 unsortedbin 类似
Largebin
管理 large chunk,63 个双向链表,FIFO.
同一个双线链表中 chunk 大小可以不一样,但是在一定范围内,bins 大小从小到大排列。
6.调用 malloc 时,程序都在做什么
-
计算真正的堆块大小(加上头部对齐)
-
在 fastbin 范围内?
是,检查对应的 bin 链表中有没有 chunk
有,分配给用户,完成 -
如果不在 fastbin 范围内,或者没有 chunk 可用。
在 smallbin 范围内?
是,检查对应大小的 bin 链表中有没有 chunk
有,取出来给程序,完成。 -
如果不在 smallbin 范围内,或者 smallbin.里面也没有。
unsortedbin 中有没有 chunk?-
有的话,从尾部取出第一个 chunk,看看大小是否满足需求。
-
满足,切分后大小是否大于 minsize?
- 大于,切分块,返回给用户,剩下的块放进 unsortedbin
- 小于等于 minsize,直接返回给用户,完成
-
-
不满足,把这个块放入 smal/largebin.对应的链表中,继续遍历下一个块。
-
-
如果 unsortedbin 中所有的块也不能满足需求。
大小是否在 largebin 范围?-
是,检查对应的 bin 链表中有没有符合的 chunk。
- 有,找到满足需求最小的 chunk,
切分块返回,剩下的放进 unsortedbin 中。
- 有,找到满足需求最小的 chunk,
-
largebin 也不行?再次遍历 small/large 找 best fit 的 chunk。
-
还是没有,那就从 topchunk 中切割。
-
topchunk 也不够?mmap 系统调用。
-
7.调用 free 时,程序都在做什么
free 的 chunk 大小属于 fastbin 吗?
- 是,放进 fastbin,:完成。
是 mmap 分配的吗?
- 是,调用 munmap 回收,完成
前一个 chunk 空闲吗?
- 是,向前合并。
后一个 chunk 是 topchunkl 吗?
- 是,和 topchunk 合并,完成。
后一个 chunk 是 free 的吗?
- 是,向后合并。
放进 unsortedbin,完成。
2.UAF-释放后利用(堆漏洞核心)--Libc2.23
UAF 全称 Jse After Free,即释放后利用。
开发者的角度看,一块内存如果被释放,按照逻辑不应该被用户访问到。
如果用户访问到了释放后的内存区域,这种情况就称之为 UAF。
在堆的利用中,核心就是构造 UAF。
因为释放后的堆块中会存储一些链表指针信息用于指向空闲内存。
如果我们能够控制这些指针,通过精心的内存布局,就可以达到 AAW(任意内存地址写)。
如果栈方面的漏洞的核心是覆盖返回地址然后直接劫持控制流的话,那么堆漏洞的核心就是构造 UAF。
如果没有 UAF,那么堆漏洞没用。
如果能够利用堆漏洞来进行利用,那么他一定需要 UAF。
在之前的学习过程中,我们知道一个 chunk 中有不同的字段。当这个 chunk 被分配给用户时,有用的字段就只有 size、prevsize。而当这个 chunk 被 free 时,
用户数据区域就会存储一些指针信息,如 fd、bk 等等。
在正常逻辑下设计者认为,一个已经被释放的堆块不应该能够被用户访问到,但是如果存在指针悬挂或者溢出等等问题时,这些问题就会间接的导致用户能
够访问并且控制已经被释放的堆块的内容,这种漏洞形式就被称之为 UAF。
UAF 的成因
1.堆溢出
2.指针悬挂
内存 free 之后指向该内存的指针没有回收
3.overlap
同一块对地址被分配两次
UAF 的利用
一个 bin 链表是通过指针方式连接。
就拿最简单的 fastbin 来举例:
fastbin 只存储 fastbin 最前面的一个 free 的 chunk,每次要用的时候直接拿
出头部 chunk,然后根据头部 chunk 的 fd 来寻找下一个 chunk。
如果我们有 UAF,那么我们是不是可以利用 UAF 来修改 fd 字段为 target,然后再次分配内存时,target 是不是就会被分配给用户。
本来程序限定的逻辑是用户只能被分配到堆的内存区域,用户只能在这一块区域为所欲为。
现在用户只要指明一个 target。.利用 UAF,程序就会把 target:分配给用户,用户是不是就可以任意内存地址访问任意内存地址写了呢?
有了任意内存地址读写后,就可以修改一些内存地址,比如 got,hook 指针,栈等等,就可以控制执行流,进而 getshell,.PWN。
构造 UAF 是堆漏洞利用的核心
构造 UAF 是堆漏洞利用的核心
构造 UAF 是堆漏洞利用的核心
堆的漏洞利用没有上述说的那么简单。
即使在低版本的 2.23,也有很多 check,到现在 2.31 更多了。
多数的 PWN 题目不会直接给你 个 UAF,因为太简单了,就算直接给也会有很
多限制条件,难以利用。而且,就算有了 uaf,堆的分配机制也不是那么简单
粗暴,他也会对一些字段进行检查,也算是一些内置的安全机制。
在后续的章节里,大多数内容都是建立在 UAF 的条件上进行利用,或者就是如
何构造 UAF 上做文章。
1.Fastbin Attack(Fastbin Attack - CTF Wiki (ctf-wiki.org))
fastbin 检查:分配时,会检查堆块头部 size 字段。比如说分配一个 0x70 大小的 chunk,会检查分配的 chunk 的 size 字段,如果 size 符合 0x7x,则可以分配
如果不是这个大小,程序会出现异常退出。
释放时,会检查 fastbin 的链表头部指针是否和当前 free 的地址相同,如果相同则会异常退出。简单来说,程序不允许我们连续释放同一个堆块。
情况一:已经有 UFA
直接修改 fd 指针。
但是注意,你想分配的目标地址附近一定要有个头部 size 字段。
通常来说,这样的 size 字段挺好找的,因为 libc 中函数地址的指针都是形如:0x7XXXXXXXX,我们把偏移改一下,就会得到一个 0x7f,由于堆块都是 0x10
字节对齐的,所以 0x7f 会被当成 0x70 处理。
通常来说,修改的地方会是__malloc_hook 或者 got 表或者栈。
情况二:没有 UFA 但有指针悬挂
没有直接的 UAF,但是我们可以访问到被 free 掉的指针。
这个时候,我们需要用 fastbin:来构造 UAF。如何构造呢?
但是不能 free 一个地址两次
2.Unsortedbin attack
不同于 fastbin,chunk 被释放进入 unsortedbin 时,fdbk 字段会留下一个 main_arenal 的地址信息,
而 fastbin 如果是第一个,fd 只会是 0,之后的 fastbin 才会在 fd 中保存一个 chunk 的地址,单链表形式存储 fastbin 链表的信息。
那么,如果在 malloc 的时候没有进行相应的内存信息清空或者设置工作,那么分配给用户的 chunk 中会留下一些脏数据,也就是 fdbk 等的 libc 地址信息。
如果程序中有相应的 show 功能,那么这时候打印出 chunk 中数据,就可以泄漏地址信息了。
由于 unsortedbint 中的 main_arena 指针信息是一个 libc 中地址,可以通过这种方式泄漏 libc 地址。
同样的,也可以利用这种方式泄漏堆地址信息。
堆漏洞利用中第二常用的攻击手法。
最简单的攻击手法,前提条件是 UAF。
任意内存地址写一个不确定的非常大的数(libc 地址)。
通常,我们利用 unsortedbin attack 来修改一些类似于修改次数限制、上限信息、伪造堆头、配合局部写等等,十分好用。
原理
利用前提是先有 UAF,修改 unsortedbin 中的 BK 字段为 target addr-0x10,然后 malloc 一个相同大小的 chunk,即可完成攻击。
Unsorted Bin 在使用的过程中,采用的遍历顺序是 FIFO,即插入的时候插入到 unsorted bin 的头部,取出的时候从链表尾获取。
在程序 malloc 时,如果在 fastbin,small bin 中找不到对应大小的 chunk,就会尝试从 Unsorted Bin 中寻找 chunk。如果取出来的 chunk 大小刚好满足,就会直接返回给用 户,否则就会把这些 chunk 分别插入到对应的 bin 中。
关键代码
当将一个 unsorted bin 取出的时候,会将 的位置写入本 Unsorted Bin 的位置。
/* remove from unsorted list */
if (__glibc_unlikely(bck->fd != victim))
malloc_printerr("malloc():corrupted unsorted chunks 3");
unsorted_chunks(av)->bk=bck;
bck->fd=unsorted_chunks(av);
也就是说,只要控制了一个 unsortedbin 的 bk 字段,就可以往 BK+0x10 位置写入 unsorted bin 的地址。
3.offbyone
利用堆,必须有 uaf,其实本质就是修改一些 fdbk 指针。
正常来说,我们希望程序的堆溢出能够溢出到 next chunk 的 fd 或者 bk 字段,进而完成利用。
offbyone 属于一种特殊的堆溢出形式,他的溢出字节就如他的名字一样,只能溢出一个字节(任意字符,如果只是空字符则是 offbynot ) 。但是实际生活中这种漏洞很常见,程序员很容易犯这种错误,比如边界检查不严格等情况。
试想一下这种情况:如果我们申请了一个 0x78 或者 0x98 这种长度为 x8 的堆块,程序会分配给我们 0xx0+0x10 长度的 chunk 给我们使用,也就是说,这个 chunk 会使用 prevsize 字段。
如果这时候,程序存在 offbyone 漏洞,我们就可以修改 nextchunk 的 size 字段。
例子
程序中存在 offbyone 漏洞
堆中有 ABCD 四个已经被分配的大小为 0x70 的 chunk,现在都是使用状态。
然后 A 是我们进行 offbyone 的 chunk,我们目的是将 B 的 size 改掉
我们输入 'A'*0x68+'\xe1'
此时,堆块的布局如下:
可以看到 b 的 size 被改大了,正好覆盖到了 c 的未尾,我们构造了 chunkoverlap.
这时候我们将 C free 掉,他会进入 fastbin。
我们再将 B free 掉,B+C 这一段区域会进入 unsorted bin。
我们再次申请一个大小为 0xd0 的堆块,也就是说 B+C 这段内存又被我们控制了,此时我们就可以控制 C 的 fd 字段,就可以进行 fastbinattack 了。
4.offbynull
特殊的 offbyone.,字面意思,溢出的字节是个空字符 null,也就是 x00。现实中更为常见,因为字符串截断会在未尾加个空字符,如果边界检查不严格,
就会出现 offbynull。。
offbynul 比起常规的 offbyone 利用方式稍微复杂一些,但是归究其本质,都是构造一个 UAF 供我们使用。
由于溢出字节只能是 xO0,所以思路通常是改变其 prev inuse 位,通过合并构造 overlap,然后构造 UAF。
例子
堆布局位 abcd 四个大小位 0x100 的堆块,都是使用状态,本次攻击的堆块是 C
我们在 B 中输入 'A*0x90+p64(0x200)+1x00'
此时的内存布局如下可以看到 c 的 previnuse 位被改成了 0
也就是说,程序会将 B 看作已经被释放的堆块。
系统如何定位前一个堆块呢?是通过 prevsize 位,在这里,我们将其改成了 0x200,也就是说定位到了 A 堆块。
但是 A 堆块明明没有被 free,这时候我们如果 free C 会出现异常。 我们需要做的就是骗系统,绕过检查,其实我们需要做的仅仅就是先将 A free 掉放入 unsortedbin,这时候再 freeC,就会触发 合并操作。触发合并操作后,ABC 会被看作一个大小为 0x300 的堆块放入 unsortedbin 中。然而实际上,B 并没有被 free,我们也就通过这样的方式构造了 overlap。
然后后续的操作也就和 offbyone 一样,通过 overlap 构造 uaf,进而完成利用。
3. unlink(局限性较大)
unlink 指的是在一个堆块进行 free 的时候,由于涉及合并等操作,会将 chunk 从双向链表中取出来,这个过程就是 uink。
unlink 攻击局限性比较大,需要有 UAF 还需要修改 inuse 位,而且攻击的完成效果也是比较局限,可以在堆上任意内存地址写。
通常来说,有了这么多的限定条件足够利用别的方式来 getshell,.所以 unlink 的内容仅作为了解。
P->fd 就是 P+0x10
P->bk 就是 P+0x18
所以如果我们控制了 P 的 fd 和 bk 指针,比如我们将 fd 控制成 addr1,bk 控制成 addr2。
这时,P->fd 也就是 addr1,addr1 的 bk 就是 addr1+0x18,P->bk 就是 addr2,addr2 的 fd 就是 addr2+0x10
那么此时,我们执行 unlink 操作,实际上就是:
Addr1 + 0x18 = addr2
Addr2 + 0x10 = addr1
P->fd->bk = P->bk
P->bk->fd = P->fd
绕过:
令 fd=&P-0x18,bk=&P-0x10 即可绕过。
最终的效果,就是 P=&P-0x18。
也就是说,P 存储指向其自身内存地址往前 0x18 字节的地址。
如果存储堆的列表在堆上,那么用户实际访问的读写地址就存储在堆上。
通过 unlink 改那个列表地址信息,任意内存地址写。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于