Repository: pwndbg/pwndbg

Issue:
Option for `find_fast_chunk` to find aligned chunks
When I'm looking for a fastbin chunk to free, I need to make sure it's aligned. More generally, I want the chunk returned by `find_fast_chunk` to satisfy all the constraints checked by `try_free`. It'd be useful if there was an option that only returned fastbin chunks that could be freed without failing any glibc checks.


Discussion:
@gsingh93 can you add the code related to it to our heap code? also, u should probably rely on the latest heap PR (we will merge it soon)

Resolving patch:
diff --git a/pwndbg/commands/heap.py b/pwndbg/commands/heap.py
--- a/pwndbg/commands/heap.py
+++ b/pwndbg/commands/heap.py
@@ -544,49 +544,110 @@ def tcachebins(addr=None, verbose=False):
 
 
 parser = argparse.ArgumentParser()
-parser.description = "Find candidate fake fast chunks overlapping the specified address."
+parser.description = "Find candidate fake fast or tcache chunks overlapping the specified address."
 parser.add_argument("addr", type=int, help="Address of the word-sized value to overlap.")
-parser.add_argument("size", nargs="?", type=int, default=None, help="Size of fake chunks to find.")
+parser.add_argument(
+    "size", nargs="?", type=int, default=None, help="Maximum size of fake chunks to find."
+)
+parser.add_argument(
+    "--align",
+    "-a",
+    action="store_true",
+    default=False,
+    help="Whether the fake chunk must be aligned to MALLOC_ALIGNMENT. This is required for tcache "
+    + "chunks and for all chunks when Safe Linking is enabled",
+)
 
 
 @pwndbg.commands.ArgparsedCommand(parser)
 @pwndbg.commands.OnlyWhenRunning
 @pwndbg.commands.OnlyWithResolvedHeapSyms
 @pwndbg.commands.OnlyWhenHeapIsInitialized
-def find_fake_fast(addr, size=None):
+def find_fake_fast(addr, size=None, align=False):
     """Find candidate fake fast chunks overlapping the specified address."""
     psize = pwndbg.gdblib.arch.ptrsize
     allocator = pwndbg.heap.current
-    align = allocator.malloc_alignment
+    malloc_alignment = allocator.malloc_alignment
+
     min_fast = allocator.min_chunk_size
     max_fast = allocator.global_max_fast
     max_fastbin = allocator.fastbin_index(max_fast)
-    start = int(addr) - max_fast + psize
-    if start < 0:
+
+    if size is None:
+        size = max_fast
+    elif size > addr:
+        print(message.warn("Size of 0x%x is greater than the target address 0x%x", (size, addr)))
+        size = addr
+    elif size > max_fast:
         print(
             message.warn(
-                "addr - global_max_fast is negative, if the max_fast is not corrupted, you gave wrong address"
+                "0x%x is greater than the global_max_fast value of 0x%x" % (size, max_fast)
             )
         )
-        start = 0  # TODO, maybe some better way to handle case when global_max_fast is overwritten with something large
-    mem = pwndbg.gdblib.memory.read(start, max_fast - psize, partial=True)
+    elif size < min_fast:
+        print(
+            message.warn(
+                "0x%x is smaller than the minimum fastbin chunk size of 0x%x" % (size, min_fast)
+            )
+        )
+        size = min_fast
 
-    fmt = {"little": "<", "big": ">"}[pwndbg.gdblib.arch.endian] + {4: "I", 8: "Q"}[psize]
+    # Clear the flags
+    size &= ~0xF
 
-    if size is None:
-        sizes = range(min_fast, max_fast + 1, align)
-    else:
-        sizes = [int(size)]
+    start = int(addr) - size + psize
+
+    if align:
+        # If a chunk is aligned to MALLOC_ALIGNMENT, the size field should be at
+        # offset `psize`. First we align up to a multiple of `psize`
+        new_start = pwndbg.lib.memory.align_up(start, psize)
+
+        # Then we make sure we're at a multiple of `psize` but not `psize*2` by
+        # making sure the bottom nibble gets set to `psize`
+        new_start |= psize
+
+        # We should not have increased `start` by more than `psize*2 - 1` bytes
+        distance = new_start - start
+        assert distance < psize * 2
+
+        # If we're starting at a higher address, we still only want to read
+        # enough bytes to reach our target address
+        size -= distance
+
+        # Clear the flags
+        size &= ~0xF
+
+        start = new_start
+
+    print(
+        message.notice(
+            "Searching for fastbin sizes up to 0x%x starting at 0x%x resulting in an overlap of 0x%x"
+            % (size, start, addr)
+        )
+    )
+
+    # Only consider `size - psize` bytes, since we're starting from after `prev_size`
+    mem = pwndbg.gdblib.memory.read(start, size - psize, partial=True)
+
+    fmt = {"little": "<", "big": ">"}[pwndbg.gdblib.arch.endian] + {4: "I", 8: "Q"}[psize]
 
     print(C.banner("FAKE CHUNKS"))
