Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

volshell: change dt() output to show where pointers lead #1028

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
171 changes: 148 additions & 23 deletions volatility3/cli/volshell/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
except ImportError:
has_capstone = False

MAX_DEREFERENCE_COUNT = 4 # the max number of times display_type should follow pointers


class Volshell(interfaces.plugins.PluginInterface):
"""Shell environment to directly interact with a memory image."""
Expand Down Expand Up @@ -312,6 +314,30 @@ def disassemble(self, offset, count=128, layer_name=None, architecture=None):
for i in disasm_types[architecture].disasm(remaining_data, offset):
print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")

def _get_type_name_with_pointer(
self,
member_type: Union[
str, interfaces.objects.ObjectInterface, interfaces.objects.Template
],
depth: int = 0,
) -> str:
"""Takes a member_type from and returns the subtype name with a * if the member_type is
a pointer otherwise it returns just the normal type name."""
pointer_marker = "*" * depth
try:
if member_type.vol.object_class == objects.Pointer:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be better to use isinstance than equality? It's interesting this is a different test than the isinstance below used for display_type They should probably match to avoid weird discrepancies! 5:P

sub_member_type = member_type.vol.subtype
# follow at most MAX_DEREFERENCE_COUNT pointers. A guard against, hopefully unlikely, infinite loops
if depth < MAX_DEREFERENCE_COUNT:
return self._get_type_name_with_pointer(sub_member_type, depth + 1)
else:
return member_type_name
except AttributeError:
pass # not all objects get a `object_class`, and those that don't are not pointers.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They should all get a type_name, but then you're having to do string comparisons (although pointer should be a built-in type, so the name shouldn't change?). Happy with either route...

finally:
member_type_name = pointer_marker + member_type.vol.type_name
return member_type_name

