Add a `chain` command to traverse liked lists (#1795)

This commit adds a command that 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.
pull/1811/head
Matheus Branco Borella 2 years ago committed by GitHub
parent f218532073
commit 3e8b597929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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 <node_a>: {
value = 0,
next = 0x4000011040 <node_b>
}
0x4000011040 <node_b>: {
value = 1,
next = 0x4000011010 <node_c>
}
0x4000011010 <node_c>: {
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 <inner_a_node_a>: {
value = 0,
inner = {
next = 0x4000011068 <inner_a_node_b+8>
}
}
0x4000011060 <inner_a_node_b>: {
value = 1,
inner = {
next = 0x4000011028 <inner_a_node_c+8>
}
}
0x4000011020 <inner_a_node_c>: {
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 <inner_b_node_a>: {
value = 0,
inner = {
next = 0x4000011080 <inner_b_node_b>
}
}
0x4000011080 <inner_b_node_b>: {
value = 1,
inner = {
next = 0x4000011030 <inner_b_node_c>
}
}
0x4000011030 <inner_b_node_c>: {
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

@ -0,0 +1,46 @@
#include <stddef.h> /* 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;
}

@ -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"

@ -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]+ <node_a>: {\\s*
value = 0,\\s*
next = 0[xX][0-9a-fA-F]+ <node_b>\\s*
}\\s*
0[xX][0-9a-fA-F]+ <node_b>: {\\s*
value = 1,\\s*
next = 0[xX][0-9a-fA-F]+ <node_c>\\s*
}\\s*
0[xX][0-9a-fA-F]+ <node_c>: {\\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]+ <node_a>: 0\\s*
0[xX][0-9a-fA-F]+ <node_b>: 1\\s*
0[xX][0-9a-fA-F]+ <node_c>: 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]+ <node_a>: {\\s*
value = 0,\\s*
next = 0[xX][0-9a-fA-F]+ <node_b>\\s*
}\\s*
0[xX][0-9a-fA-F]+ <node_b>: {\\s*
value = 1,\\s*
next = 0[xX][0-9a-fA-F]+ <node_c>\\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]+ <inner_b_node_a>: {\\s*
value = 0,\\s*
inner = {\\s*
next = 0[xX][0-9a-fA-F]+ <inner_b_node_b>\\s*
}\\s*
}\\s*
0[xX][0-9a-fA-F]+ <inner_b_node_b>: {\\s*
value = 1,\\s*
inner = {\\s*
next = 0[xX][0-9a-fA-F]+ <inner_b_node_c>\\s*
}\\s*
}\\s*
0[xX][0-9a-fA-F]+ <inner_b_node_c>: {\\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]+ <inner_a_node_a>: {\\s*
value = 0,\\s*
inner = {\\s*
next = 0[xX][0-9a-fA-F]+ <inner_a_node_b\\+8>\\s*
}\\s*
}\\s*
0[xX][0-9a-fA-F]+ <inner_a_node_b>: {\\s*
value = 1,\\s*
inner = {\\s*
next = 0[xX][0-9a-fA-F]+ <inner_a_node_c\\+8>\\s*
}\\s*
}\\s*
0[xX][0-9a-fA-F]+ <inner_a_node_c>: {\\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
Loading…
Cancel
Save