-    for size in sizes:
-        fastbin = allocator.fastbin_index(size)
-        for offset in range((max_fastbin - fastbin) * align, max_fast - align + 1):
-            candidate = mem[offset : offset + psize]
-            if len(candidate) == psize:
-                value = struct.unpack(fmt, candidate)[0]
-                if allocator.fastbin_index(value) == fastbin:
-                    malloc_chunk(start + offset - psize, fake=True)
+    step = malloc_alignment if align else 1
+    for i in range(0, len(mem), step):
+        candidate = mem[i : i + psize]
+        if len(candidate) == psize:
+            value = struct.unpack(fmt, candidate)[0]
+
+            # Clear any flags
+            value &= ~0xF
+
+            if value < min_fast:
+                continue
+
+            # The value must be less than or equal to the max size we're looking
+            # for, but still be able to reach the target address
+            if value <= size and i + value >= size:
+                malloc_chunk(start + i - psize, fake=True)
 
 
 pwndbg.gdblib.config.add_param(


Test patch:
diff --git a/tests/binaries/heap_find_fake_fast.c b/tests/binaries/heap_find_fake_fast.c
--- a/tests/binaries/heap_find_fake_fast.c
+++ b/tests/binaries/heap_find_fake_fast.c
@@ -71,7 +71,7 @@ int main(void) {
     setup_mem(0x80, 0x78);
     break_here();
 
-    // A valid unaligned fastbin chunk just in range of the target address
+    /* // A valid unaligned fastbin chunk just in range of the target address */
     /* setup_mem(0x80, 0x7F); */
     /* break_here(); */
 
diff --git a/tests/heap/test_find_fake_fast.py b/tests/heap/test_find_fake_fast.py
--- a/tests/heap/test_find_fake_fast.py
+++ b/tests/heap/test_find_fake_fast.py
@@ -61,39 +61,60 @@ def test_find_fake_fast_command(start_binary):
     # setup_mem(0x20, 0x8)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_result(result, 0x20)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_result(result, 0x20)
     gdb.execute("continue")
 
     # setup_mem(0x2F, 0x8)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_result(result, 0x2F)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_result(result, 0x2F)
     gdb.execute("continue")
 
     # setup_mem(0x20, 0x9)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_result(result, 0x20)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_no_results(result)
     gdb.execute("continue")
 
     # setup_mem(0x20, 0x0)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_no_results(result)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_no_results(result)
     gdb.execute("continue")
 
     # setup_mem(0x20, 0x7)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_no_results(result)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_no_results(result)
     gdb.execute("continue")
 
     # setup_mem(0x1F, 0x8)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_no_results(result)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_no_results(result)
     gdb.execute("continue")
 
     # setup_mem(0x80, 0x78)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_result(result, 0x80)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_result(result, 0x80)
     gdb.execute("continue")
 
-    # setup_mem(0x80, 0x7F)
+    # # setup_mem(0x80, 0x7F)
     # result = gdb.execute("find_fake_fast &target_address", to_string=True)
     # check_result(result, 0x80)
     # gdb.execute("continue")
@@ -101,17 +122,31 @@ def test_find_fake_fast_command(start_binary):
     # setup_mem(0x80, 0x80)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_no_results(result)
+
+    result = gdb.execute("find_fake_fast --align &target_address", to_string=True)
+    check_no_results(result)
     gdb.execute("continue")
 
     # setup_mem(0x100, 0x10)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_no_results(result)
+
+    result = gdb.execute("find_fake_fast &target_address 0x100", to_string=True)
+    check_result(result, 0x100)
     gdb.execute("continue")
 
     # setup_mem(0x100, 0x90)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_no_results(result)
 
+    result = gdb.execute("find_fake_fast &target_address 0x100", to_string=True)
+    check_result(result, 0x100)
+    gdb.execute("continue")
+
     # setup_mem(0x100, 0x100)
     result = gdb.execute("find_fake_fast &target_address", to_string=True)
     check_no_results(result)
+
+    result = gdb.execute("find_fake_fast &target_address 0x100", to_string=True)
+    check_no_results(result)
+    gdb.execute("continue")