1# This file is Copyright 2019 Volatility Foundation and licensed under the Volatility Software License 1.0
2# which is available at https://www.volatilityfoundation.org/license/vsl-v1.0
3#
4"""This module attempts to automatically stack layers.
5
6This automagic module fulfills :class:`~volatility.framework.interfaces.configuration.TranslationLayerRequirement` that are not already fulfilled, by attempting to
7stack as many layers on top of each other as possible.  The base/lowest layer is derived from the
8"automagic.general.single_location" configuration path.  Layers are then attempting in likely height order, and
9once a layer successfully stacks on top of the existing layers, it is removed from the possible choices list
10(so no layer type can exist twice in the layer stack).
11"""
12
13import logging
14import sys
15import traceback
16from typing import List, Optional, Tuple
17
18from volatility import framework
19from volatility.framework import interfaces, constants, import_files
20from volatility.framework.automagic import construct_layers
21from volatility.framework.configuration import requirements
22from volatility.framework.layers import physical
23
24vollog = logging.getLogger(__name__)
25
26
27class LayerStacker(interfaces.automagic.AutomagicInterface):
28    """Builds up layers in a single stack.
29
30    This class mimics the volatility 2 style of stacking address spaces.  It builds up various layers based on
31    separate :class:`~volatility.framework.interfaces.automagic.StackerLayerInterface` classes.  These classes are
32    built up based on a `stack_order` class variable each has.
33
34    This has a high priority to provide other automagic modules as complete a context/configuration tree as possible.
35    Upon completion it will re-call the :class:`~volatility.framework.automagic.construct_layers.ConstructionMagic`,
36    so that any stacked layers are actually constructed and added to the context.
37    """
38    # Most important automagic, must happen first!
39    priority = 10
40
41    def __init__(self, *args, **kwargs):
42        super().__init__(*args, **kwargs)
43        self._cached = None
44
45    def __call__(self,
46                 context: interfaces.context.ContextInterface,
47                 config_path: str,
48                 requirement: interfaces.configuration.RequirementInterface,
49                 progress_callback: constants.ProgressCallback = None) -> Optional[List[str]]:
50        """Runs the automagic over the configurable."""
51
52        framework.import_files(sys.modules['volatility.framework.layers'])
53
54        # Quick exit if we're not needed
55        if not requirement.unsatisfied(context, config_path):
56            return None
57
58        # Bow out quickly if the UI hasn't provided a single_location
59        unsatisfied = self.unsatisfied(self.context, self.config_path)
60        if unsatisfied:
61            vollog.info("Unable to run LayerStacker, unsatisfied requirement: {}".format(unsatisfied))
62            return list(unsatisfied)
63        if not self.config or not self.config.get('single_location', None):
64            raise ValueError("Unable to run LayerStacker, single_location parameter not provided")
65
66        # Search for suitable requirements
67        self.stack(context, config_path, requirement, progress_callback)
68
69        return None
70
71    def stack(self, context: interfaces.context.ContextInterface, config_path: str,
72              requirement: interfaces.configuration.RequirementInterface,
73              progress_callback: constants.ProgressCallback) -> None:
74        """Stacks the various layers and attaches these to a specific
75        requirement.
76
77        Args:
78            context: Context on which to operate
79            config_path: Configuration path under which to store stacking data
80            requirement: Requirement that should have layers stacked on it
81            progress_callback: Function to provide callback progress
82        """
83        # If we're cached, find Now we need to find where to apply the stack configuration
84        if self._cached:
85            top_layer_name, subconfig = self._cached
86            result = self.find_suitable_requirements(context, config_path, requirement, [top_layer_name])
87            if result:
88                appropriate_config_path, layer_name = result
89                context.config.merge(appropriate_config_path, subconfig)
90                context.config[appropriate_config_path] = top_layer_name
91                return
92            self._cached = None
93
94        new_context = context.clone()
95        location = self.config.get('single_location', None)
96
97        # Setup the local copy of the resource
98        current_layer_name = context.layers.free_layer_name("FileLayer")
99        current_config_path = interfaces.configuration.path_join(config_path, "stack", current_layer_name)
100
101        # This must be specific to get us started, setup the config and run
102        new_context.config[interfaces.configuration.path_join(current_config_path, "location")] = location
103        physical_layer = physical.FileLayer(new_context, current_config_path, current_layer_name)
104        new_context.add_layer(physical_layer)
105
106        # Repeatedly apply "determine what this is" code and build as much up as possible
107        stacked = True
108        stacked_layers = [current_layer_name]
109        stack_set = sorted(framework.class_subclasses(interfaces.automagic.StackerLayerInterface),
110                           key = lambda x: x.stack_order)
111        while stacked:
112            stacked = False
113            new_layer = None
114            stacker_cls = None
115            for stacker_cls in stack_set:
116                stacker = stacker_cls()
117                try:
118                    vollog.log(constants.LOGLEVEL_VV, "Attempting to stack using {}".format(stacker_cls.__name__))
119                    new_layer = stacker.stack(new_context, current_layer_name, progress_callback)
120                    if new_layer:
121                        new_context.layers.add_layer(new_layer)
122                        vollog.log(constants.LOGLEVEL_VV,
123                                   "Stacked {} using {}".format(new_layer.name, stacker_cls.__name__))
124                        break
125                except Exception as excp:
126                    # Stacking exceptions are likely only of interest to developers, so the lowest level of logging
127                    fulltrace = traceback.TracebackException.from_exception(excp).format(chain = True)
128                    vollog.log(constants.LOGLEVEL_VVV, "Exception during stacking: {}".format(str(excp)))
129                    vollog.log(constants.LOGLEVEL_VVVV, "\n".join(fulltrace))
130            else:
131                stacked = False
132            if new_layer and stacker_cls:
133                stacked_layers = [new_layer.name] + stacked_layers
134                current_layer_name = new_layer.name
135                stacked = True
136                stack_set.remove(stacker_cls)
137
138        if stacked_layers is not None:
139            # Applies the stacked_layers to each requirement in the requirements list
140            result = self.find_suitable_requirements(new_context, config_path, requirement, stacked_layers)
141            if result:
142                path, layer = result
143                # splice in the new configuration into the original context
144                context.config.merge(path, new_context.layers[layer].build_configuration())
145
146                # Call the construction magic now we may have new things to construct
147                constructor = construct_layers.ConstructionMagic(
148                    context, interfaces.configuration.path_join(self.config_path, "ConstructionMagic"))
149                constructor(context, config_path, requirement)
150
151                # Stash the changed config items
152                self._cached = context.config.get(path, None), context.config.branch(path)
153
154        vollog.debug("Stacked layers: {}".format(stacked_layers))
155
156    def find_suitable_requirements(self, context: interfaces.context.ContextInterface, config_path: str,
157                                   requirement: interfaces.configuration.RequirementInterface,
158                                   stacked_layers: List[str]) -> Optional[Tuple[str, str]]:
159        """Looks for translation layer requirements and attempts to apply the
160        stacked layers to it.  If it succeeds it returns the configuration path
161        and layer name where the stacked nodes were spliced into the tree.
162
163        Returns:
164            A tuple of a configuration path and layer name for the top of the stacked layers
165                or None if suitable requirements are not found
166        """
167        child_config_path = interfaces.configuration.path_join(config_path, requirement.name)
168        if isinstance(requirement, requirements.TranslationLayerRequirement):
169            if requirement.unsatisfied(context, config_path):
170                original_setting = context.config.get(child_config_path, None)
171                for layer_name in stacked_layers:
172                    context.config[child_config_path] = layer_name
173                    if not requirement.unsatisfied(context, config_path):
174                        return child_config_path, layer_name
175                else:
176                    # Clean-up to restore the config
177                    if original_setting:
178                        context.config[child_config_path] = original_setting
179                    else:
180                        del context.config[child_config_path]
181            else:
182                return child_config_path, context.config.get(child_config_path, None)
183        for req_name, req in requirement.requirements.items():
184            result = self.find_suitable_requirements(context, child_config_path, req, stacked_layers)
185            if result:
186                return result
187        return None
188
189    @classmethod
190    def get_requirements(cls) -> List[interfaces.configuration.RequirementInterface]:
191        # This is not optional for the stacker to run, so optional must be marked as False
192        return [
193            requirements.URIRequirement("single_location",
194                                        description = "Specifies a base location on which to stack",
195                                        optional = True)
196        ]
197