1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """Plugins allow the core rekall system to be extended."""
21
22 __author__ = "Michael Cohen <scudette@gmail.com>"
23
24 import collections
25 import copy
26 import re
27 import StringIO
28
29 from rekall import config
30 from rekall import obj
31 from rekall.ui import text as text_renderer
32
33 from rekall_lib import registry
34 from rekall_lib import utils
35
36
37 -class Error(Exception):
38 """Raised for plugin errors."""
39
42 """An error occured in a plugin."""
43
46 """Invalid arguments."""
47
50 """Signal aborting of the plugin."""
51
54 """An option specification."""
55
56 - def __init__(self, name=None, default=None, type="String", choices=None,
57 help="", positional=False, required=False, override=False,
58 hidden=False):
59 self.name = name
60 self._default = default
61 self.type = type
62 self.help = help
63 self._choices = choices
64 self.required = required
65 self.positional = positional
66 self.override = override
67 self.hidden = hidden
68
69 @utils.safe_property
71 if callable(self._default):
72 return self._default()
73
74 return self._default
75
76 @utils.safe_property
78 if callable(self._choices):
79 return list(self._choices())
80
81 return self._choices
82
84 """Add ourselves to the parser."""
85 prefix = "" if self.positional else "--"
86 parser.add_argument(prefix + self.name, default=self.default,
87 type=self.type, help=self.help,
88 positional=self.positional, hidden=self.hidden,
89 required=self.required, choices=self.choices)
90
91 - def parse(self, value, session):
92 """Parse the value as passed."""
93 if value is None:
94 if self.default is None:
95
96 if self.type == "AddressSpace":
97 return session.GetParameter("default_address_space")
98
99 if self.type in ["ArrayStringParser", "ArrayString",
100 "ArrayIntParser", "Array"]:
101 return []
102
103 if self.type in ["Bool", "Boolean"]:
104 return False
105
106 elif self.type == "RegEx":
107 if isinstance(self.default, basestring):
108 return re.compile(self.default)
109
110 return self.default
111
112
113
114
115
116 if self.type == "Address" or self.type == "SymbolAddress":
117 value = session.address_resolver.get_address_by_name(value)
118
119 elif self.type == "IntParser":
120 if isinstance(value, basestring):
121 value = int(value, 0)
122 else:
123 value = int(value)
124
125 elif self.type == "Choices":
126 if value not in self.choices:
127 raise TypeError("Arg %s must be one of %s" % (
128 self.name, self.choices))
129
130 elif self.type == "ChoiceArray":
131 if isinstance(value, basestring):
132 value = [value]
133
134 for item in value:
135 if item not in self.choices:
136 raise TypeError("Arg %s must be one of %s" % (
137 self.name, self.choices))
138
139 elif self.type in ["ArrayString", "ArrayStringParser"]:
140 if isinstance(value, basestring):
141 value = [value]
142
143 if not isinstance(value, collections.Iterable):
144 raise TypeError("Arg %s must be a list of strings" % self.name)
145
146 for item in value:
147 if not isinstance(item, basestring):
148 raise TypeError("Arg %s must be a list of strings" %
149 self.name)
150
151 elif self.type == "Array":
152 if isinstance(value, basestring):
153 value = [value]
154
155 if not isinstance(value, collections.Iterable):
156 raise TypeError("Arg %s must be a list of strings" % self.name)
157
158 elif self.type == "RegEx":
159 if isinstance(value, basestring):
160 value = re.compile(value, re.I)
161
162 elif self.type == "ArrayIntParser":
163 try:
164 value = [int(value)]
165 except (ValueError, TypeError):
166 result = []
167 for x in value:
168
169
170 if x.__class__.__name__ == "RowTuple":
171 if len(x) != 1:
172 raise PluginError(
173 "Subselect must only select a single row when "
174 "expanding into a list.")
175 x = int(x[0])
176 else:
177 x = int(x)
178 result.append(x)
179 value = result
180
181
182 elif self.type == "AddressSpace":
183 load_as = session.plugins.load_as(session=session)
184 value = load_as.ResolveAddressSpace(value)
185
186 return value
187
190
191
192
193 mode = None
194
195 @classmethod
197 """Checks we are active.
198
199 This method will be called with the session to check if this specific
200 class is active. This mechanism allows multiple implementations to all
201 share the same name, as long as only one is actually active. For
202 example, we can have a linux, windows and mac version of plugins with
203 the "pslist" name.
204
205 This mixin provides the mixed class with a basic is_active() method
206 which honors a mode member defined on the class and all its
207 subclasses. The mode is additive (meaning each class and its subclasses
208 are only active if the mode is active).
209 """
210 for subclass in cls.__mro__:
211 mode = getattr(subclass, "mode", None)
212
213 if isinstance(mode, basestring):
214 if not session.GetParameter(mode):
215 return False
216
217 elif isinstance(mode, (list, tuple)):
218 for i in mode:
219 if not session.GetParameter(i):
220 return False
221
222 return True
223
224
225
226 -class Command(ModeBasedActiveMixin):
227 """A command can be run from the rekall command line.
228
229 Commands can be automatically imported into the shell's namespace and are
230 expected to produce textual (or other) output.
231
232 In order to define a new command simply extend this class.
233 """
234
235
236
237
238
239
240 __name = ""
241
242
243
244 __category = ""
245
246
247 __abstract = True
248 __metaclass__ = registry.MetaclassRegistry
249
250
251 interactive = False
252
253
254
255 producer = False
256
257
258 error_status = None
259
260 mode = None
261
262 @classmethod
263 - def args(cls, parser):
264 """Declare the command line args this plugin needs."""
265
266 @classmethod
268 """Return an instance of this plugin with suitable default arguments.
269
270 In most general applications, types are declared at compile time and
271 remain immutable, or at least available throughout the program's
272 lifecycle. Rekall, on the other hand, leave many of the decisions
273 usually made at type declaration time until late in the runtime,
274 when the profile data is available. For this reason, in many of the
275 cases when other applications would interrogate classes (for attributes
276 and properties, among other things), in Rekall we must interrogate
277 their instances, which have access to profile data. In order to
278 make this possible slightly earlier in the runtime than when running
279 the plugin, we introduce the concept of prototypes, which are
280 instances of the plugin or struct with the current session and profile
281 available, but with no data or arguments set.
282
283 Arguments:
284 session
285
286 Returns:
287 And instance of this Command with suitable default arguments.
288 """
289 try:
290 return cls(session=session, ignore_required=True)
291 except (TypeError, ValueError):
292 raise NotImplementedError("Subclasses must override GetPrototype "
293 "if they require arguments.")
294
295 @registry.classproperty
297 return getattr(cls, "_%s__name" % cls.__name__, None)
298
299 - def __init__(self, ignore_required=False, **kwargs):
300 """The constructor for this command.
301
302 Commands can take arbitrary named args and have access to the running
303 session.
304
305 Args:
306 session: The session we will use. Many options are taken from the
307 session by default, if not provided. This allows users to omit
308 specifying many options.
309
310 ignore_required: If this is true plugin constructors must allow the
311 plugin to be instantiated with no parameters. All parameter
312 validation shall be disabled and construction must succeed.
313 """
314 session = kwargs.pop("session", None)
315 if kwargs:
316 raise InvalidArgs("Invalid arguments: %s" % unicode(kwargs.keys()))
317
318 super(Command, self).__init__(**kwargs)
319
320 if session == None:
321 raise InvalidArgs("A session must be provided.")
322
323 self.session = session
324 self.ignore_required = ignore_required
325
327 """Returns an instance of the named plugin.
328
329 The new plugin will initialized with the current session and optional
330 kwargs.
331 Args:
332 name: The generic name of the plugin (i.e. the __name attribute,
333 e.g. pslist).
334 kwargs: Extra args to use for instantiating the plugin.
335 """
336 for cls in self.classes.values():
337 if cls.name == name and cls.is_active(self.session):
338 return cls(session=self.session, profile=self.profile,
339 **kwargs)
340
351
353 return "Plugin: %s (%s)" % (self.name, self.__class__.__name__)
354
356 """Make plugins that define collect iterable, as convenience.
357
358 Because this:
359 for x in session.plugins.get_some_data():
360 # do stuff
361
362 Is nicer than this:
363 for x in session.plugins.get_some_data().collect():
364 # do stuff
365 """
366 if callable(getattr(self, "collect", None)):
367 for x in self.collect():
368 if x:
369 yield x
370
371 else:
372 raise TypeError("%r is not iterable." % self)
373
375 """Produce results on the renderer given.
376
377 Each plugin should implement this method to produce output on the
378 renderer. The framework will initialize the plugin and provide it with
379 some kind of renderer to write output on. The plugin should not assume
380 that the renderer is actually TextRenderer, only that the methods
381 defined in the BaseRenderer exist.
382
383 Args:
384 renderer: A renderer based at rekall.ui.renderer.BaseRenderer.
385 """
386
387 @classmethod
389 """Return only the active commands based on config."""
390 for command_cls in cls.classes.values():
391 if command_cls.is_active(session):
392 yield command_cls
393
396 """A baseclass for all commands which require a profile."""
397
398 __abstract = True
399
400 PROFILE_REQUIRED = True
401
402 @classmethod
403 - def args(cls, metadata):
404
405 metadata.add_argument(
406 "-p", "--profile", critical=True, hidden=True,
407 help="Name of the profile to load. This is the "
408 "filename of the profile found in the profiles "
409 "directory. Profiles are searched in the profile "
410 "path order.")
411
412 metadata.add_requirement("profile")
413
414 @classmethod
428
429 - def __init__(self, profile=None, **kwargs):
460
463 header = None
464 by_name = None
465
467 self.by_name = {}
468
469 for column in columns:
470 if not isinstance(column, dict):
471 raise TypeError("Plugins declaring table header ahead of "
472 "time MUST do so using the new format ("
473 "using dicts, NOT tuples). Table header %r "
474 "is invalid." % columns)
475
476 name = column.get("name")
477 if not name:
478 raise ValueError(
479 "Plugins declaring table headers ahead of "
480 "time MUST specify 'name' for each column. "
481 "Table header %r is invalid." % (columns,))
482
483 self.by_name[name] = column
484
485 self.header = copy.deepcopy(columns)
486
487 @utils.safe_property
489 """What types of thing does this plugin output?
490
491 Returns a set of declared types, each type being either a class object
492 or a string name of the class (for profile types, mostly).
493
494 This helps the self-documentation features find plugins based on their
495 declared headers. It's also used by 'collect' to find producers.
496 """
497 for column in self.header:
498 t = column.get("type")
499 if t:
500 yield t
501
504
507
509 """Fills out dict with all the declared columns."""
510 for header in self.header:
511 column_name = header["name"]
512 if column_name not in row:
513 row[column_name] = None
514
515 return row
516
518 """Convert an ordered row into a dict.
519
520 Uses the internal column order to map row names to the dict.
521 """
522 result = {}
523 for idx, header in enumerate(self.header):
524 column_name = header["name"]
525
526 try:
527 result[column_name] = row[idx]
528 except IndexError:
529 result[column_name] = None
530
531 return result
532
533 @utils.safe_property
535 return set(self.by_name.iterkeys())
536
538 """Get the column spec in 'name'."""
539 return self.by_name.get(name)
540
542 """A Mixin which provides argument parsing and validation."""
543
544
545
546 __args = []
547
548
549
550 plugin_args = None
551
552 - def __init__(self, *pos_args, **kwargs):
553 self.ignore_required = kwargs.get("ignore_required", False)
554
555
556
557
558 if self.plugin_args is None:
559 self.plugin_args = utils.AttributeDict()
560
561
562
563 definitions = []
564 definitions_classes = {}
565 for cls in self.__class__.__mro__:
566 args_definition = getattr(cls, "_%s__args" % cls.__name__, [])
567 for definition in args_definition:
568
569 if isinstance(definition, dict):
570 definition = CommandOption(**definition)
571
572
573 previous_definition = definitions_classes.get(definition.name)
574 if previous_definition:
575
576
577
578 continue
579
580 definitions_classes[definition.name] = cls
581 definitions.append(definition)
582
583
584
585
586 positional_args = [x for x in definitions if x.positional]
587 if len(positional_args) < len(pos_args):
588 raise TypeError("Too many positional args provided.")
589
590 for pos_arg, definition in zip(pos_args, positional_args):
591
592
593 if definition.name in kwargs:
594 raise TypeError(
595 "Positional Args %s is also supplied as a keyword arg." %
596 definition.name)
597
598 kwargs[definition.name] = pos_arg
599
600
601 for definition in definitions:
602 value = kwargs.pop(definition.name, None)
603 if (value is None and definition.required and
604 not self.ignore_required):
605 raise InvalidArgs("%s is required." % definition.name)
606
607 self.plugin_args[definition.name] = definition.parse(
608 value, session=kwargs.get("session"))
609
610 super(ArgsParserMixin, self).__init__(**kwargs)
611
614 """Mixin that provides the plugin with standardized table output."""
615
616
617
618 table_header = None
619 table_options = {}
620
621 __args = [
622 dict(name="verbosity", default=1, type="IntParser",
623 help="An integer reflecting the amount of desired output: "
624 "0 = quiet, 10 = noisy."),
625 ]
626
627 - def __init__(self, *pos_args, **kwargs):
638
639 @classmethod
640 - def args(cls, parser):
641 super(TypedProfileCommand, cls).args(parser)
642
643
644 for cls_i in cls.__mro__:
645 args_definition = getattr(cls_i, "_%s__args" % cls_i.__name__, [])
646 for definition in args_definition:
647 if isinstance(definition, dict):
648 definition = CommandOption(**definition)
649
650
651 if definition.name in parser.args:
652 continue
653
654 definition.add_argument(parser)
655
657 """Returns instances for each column definition.
658
659 The actual objects that are returned when the plugin runs are often
660 determined at run time because they depend on the profile loaded.
661
662 This method is used in order to introspect the types of each column
663 without actually running the plugin. A plugin must provide an instance
664 for each column without running any code. This allows interospectors to
665 learn about the output format before running the actual plugin.
666
667 Note that this method should almost always be overloaded. We try to do
668 our best here but it is not ideal. Ultimately all plugins will override
669 this method and just declare a column_types() method.
670 """
671 self.session.logging.warn(
672 "FIXME: Plugin %s (%s) does not produce typed output. "
673 "Please define a column_types() method.",
674 self.name, self.__class__.__name__)
675
676 result = {}
677 columns = []
678 for column in self.table_header:
679 column_name = column["name"]
680
681 columns.append(column_name)
682 result[column_name] = None
683
684 try:
685 for row_count, row in enumerate(self.collect()):
686 if isinstance(row, dict):
687 result.update(row)
688
689 elif isinstance(row, (tuple, list)):
690 for item, column_name in zip(row, columns):
691 if result.get(column_name) is None:
692 result[column_name] = item
693
694
695
696
697 if None not in result.values() or row_count > 5:
698 break
699
700 except (NotImplementedError, TypeError):
701 pass
702
703 return result
704
706 """Collect data that will be passed to renderer.table_row."""
707 raise NotImplementedError()
708
716
717
718 ROW_OPTIONS = set(
719 ["depth",
720 "annotation",
721 "highlight",
722 "nowrap",
723 "hex_width"]
724 )
725 - def render(self, renderer, **options):
750
752 column = self.table_header.by_name.get(member)
753 if not column:
754 raise KeyError("Plugin %r has no column %r." % (self, member))
755
756 t = column.get("type")
757
758 if isinstance(t, type):
759 return t
760
761 if not t:
762 return None
763
764 if isinstance(t, basestring):
765 return self.profile.object_classes.get(t)
766
769
773
784
785
786 -class Producer(TypedProfileCommand):
787 """Finds and outputs structs of a particular type.
788
789 Producers are very simple plugins that output only a single column
790 which contains a struct of 'type_name'. A good example of a producer are
791 the individual pslist enumeration methods.
792 """
793
794
795 type_name = None
796
797
798 producer = True
799
800 @registry.classproperty
801 @registry.memoize
804
806 raise NotImplementedError()
807
809 """Like collect, but yields the first column instead of whole row."""
810 for row in self.collect():
811 yield row[0]
812
815 """A producer backed by a cached session parameter hook."""
816
817 @utils.safe_property
819 """By convention, the hook name should be the same as our name."""
820
821 return self.name
822
828
831 """A mixin for those plugins which require a valid kernel address space.
832
833 This class ensures a valid kernel AS exists or an exception is raised.
834 """
835
836 __args = [
837 dict(name="dtb", type="IntParser", default=None, hidden=True,
838 help="The DTB physical address.")
839 ]
840
842 """A mixin for plugins which require a valid kernel address space.
843
844 Args:
845 dtb: A potential dtb to be used.
846 """
847 super(KernelASMixin, self).__init__(*args, **kwargs)
848
849
850 if self.plugin_args.dtb is not None:
851 self.kernel_address_space = (
852 self.session.kernel_address_space.__class__(
853 base=self.physical_address_space,
854 dtb=self.plugin_args.dtb))
855 else:
856
857 self.kernel_address_space = self.session.kernel_address_space
858
859 if self.kernel_address_space == None:
860
861 self.session.plugins.load_as().GetVirtualAddressSpace()
862
863 self.kernel_address_space = self.session.kernel_address_space
864
865 if self.kernel_address_space == None:
866 raise PluginError("kernel_address_space not specified.")
867
870 """A mixin for those plugins which require a valid physical address space.
871
872 This class ensures a valid physical AS exists or an exception is raised.
873 """
874
875 PHYSICAL_AS_REQUIRED = True
876
877 @classmethod
878 - def args(cls, metadata):
881
907
916
919 """This declares a plugin to present a table-like data interface."""
920
921 COLUMNS = ()
922
926
985