“ 作者:Ally Switch”
前言
本文接《常见堆利用手法解析(上)》,点击蓝字可跳转?
学习材料:shellphish 团队在 Github 上开源的堆漏洞系统教程 “how2heap”。
glibc版本:glibc2.31
操作系统:Ubuntu 20.04
示例选择:本篇参考了pukrquq师傅的分析文章,选用了在高版本依然存在的利用方式进行分析。
编译选项:gcc xxx.c -o xxx -g,添加 -g 选项以创建调试符号表,并关闭所有的优化机制。
1. 源码分析
由于源码较长,本篇中的 poc 示例会先贴出源码,删减掉不影响程序逻辑的代码后,再对简化后的版本进行分析。
完整版:
简化版:
· 橙色方框:
a. malloc() 14 个可以容纳 0x40 大小的内存块(即大小 0x50 的 chunk);
b. 先 free() 7 个以填满 0x50 大小的 tcache bin;
c. 再 free() 1 个到 0x50 大小的 fast bin 中,free() 前获取它的指针保存为 victim;
d. 再 free() 6 个到 0x50 大小的 fast bin 中,fast bin 采用头插法入链,victim 会变成链上最后一个 chunk。
· 蓝色方框:
a. 初始化一个位于栈中的数组;
b. 填充 0xcd 用于实验后的对照。
· 红色方框:
a. victim 指向的值就是 victim 这个 chunk 对应的 fd 指向的值,由于 victim 位于 fast bin 末尾,因此其指向 NULL;
b. 设置 victim 指向 stack_var[0] 对应的地址;
c. 此操作,相当于把 stack_var[0] 所在地址作为 chunk 首地址放入 fast bin 中;
d. 换句话说,stack_var 此时成为 fast bin 中的最后一个 chunk,而 victim 则是倒数第二个 chunk,此时 fast bin 中有 8 个 chunk。
· 粉色方框:
a. 先 malloc() 7 个可以容纳 0x40 大小的内存块,清空 tcache;
b. 打印一下 stack_var 这个数组中的值,目前应该一切正常,全部为 0xcd;
c. 再 malloc() 1 个可以容纳 0x40 大小的内存块,回顾一下此时 malloc() 会进行哪些操作:
i. 由于 0x50 大小的 tcache bin 已经空了,所以会进入 _int_malloc() 进行分配;
ii. _int_malloc() 会先找 fast bin,发现 fast bin 中有符合要求的 chunk;
iii. 这时会先把符合要求的 chunk 从 fast bin 中取下来,稍后会返回给用户;
iv. 进入一个循环,对 fast bin 中剩下的 chunk 进行遍历,把 0x50 大小的 chunk 全部放入 0x50 大小的 tcache bin 中,直到 tcache bin 满了(达到 7 个)或 fast bin 空了,循环才停止;
v. 然后把先前取下来的 chunk 返回给用户。
d. 在实际场景中,经过上述操作:
i. 由于 victim 在 fast bin 是倒数第二个,stack_var 是倒数第一个,因此不会率先返回给用户;
ii. fast bin 上的第一个 chunk 会被取下,用于稍后返回,此时 fast bin 上还剩下 7 个 chunk;
iii. 接下来,这 7 个 chunk 会在一个循环中,被放入到 0x50 大小的 tcache bin 中;
iv. stack_var 这个 chunk 会最后从 fast bin 被取出来,进入 tcache bin 后会成为第一个;
v. 此时由于 tcache_put() 会设置 chunk 的 next 与 key 两个字段,因此理论上 stack_var[2] 与 stack_var[3] 处会被赋值。
e. 然后就是对照实验,再打印一下 stack_var,会发现,此时 stack_var[2] 与 stack_var[3] 对应的值应该已经被修改;
f. 最后再 malloc() 一下,由于 stack_var 这个 chunk 位于 tcache bin 的开头,所以我们拿到的就是 stack_var 这个 chunk,返回的指针也就指向 stack_var[2] 的位置。
2. 执行分析
-
这里比较直观的感受就是 stack_var 的值确实变了,变的也确实是 stack_var[2] 与 stack_var[3] 位置;
-
这大概也能推论出 stack_var 确实是进入 bin 中了,但仍无法看出 tcache bin 与 fast bin 中发生的变化,这需要进一步调试。
3. 调试分析
1. free() victim 后:
可以看到,此轮运行,victim 对应的 chunk 地址为 0x5555555594c0。
2. 14 个块全部 free() 后:
由于我环境中 pwndbg 的 bins 指令显示不全,我也不清楚是怎么回事,这里只好用 heap 指令查看堆空间。
3. 设置了 victim 指向 stack_var 后:
可以看到此时 victim 的 fd 被设置成了栈中的地址,也就是 stack_var。
4. malloc() 触发 fastbin reverse into tcache:
最后可以看到,fast bin 中的 chunk 进入 tcache bin 中,并且 stack_var 成了 tcache bin 中的第一个 chunk,此时只需再 malloc() 一次就可以将其取出。
原理小结
-
利用手法是通过溢出等手段,修改进入 fast bin 中 chunk 的 fd 指针,指向一个伪造的 chunk,实现任意地址覆盖。
-
原理是当进入 _int_malloc() 中分配 chunk 时,如果在 fast bin 中发现了合适的 chunk ,那么会将剩余的 chunk 填入 tcache bin 中。
1. 源码分析
完整版:
简化版:
· 橙色方框:
a. 先 malloc() 9 个可以容纳 0x90 大小的内存块,即 9 个 0xa0 大小的 chunk;
b. free() 掉其中 7 个去填满 0xa0 大小的 tcache bin;
c. 这里有个细节,剩下 2 个还没 free() 掉的,在堆上是不连续的,也就是说,这样进入 unsorted bin 后,他俩就不会合并了。因为 tcache bin 中的 chunk 本身是处于 inuse 状态,因此不会与 unsorted bin 中的 chunk 进行合并;
d. 接下来把剩下俩给 free() 掉,由于大小为 0xa0,是不会进入 fast bin 的(最大支持 0x80 的 chunk),所以会进入 unsorted bin,注意 unsorted bin 进入时会放在 bin 与 bin->fd 之间,取的是 bin 与 bin->bk->bk 之间的 chunk,所以是先进先出(FIFO),small bin 同理。
· 蓝色方框:
a. 第一次 malloc() 一个可以容纳 0xa0 大小的 chunk(即 0xb0 大小的 chunk),显然此时堆中没有合适大小的 chunk,所以最终会通过 top chunk 进行分配,但是在 _int_malloc() 的查找过程中,会将 unsorted bin 中的 chunk 放入对应的 bin 中;
b. 因此先前 free() 掉的 chunk_lis[0] 与 chunk_lis[2] 这俩指针对应的 chunk 就进入了 small bin;
c. small bin 与 unsorted bin 都是先进先出,因此这波操作完 small bin 大概是:smallbin(0xa0) <-> chunk_lis[2] <-> chunk_lis[0];
d. 然后再 malloc() 两个 0xa0 的 chunk,用于在 tcache bin 中腾出位置。
· 红色方框:
a. 核心操作都在这了,先给 stack_var[3] 赋值为 &stack_var[2],这是将 stack_var 伪造成一个 fake chunk,其中 stack_var[3] 表示这个 fake chunk 的 bk,此时赋值为一个合理存在的地址,仅此而已,用于过掉 glibc 中的校验,具体咋过,后面会介绍到;
b. 这里将 stack_var 这个 fake chunk 赋值给 chunk_lis[2][1],chunk_lis[2] 本身位于 fd 的位置,chunk_lis[2][1] 则是 bk 的位置,所以这一步,就是将 chunk_lis[2] 在 small bin 中对应的 chunk 的 bk 设置为了 stack_var,上面这两步的操作变化大概是这样:
c. 接下来,进行了一次 calloc() 的操作来申请内存块,calloc() 不会访问 tcache bin,而是直接进入 _int_malloc();
d. 根据先前的知识,small bin 是先进先出,因此先进入 small bin 的 chunk_lis[0] 会被取出来分配给用户;
e. 接下来,_int_malloc() 中会将 small bin 中余下的 chunk 往对应大小(也就是 0xa0)的 tcache bin 中放,由于 tcache bin 中目前已经有 5 个 chunk 了,因此最多只能再放 2 个,按照先进先出的顺序,chunk_lis[2] 会被先行放入 chunk 中,接下来,会进行如下判断(下面放上源码):
i. chunk_lis[2]->bk 是否为链表头 bin,此时这个值为 stack_var(默认情况下应该是链表头 bin);
ii. 然后会再判断一次这个值是否为空;
iii. 若不为空,说明这是一个 chunk,则获取这个 chunk 的 bk,也就是这里 fake chunk 的 bk,将其赋值给 bck。前面设置了这个 bk 的值为一个合理存在的地址,也就是为了过掉这里的校验,不然 bck 的值不合法,那么后面执行 bck->fd = bin 可能就出错了。由于 tcache bin 的容量是 7 个,此时 fake chunk 会在 chunk_lis[2] 之后进入 tcache bin 中,并位于第一个。
· 粉色方框:
a. 最后再 malloc() 一下,因为 tcache bin 中的第一个位置已经是我们伪造的 fake chunk,自然也就获取到了这个 chunk。
2. 执行分析
-
这里可以看出最后一次 malloc() 得到的是一个栈中的地址,并且刚好是 fake_chunk->bk 处的值,因为这里保存的值为 fake_chunk[2] 的地址,也就是 fake_chunk 的 fd 的位置。
3. 调试分析
1. 9 个内存块都 free() 后:
可以看到,此时 0xa0 大小的 tcache bin 已满,unsorted bin 中有两个 chunk。
2. malloc(0xa0) 后:
此时 unsorted bin 中的两个 chunk 已进入到 0xa0 大小的 small bin 中。
3. 两次 malloc(0x90) 后:
两次 malloc() 给 tcache bin 中腾出了两个位置。
4. 设置 fake chunk 后:
此时 chunk_lis[2] -> bk 处的值已经被修改为 fake chunk 的地址。
5. calloc() 执行后:
此时 fake chunk 已进入 0xa0 大小的 tcache bin 中。
原理小结
-
本例和前面的
fastbin_reverse_into_tcache 很像,这里也是通过修改 small bin 然后利用 small bin 进入 tcache bin 来实现任意地址写
-
unsorted bin 与 small bin 都是双链表,且都是先进先出,都是进入时插入 bin 与 bin->fd 之间,取的是 bin 与 bin->bk->bk 之间的 chunk
-
因此修改 small bin 与前一个例子修改 tcache 不太一样,这里修改的是 victim->bk 的值
-
造成漏洞的原因主要是 glibc 中将 small bin 导入 tcache bin 时,只检查了 tc_victim->bk 这个指针,没有检查链表完整性,从而有了利用的空间
1. 源码分析
完整版:
简化版:
· 橙色方框:
a. 定义一个数组,位于栈中,用作后续 malloc() 的返回值,也可以理解为本例中的 fake chunk;
b. 初始化 7 个 0x110 大小的 chunk,用于填充 tcache bin;
c. malloc() 1 个 0x110 大小的 chunk,用于 free() 后的块合并;
d. malloc() 1 个 0x110 大小的 chunk,作为 victim chunk;
e. malloc() 1 个 0x20 大小的 chunk,作为 padding,防止 victim chunk 与 top chunk 合并。
· 蓝色方框:
a. free() 掉 7 个 0x110 大小的 chunk,填充满 0x110 大小的 tcache bin;
b. 首次 free() 掉 victim chunk,victim chunk 此时会进入 unsorted bin;
c. free() 掉用于合并的 chunk,它会与上一步 free() 掉的 victim chunk 进行合并,形成一个 0x220 大小的 chunk,然后进入 unsorted bin 中;
d. malloc() 一个 0x110 大小的 chunk,使得 tcache bin 中腾出一个位置。
· 红色方框:
a. 第二次 free() 掉 victim chunk,第二次 free 的 victim chunk 会进入刚刚腾出位置的 0x110 大小的 tcache bin 中;
b. malloc() 一个 0x130 大小的 chunk,由于 tcache bin 中没有符合的,fast bin 与 small bin 也没有符合大小的;
c. 所以会进入 unsorted bin 的处理流程,此时:
i. unsorted bin 中刚好只有一个
chunk;
ii. 分割后的剩余大小也大于
MINSIZE;
iii. 这个 chunk 符合 small bin 的范围;
iv. 同时也是 last remainder chunk。
d. 因此,分配器就会从这个 0x220 的 chunk 里面分出 0x130 大小返回给用户;
e. 接下来修改刚刚拿到的这个
chunk[0x130 – 0x8*2] 的位置,原先合并前两个 chunk 大小都是 0x110,这里修改 0x120 位置的值为 stack_var,也就是修改了原先第二个 chunk 的 fd 指针的位置,而这个被修改的 fd 的 chunk 也就是 victim chunk,它已经经过 double free 被放在了 tcache bin 里;
f. 在修改 fd 指针后,相等于修改了 tcache bin 中 victim chunk 的后一个 chunk,将其设置为了 stack_var,此时 tcache bin 中的状态大概为:
· 粉色方框:
a. 先 malloc() 一个 0x110 的 chunk,把 victim chunk 给申请走;
b. 再 malloc() 一个 0x110 的 chunk,就可以拿到一个 &stack_var[-2] 开始的 chunk 了,返回的指针刚好是 stack_var。
2. 执行分析
-
执行结果看不出什么东西,只能看出最后成功申请了一个栈空间的地址。
3. 调试分析
1. unsorted bin 中 chunk 合并后:
合并前,unsorted bin 中的 chunk 位于 0x555555559b10;
合并后,unsorted bin 中的 chunk 位于 0x555555559a00。
2. double free 后:
double free 后发现 tcache bin 中出现了合并前的那个 chunk,之所以是 0x555555559b20,是因为 tcache bin 中是用 fd 来链接 chunk。
3. unsorted bin 中 chunk 分割后:
分割后,unsorted bin 中的 chunk 变成了 0x555555559b30;
用新申请的 chunk 修改的位置则是 0x555555559b20,也就是合并前的 victim chunk 的 fd。
4. 修改 victim chunk 的 fd 后:
修改 victim chunk 的 fd 后发现 tcache bin 中的第二个 chunk 已经变成了我们可以控制的栈中的地址。
原理小结
-
原理非常简单,double free 实现 tcache bin 与 unsorted bin 中包含相同 chunk
-
利用分配内存时会对 unsorted bin 的 last remainder chunk 进行分割,获取到 victim chunk 前面地址的一个 chunk,然后利用覆盖实现任意地址写入
1. 源码分析
完整版:
简化版:
· 橙色方框:
a. 定义一个局部变量 target,位于栈中;
b. malloc() 一个 0x430 大小的 chunk,然后再 malloc() 一个 0x20 大小的 padding 防止 free() 时与 top chunk 合并;
c. malloc() 一个 0x420 大小的 chunk(略小于前一个 chunk,但属于 large bin 的范围内,且与前一个 chunk 位于同一个 idx 对应的 large bin 上),然后再 malloc() 一个 0x20 大小的 padding 防止 free() 时与 top chunk 合并。
· 蓝色方框:
a. free() 掉 0x430 大小的 chunk 进入 unsorted bin;
b. malloc() 一个 0x440 大小的 chunk,使得 unsorted bin 处理流程中将 0x430 大小的 chunk 传入 large bin 中;
c. free() 掉 0x420 大小的 chunk 进入 unsorted bin;
d. 此时,unsorted bin 中有一个 0x420 大小的 chunk,large bin 中有一个 0x430 大小的 chunk。
· 红色方框:
a. 将 p1[3] 位置的值设置为 &target – 0x20;
b. p1[3] 这个位置就是 0x430 大小的 chunk 对应的 bk_nextsize;
c. 这个操作相当于 p1->bk_nextsize = &target – 0x20。
· 粉色方框:
a. 此时再 malloc() 一个 0x440 大小的 chunk,该操作再次触发 unsorted bin 处理流程,使得 0x420 大小的 chunk 传入 large bin 中;
b. 从 unsorted bin 进入 large bin 中的过程有 4 种可能的情况,具体可以参考 malloc源码分析-Unsorted Bin处理流程,此处 0x420 大小的 chunk 进入 large bin 属于文中描述的情形二,具体在源码中为下图框出部分:
c. 红框中圈出了关键的两个运算:
i. victim->bk_nextsize =
fwd->fd->bk_nextsize:
1) fwd->fd 指向的就是原本位于
large bin 中的那个 0x430 大小的 chunk;
2)这个 chunk 的 bk_nextsize
已经设置为了 &target – 0x20;
3)victim 就是刚刚准备进入
large bin 的0x420 大小的 chunk;
4)这里就是将 victim 的 bk_nextsize 设置为了 &target – 0x20。
ii. victim->bk_nextsize->fd_nextsize = victim:
1)将 victim->bk_nextsize 换成 &target – 0x20;
2)对于 large bin chunk 来说,
fd_nextsize 的位置就是 chunk首地址 + 0x20/ 返回指针ptr + 0x10 的位置;
3)假设我们把 fd_nextsize 以 chunk首地址 + 0x20 代入,可以得到 &chunk = victim;
4)也就是说,此时 target 所在的这个地址,被赋上了 victim 对应 chunk 的首地址(0x420 大小的 chunk,对应指针 p2);
5)我们知道,返回指针位于 chunk首地址 + 2*SIZE_SZ 的位置,即可满足示例中 p2 – 2*0x8 == target 这个逻辑。
2. 执行分析
-
这个 poc 的描述语句也是比较细的,可以清晰的看到,位于栈中的局部变量 target,原本保存的值为 0,之后被替换成了 p2 所对应的 chunk 的首地址(源码中使用的都是 p2-2*SIZE_SZ 来表示 p2)。
3. 调试分析
1. 0x430 大小的 chunk 进入 large bin 后:
进入 large bin 后,这个 0x430 大小的 chunk 位于 0x555555559290。
2. 0x420 大小的 chunk 进入 unsorted bin 后:
进入 unsorted bin 后,这个 0x420 大小的 chunk 位于 0x5555555596e0,因为是后申请的,所以位置更靠近 top chunk,由于都申请了 padding,所以不会与 top chunk 合并。
3. 修改 0x430 对应 chunk 的 bk_nextsize 后:
这步比较关键,但是 bins 看不出来,直接用 heap 指令看,可以发现:
-
修改了位于 large bin 中这个 0x430 大小的 chunk 的 bk_nextsize,设置成了一个栈中的地址,正是 &target – 0x20 的位置;
-
修改前,由于 large bin 中只包含这一个 chunk,因此 fd_nextsize/bk_nextsize 都指向自身。
4. 0x420 大小的 chunk 进入 large bin 后:
将 0x420 大小的 chunk 从 unsorted bin 移入 large bin 后:
-
0x420 大小的这个 chunk 的 bk_nextsize 的值修改为了 &target – 0x20 这个地址,这是执行 victim->bk_nextsize = fwd->fd->bk_nextsize 这条语句后的结果;
-
&target 地址处(0x7fffffffdd40)保存的值已经变成了 0x420 这个 chunk 的首地址,这是执行 victim->bk_nextsize->fd_nextsize = victim 这条语句后的结果,注意这条语句中的 victim,正是 0x420 这个 chunk 的首地址。
原理小结
-
利用了 unsorted bin 中的 chunk 进入 large bin 时校验不全的漏洞;
-
当进入 large bin 的 chunk 小于这条 bin 上的所有 chunk 时,则会放在一个边缘的位置。此时原先最小的 chunk,会与当前准备放入的 chunk 通过 fd_nextsize/bk_nextsize 进行链接,但是 glibc 中未对原先最小的 chunk 的 bk_nextsize 的值进行校验,如果我们修改了这个值(例如改成了一个地址 0x7fffffff0000),那么在经过 glibc 中的运算后,这个 0x7fffffff0020(0x7fffffff0000 -> fd_nextsize) 这个地址就会存着先前准备放入的 chunk 的首地址。这样就获取了一个可以编辑的地址。
未完待续…
原文始发于微信公众号(中孚安全技术研究):常见堆利用手法解析(中)