最近准备好好学习一下 Tcache,然后下面的很多东西都是参考 CTF Wiki,并非原创,只是记录一下我学习时所重点关注的知识点和题目。
Overview
在tcache 中新增了两个结构体,分别是 tcache_entry 和 tcache_perthread_struct
1 | /* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ |
有两个重要函数:tcache_get()
和 tcache_put()
1 | static void |
这两个函数会在函数 int_free 和 _libc_malloc 的开头被调用,其中 tcache_put
当所请求的分配大小不大于 0x408 并且当给大小的 tcache bin 未满时调用,一个 tcache bin 中的最大块数 `mp.tcache_count` 是 7.
1 | /* This is another arbitrary limit, which tunables can change. Each |
在 tcache_get
中,仅检查了 tc_idx,此外 可以将 tcache 当作一个类似于 fastbin 的单独链表,只是它的 check,并没有 fastbin 复杂,仅仅检查 tcache->entries[tc_idx] = e->next;
Tcache Usage
内存释放
在 free 函数的最先处理部分,首先是检查释放块是否 页对齐 及前后堆块的释放情况,便优先放入 tcache 结构中:
1 | _int_free (mstate av, mchunkptr p, int have_lock) |
内存申请
在内存分配的 Malloc 函数中有多处,会将内存块移入 tcache 中:
(1)首先,申请的内存块符合 fastbin 大小时,并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的 其他内存块放入 tcache 中;
(2)其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中;
(3)当在unsortedbin bin链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。
fastbin处理时,如下:
1 | if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) |
- tcache取出:在内存申请的开始部分,首先会判断申请大小块,在tcache是否存在,如果存在就直接从tcache中摘取,否则再使用 _int_malloc 分配。
- 在循环处理 unsortedbin 内存块时,如果达到放入 unsortedbin 块最大数量,会立即返回,默认是0,即不存在上限。
1 |
|
- 在循环处理 unsortedbin 内存块时,如果之前曾放入过 tcache 块,则会取出一个并返回。
1 |
|
Pwn Tcache
tcache poisoning
通过覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。
以 how2heap 中的 tcache_poisoning 为例:
1 |
|
tcache dup
类似 fastbin dup,利用的是 tcache_put()
的不严谨
1 | static __always_inline void |
tcache_put()
的检查可以忽略不计,甚至没有对 tcache->counts[tc_idx]
的检查,大幅提高性能时安全性也下降了很多。
因为没有任何检查,我们可以对同一个 chunk 多次 free,造成 cycliced list.
以 how2heap 的 tcache_dup 为例分析,源码如下:
1 | glibc_2.26 [master●] bat ./tcache_dup.c |
tcache perthread corruption
在 tcache_perthread_strcut
是整个 tcache 的管理结构,如果能控制这个结构体,无论 malloc 的 size 是多少,地址都是可控的。
设想有如下的堆排布情况:
1 | tcache_ +------------+<---------------------------+ |
两次 malloc 后我们就返回了 tcache_prethread_struct
的地址,可以控制整个 tcache 了。
因为 tcache_prethread_struct 也在堆上,因此这种方法一般只需要 Partial overwrite 就可以达到目的。
tcache house of spirit
how2heap的源码:
1 |
|
攻击之后的目的是,去控制栈上的内容,malloc 一块chunk,然后通过在栈上 fake 的 chunk,然后去free 掉他。最后去 malloc,就可以控制这块 栈的内容。
smallbin chunk
在 smallbin 中包含有空闲块的时候,会同时将同大小的其他空闲块,放入 tcache中,此时也会出现解链操作,但相比于 unlink宏,缺少了链完整性校验。因此,原本 unlink 操作在该条件下也可以使用。
tcache stashing unlink attack
这种攻击利用的是 tcache bin 有剩余(数量小于 TCACHE_MAX_BINS
)时,同大小的 small bin 回放进 tcache 中,该情况可以用 calloc 分配同大小的堆块触发,因为 calloc 分配堆块时不从 tcache bin 中选取。 在获取到一个 smallbin
中的一个 chunk 后会如果 tcache 仍有足够空闲位置,会将剩余的 small bin 链入 tcache,在这个过程中只对 第一个 bin 进行了完整性检查,后面的堆块的检查缺少。当攻击者可以写一个 small bin 的 bk 指针时,其可以在任意地址上写一个 libc 地址(类似 unsroted bin attack
的效果)。构造得当的情况下也可以分配 fake chunk 到任意地址。
这里以 how2heap
中的 tcache_stashing_unlink_attack.c
为例。
我们按照释放的先后顺序称 smallbin[sz]
中的两个 chunk 分别为 chunk0 和 chunk1。我们修改 chunk1 的 bk
为 fake_chunk_addr
。同时还要在 fake_chunk_addr->bk
处提前写一个可写地址 writable_addr
。调用 calloc(size-0x10)
的时候会返回给用户 chunk0 (这是因为 smallbin 的 FIFO
分配机制),假设 tcache[sz]
中有 5 个空闲堆块,则有足够的位置容纳 chunk1
以及 fake_chunk
。在源码的检查中,只对第一个 chunk 的链表完整性做了检测 __glibc_unlikely (bck->fd != victim)
,后续堆块在放入过程中并没有检测。
因为 tcache 的分配机制是 LIFO
,所以位于 fake_chunk->bk
指针处的 fake_chunk
在链入 tcache 的时候反而会放到链表表头。在下一次调用 malloc(sz-0x10)
时会返回 fake_chunk+0x10
给用户,同时,由于 bin->bk = bck;bck->fd = bin;
的 unlink 操作,会使得 writable_addr+0x10
处被写入一个 libc 地址。
1 |
|
libc leak
在以前的 Libc 版本中,会在 unsortedbin 或者 leagre bin 的 fd 和 bk指针中 存在 Libc 地址。
1 |
|
在 2.26 之后的 libc 版本中,首先会把 tcache 填满:
1 |
|
然后才可以按照之前的方法,leak libc:
1 | gdb-peda$ heapinfo |
Tcache Check
在最新的 libc 中已经更新了 Tcache 的 doubel free 的 check:
1 | index 6d7a6a8..f730d7a 100644 (file) |
Hitbxctf gundam
程序分析
程序 Destroy() 函数,首先是 检测 idx 时存在缺陷,在 free一个 chunk 后,其实他的 chunk_list[idx] 没有被置为0;同时在 free(*(chunk_list[idx]+8)) 时,存在 UAF 漏洞。
利用分析
由于 存在 UAF 漏洞,同时 tcache 没有对 double free 进行检查,可以直接使用 double free 两次同一个 tcache,然后再申请一个 tcache,修改 tcache 的 fd和bk 指针,就能分配 伪造的 tcache 到指定地址。
EXP
1 | from pwn import * |
LCTF2018 PWN easy_heap
程序分析
存在一个 off-by-null 的漏洞,会在 buf 的 size 处输入一个 0。我么可以利用这个 0 来 修改下一个 chunk的 Pre_inuse 位,最后造成 chunk over_lap。
同时,可以看到此处的 读取输入时
利用分析
这道题也存在 off-by-null,如果按照 glibc 2.23 的想法,应该是利用 chunk off-by-null 修改后一个堆块的 pre_inuse位,然后构造 chunk overlap,最后再重用堆块。通过 unsortedbin attack 来泄露地址, fastbin attack 来 getshell。
但是这道题环境是 glibc2.26,有了 tcache,并且分配的堆块数 不能超过10,且我们输入时 不能输入 \x00,导致我们伪造 pre_size时 有困难。
这道题的关键就是 利用 Unsortedbin 释放时,会残留 pre_size 和填充 tcache 来攻击。
- 利用unsortedbin pre_size残留 构造 chunk overlap
1 | 1.申请 A、B、C 三个0x100 unsortedbin ,先释放 A、B,此时A和B合并,在C前的 Pre_size 大小为 0x200 |
- 绕过tcache机制
由于tcache 的分配和释放都在 fastbin 、unsortedbin之前,所以当我们想利用 unsortedbin attack时,首先得保证 tcache链为满。否则会先分配 tcache链,再分配 unsortedbin 链。
- tcache dup
最后利用 tcache dup 攻击即可,该攻击使用 双重释放漏洞 来 修改 malloc_hook 或 free_hook 来getshell。
EXP
1 | from pwn import * |
HITCON 2018 PWN baby_tcache
程序分析
和上一题类似,也是一个 off-by-null漏洞,当我们输入与我们是申请的 size 大小一致时,会超出一个 \x00,所以也能够造成 chunk overlap。
难点是,这道题 没有了输出函数,所以需要使用 IO attack,修改 stdout 结构来泄露 Libc 地址。
利用分析
- 构造 chunk overlap
该题 构造 chunk overlap的方法,和上一题类似,最终是需要将 伪造的chuk 分配到 io_stdout 结构体。这里与上面的不同在与 此处能够直接伪造 prev_size,不需要如同上一题 哪样去伪造。
然后这道题,有一个思想就是 将一个块在放入 tcache后,再通过 chunk overlap 通过unsortedbin 分配来修改 tcache 的 fd 指针。
- 修改 stdout 结构体,泄露 libc 地址
修改 IO 结构体的方法,在之前 IO泄露 讲过,只不过现在环境换成了 2.27,但是思想差不多,主要满足:
1 | flags= 0xfbad1800 |
- 通过 double free 修改 malloc_hook 来 getshell
最后就是 通过 tcache poisoning 修改 tcache的指针指向 malloc_hook或free_hook 来 getshell。
EXP
1 | from pwn import * |
Glibc 2.29 tcache变化
在glibc 2.29下 tcache新增加了一些保护机制
tcache_entry 结构体新增了一个指针 key 存放在 chunk 的 bk 处,tcache_put 写入,tcache_get 清空。
1 | tcache_put (mchunkptr chunk, size_t tc_idx) |
在 free一个 tcache 时,新增一个判断分支:如果 bk == key 则会遍历 tcache 检查是否有相同的 chunk
1 |
|
绕过方式:
- 修改 bk 使其不进入循环分支
- 修改 size 从而修改 tc_idx
- house of botcacke:合并 chunk1 chunk2 进 unsortedbins,将 chunk2 链进 tcache,从 chunk1 分配一个大 chunk造成 overlapped 到 chunk2 修改其 fd
- fastbin_reverse_into_tcache
unsortedbins 检测机制
新增检查:
- size 是否合理
- next chunk 的 prev_size 是否等于 victim 的size
- 双向链表完整性检查
- next chunk 的 prev_inuse 位是否为0
1 | if (__glibc_unlikely (size <= 2 * SIZE_SZ) |
HITCON 2019 one_punch_man
程序分析
seccomp 保护,只有上图白名单中的 函数才能够使用。
Delete函数,释放堆块后,没有将 堆块指针设置为 Null,那么可以通过 Edit 函数 实现 UAF 攻击。
函数存在一个后门函数。如果 初始设置的 *(heap_ptr+0x20) > 6
时,就可以申请一个 0x217
大小的 chunk
。heap_ptr
在 main
函数起始时 设置为了 heap_base +0x10
,对于glibc 2.29
该位置存储的是 tcache_perthread_struct
的 0x220
大小的 tcache_bin
的数量。也就是 我们首先需要将 0x220
大小的 tcache
填充为7,但是这样就不能再利用 tcache poisoning
的方法 修改 fd 指针达到任意地址写。因此,需要思考其他方式 tcache_stashing_unlink_attack
。
tcache_stashing_unlink_attack
前置知识House of Lore Attack
Tcache Stashing Unlink Attack 也是利用了 Smallbin 的相关分配机制进行攻击,可先了解 House of Lore:
攻击目标
分配任意指定位置的 chunk,从而修改任意地址的内存
攻击前提
能控制 Small Bin Chunk 的 bk 指针,并且控制指定位置 chunk 的fd 指针
攻击原理
1 | /* |
在Glibc2.29中也没有对 Small bin的 malloc 做更多保护,利用点如下:
1 | // 获取 small bin 中倒数第二个 chunk 。 |
即如果能够控制 small bin的最后一个 chunk 的bk 指向需要修改的内存地址,通过保证 该内存地址的 fd 的指针的值 为 该 chunk 地址,就能够通过检查 __glibc_unlikely(bck->fd != victim)
,在small bin 中加入我们想要的 chunk,进而在内存的任意地址分配一个 chunk。
tcache_stashing_unlink_attack
攻击目标
- 向任意指定位置写入 指定值
- 向任意地址分配一个 Chunk
攻击前提
- 能控制 Small Bin Chunk 的bk指针
- 程序可以越过 Tcache取 Chunk(使用 calloc 即可做到)
- 程序至少可以分配两种不同大小且大小为 unsortedbin 的 chunk
攻击原理
在上述在smallbin中查找的 tcache部分,可以看到没有经过 House of Lore
必须经过的检查:
1 | // 检查 bck->fd 是不是 victim,防止伪造 |
在 tcache_put
函数中,也没有对置入tcache做任何检查,所以可以让fake chunk进入 Tcache:
1 | static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx) |
利用分析
- 泄露 libc 和 heap 地址
分配并释放多个 chunk 使其进入 tcache,通过 show 函数输出 tcache bin
的 fd
值来泄露堆地址。释放 small bin size
的chunk 7个填充满 tcache bin,然后再申请和释放一个 同样大小的small bin size
chunk 使其进入 unsortedbin,通过 show 函数泄露 libc 地址。
- 修改 *(heap_ptr+0x20)的值
由于 glibc 2.29 新增了对于 unsortedbin
链表的完整性检查,使得 unsortedbin attack
修改一个地址为 large value 失效。可以使用 tcache stashing unlink attack
攻击。
通过 UAF 将 malloc_hook 链入
EXP
1 | from pwn import * |
参考文献
https://www.anquanke.com/post/id/198173
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2020/09/18/Tcache学习/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!