From 82668af60960d6aa8467a58e6e6b4979a8e26a9b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 9 Jan 2024 16:12:56 -0300 Subject: [PATCH 01/14] Linux - Added linux.ifconfig.Ifconfig plugin --- .../framework/constants/linux/__init__.py | 12 + .../framework/plugins/linux/ifconfig.py | 80 +++++++ .../framework/symbols/linux/__init__.py | 5 + .../symbols/linux/extensions/__init__.py | 205 +++++++++++++++++- 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 volatility3/framework/plugins/linux/ifconfig.py diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 6e8883f195..aa0692365d 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -281,3 +281,15 @@ ) ELF_MAX_EXTRACTION_SIZE = 1024 * 1024 * 1024 * 4 - 1 + +# For IFA_* below - Ref: include/net/ipv6.h +IPV6_ADDR_LOOPBACK = 0x0010 +IPV6_ADDR_LINKLOCAL = 0x0020 +IPV6_ADDR_SITELOCAL = 0x0040 +# For inet6_ifaddr - Ref: include/net/if_inet6.h +IFA_HOST = IPV6_ADDR_LOOPBACK +IFA_LINK = IPV6_ADDR_LINKLOCAL +IFA_SITE = IPV6_ADDR_SITELOCAL + +# Promiscous mode +IFF_PROMISC = 0x100 diff --git a/volatility3/framework/plugins/linux/ifconfig.py b/volatility3/framework/plugins/linux/ifconfig.py new file mode 100644 index 0000000000..39f4208bfe --- /dev/null +++ b/volatility3/framework/plugins/linux/ifconfig.py @@ -0,0 +1,80 @@ +# This file is Copyright 2023 Volatility Foundation and licensed under the Volatility Software License 1.0 +# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0 +# + +from typing import List +from volatility3.framework import interfaces, renderers, constants +from volatility3.framework.configuration import requirements +from volatility3.framework.interfaces import plugins +from volatility3.framework.objects import utility + + +class Ifconfig(plugins.PluginInterface): + """Lists network interface information for all devices""" + + _required_framework_version = (2, 0, 0) + + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ), + ] + + def _gather_net_dev_info(self, net_dev): + mac_addr = net_dev.get_mac_address() + promisc = net_dev.promisc + iface_name = utility.array_to_string(net_dev.name) + iface_ifindex = net_dev.ifindex + try: + net_ns_id = net_dev.get_net_namespace_id() + except AttributeError: + net_ns_id = renderers.NotAvailableValue() + + # Interface IPv4 Addresses + in_device = net_dev.ip_ptr.dereference().cast("in_device") + for in_ifaddr in in_device.get_addresses(): + prefix_len = in_ifaddr.get_prefix_len() + scope_type = in_ifaddr.get_scope_type() + ip_addr = in_ifaddr.get_address() + yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip_addr, prefix_len, scope_type + + # Interface IPv6 Addresses + ip6_ptr = net_dev.ip6_ptr.dereference().cast("inet6_dev") + for inet6_ifaddr in ip6_ptr.get_addresses(): + prefix_len = inet6_ifaddr.get_prefix_len() + scope_type = inet6_ifaddr.get_scope_type() + ip6_addr = inet6_ifaddr.get_address() + yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip6_addr, prefix_len, scope_type + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + net_type_symname = vmlinux.symbol_table_name + constants.BANG + "net" + net_device_symname = vmlinux.symbol_table_name + constants.BANG + "net_device" + + # 'net_namespace_list' exists from kernels >= 2.6.24 + net_namespace_list = vmlinux.object_from_symbol("net_namespace_list") + for net_ns in net_namespace_list.to_list(net_type_symname, "list"): + for net_dev in net_ns.dev_base_head.to_list(net_device_symname, "dev_list"): + for fields in self._gather_net_dev_info(net_dev): + yield 0, fields + + def run(self): + headers = [ + ("NetNS", int), + ("Index", int), + ("Interface", str), + ("MAC", str), + ("Promiscuous", bool), + ("IP", str), + ("Prefix", int), + ("Scope Type", str), + ] + + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/symbols/linux/__init__.py b/volatility3/framework/symbols/linux/__init__.py index c4e2587f49..10cc546b77 100644 --- a/volatility3/framework/symbols/linux/__init__.py +++ b/volatility3/framework/symbols/linux/__init__.py @@ -43,6 +43,11 @@ def __init__(self, *args, **kwargs) -> None: # Network self.set_type_class("net", extensions.net) + self.set_type_class("net_device", extensions.net_device) + self.set_type_class("in_device", extensions.in_device) + self.set_type_class("in_ifaddr", extensions.in_ifaddr) + self.set_type_class("inet6_dev", extensions.inet6_dev) + self.set_type_class("inet6_ifaddr", extensions.inet6_ifaddr) self.set_type_class("socket", extensions.socket) self.set_type_class("sock", extensions.sock) self.set_type_class("inet_sock", extensions.inet_sock) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index d8a2867cce..085d91bff4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -4,6 +4,7 @@ import collections.abc import logging +import struct import socket as socket_module from typing import Generator, Iterable, Iterator, Optional, Tuple, List @@ -13,7 +14,8 @@ from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES -from volatility3.framework.constants.linux import CAPABILITIES +from volatility3.framework.constants.linux import CAPABILITIES, IFF_PROMISC +from volatility3.framework.constants.linux import IFA_HOST, IFA_LINK, IFA_SITE from volatility3.framework import exceptions, objects, interfaces, symbols from volatility3.framework.layers import linear from volatility3.framework.objects import utility @@ -1213,6 +1215,15 @@ def get_mount_points(self): class net(objects.StructType): def get_inode(self): + """Get the namespace id for this network namespace. + + Raises: + AttributeError: If it cannot find the network namespace id for the + current kernel. + + Returns: + int: the namespace id + """ if self.has_member("proc_inum"): # 3.8.13 <= kernel < 3.19.8 return self.proc_inum @@ -1224,6 +1235,198 @@ def get_inode(self): raise AttributeError("Unable to find net_namespace inode") +class net_device(objects.StructType): + @staticmethod + def _format_as_mac_address(hwaddr): + return ":".join([f"{x:02x}" for x in hwaddr[:6]]) + + def get_mac_address(self): + """Get the MAC address of this network interface. + + Returns: + str: the MAC address of this network interface. + """ + if self.has_member("perm_addr"): + mac_addr = self._format_as_mac_address(self.perm_addr) + if mac_addr != "00:00:00:00:00:00": + return mac_addr + + parent_layer = self._context.layers[self.vol.layer_name] + try: + hwaddr = parent_layer.read(self.dev_addr, 6) + except exceptions.InvalidAddressException: + vollog.debug( + f"Unable to read network inteface mac address from {self.dev_addr:#x}" + ) + return + + return self._format_as_mac_address(hwaddr) + + @property + def promisc(self): + """Return if this network interface is in promiscuous mode. + + Returns: + bool: True if this network interface is in promiscuous mode. Otherwise, False + """ + return self.flags & IFF_PROMISC == IFF_PROMISC + + def get_net_namespace_id(self): + """Return the network namespace id for this network interface. + + Returns: + int: the network namespace id for this network interface + """ + nd_net = self.nd_net + if nd_net.has_member("net"): + # In kernel 4.1.52 the 'nd_net' member type was changed from + # 'struct net*' to 'possible_net_t' which has a 'struct net *net' member. + net_ns_id = nd_net.net.get_inode() + else: + # In kernels < 4.1.52 the 'nd_net'member type was 'struct net*' + net_ns_id = nd_net.get_inode() + + return net_ns_id + + +class in_device(objects.StructType): + def get_addresses(self): + """Yield the IPv4 ifaddr addresses + + Yields: + in_ifaddr: An IPv4 ifaddr address + """ + cur = self.ifa_list + while cur and cur.vol.offset: + yield cur + cur = cur.ifa_next + + +class inet6_dev(objects.StructType): + def get_addresses(self): + """Yield the IPv6 ifaddr addresses + + Yields: + inet6_ifaddr: An IPv6 ifaddr address + """ + if not self.has_member( + "addr_list" + ) or not self.addr_list.vol.type_name.endswith(constants.BANG + "list_head"): + # kernels < 3.0 + # FIXME: struct inet6_ifaddr *addr_list; + vollog.warning( + "IPv6 is unsupported for this kernel. Check if the ISF contains the appropriate 'inet6_dev' type" + ) + return + + symbol_space = self._context.symbol_space + table_name = self.vol.type_name.split(constants.BANG)[0] + inet6_ifaddr_symname = table_name + constants.BANG + "inet6_ifaddr" + if not symbol_space.has_type(inet6_ifaddr_symname) or not symbol_space.get_type( + inet6_ifaddr_symname + ).has_member("if_list"): + vollog.warning( + "IPv6 is unsupported for this kernel. Check if the ISF contains the appropriate 'inet6_ifaddr' type" + ) + return + + # 'if_list' member was added to 'inet6_ifaddr' type in kernels 3.0 + for inet6_ifaddr in self.addr_list.to_list(inet6_ifaddr_symname, "if_list"): + yield inet6_ifaddr + + +class in_ifaddr(objects.StructType): + # Translation to text based on iproute2 package. See 'rtnl_rtscope_tab' in lib/rt_names.c + _rtnl_rtscope_tab = { + "RT_SCOPE_UNIVERSE": "global", + "RT_SCOPE_NOWHERE": "nowhere", + "RT_SCOPE_HOST": "host", + "RT_SCOPE_LINK": "link", + "RT_SCOPE_SITE": "site", + } + + def get_scope_type(self): + """Get the scope type for this IPv4 address + + Returns: + str: the IPv4 scope type. + """ + table_name = self.vol.type_name.split(constants.BANG)[0] + rt_scope_enum = self._context.symbol_space.get_enumeration( + table_name + constants.BANG + "rt_scope_t" + ) + try: + rt_scope = rt_scope_enum.lookup(self.ifa_scope) + except ValueError: + return "unknown" + + return self._rtnl_rtscope_tab.get(rt_scope, "unknown") + + def get_address(self): + """Get an string with the IPv4 address + + Returns: + str: the IPv4 address + """ + ipv4_bytes = struct.pack(" Date: Tue, 9 Jan 2024 16:13:33 -0300 Subject: [PATCH 02/14] Fix exception. Although it will be auto-instantiated it's better to explicitily use the exception instance --- volatility3/framework/interfaces/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/interfaces/objects.py b/volatility3/framework/interfaces/objects.py index ab568b927a..b742671976 100644 --- a/volatility3/framework/interfaces/objects.py +++ b/volatility3/framework/interfaces/objects.py @@ -133,7 +133,7 @@ def __init__( def __getattr__(self, attr: str) -> Any: """Method for ensuring volatility members can be returned.""" - raise AttributeError + raise AttributeError() @property def vol(self) -> ReadOnlyMapping: From 6e0ffc9d624fa9f7845fd5b8510a977b619d52d3 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 27 Jan 2024 17:39:34 -0300 Subject: [PATCH 03/14] Plugin renamed. Added net iface status and other improvements from @eve #1029 * IP address conversion via renderers.coversion.* * Use MAC address internal size instead of hardcoded. * Read NET_DEVICE_FLAGS from enumeration --- .../framework/constants/linux/__init__.py | 36 +++++++- .../plugins/linux/{ifconfig.py => ip.py} | 12 +-- .../symbols/linux/extensions/__init__.py | 83 ++++++++++++------- 3 files changed, 94 insertions(+), 37 deletions(-) rename volatility3/framework/plugins/linux/{ifconfig.py => ip.py} (88%) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index aa0692365d..21e0b47f76 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -291,5 +291,37 @@ IFA_LINK = IPV6_ADDR_LINKLOCAL IFA_SITE = IPV6_ADDR_SITELOCAL -# Promiscous mode -IFF_PROMISC = 0x100 +# Only for kernels < 3.15 when the net_device_flags enum didn't exist +# ref include/uapi/linux/if.h +NET_DEVICE_FLAGS = { + "IFF_UP": 0x1, + "IFF_BROADCAST": 0x2, + "IFF_DEBUG": 0x4, + "IFF_LOOPBACK": 0x8, + "IFF_POINTOPOINT": 0x10, + "IFF_NOTRAILERS": 0x20, + "IFF_RUNNING": 0x40, + "IFF_NOARP": 0x80, + "IFF_PROMISC": 0x100, + "IFF_ALLMULTI": 0x200, + "IFF_MASTER": 0x400, + "IFF_SLAVE": 0x800, + "IFF_MULTICAST": 0x1000, + "IFF_PORTSEL": 0x2000, + "IFF_AUTOMEDIA": 0x4000, + "IFF_DYNAMIC": 0x8000, + "IFF_LOWER_UP": 0x10000, + "IFF_DORMANT": 0x20000, + "IFF_ECHO": 0x40000, +} + +# RFC 2863 operational status. Kernels >= 2.6.17. See IF_OPER_* in include/uapi/linux/if.h +IF_OPER_STATES = ( + "UNKNOWN", + "NOTPRESENT", + "DOWN", + "LOWERLAYERDOWN", + "TESTING", + "DORMANT", + "UP", +) diff --git a/volatility3/framework/plugins/linux/ifconfig.py b/volatility3/framework/plugins/linux/ip.py similarity index 88% rename from volatility3/framework/plugins/linux/ifconfig.py rename to volatility3/framework/plugins/linux/ip.py index 39f4208bfe..19393a853e 100644 --- a/volatility3/framework/plugins/linux/ifconfig.py +++ b/volatility3/framework/plugins/linux/ip.py @@ -9,7 +9,7 @@ from volatility3.framework.objects import utility -class Ifconfig(plugins.PluginInterface): +class Addr(plugins.PluginInterface): """Lists network interface information for all devices""" _required_framework_version = (2, 0, 0) @@ -29,6 +29,7 @@ def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface] def _gather_net_dev_info(self, net_dev): mac_addr = net_dev.get_mac_address() promisc = net_dev.promisc + operational_state = net_dev.get_operational_state() iface_name = utility.array_to_string(net_dev.name) iface_ifindex = net_dev.ifindex try: @@ -42,15 +43,15 @@ def _gather_net_dev_info(self, net_dev): prefix_len = in_ifaddr.get_prefix_len() scope_type = in_ifaddr.get_scope_type() ip_addr = in_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip_addr, prefix_len, scope_type + yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip_addr, prefix_len, scope_type, operational_state # Interface IPv6 Addresses - ip6_ptr = net_dev.ip6_ptr.dereference().cast("inet6_dev") - for inet6_ifaddr in ip6_ptr.get_addresses(): + inet6_dev = net_dev.ip6_ptr.dereference().cast("inet6_dev") + for inet6_ifaddr in inet6_dev.get_addresses(): prefix_len = inet6_ifaddr.get_prefix_len() scope_type = inet6_ifaddr.get_scope_type() ip6_addr = inet6_ifaddr.get_address() - yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip6_addr, prefix_len, scope_type + yield net_ns_id, iface_ifindex, iface_name, mac_addr, promisc, ip6_addr, prefix_len, scope_type, operational_state def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] @@ -75,6 +76,7 @@ def run(self): ("IP", str), ("Prefix", int), ("Scope Type", str), + ("State", str), ] return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 085d91bff4..3a088a959f 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -4,9 +4,8 @@ import collections.abc import logging -import struct import socket as socket_module -from typing import Generator, Iterable, Iterator, Optional, Tuple, List +from typing import Generator, Iterable, Iterator, Optional, Tuple, List, Dict from volatility3.framework import constants from volatility3.framework.constants.linux import SOCK_TYPES, SOCK_FAMILY @@ -14,8 +13,10 @@ from volatility3.framework.constants.linux import TCP_STATES, NETLINK_PROTOCOLS from volatility3.framework.constants.linux import ETH_PROTOCOLS, BLUETOOTH_STATES from volatility3.framework.constants.linux import BLUETOOTH_PROTOCOLS, SOCKET_STATES -from volatility3.framework.constants.linux import CAPABILITIES, IFF_PROMISC +from volatility3.framework.constants.linux import CAPABILITIES, NET_DEVICE_FLAGS from volatility3.framework.constants.linux import IFA_HOST, IFA_LINK, IFA_SITE +from volatility3.framework.constants.linux import IF_OPER_STATES +from volatility3.framework.renderers import conversion from volatility3.framework import exceptions, objects, interfaces, symbols from volatility3.framework.layers import linear from volatility3.framework.objects import utility @@ -1236,24 +1237,25 @@ def get_inode(self): class net_device(objects.StructType): - @staticmethod - def _format_as_mac_address(hwaddr): - return ":".join([f"{x:02x}" for x in hwaddr[:6]]) + def _format_as_mac_address(self, hwaddr): + return ":".join([f"{x:02x}" for x in hwaddr[: self.addr_len]]) - def get_mac_address(self): + def get_mac_address(self) -> str: """Get the MAC address of this network interface. Returns: str: the MAC address of this network interface. """ if self.has_member("perm_addr"): + null_mac_addr_bytes = b"\x00" * self.addr_len + null_mac_addr = self._format_as_mac_address(null_mac_addr_bytes) mac_addr = self._format_as_mac_address(self.perm_addr) - if mac_addr != "00:00:00:00:00:00": + if mac_addr != null_mac_addr: return mac_addr parent_layer = self._context.layers[self.vol.layer_name] try: - hwaddr = parent_layer.read(self.dev_addr, 6) + hwaddr = parent_layer.read(self.dev_addr, self.addr_len, pad=True) except exceptions.InvalidAddressException: vollog.debug( f"Unable to read network inteface mac address from {self.dev_addr:#x}" @@ -1262,6 +1264,31 @@ def get_mac_address(self): return self._format_as_mac_address(hwaddr) + def _get_flag_choices(self) -> Dict: + """Return the net_deivce flags as a list of strings""" + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + try: + # kernels >= 3.15 + net_device_flags_enum = vmlinux.get_enumeration("net_device_flags") + choices = net_device_flags_enum.choices + except exceptions.SymbolError: + # kernels < 3.15 + choices = NET_DEVICE_FLAGS + + return choices + + def _get_net_device_flag_value(self, name): + """Return the net_deivce flag value based on the flag name""" + return self._get_flag_choices()[name] + + def get_flag_names(self) -> List[str]: + """Return the net_deivce flags as a list of strings + + Returns: + List[str]: A list of flag names + """ + return list(self._get_flag_choices()) + @property def promisc(self): """Return if this network interface is in promiscuous mode. @@ -1269,9 +1296,9 @@ def promisc(self): Returns: bool: True if this network interface is in promiscuous mode. Otherwise, False """ - return self.flags & IFF_PROMISC == IFF_PROMISC + return self.flags & self._get_net_device_flag_value("IFF_PROMISC") != 0 - def get_net_namespace_id(self): + def get_net_namespace_id(self) -> int: """Return the network namespace id for this network interface. Returns: @@ -1288,6 +1315,17 @@ def get_net_namespace_id(self): return net_ns_id + def get_operational_state(self) -> str: + """Return the netwok device oprational state (RFC 2863) string + + Returns: + str: A string with the operational state + """ + if self.operstate >= len(IF_OPER_STATES): + vollog.warning(f"Invalid net_device operational state '{self.operstate}'") + return "INVALID" + + return IF_OPER_STATES[self.operstate] class in_device(objects.StructType): def get_addresses(self): @@ -1320,7 +1358,7 @@ def get_addresses(self): return symbol_space = self._context.symbol_space - table_name = self.vol.type_name.split(constants.BANG)[0] + table_name = self.get_symbol_table_name() inet6_ifaddr_symname = table_name + constants.BANG + "inet6_ifaddr" if not symbol_space.has_type(inet6_ifaddr_symname) or not symbol_space.get_type( inet6_ifaddr_symname @@ -1351,7 +1389,7 @@ def get_scope_type(self): Returns: str: the IPv4 scope type. """ - table_name = self.vol.type_name.split(constants.BANG)[0] + table_name = self.get_symbol_table_name() rt_scope_enum = self._context.symbol_space.get_enumeration( table_name + constants.BANG + "rt_scope_t" ) @@ -1368,8 +1406,7 @@ def get_address(self): Returns: str: the IPv4 address """ - ipv4_bytes = struct.pack(" Date: Sat, 27 Jan 2024 17:48:09 -0300 Subject: [PATCH 04/14] Minor fixes --- volatility3/framework/symbols/linux/extensions/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 3a088a959f..a7dc28bea2 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -365,7 +365,7 @@ def _parse_maple_tree_node( # None. If however you wanted to parse from a node, but ignore some parts of the tree below it then # this could be populated with the addresses of the nodes you wish to ignore. - if seen == None: + if seen is None: seen = set() # protect against unlikely loop @@ -1265,7 +1265,7 @@ def get_mac_address(self) -> str: return self._format_as_mac_address(hwaddr) def _get_flag_choices(self) -> Dict: - """Return the net_deivce flags as a list of strings""" + """Return the net_device flags as a list of strings""" vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) try: # kernels >= 3.15 @@ -1327,6 +1327,7 @@ def get_operational_state(self) -> str: return IF_OPER_STATES[self.operstate] + class in_device(objects.StructType): def get_addresses(self): """Yield the IPv4 ifaddr addresses From 620b9b41838119153dd22672d41c573962734438 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Sat, 27 Jan 2024 17:52:26 -0300 Subject: [PATCH 05/14] Fix: Explicit returns mixed with implicit (fall through) returns --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index a7dc28bea2..915d92cd1b 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1260,7 +1260,7 @@ def get_mac_address(self) -> str: vollog.debug( f"Unable to read network inteface mac address from {self.dev_addr:#x}" ) - return + return None return self._format_as_mac_address(hwaddr) From 5a8a0def35d30fef31f90eecca67f32c0953c9bc Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 30 Jan 2024 12:53:09 -0300 Subject: [PATCH 06/14] Fix docstring typos --- volatility3/framework/symbols/linux/extensions/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 915d92cd1b..ccb6265699 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1278,11 +1278,11 @@ def _get_flag_choices(self) -> Dict: return choices def _get_net_device_flag_value(self, name): - """Return the net_deivce flag value based on the flag name""" + """Return the net_device flag value based on the flag name""" return self._get_flag_choices()[name] def get_flag_names(self) -> List[str]: - """Return the net_deivce flags as a list of strings + """Return the net_device flags as a list of strings Returns: List[str]: A list of flag names From c72fa7544db63bbe72bd1dd1576df2d8ed6baa3b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 30 Jan 2024 12:55:16 -0300 Subject: [PATCH 07/14] Convert IF_OPER_STATES to enum --- .../framework/constants/linux/__init__.py | 23 +++++++++++-------- .../symbols/linux/extensions/__init__.py | 10 ++++---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/volatility3/framework/constants/linux/__init__.py b/volatility3/framework/constants/linux/__init__.py index 21e0b47f76..b5972ca519 100644 --- a/volatility3/framework/constants/linux/__init__.py +++ b/volatility3/framework/constants/linux/__init__.py @@ -5,6 +5,7 @@ Linux-specific values that aren't found in debug symbols """ +from enum import Enum KERNEL_NAME = "__kernel__" @@ -315,13 +316,15 @@ "IFF_ECHO": 0x40000, } -# RFC 2863 operational status. Kernels >= 2.6.17. See IF_OPER_* in include/uapi/linux/if.h -IF_OPER_STATES = ( - "UNKNOWN", - "NOTPRESENT", - "DOWN", - "LOWERLAYERDOWN", - "TESTING", - "DORMANT", - "UP", -) + +# Kernels >= 2.6.17. See IF_OPER_* in include/uapi/linux/if.h +class IF_OPER_STATES(Enum): + """RFC 2863 - Network interface operational status""" + + UNKNOWN = 0 + NOTPRESENT = 1 + DOWN = 2 + LOWERLAYERDOWN = 3 + TESTING = 4 + DORMANT = 5 + UP = 6 diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index ccb6265699..510302cdb3 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -16,7 +16,7 @@ from volatility3.framework.constants.linux import CAPABILITIES, NET_DEVICE_FLAGS from volatility3.framework.constants.linux import IFA_HOST, IFA_LINK, IFA_SITE from volatility3.framework.constants.linux import IF_OPER_STATES -from volatility3.framework.renderers import conversion +from volatility3.framework.renderers import conversion, UnparsableValue from volatility3.framework import exceptions, objects, interfaces, symbols from volatility3.framework.layers import linear from volatility3.framework.objects import utility @@ -1321,11 +1321,11 @@ def get_operational_state(self) -> str: Returns: str: A string with the operational state """ - if self.operstate >= len(IF_OPER_STATES): + try: + return IF_OPER_STATES(self.operstate).name + except ValueError: vollog.warning(f"Invalid net_device operational state '{self.operstate}'") - return "INVALID" - - return IF_OPER_STATES[self.operstate] + return UnparsableValue() class in_device(objects.StructType): From 1b153cc5676fe2161162ef208ab3dfa0e4fcb342 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Tue, 30 Jan 2024 13:13:19 -0300 Subject: [PATCH 08/14] Manage net_device flag default value & error --- volatility3/framework/symbols/linux/extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index 510302cdb3..b5f650f6b4 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -1279,7 +1279,7 @@ def _get_flag_choices(self) -> Dict: def _get_net_device_flag_value(self, name): """Return the net_device flag value based on the flag name""" - return self._get_flag_choices()[name] + return self._get_flag_choices().get(name, UnparsableValue()) def get_flag_names(self) -> List[str]: """Return the net_device flags as a list of strings From 6f1e7c2145ba34643587db4322237731c88b2a5d Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Feb 2024 16:43:58 +1100 Subject: [PATCH 09/14] Add test for linux.ip.Addr and linux-sample-1.bin --- test/test_volatility.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/test_volatility.py b/test/test_volatility.py index aaad615bcc..a5d97ce6e3 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -6,6 +6,7 @@ # import os +import re import subprocess import sys import shutil @@ -331,6 +332,22 @@ def test_linux_tty_check(image, volatility, python): assert rc == 0 +def test_linux_ip_addr(image, volatility, python): + rc, out, err = runvol_plugin("linux.ip.Addr", image, volatility, python) + out = out.lower() + + assert re.search( + rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+false\s+192.168.201.161\s+24\s+global\s+up", + out, + ) + assert re.search( + rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+false\s+fe80::20c:29ff:fe8f:edca\s+64\s+link\s+up", + out, + ) + assert out.count(b"\n") >= 8 + assert rc == 0 + + # MAC From 260fbd8d241013c866b0dc4ef54b73793ee9f501 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Feb 2024 17:03:14 +1100 Subject: [PATCH 10/14] Python os.path module precisely does that by checking the current platform and, based on it, utilizes either the posixpath or ntpath modules --- test/test_volatility.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/test_volatility.py b/test/test_volatility.py index a5d97ce6e3..61e6fe029f 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -12,7 +12,6 @@ import shutil import tempfile import hashlib -import ntpath import json # @@ -125,11 +124,7 @@ def test_windows_dumpfiles(image, volatility, python): known_files = json.load(json_file) failed_chksms = 0 - - if sys.platform == "win32": - file_name = ntpath.basename(image) - else: - file_name = os.path.basename(image) + file_name = os.path.basename(image) try: for addr in known_files["windows_dumpfiles"][file_name]: From 7b1c75cc87791218fe13a4b4e10bf8a0773f2198 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Feb 2024 20:57:26 +1100 Subject: [PATCH 11/14] Linux: Add the linux.ip.Link plugin by @eve-mem - On top of the @eve-mem, I've added the queue length field to mimic the ip link command. - Furthermore, I've included some functions to export the network device flags exactly as they are presented to userland --- volatility3/framework/plugins/linux/ip.py | 91 ++++++++++++- .../symbols/linux/extensions/__init__.py | 121 +++++++++++++++++- 2 files changed, 208 insertions(+), 4 deletions(-) diff --git a/volatility3/framework/plugins/linux/ip.py b/volatility3/framework/plugins/linux/ip.py index 19393a853e..a8d7e813e7 100644 --- a/volatility3/framework/plugins/linux/ip.py +++ b/volatility3/framework/plugins/linux/ip.py @@ -6,7 +6,6 @@ from volatility3.framework import interfaces, renderers, constants from volatility3.framework.configuration import requirements from volatility3.framework.interfaces import plugins -from volatility3.framework.objects import utility class Addr(plugins.PluginInterface): @@ -30,7 +29,7 @@ def _gather_net_dev_info(self, net_dev): mac_addr = net_dev.get_mac_address() promisc = net_dev.promisc operational_state = net_dev.get_operational_state() - iface_name = utility.array_to_string(net_dev.name) + iface_name = net_dev.get_device_name() iface_ifindex = net_dev.ifindex try: net_ns_id = net_dev.get_net_namespace_id() @@ -80,3 +79,91 @@ def run(self): ] return renderers.TreeGrid(headers, self._generator()) + + +class Link(plugins.PluginInterface): + """Lists information about network interfaces similar to `ip link show`""" + + _required_framework_version = (2, 0, 0) + _version = (1, 0, 0) + + @classmethod + def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]: + return [ + requirements.ModuleRequirement( + name="kernel", + description="Linux kernel", + architectures=["Intel32", "Intel64"], + ) + ] + + def _gather_net_dev_link_info(self, net_device): + mac_addr = net_device.get_mac_address() + operational_state = net_device.get_operational_state() + iface_name = net_device.get_device_name() + mtu = net_device.mtu + qdisc_name = net_device.get_qdisc_name() + qlen = net_device.get_queue_length() + + # Format flags to string. Drop IFF_ to match iproute2 'ip link' output. + # Also, note that iproute2 removes IFF_RUNNING, see print_link_flags() + flags_list = [ + flag.replace("IFF_", "") + for flag in net_device.get_flag_names() + if flag != "IFF_RUNNING" + ] + flags_str = ",".join(flags_list) + + yield iface_name, mac_addr, operational_state, mtu, qdisc_name, qlen, flags_str + + @classmethod + def list_net_devices( + cls, + vmlinux: interfaces.context.ModuleInterface, + ) -> (interfaces.objects.ObjectInterface, interfaces.objects.ObjectInterface): + """Lists network devices + + Args: + vmlinux (ModuleInterface): The kernel symbols object + + Yields: + tuple: + net: Network namespace + net_device: Network device structure + """ + table_name = vmlinux.symbol_table_name + net_type_symname = table_name + constants.BANG + "net" + net_device_symname = table_name + constants.BANG + "net_device" + + # 'net_namespace_list' exists from kernels >= 2.6.24 + net_namespace_list = vmlinux.object_from_symbol("net_namespace_list") + for net in net_namespace_list.to_list(net_type_symname, "list"): + for net_device in net.dev_base_head.to_list(net_device_symname, "dev_list"): + yield net, net_device + + def _generator(self): + vmlinux = self.context.modules[self.config["kernel"]] + + for net_dev, net_device in self.list_net_devices(vmlinux): + for device_link_info in self._gather_net_dev_link_info(net_device): + try: + net_ns_id = net_dev.get_net_namespace_id() + except AttributeError: + net_ns_id = renderers.NotAvailableValue() + + fields = [net_ns_id, *device_link_info] + yield (0, fields) + + def run(self): + headers = [ + ("NS", int), + ("Interface", str), + ("MAC", str), + ("State", str), + ("MTU", int), + ("Qdisc", str), + ("Qlen", int), + ("Flags", str), + ] + + return renderers.TreeGrid(headers, self._generator()) diff --git a/volatility3/framework/symbols/linux/extensions/__init__.py b/volatility3/framework/symbols/linux/extensions/__init__.py index b5f650f6b4..a1b8a3e839 100644 --- a/volatility3/framework/symbols/linux/extensions/__init__.py +++ b/volatility3/framework/symbols/linux/extensions/__init__.py @@ -21,6 +21,7 @@ from volatility3.framework.layers import linear from volatility3.framework.objects import utility from volatility3.framework.symbols import generic, linux, intermed +from volatility3.framework.symbols.wrappers import Flags from volatility3.framework.symbols.linux.extensions import elf vollog = logging.getLogger(__name__) @@ -1237,6 +1238,14 @@ def get_inode(self): class net_device(objects.StructType): + def get_device_name(self) -> str: + """Return the network device name + + Returns: + str: The network device name + """ + return utility.array_to_string(self.name) + def _format_as_mac_address(self, hwaddr): return ":".join([f"{x:02x}" for x in hwaddr[: self.addr_len]]) @@ -1281,13 +1290,105 @@ def _get_net_device_flag_value(self, name): """Return the net_device flag value based on the flag name""" return self._get_flag_choices().get(name, UnparsableValue()) + def _get_netdev_state_t(self): + vmlinux = linux.LinuxUtilities.get_module_from_volobj_type(self._context, self) + try: + # At least from kernels 2.6.30 + return vmlinux.get_enumeration("netdev_state_t") + except exceptions.SymbolError: + raise exceptions.VolatilityException( + "Unsupported kernel or wrong ISF. Cannot find 'netdev_state_t' enumeration" + ) + + def is_running(self) -> bool: + """Test if the network device has been brought up + Based on netif_running() + + Returns: + bool: True if the device is UP + """ + netdev_state_t_enum = self._get_netdev_state_t() + + # It should be safe. netdev_state_t::__LINK_STATE_START has been available since + # at least kernels 2.6.30 + return ( + self.state & (1 << netdev_state_t_enum.choices["__LINK_STATE_START"]) != 0 + ) + + def is_carrier_ok(self) -> bool: + """Check if carrier is present on network device + Based on netif_carrier_ok() + + Returns: + bool: True if carrier present + """ + netdev_state_t_enum = self._get_netdev_state_t() + + # It should be safe. netdev_state_t::__LINK_STATE_NOCARRIER has been available + # since at least kernels 2.6.30 + return ( + self.state & (1 << netdev_state_t_enum.choices["__LINK_STATE_NOCARRIER"]) + == 0 + ) + + def is_dormant(self) -> bool: + """Check if the network device is dormant + Based on netif_dormant(() + + Returns: + bool: True if the network device is dormant + """ + netdev_state_t_enum = self._get_netdev_state_t() + + # It should be safe. netdev_state_t::__LINK_STATE_DORMANT has been available + # since at least kernels 2.6.30 + return ( + self.state & (1 << netdev_state_t_enum.choices["__LINK_STATE_DORMANT"]) != 0 + ) + + def is_operational(self) -> bool: + """Test if the carrier is operational + Based on netif_oper_up() + + Returns: + bool: True if the device is UP + """ + + return self.get_operational_state() in ("UP", "UNKNOWN") + def get_flag_names(self) -> List[str]: - """Return the net_device flags as a list of strings + """Return the net_device flags as a list of strings. + This is the combination of flags exported through kernel APIs to userspace. + Based on dev_get_flags() Returns: List[str]: A list of flag names """ - return list(self._get_flag_choices()) + choices = self._get_flag_choices() + clear_flags = choices.get("IFF_PROMISC", 0) + clear_flags |= choices.get("IFF_ALLMULTI", 0) + clear_flags |= choices.get("IFF_RUNNING", 0) + clear_flags |= choices.get("IFF_LOWER_UP", 0) + clear_flags |= choices.get("IFF_DORMANT", 0) + + clear_gflags = choices.get("IFF_PROMISC", 0) + clear_gflags |= choices.get("IFF_ALLMULTI)", 0) + + flags = (self.flags & ~clear_flags) | (self.gflags & ~clear_gflags) + + if self.is_running(): + if self.is_operational(): + flags |= choices.get("IFF_RUNNING", 0) + if self.is_carrier_ok(): + flags |= choices.get("IFF_LOWER_UP", 0) + if self.is_dormant(): + flags |= choices.get("IFF_DORMANT", 0) + + net_device_flags_enum_flags = Flags(choices) + net_device_flags = net_device_flags_enum_flags(flags) + + # It's preferable to provide a deterministic list of items. i.e. for testing + return sorted(net_device_flags) @property def promisc(self): @@ -1327,6 +1428,22 @@ def get_operational_state(self) -> str: vollog.warning(f"Invalid net_device operational state '{self.operstate}'") return UnparsableValue() + def get_qdisc_name(self) -> str: + """Return the network device queuing discipline (qdisc) name + + Returns: + str: A string with the queuing discipline (qdisc) name + """ + return utility.array_to_string(self.qdisc.ops.id) + + def get_queue_length(self) -> int: + """Return the netwrok device transmision qeueue length (qlen) + + Returns: + int: the netwrok device transmision qeueue length (qlen) + """ + return self.tx_queue_len + class in_device(objects.StructType): def get_addresses(self): From 8b6bd0f74ea65e4a1394b971c7aaf521dff36d14 Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Feb 2024 21:02:05 +1100 Subject: [PATCH 12/14] Add linux.ip.Link test using linux-sample-1.bin image --- test/test_volatility.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_volatility.py b/test/test_volatility.py index 61e6fe029f..3185b7d58a 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -343,6 +343,21 @@ def test_linux_ip_addr(image, volatility, python): assert rc == 0 +def test_linux_ip_link(image, volatility, python): + rc, out, err = runvol_plugin("linux.ip.Link", image, volatility, python) + + assert re.search( + rb"-\s+lo\s+00:00:00:00:00:00\s+UNKNOWN\s+16436\s+noqueue\s+0\s+LOOPBACK,LOWER_UP,UP", + out, + ) + assert re.search( + rb"-\s+eth0\s+00:0c:29:8f:ed:ca\s+UP\s+1500\s+pfifo_fast\s+1000\s+BROADCAST,LOWER_UP,MULTICAST,UP", + out, + ) + assert out.count(b"\n") >= 6 + assert rc == 0 + + # MAC From 5219e8aaae87e0b39c707765fb9a09717ffe3f2c Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Feb 2024 21:03:03 +1100 Subject: [PATCH 13/14] Remove lowercase matching from linux.ip.Addr test --- test/test_volatility.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_volatility.py b/test/test_volatility.py index 3185b7d58a..348c161da4 100644 --- a/test/test_volatility.py +++ b/test/test_volatility.py @@ -329,14 +329,13 @@ def test_linux_tty_check(image, volatility, python): def test_linux_ip_addr(image, volatility, python): rc, out, err = runvol_plugin("linux.ip.Addr", image, volatility, python) - out = out.lower() assert re.search( - rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+false\s+192.168.201.161\s+24\s+global\s+up", + rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+192.168.201.161\s+24\s+global\s+UP", out, ) assert re.search( - rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+false\s+fe80::20c:29ff:fe8f:edca\s+64\s+link\s+up", + rb"2\s+eth0\s+00:0c:29:8f:ed:ca\s+False\s+fe80::20c:29ff:fe8f:edca\s+64\s+link\s+UP", out, ) assert out.count(b"\n") >= 8 From 828b6882328b214419de325dc90ed29891f9794b Mon Sep 17 00:00:00 2001 From: Gustavo Moreira Date: Fri, 2 Feb 2024 21:29:10 +1100 Subject: [PATCH 14/14] Fix issue with net namespace id and improve code --- volatility3/framework/plugins/linux/ip.py | 47 +++++++---------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/volatility3/framework/plugins/linux/ip.py b/volatility3/framework/plugins/linux/ip.py index a8d7e813e7..6b523379bc 100644 --- a/volatility3/framework/plugins/linux/ip.py +++ b/volatility3/framework/plugins/linux/ip.py @@ -104,6 +104,10 @@ def _gather_net_dev_link_info(self, net_device): mtu = net_device.mtu qdisc_name = net_device.get_qdisc_name() qlen = net_device.get_queue_length() + try: + net_ns_id = net_device.get_net_namespace_id() + except AttributeError: + net_ns_id = renderers.NotAvailableValue() # Format flags to string. Drop IFF_ to match iproute2 'ip link' output. # Also, note that iproute2 removes IFF_RUNNING, see print_link_flags() @@ -114,45 +118,20 @@ def _gather_net_dev_link_info(self, net_device): ] flags_str = ",".join(flags_list) - yield iface_name, mac_addr, operational_state, mtu, qdisc_name, qlen, flags_str - - @classmethod - def list_net_devices( - cls, - vmlinux: interfaces.context.ModuleInterface, - ) -> (interfaces.objects.ObjectInterface, interfaces.objects.ObjectInterface): - """Lists network devices - - Args: - vmlinux (ModuleInterface): The kernel symbols object - - Yields: - tuple: - net: Network namespace - net_device: Network device structure - """ - table_name = vmlinux.symbol_table_name - net_type_symname = table_name + constants.BANG + "net" - net_device_symname = table_name + constants.BANG + "net_device" - - # 'net_namespace_list' exists from kernels >= 2.6.24 - net_namespace_list = vmlinux.object_from_symbol("net_namespace_list") - for net in net_namespace_list.to_list(net_type_symname, "list"): - for net_device in net.dev_base_head.to_list(net_device_symname, "dev_list"): - yield net, net_device + yield net_ns_id, iface_name, mac_addr, operational_state, mtu, qdisc_name, qlen, flags_str def _generator(self): vmlinux = self.context.modules[self.config["kernel"]] - for net_dev, net_device in self.list_net_devices(vmlinux): - for device_link_info in self._gather_net_dev_link_info(net_device): - try: - net_ns_id = net_dev.get_net_namespace_id() - except AttributeError: - net_ns_id = renderers.NotAvailableValue() + net_type_symname = vmlinux.symbol_table_name + constants.BANG + "net" + net_device_symname = vmlinux.symbol_table_name + constants.BANG + "net_device" - fields = [net_ns_id, *device_link_info] - yield (0, fields) + # 'net_namespace_list' exists from kernels >= 2.6.24 + net_namespace_list = vmlinux.object_from_symbol("net_namespace_list") + for net_ns in net_namespace_list.to_list(net_type_symname, "list"): + for net_dev in net_ns.dev_base_head.to_list(net_device_symname, "dev_list"): + for fields in self._gather_net_dev_link_info(net_dev): + yield 0, fields def run(self): headers = [