0%

heap总结

heap总结

堆溢出查找

calloc与malloc与realloc

calloc会清空堆块,malloc不会,通过堆溢出,修改insue位为IS_MAPPED可以让calloc不清空堆块

realloc 的操作并不是像字面意义上那么简单,其内部会根据不同的情况进行不同操作

  • 当 realloc(ptr,size) 的 size 不等于 ptr 的 size 时
    • 如果申请 size > 原来 size
      • 如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
      • 如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
    • 如果申请 size < 原来 size
      • 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
      • 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
  • 当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)
  • 当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作

利用realloc这个特性,可以free二次,造成double free

危险函数

scanf, gets,strcpy

自写的输入函数, 注意结尾处理,有加\x00可能有off-by-null

源码分析

  1. malloc过程
  2. free的过程

malloc

  1. 首先查看有没有hook函数,有则执行,无继续 (攻击点)
  2. 获取arena
  3. 调用_int_malloc分配内存,分配失败重新找一个arena,分配成功在结束时候则需要解锁
  4. 检测
  5. 返回堆块

fastbin

  1. 首先查看其所在链表,即在0x20–0x80哪一条链表上
  2. 若在的话,则遍历该链表,不在则转到smallbin
  3. 若找到空闲chunk,则检测其大小,若正确则分配

由于采用的单链表,同时用的头插法插入,所以策略为LIFO

可能利用点::

  1. 遍历链表过程

smallbin

  1. 获取small bin索引
  2. 获取bin
  3. 求smallbin最后一个chunk
    • 若不存在一个,则初始化,这里会执行malloc_consolidate,会将fastbin合并成unsortedbin
    • 存在,则检测倒数第二个是否指向当前chunk,若是,设置标志位,然后取出

可能利用点:

  1. malloc_consolidate 申请大量fastbin,让其合并成unsortedbin

largebin

  1. 获取索引
  2. 存在fastbin则合并fastbin先

可能利用点:

malloc_consolidate 申请大量fastbin,让其合并成unsortedbin

unsortedbin

策略FIFO,根据bk遍历

先考虑unsortedbin然后考虑remainder, small request例外

如果用户的请求为 small bin chunk,那么我们首先考虑 last remainder,如果 last remainder 是 unsorted bin 中的唯一一块的话, 并且 last remainder 的大小分割后还可以作为一个 chunk

sysmalloc

  1. mmap
  2. morecore(可以攻击这个函数, 当malloc_hook和free_hook都无法使用时候)

free

  1. 检测hook函数(攻击点)
  2. mmap标志特殊处理, 若不是调用_int_free处理

报错1,检测size是否大于最小的chunk

1
2
3
4
5
// 大小没有最小的chunk大,或者说,大小不是MALLOC_ALIGNMENT的整数倍
if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size))) {
errstr = "free(): invalid size";
goto errout;
}

fastbin检查

  1. 下一个chunk的大小不能小于两倍的SIZE_SZ
  2. double free检查

合并:

  1. 前向合并
  2. 后向合并

与top_chunk合并,靠近的话

tcache

相关结构体

