Skip to content

Heap tracking

Heap Tracking

This module implements runtime tracking of the heap, allowing pwndbg to detect heap related misbehavior coming from an inferior in real time, which lets us catch UAF bugs, double frees (and more), and report them to the user.

Approach

The approach used starting with using breakpoints to hook into the following libc symbols: malloc, free, calloc, and realloc. Each hook has a reference to a shared instance of the Tracker class, which is responsible for handling the tracking of the chunks of memory from the heap.

The tracker keeps two sorted maps of chunks, for freed and in use chunks, keyed by their base address. Newly allocated chunks are added to the map of in use chunks right before an allocating call returns, and newly freed chunks are moved from the map of in use chunks to the map of free ones right before a freeing call returns. The tracker is also responsible for installing watchpoints for free chunks when they're added to the free chunk map and deleting them when their corresponding chunks are removed from the map.

Additionally, because going through the data structures inside of libc to determine whether a chunk is free or not is, more often than not, a fairly slow operation, this module will only do so when it determines its view of the chunks has diverged from the one in libc in a way that would affect behavior. When such a diffence is detected, this module will rebuild the chunk maps in the range it determines to have been affected.

Currently, the way it does this is by deleting and querying from libc the new status of all chunks that overlap the region of a new allocation when it detects that allocation overlaps chunks it previously considered free.

This approach lets us avoid a lot of the following linked lists that comes with trying to answer the allocation status of a chunk, by keeping at hand as much known-good information as possible about them. Keep in mind that, although it is much faster than going to libc every time we need to know the allocation status of a chunk, this approach does have drawbacks when it comes to memory usage.

Compatibility

Currently module assumes the inferior is using GLibc.

There are points along the code in this module where the assumptions it makes are explicitly documented and checked to be valid for the current inferior, so that it may be immediately clear to the user that something has gone wrong if they happen to not be valid. However, be aware that there may be assumptions that were not made explicit.

CALLOC_NAME = 'calloc' module-attribute

FREE_NAME = 'free' module-attribute

LIBC_NAME = 'libc.so.6' module-attribute

MALLOC_NAME = 'malloc' module-attribute

PRINT_DEBUG = False module-attribute

REALLOC_NAME = 'realloc' module-attribute

calloc_enter = None module-attribute

free_enter = None module-attribute

last_issue: str | None = None module-attribute

malloc_enter = None module-attribute

realloc_enter = None module-attribute

stop_on_error = True module-attribute

AllocChunkWatchpoint

Bases: Breakpoint

chunk = chunk instance-attribute

__init__(chunk)

stop()

AllocExitBreakpoint

Bases: FinishBreakpoint

name = name instance-attribute

requested_size = requested_size instance-attribute

tracker = tracker instance-attribute

__init__(tracker, requested_size, name)

out_of_scope()

stop()

CallocEnterBreakpoint

Bases: Breakpoint

tracker = tracker instance-attribute

__init__(address, tracker)

stop()

Chunk

address = address instance-attribute

flags = flags instance-attribute

requested_size = requested_size instance-attribute

size = size instance-attribute

__init__(address, size, requested_size, flags)

FreeChunkWatchpoint

Bases: Breakpoint

chunk = chunk instance-attribute

tracker = tracker instance-attribute

__init__(chunk, tracker)

stop()

FreeEnterBreakpoint

Bases: Breakpoint

tracker = tracker instance-attribute

__init__(address, tracker)

stop()

FreeExitBreakpoint

Bases: FinishBreakpoint

ptr = ptr instance-attribute

tracker = tracker instance-attribute

__init__(tracker, ptr)

out_of_scope()

stop()

MallocEnterBreakpoint

Bases: Breakpoint

tracker = tracker instance-attribute

__init__(address, tracker)

stop()

ReallocEnterBreakpoint

Bases: Breakpoint

tracker = tracker instance-attribute

__init__(address, tracker)

stop()

ReallocExitBreakpoint

Bases: FinishBreakpoint

freed_ptr = freed_ptr instance-attribute

requested_size = requested_size instance-attribute

tracker = tracker instance-attribute

__init__(tracker, freed_ptr, requested_size)

out_of_scope()

stop()

Tracker

alloc_chunks: SortedDict[int, Chunk] = SortedDict() instance-attribute

free_chunks: SortedDict[int, Chunk] = SortedDict() instance-attribute

free_watchpoints: Dict[int, FreeChunkWatchpoint] = {} instance-attribute

memory_management_calls: Dict[int, bool] = {} instance-attribute

__init__()

enter_memory_management(name)

exit_memory_management()

free(address)

is_performing_memory_management()

malloc(chunk)

get_chunk(address, requested_size)

Reads a chunk from a given address.

in_program_code_stack()

install(disable_hardware_watchpoints=True)

is_enabled()

Whether the heap tracker in enabled.

resolve_address(name)

Checks whether a given symbol is available and part of libc, and returns its address.

uninstall()