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
¶
AllocExitBreakpoint
¶
CallocEnterBreakpoint
¶
Chunk
¶
FreeChunkWatchpoint
¶
FreeEnterBreakpoint
¶
FreeExitBreakpoint
¶
MallocEnterBreakpoint
¶
ReallocEnterBreakpoint
¶
ReallocExitBreakpoint
¶
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.