def display_type(
self,
object: Union[
Expand Down Expand Up @@ -344,67 +370,166 @@ def display_type(
volobject.vol.type_name, layer_name=self.current_layer, offset=offset
)

if hasattr(volobject.vol, "size"):
print(f"{volobject.vol.type_name} ({volobject.vol.size} bytes)")
elif hasattr(volobject.vol, "data_format"):
data_format = volobject.vol.data_format
print(
"{} ({} bytes, {} endian, {})".format(
volobject.vol.type_name,
data_format.length,
data_format.byteorder,
"signed" if data_format.signed else "unsigned",
)
)
# add special case for pointer so that information about the struct the
# pointer is pointing to is shown rather than simply the fact this is a
# pointer object. The "dereference_count < MAX_DEREFERENCE_COUNT" is to
# guard against loops
dereference_count = 0
while (
isinstance(volobject, objects.Pointer)
and dereference_count < MAX_DEREFERENCE_COUNT
):
# before defreerencing the pointer, show it's information
print(f'{" " * dereference_count}{self._display_simple_type(volobject)}')

# check that we can follow the pointer before dereferencing and do not
# attempt to follow null pointers.
if volobject.is_readable() and volobject != 0:
# now deference the pointer and store this as the new volobject
volobject = volobject.dereference()
dereference_count = dereference_count + 1
else:
# if we aren't able to follow the pointers anymore then there will
ikelos marked this conversation as resolved.
Show resolved Hide resolved
# be no more information to display as we've already printed the
# details of this pointer including the fact that we're not able to
# follow it anywhere
return

if hasattr(volobject.vol, "members"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is a good way of telling a struct from a pointer? Can't immediately think of how to do it, but I think there is a single overarching struct type that covers the 3 different children, probably better to isinstance that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question - I'll dig into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been exploring this issue and found that when dealing with actual objects or types with offsets, checking if it's an instance of objects.AggregateType works really well. 👍

However, I ran into difficulties when working with templates, such as when executing dt('task_struct') without any offsets. Both cases appear as ObjectTemplate types, making it challenging to distinguish between them with type checks:

(layer_name) >>> type(self.context.symbol_space.get_type('symbol_table_name1!task_struct'))       
<class 'volatility3.framework.objects.templates.ObjectTemplate'>
(layer_name) >>> type(self.context.symbol_space.get_type('symbol_table_name1!int'))            
<class 'volatility3.framework.objects.templates.ObjectTemplate'>

I found .vol.object_class promising for getting the types:

(layer_name) >>> self.context.symbol_space.get_type('symbol_table_name1!task_struct').vol.object_class
<class 'volatility3.framework.symbols.linux.extensions.task_struct'>
(layer_name) >>> self.context.symbol_space.get_type('symbol_table_name1!int').vol.object_class         
<class 'volatility3.framework.objects.Integer'>

However, .vol.object_class ends up as abc.ABCMeta, making isinstance checks infeasible as far as I understand. Which is kind of understandable since they're not actual instances.

(layer_name) >>> type(self.context.symbol_space.get_type('symbol_table_name1!task_struct').vol.object_class) 
<class 'abc.ABCMeta'>
(layer_name) >>> type(self.context.symbol_space.get_type('symbol_table_name1!int').vol.object_class)          
<class 'abc.ABCMeta'>

I've looked for solutions and found some discussions like this one on StackOverflow, but I'm still uncertain about the best approach.

It might be possible to adjust the templates to facilitate isinstance checks, but it's currently beyond my expertise. 😕 For now, I've resorted to using hasattr to differentiate between types, though it feels like a workaround:

(layer_name) >>> hasattr(self.context.symbol_space.get_type('symbol_table_name1!task_struct'), "members")                  
True
(layer_name) >>> hasattr(self.context.symbol_space.get_type('symbol_table_name1!int'), "members")         
False

I'm open to suggestions or guidance on how to approach this more effectively. 🙃 Let me know if you have any good ideas!

# display the header for this object, if the orginal object was just a type string, display the type information
struct_header = f'{" " * dereference_count}{volobject.vol.type_name} ({volobject.vol.size} bytes)'
if isinstance(object, str) and offset is None:
suffix = ":"
else:
# this is an actual object or an offset was given so the offset should be displayed
suffix = f" @ {hex(volobject.vol.offset)}:"
print(struct_header + suffix)

# it is a more complex type, so all members also need information displayed
longest_member = longest_offset = longest_typename = 0
for member in volobject.vol.members:
relative_offset, member_type = volobject.vol.members[member]
longest_member = max(len(member), longest_member)
longest_offset = max(len(hex(relative_offset)), longest_offset)
longest_typename = max(len(member_type.vol.type_name), longest_typename)
member_type_name = self._get_type_name_with_pointer(
member_type
) # special case for pointers to show what they point to
longest_typename = max(len(member_type_name), longest_typename)

for member in sorted(
volobject.vol.members, key=lambda x: (volobject.vol.members[x][0], x)
):
relative_offset, member_type = volobject.vol.members[member]
len_offset = len(hex(relative_offset))
len_member = len(member)
len_typename = len(member_type.vol.type_name)
member_type_name = self._get_type_name_with_pointer(
member_type
) # special case for pointers to show what they point to
len_typename = len(member_type_name)

if isinstance(volobject, interfaces.objects.ObjectInterface):
# We're an instance, so also display the data
print(
" " * dereference_count,
" " * (longest_offset - len_offset),
hex(relative_offset),
": ",
member,
" " * (longest_member - len_member),
" ",
member_type.vol.type_name,
member_type_name,
" " * (longest_typename - len_typename),
" ",
self._display_value(getattr(volobject, member)),
)
else:
# not provided with an actual object, nor an offset so just display the types
print(
" " * dereference_count,
" " * (longest_offset - len_offset),
hex(relative_offset),
": ",
member,
" " * (longest_member - len_member),
" ",
member_type.vol.type_name,
member_type_name,
)

@classmethod
def _display_value(cls, value: Any) -> str:
if isinstance(value, objects.PrimitiveObject):
return repr(value)
elif isinstance(value, objects.Array):
return repr([cls._display_value(val) for val in value])
else: # simple type with no members, only one line to print
# if the orginal object was just a type string, display the type information
if isinstance(object, str) and offset is None:
print(self._display_simple_type(volobject, include_value=False))

# if the original object was an actual volobject or was a type string
# with an offset. Then append the actual data to the display.
else:
print(" " * dereference_count, self._display_simple_type(volobject))

def _display_simple_type(
self,
volobject: Union[
interfaces.objects.ObjectInterface, interfaces.objects.Template
],
include_value: bool = True,
) -> str:
# build the display_type_string based on the aviable information

if hasattr(volobject.vol, "size"):
# the most common type to display, this shows their full size, e.g.:
# (layer_name) >>> dt('task_struct')
# symbol_table_name1!task_struct (1784 bytes)
display_type_string = (
f"{volobject.vol.type_name} ({volobject.vol.size} bytes)"
)
elif hasattr(volobject.vol, "data_format"):
# this is useful for very simple types like ints, e.g.:
# (layer_name) >>> dt('int')
# symbol_table_name1!int (4 bytes, little endian, signed)
data_format = volobject.vol.data_format
display_type_string = "{} ({} bytes, {} endian, {})".format(
ikelos marked this conversation as resolved.
Show resolved Hide resolved
volobject.vol.type_name,
data_format.length,
data_format.byteorder,
"signed" if data_format.signed else "unsigned",
)
elif hasattr(volobject.vol, "type_name"):
# types like void have almost no values to display other than their name, e.g.:
# (layer_name) >>> dt('void')
# symbol_table_name1!void
display_type_string = volobject.vol.type_name
else:
# it should not be possible to have a volobject without at least a type_name
raise AttributeError("Unable to find any details for object")

if include_value: # if include_value is true also add the value to the display
if isinstance(volobject, objects.Pointer):
# for pointers include the location of the pointer and where it points to
return f"{display_type_string} @ {hex(volobject.vol.offset)} -> {self._display_value(volobject)}"
else:
return f"{display_type_string}: {self._display_value(volobject)}"
else:
return hex(value.vol.offset)
return display_type_string

def _display_value(self, value: Any) -> str:
try:
if isinstance(value, objects.Pointer):
# show pointers in hex to match output for struct addrs
# highlight null or unreadable pointers
if value == 0:
suffix = " (null pointer)"
elif not value.is_readable():
suffix = " (unreadable pointer)"
else:
suffix = ""
return f"{hex(value)}{suffix}"
elif isinstance(value, objects.PrimitiveObject):
return repr(value)
elif isinstance(value, objects.Array):
return repr([self._display_value(val) for val in value])
else:
return hex(value.vol.offset)
except exceptions.InvalidAddressException:
return "-"

def generate_treegrid(
self, plugin: Type[interfaces.plugins.PluginInterface], **kwargs
Expand Down
Loading