Skip to main content

5. Heap Use-After-Free

Goals

  • Learn to identify and exploit heap use-after-free vulnerability.
  • Understand heap memory management in glibc.

Challenge description

The program is a simple system for managing messages and leaving feedback. Your task is to find and exploit a heap use-after-free vulnerability in order to execute the win function and gain shell access to the server.

Feel free to use the provided template.py as needed.

Tips on Exploit

  • The heap allocator generally returns a different memory address depending on the size of the allocation request.
  • You can easily inspect the heap state using pwndbg's vis_heap_chunks and heap commands.
  • You might need to use gdb's call command to call functions during debugging. See the following example for reference.
    (gdb) run
    ...

    # Press Ctrl^C to interrupt the program
    (gdb) info sharedlibrary libc
    From To *Syms* Read Shared Object Library
    0x00007ffff7da4700 0x00007ffff7f3693d *Yes * /lib/x86_64-linux-gnu/libc.so.6
    (gdb) p malloc
    $1 = {void *(size_t)} 0x7ffff7e210a0 <__GI___libc_malloc>

    # Since we have the symbol for malloc, we can call it directly.
    (gdb) call malloc(30)
    $2 = (void *) 0x5555555592a0
    # If the above malloc call returns an odd heap address, try using `call __libc_malloc(30)` instead.

    # When we don't have the symbol for a function, we need to provide both the type and its address.
    # Pay attention to the parentheses.
    (gdb) call ((void* (*)(size_t))0x7ffff7e210a0)(30)
    $3 = (void *) 0x5555555592d0

Tips on Debugging

warning

This section may not be very useful for solving this challenge. Please use it as a reference. With modern GDB plugins like pwndbg or gef, you can achieve the same tasks with much simpler commands.

  • Sometimes, you may want to log the return values from malloc and the addresses being freed by free. In such cases, you can write the following Python script to register commands for the breakpoints. Although gdb has a built-in commands feature, it sometimes doesn't work properly, so I will use a Python script instead.

    import gdb

    # Custom breakpoint for malloc function
    class PrintAtMalloc(gdb.Breakpoint):
    def __init__(self):
    super(PrintAtMalloc, self).__init__('malloc', gdb.BP_BREAKPOINT)

    def stop(self):
    # Capture rdi (the size argument to malloc)
    malloc_size = gdb.parse_and_eval('$rdi')
    # Set a FinishBreakpoint to capture the return value (rax) after malloc finishes
    PrintMallocReturn(malloc_size)
    return False # Continue execution after setting the FinishBreakpoint

    # FinishBreakpoint to capture the return value of malloc
    class PrintMallocReturn(gdb.FinishBreakpoint):
    def __init__(self, size):
    super(PrintMallocReturn, self).__init__(gdb.newest_frame())
    self._size = size # Save the size for printing later

    def stop(self):
    # Print the return value (rax) and the size argument
    print(f"[!] {hex(gdb.parse_and_eval('$rax'))} = malloc({self._size})")
    return False # Continue execution

    # Custom breakpoint for free function
    class PrintAtFree(gdb.Breakpoint):
    def __init__(self):
    super(PrintAtFree, self).__init__('free', gdb.BP_BREAKPOINT)

    def stop(self):
    # Print the argument (rdi) passed to free
    print(f"[!] free({hex(gdb.parse_and_eval('$rdi'))})")
    return False # Continue execution

    # Register the breakpoints
    PrintAtMalloc()
    PrintAtFree()

    After saving the above script as heap_tracer.py, run it in gdb with the source heap_tracer.py command. Afterward, you will see logs every time malloc or free is called.

  • While this might not be necessary for our simple challenge, using Python scripts in gdb can also be helpful when exploiting more complex heap-related vulnerabilities, especially when heap spraying is required. Alternatively, you can use pwntools to interact with the gdb session and automate some tasks (e.g. process("gdb ./chall", shell=True)).

    import gdb
    class MallocLoop(gdb.Command):
    def __init__(self):
    super(MallocLoop, self).__init__("call_malloc", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
    size = 0

    while size <= 0x100:
    # Call malloc using gdb.execute
    gdb.execute(f"set $ptr = (void*) __libc_malloc({size})", to_string=True)

    # Fetch the value of the pointer returned by malloc
    ptr = gdb.parse_and_eval("$ptr")

    print(f"malloc({size:#x}) returned: {ptr}")
    size += 0x10

    # Register the command in gdb
    MallocLoop()

    Save the above script as malloc.py, and type source malloc.py command while the process is running within gdb. Now you can use the custom call_malloc command, and you will see output like this:

    (gdb) source malloc.py
    (gdb) call_malloc
    malloc(0x0) returned: 0x5555555592a0
    malloc(0x10) returned: 0x5555555592c0
    malloc(0x20) returned: 0x5555555592e0
    malloc(0x30) returned: 0x555555559310
    malloc(0x40) returned: 0x555555559350
    malloc(0x50) returned: 0x5555555593a0
    malloc(0x60) returned: 0x555555559400
    malloc(0x70) returned: 0x555555559470
    malloc(0x80) returned: 0x5555555594f0
    malloc(0x90) returned: 0x555555559580
    malloc(0xa0) returned: 0x555555559620
    malloc(0xb0) returned: 0x5555555596d0
    malloc(0xc0) returned: 0x555555559790
    malloc(0xd0) returned: 0x555555559860
    malloc(0xe0) returned: 0x555555559940
    malloc(0xf0) returned: 0x555555559a30
    malloc(0x100) returned: 0x555555559b30
  • For more information refer to the following link: GDB Python API.

Submission

Once you've obtained the flag, please submit it to our CTF server. Then, submit both your exploit code and a 1-page report through ETL. Your report should briefly explain the code you used to get the flag and how you solved the challenge. Please do not cheat, share your flag, or disclose your solutions. Ensure that your report is strictly limited to 1 page.

Before submitting, use this command to compress your files: zip report.zip solve.py report.pdf. Make sure to rename your exploit code to solve.py and your report to report.pdf before running this command. Finally, submit report.zip through ETL.

If you fail to get the flag, you don't need to include the solve.py in the report.zip. Instead, please document your findings and attempts (such as identified vulnerabilities, exploitation approach, etc.) in report.pdf. Partial credit will be awarded based on the content.