tcache_entry
1
2
3
4
5
6
/* 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;

tcache_entry 用于链接空闲的 chunk 结构体,其中的 next 指针指向下一个大小相同的 chunk。

需要注意的是这里的 next 指向 chunk 的 user data,而 fastbin 的 fd 指向 chunk 开头的地址。

而且,tcache_entry 会复用空闲 chunk 的 user data 部分。

tcache_perthread_struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 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;

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;

每个 thread 都会维护一个 tcache_prethread_struct,它是整个 tcache 的管理结构,一共有 TCACHE_MAX_BINS 个计数器和 TCACHE_MAX_BINS项 tcache_entry,其中

  • tcache_entry 用单向链表的方式链接了相同大小的处于空闲状态(free 后)的 chunk,这一点上和 fastbin 很像。
  • counts 记录了 tcache_entry 链上空闲 chunk 的数目,每条链上最多可以有 7 个 chunk。

用图表示大概是:

img

基本工作方式

  • 第一次 malloc 时,会先 malloc 一块内存用来存放 tcache_prethread_struct
  • free 内存,且 size 小于 small bin size 时
  • tcache 之前会放到 fastbin 或者 unsorted bin 中
  • tcache 后:
    • 先放到对应的 tcache 中,直到 tcache 被填满(默认是 7 个)
    • tcache 被填满之后,再次 free 的内存和之前一样被放到 fastbin 或者 unsorted bin 中
    • tcache 中的 chunk 不会合并(不取消 inuse bit)
  • malloc 内存,且 size 在 tcache 范围内
  • 先从 tcache 取 chunk,直到 tcache 为空
  • tcache 为空后,从 bin 中找
  • tcache 为空时,如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来

tcache_put()

1
2
3
4
5
6
7
8
9
10
11
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

这里采用的是头插法插入, 每次直接从链表头插入节点

tcache_get()

看一下 tcache_get()

1
2
3
4
5
6
7
8
9
10
11
12
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]); // 获得一个 chunk,counts 减一
return (void *) e;
}

tcache_get() 就是获得 chunk 的过程了。可以看出这个过程还是很简单的,从 tcache->entries[tc_idx] 中获得第一个 chunk,tcache->counts 减一,几乎没有任何保护。

这里也是直接从头部取出

tcache_init()

此libc版本为2.27,2.29将tcache结构改掉了,他不是用char来存储数值了,而是用两个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(0x28);
int *q = (int*)malloc(0x38);
free(p);
free(q);
p =(int*)malloc(0x48);
free(p);
p = (int*)malloc(0x18);
free(p);
p = (int*)malloc(0x8);
free(p);
getchar();
return 0;
}

我就以这份代码来调试

以一份调试结果来说明吧

image-20200402171744891

这里看到,首先有一个大堆块,我们称其为tcache_perthread_struct

这个块的大小在64位系统中为

641+ 64 *8 = =0x40\9 = 0x240再加个堆头为250,标志为1

前面的便是记录链表的长度,后面的均为链表的头结点,,分别对应0x20, 0x30,0x40….0x410,总共64条链

总结

  1. tcache是一个后进先出的模式,他是通过头插法插入节点,同时也直接从头部取出节点, 可以把它当做栈的模式,每次压入栈,然后从栈里取出节点
  2. 第一个tcache链表头大小为0x20,总共有64个链表,每次递增0x10, tcache最大为0x410,超过410便不进入tcache了,也就是说malloc(0x400) 释放后还进入tcache, malloc(0x410)释放后便不进入了,因为超过了最大的限制大小
  3. unsortedbin攻击在2.27版本变成了main_arena+96? 待确认

堆溢出的攻击方式

  1. off-by-one
  2. unlink
  3. fastbin attack
  4. tcache attack
  5. overlap
  6. chunk extends
  7. uaf
  8. largebin attack
  9. unsortedbin(大数值改写,改写循环条件,改写global_max_fast)
  10. house of系列

off-by-one

  1. off-by-null
  2. off-by-one

通常利用其造成堆块重叠进行攻击

  1. off-by-one过后, free掉,可以获得更大的堆块,比如本身为0x21,覆盖成0x90, 这样可以进unsorted bin了
  2. off-by-one造成unlink攻击, 覆盖下一chunk的pre_size和size标志位为0, 同时需要bypass检测, 伪造下一chunk的下一个chunk不能小于最小的chunk
  3. off-by-null可以选择特定大小堆块进行free, 比如0x101,0x201这种,刚好只修改标志位
  4. off-by-one构成overlap, 利用前向合并, 覆盖掉pre_size和标志位过后, free掉, 合并过后造成overlap了

overlap

uaf

  1. uaf造成堆块信息泄露,
    • free掉后,没清空堆块数据内容,我们可以拿到fd指针, 输出后便可泄露
  2. uaf过后可以修改fd指针, 修改后我们可以伪造地点, 让其malloc到指定地点处, 比如chunk-0x10处, 就能修改pre_size跟size了,当然要过掉检测. 随后便是堆溢出常规手法了, unlink, overlap等等

双链表解链,这个bin必须得非fastbin,通常将其覆盖成目标地址-0x10和-0x18处,同时该目标地址存了一个指向当前堆块用户空间的指针

这里注意可以利用前向合并跟后向合并

fd = target-0x10

  • 首先,第一步要过掉unlink的size检测,覆盖chunk3的pre_size为fake_chunk大小
  • 其次chunk3的insue位要为0,标志前面一个堆块未在使用当中
  • 然后关键点就是伪造fd跟bk了
  • 在第一点中我将ptr设置为global+0x10意思就是第二块堆块地址,这就是存放p的地方
  • unlink第一步 FD = p->fd = ptr-0x18
  • unlink第二步 BK=p->bk = ptr-0x10
  • unlink第三步 判断FD->bk == p && BK->fd == p ?
  • 过了检验后
  • FD->bk = * (ptr-0x18 + 0x18 )= BK = ptr -0x10
  • BK->fd = * (ptr-0x10+0x10) = FD = ptr-0x18
    最终结果就是*ptr = ptr-0x18,而ptr是0x0000000000602150故最终就是将global+0x10处的值改为0x602138
    然后我们在编辑第二块的时候实际上就是编辑0x602138处,也就是global-0x8处

fastbin attack

  1. double free
  2. house of sprit 伪造前后堆块
  3. arbitrary alloc(通常用来打malloc_hook,错位伪造)

house of spirt检测

要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

double free

Fastbin Double Free 能够成功利用主要有两部分的原因

  1. fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
  2. fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。

利用过程

  1. 首先构造三个堆块
  2. free(1), free(2), free(1)
  3. 造成double free, 这时在malloc

Alloc to Stack

这个技术利用挺难的, 因为大部分堆题都开启了全保护, 而栈地址无法泄露, 到栈里干事很难, 如果我能泄露栈,并且栈中有特定位置可以构造, 我们malloc到栈中,常规ROP就行

Arbitrary Alloc

  1. 打malloc_hook常用手法
  2. main_arena附近有0x5x开头的堆块,进行错位伪造, 申请0x5x,修改main_arena->top, 然后就可以改top了,这里有概率失败,因为不一定是0x56,可能为0x55, 不过作为 fastbin attack 的 size 不能 为 0x55
  3. 利用unsorted bin attack将值写入free_hook附近,这样就可以错位伪造了,注意这里写入的是 *(bk+0x10)
  4. 分配到bss, 伪造_IO_FILE_plus,然后修改vtable为get_system

有一个小坑要注意,在 __free_hook-0x30 开始 的 0x30 个字节 是 _IO_stdfile_*_lock 区域,用于 std* 类文件的锁操作,这个区域的内存会被经常清零

所以 unsorted bin attack 应该往上面一点, 比如 libc.symbols[‘__free_hook’] - 0x50

还有一点就是在进行 unsorted bin attack 以后 , unsorted bin 链表就被破坏了,所以 就只能通过 fastbin 或者 smallbin 进行内存的分配,所以我们应该先劫持 fastbinfd 到 目标位置,然后触发 unsorted bin attack 写入 size, 最后进行 fastbin attack ,修改 __free_hook

unsorted bin attack

  1. 利用其改循环数值
  2. 改global_max_fast

tcache attack

也是挺多攻击方式,最常用也是double free

house of 系列

本文作者:NoOne
本文地址https://noonegroup.xyz/posts/6a7f4fcc/
版权声明:转载请注明出处!