diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py index 88506ab31..93ffab2c1 100644 --- a/pwndbg/commands/__init__.py +++ b/pwndbg/commands/__init__.py @@ -615,6 +615,7 @@ def load_commands() -> None: import pwndbg.commands.auxv import pwndbg.commands.branch import pwndbg.commands.canary + import pwndbg.commands.chain import pwndbg.commands.checksec import pwndbg.commands.comments import pwndbg.commands.config diff --git a/pwndbg/commands/chain.py b/pwndbg/commands/chain.py new file mode 100644 index 000000000..10016c34a --- /dev/null +++ b/pwndbg/commands/chain.py @@ -0,0 +1,393 @@ +import argparse + +import gdb + +import pwndbg.chain +import pwndbg.commands +import pwndbg.gdblib.memory +from pwndbg.color import message + +parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + description="""Dumps the elements of a linked list. + +This command traverses the linked list beginning at a given element, dumping its +contents and the contents of all the elements that come after it in the list. +Traversal is configurable and can handle multiple types of chains, but will +always stop when a cycle is detected. + +The path to the first element can be any GDB expression that evaluates to either +the first element directly, or a to pointer to it. The next element is the name +of the field containing the next pointer, in either the structure itself or in +the structure given by --inner. + +An address value may be given with --sentinel that signals the end of the list. +By default, the value used is NULL (0). + +If only one field inside each node is desired, it can be printed exclusively by +specifying its name with --field. + +This command supports traversing three types of linked lists, classified by how +the next pointer can be found in the structure and what type it is: + 1 - Next pointer is field of structure, type is the same as structure. + 2 - Next pointer is field of inner nested structure, pointed to type is the + same as outer structure. + 3 - Next pointer is field of inner nested structure, pointed to type is the + same as inner structure. +Types 2 and 3 require --inner to be specified. + +Example 1: + +``` +struct node { + int value; + struct node *next; +}; +struct node node_c = { 2, NULL }; +struct node node_b = { 1, &node_c }; +struct node node_a = { 0, &node_b }; +``` + +pwndbg> chain node_a next +0x4000011050 : { + value = 0, + next = 0x4000011040 +} +0x4000011040 : { + value = 1, + next = 0x4000011010 +} +0x4000011010 : { + value = 2, + next = 0x0 +} + +Example 2: + +``` +struct node_inner_a { + struct node_inner_a *next; +}; +struct inner_a_node { + int value; + struct node_inner_a inner; +}; +struct inner_a_node inner_a_node_c = { 2, { NULL } }; +struct inner_a_node inner_a_node_b = { 1, { &inner_a_node_c.inner } }; +struct inner_a_node inner_a_node_a = { 0, { &inner_a_node_b.inner } }; +``` + +pwndbg> chain inner_a_node_a -i inner next +0x4000011070 : { + value = 0, + inner = { + next = 0x4000011068 + } +} +0x4000011060 : { + value = 1, + inner = { + next = 0x4000011028 + } +} +0x4000011020 : { + value = 2, + inner = { + next = 0x0 + } +} + +Example 3: + +``` +struct inner_b_node; +struct node_inner_b { + struct inner_b_node *next; +}; +struct inner_b_node { + int value; + struct node_inner_b inner; +}; +struct inner_b_node inner_b_node_c = { 2, { NULL } }; +struct inner_b_node inner_b_node_b = { 1, { &inner_b_node_c } }; +struct inner_b_node inner_b_node_a = { 0, { &inner_b_node_b } }; +``` + +pwndbg> chain inner_b_node_a -i inner next +0x4000011090 : { + value = 0, + inner = { + next = 0x4000011080 + } +} +0x4000011080 : { + value = 1, + inner = { + next = 0x4000011030 + } +} +0x4000011030 : { + value = 2, + inner = { + next = 0x0 + } +} + +""", +) +parser.add_argument( + "path", + type=str, + help="The first element of the chain", +) +parser.add_argument( + "next", type=str, help="The name of the field pointing to the next element in the chain" +) +parser.add_argument( + "-s", + "--sentinel", + dest="sentinel", + type=int, + default=0, + help="The address that stands in for an end of list marker in a non-cyclic list", +) +parser.add_argument( + "-i", + "--inner", + dest="inner_name", + type=str, + help="The name of the inner nested structure where the next pointer is stored", +) +parser.add_argument( + "-f", + "--field", + dest="field_name", + type=str, + help="The name of the field to be displayed, if only one is desired", +) + + +@pwndbg.commands.ArgparsedCommand(parser, command_name="chain") +def chain(path, next, sentinel, inner_name, field_name) -> None: + # Have GDB parse the path for us and check if it's valid. + try: + first = gdb.parse_and_eval(path) + except gdb.error as e: + print(message.error(f"{e}")) + return + + if first.is_optimized_out: + print(message.error(f"{path} has been optimized out")) + return + + # We suport being passed either a pointer to the first structure or the + # structure itself, for the sake of convenience. But we don't bother with + # chains of pointers. Additionally, we pick the correct separator to use + # for the error mesages. + sep = "." + deref = "" + if first.type.code == gdb.TYPE_CODE_PTR: + sep = "->" + deref = "*" + try: + first = first.dereference() + except gdb.error as e: + print(message.error(f"Pointer at {path} could not be dereferenced: {e}")) + return + + if first.type.code == gdb.TYPE_CODE_PTR: + print(message.error(f"{path} is not a value or a single pointer to one")) + return + + if first.address is None: + print(message.error(f"{deref}{path} is not addressable")) + return + + if first.is_optimized_out: + print(message.error(f"{deref}{path} has been optimized out")) + return + + # If there is an inner element we have to use, find it. + inner = None + inner_sep = "" + if inner_name is not None: + try: + inner = first[inner_name] + inner_sep = "->" + except gdb.error as e: + print(message.error(f"Cannot find component {inner_name} in {path}: {e}")) + return + if inner.is_optimized_out: + print(message.error(f"{path}{sep}{inner_name} has been optimized out")) + return + + # Resolve the pointer to the next structure, wherever it may be, and make + # sure that we can use it to traverse the chain. + next_ptr_loc = first + next_ptr_name = next + try: + if inner is None: + next_ptr = first[next] + else: + next_ptr = inner[next] + next_ptr_loc = inner + next_ptr_name = f"{inner_name}.{next}" + except gdb.error as e: + print(message.error(f"Cannot find component {next_ptr_name} in {path}: {e}")) + return + + if next_ptr.is_optimized_out: + print(message.error(f"{path}{sep}{next_ptr_name} has been optimized out")) + return + if next_ptr.type.code != gdb.TYPE_CODE_PTR: + print(message.error(f"{path}{sep}{next_ptr_name} is not a pointer")) + return + + # If the user wants a specific field to be displayed, resolve it. + field_offset = None + field_type = None + if field_name is not None: + try: + field = first[field_name] + except gdb.error as e: + print(message.error(f"Cannot find component {field_name} in {path}: {e}")) + return + field_type = field.type + + bit_offset = bit_offset_of_field(first, field_name) + if bit_offset is None: + print( + message.error( + f"{path}{sep}{field_name} has no known offset \ + from {deref}{path}" + ) + ) + return + byte_offset = get_byte_offset(bit_offset) + if byte_offset is None: + print( + message.error( + f"{field_name} is a non-whole number of bytes \ + {bit_offset} bits) offset from {deref}{path}" + ) + ) + return + field_offset = byte_offset + + # Figure out the offset of the inner structure, if any, in typeof(first). + inner_offset = 0 + if inner is not None: + bit_offset = bit_offset_of_field(first, inner_name) + if bit_offset is None: + print( + message.error( + f"{path}{sep}{inner_name} has no known offset \ + from {deref}{path}" + ) + ) + return + byte_offset = get_byte_offset(bit_offset) + if byte_offset is None: + print( + message.error( + f"{inner_name} is a non-whole number of bytes \ + {bit_offset} bits) offset from {deref}{path}" + ) + ) + return + inner_offset = byte_offset + + # Figure out the offset of the next pointer in its containing type. + bit_offset = bit_offset_of_field(next_ptr_loc, next) + if bit_offset is None: + print( + message.error( + f"{path}{sep}{next_ptr_name} has no known offset \ + from {deref}{path}{inner_sep}{inner_name}" + ) + ) + return + byte_offset = get_byte_offset(bit_offset) + if byte_offset is None: + print( + message.error( + f"{path}{sep}{next_ptr_name} is a non-whole \ + number of bytes {bit_offset} bits) offset from \ + {deref}{path}{inner_sep}{inner_name}" + ) + ) + return + next_offset = byte_offset + + # If the next pointer points to an intance of the inner structure, we will + # additionally have to do the equivalent of container_of(next, typeof(first), + # inner_name). + # + # Here, we figure out how many bytes to subtract from *typeof(inner) so that + # we can have a *typeof(first). + pointee_offset = 0 + if inner is not None and next_ptr.type.target() == inner.type: + pointee_offset = inner_offset + elif next_ptr.type.target() == first.type: + # We've already got everything we need for this mode. + pass + else: + print( + message.error( + f"{deref}{path}{sep}{next_ptr_name} has a \ + different type than {path}" + ) + ) + return + + # Now, we follow the chain. We have to do this in two steps, as, because we + # have the address of the first outer structure, in case pointee_offset is + # not zero, the offset for the first element will not be the same as the one + # for all of the elements after it. + offset0 = inner_offset + next_offset + offset1 = offset0 - pointee_offset + addresses = pwndbg.chain.get(int(first.address), limit=1, offset=offset0) + if len(addresses) > 1: + addresses.extend(pwndbg.chain.get(addresses[1], offset=offset1, include_start=False)) + + # Finally, dump the information in the addresses we've just gathered. + for i, address in enumerate(addresses): + if address == sentinel: + break + try: + # Always make sure we have the address of the outer structure. + if i > 0: + address -= pointee_offset + + # Read the data and print it out. + target_type = first.type + target_address = address + if field_offset is not None: + target_type = field_type + target_address = address + field_offset + + value = pwndbg.gdblib.memory.poi(target_type, target_address) + + symbol = pwndbg.gdblib.symbol.get(target_address) + symbol = f"<{symbol}>" if symbol else "" + + print(f"{target_address:#x} {symbol}: {value}") + except gdb.error as e: + print(message.error(f"Cannot dereference 0x{address:#x} for chain link #{i + 1}: {e}")) + print(message.error("Is the chain corrupted or is the sentinel value wrong?")) + return + + +# Helper functions to resolve the offsets of fields inside a structure. +def bit_offset_of_field(struct, field_name, inner_name=None): + offset = None + for field in struct.type.fields(): + if field.name == field_name: + offset = field.bitpos + return offset + + +def get_byte_offset(bit_offset): + if bit_offset % 8 != 0: + return None + return bit_offset // 8 diff --git a/tests/gdb-tests/tests/binaries/linked-lists.c b/tests/gdb-tests/tests/binaries/linked-lists.c new file mode 100644 index 000000000..9f77d7d87 --- /dev/null +++ b/tests/gdb-tests/tests/binaries/linked-lists.c @@ -0,0 +1,46 @@ +#include /* For NULL. */ + +/* Linked list in which the pointer to the next element in inside the node + * structure itself. */ +struct node { + int value; + struct node *next; +}; +struct node node_c = { 2, NULL }; +struct node node_b = { 1, &node_c }; +struct node node_a = { 0, &node_b }; + +/* Linked list in which the nodes are inner structures of a larger structure. */ +struct node_inner_a { + struct node_inner_a *next; +}; +struct inner_a_node { + int value; + struct node_inner_a inner; +}; +struct inner_a_node inner_a_node_c = { 2, { NULL } }; +struct inner_a_node inner_a_node_b = { 1, { &inner_a_node_c.inner } }; +struct inner_a_node inner_a_node_a = { 0, { &inner_a_node_b.inner } }; + +/* Linked list in which the pointer to the next element is nested inside the + * structure. */ +struct inner_b_node; +struct node_inner_b { + struct inner_b_node *next; +}; +struct inner_b_node { + int value; + struct node_inner_b inner; +}; +struct inner_b_node inner_b_node_c = { 2, { NULL } }; +struct inner_b_node inner_b_node_b = { 1, { &inner_b_node_c } }; +struct inner_b_node inner_b_node_a = { 0, { &inner_b_node_b } }; + + +void break_here(void) {} +int main(void) +{ + break_here(); + return 0; +} + diff --git a/tests/gdb-tests/tests/binaries/makefile b/tests/gdb-tests/tests/binaries/makefile index 3c246f58b..853c3dcc2 100644 --- a/tests/gdb-tests/tests/binaries/makefile +++ b/tests/gdb-tests/tests/binaries/makefile @@ -38,7 +38,7 @@ GLIBC_2_33=$(PWD)/glibcs/2.33 .PHONY : all clean -CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out symbol_1600_and_752.out initialized_heap_x64.out initialized_heap_i386_big.out +CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out symbol_1600_and_752.out initialized_heap_x64.out initialized_heap_i386_big.out linked_lists.out all: $(LINKED) $(LINKED_ASM) $(COMPILED_GO) $(CUSTOM_TARGETS) @@ -134,6 +134,9 @@ clean : @echo "[+] Cleaning stuff" @rm -f $(COMPILED) $(LINKED) $(COMPILED_ASM) $(LINKED_ASM) $(COMPILED_GO) *.out *.o +linked_lists.out: linked-lists.c + @echo "[+] Building $<" + ${ZIGCC} -fpie -g -o $@ $< reference_bin_pie.out: reference-binary.c @echo "[+] Building reference_bin_pie.out" diff --git a/tests/gdb-tests/tests/test_command_chain.py b/tests/gdb-tests/tests/test_command_chain.py new file mode 100644 index 000000000..5baf60f4e --- /dev/null +++ b/tests/gdb-tests/tests/test_command_chain.py @@ -0,0 +1,146 @@ +import re + +import gdb + +import tests + +LINKED_LISTS_BINARY = tests.binaries.get("linked-lists.out") + + +def startup(start_binary): + start_binary(LINKED_LISTS_BINARY) + + gdb.execute("break break_here") + gdb.execute("run") + gdb.execute("up") + + +def test_command_chain_flat_no_flags(start_binary): + """ + Tests the chain for a non-nested linked list + """ + startup(start_binary) + + expected_out = re.compile( + """\ +0[xX][0-9a-fA-F]+ : {\\s* + value = 0,\\s* + next = 0[xX][0-9a-fA-F]+ \\s* +}\\s* +0[xX][0-9a-fA-F]+ : {\\s* + value = 1,\\s* + next = 0[xX][0-9a-fA-F]+ \\s* +}\\s* +0[xX][0-9a-fA-F]+ : {\\s* + value = 2,\\s* + next = 0x0\\s* +}""" + ) + + result_str = gdb.execute("chain node_a next", to_string=True) + assert expected_out.match(result_str) is not None + + +def test_command_chain_flat_field(start_binary): + """ + Tests the chain command for a non-nested linked list with field flag + """ + startup(start_binary) + + expected_out = re.compile( + """\ +0[xX][0-9a-fA-F]+ : 0\\s* +0[xX][0-9a-fA-F]+ : 1\\s* +0[xX][0-9a-fA-F]+ : 2\\s* +""" + ) + + result_str = gdb.execute("chain node_a next -f value", to_string=True) + assert expected_out.match(result_str) is not None + + +def test_command_chain_flat_sentinel(start_binary): + """ + Tests the chain command for a non-nested linked list with field flag + """ + startup(start_binary) + + sentinel = int(gdb.lookup_symbol("node_c")[0].value().address) + expected_out = re.compile( + """\ +0[xX][0-9a-fA-F]+ : {\\s* + value = 0,\\s* + next = 0[xX][0-9a-fA-F]+ \\s* +}\\s* +0[xX][0-9a-fA-F]+ : {\\s* + value = 1,\\s* + next = 0[xX][0-9a-fA-F]+ \\s* +}""" + ) + + result_str = gdb.execute(f"chain node_a next -s {sentinel}", to_string=True) + assert expected_out.match(result_str) is not None + + +def test_command_chain_nested_direct(start_binary): + """ + Tests the chain for a nested linked list pointing to the outer structure + """ + startup(start_binary) + + expected_out = re.compile( + """\ +0[xX][0-9a-fA-F]+ : {\\s* + value = 0,\\s* + inner = {\\s* + next = 0[xX][0-9a-fA-F]+ \\s* + }\\s* +}\\s* +0[xX][0-9a-fA-F]+ : {\\s* + value = 1,\\s* + inner = {\\s* + next = 0[xX][0-9a-fA-F]+ \\s* + }\\s* +}\\s* +0[xX][0-9a-fA-F]+ : {\\s* + value = 2,\\s* + inner = {\\s* + next = 0x0\\s* + }\\s* +}""" + ) + + result_str = gdb.execute("chain inner_b_node_a -i inner next", to_string=True) + assert expected_out.match(result_str) is not None + + +def test_command_chain_nested_indirect(start_binary): + """ + Tests the chain for a nested linked list pointing to the inner structure + """ + startup(start_binary) + + expected_out = re.compile( + """\ +0[xX][0-9a-fA-F]+ : {\\s* + value = 0,\\s* + inner = {\\s* + next = 0[xX][0-9a-fA-F]+ \\s* + }\\s* +}\\s* +0[xX][0-9a-fA-F]+ : {\\s* + value = 1,\\s* + inner = {\\s* + next = 0[xX][0-9a-fA-F]+ \\s* + }\\s* +}\\s* +0[xX][0-9a-fA-F]+ : {\\s* + value = 2,\\s* + inner = {\\s* + next = 0x0\\s* + }\\s* +}""" + ) + + result_str = gdb.execute("chain inner_a_node_a -i inner next", to_string=True) + assert expected_out.match(result_str) is not None