Package rekall :: Module args
[frames] | no frames]

Source Code for Module rekall.args

  1  #!/usr/bin/python 
  2   
  3  # Rekall Memory Forensics 
  4  # Copyright (C) 2012 Michael Cohen <scudette@gmail.com> 
  5  # Copyright 2013 Google Inc. All Rights Reserved. 
  6  # 
  7  # This program is free software; you can redistribute it and/or modify 
  8  # it under the terms of the GNU General Public License as published by 
  9  # the Free Software Foundation; either version 2 of the License, or (at 
 10  # your option) any later version. 
 11  # 
 12  # This program is distributed in the hope that it will be useful, but 
 13  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 15  # General Public License for more details. 
 16  # 
 17  # You should have received a copy of the GNU General Public License 
 18  # along with this program; if not, write to the Free Software 
 19  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 20  # 
 21  """This module manages the command line parsing logic. 
 22   
 23  Rekall uses the argparse module for command line parsing, however this module 
 24  contains so many bugs it might be worth to implement our own parser in future. 
 25  """ 
 26   
 27  __author__ = "Michael Cohen <scudette@gmail.com>" 
 28   
 29  import argparse 
 30  import logging 
 31  import re 
 32  import os 
 33  import sys 
 34  import zipfile 
 35   
 36  from rekall import config 
 37  from rekall import constants 
 38  from rekall import plugin 
 39  from rekall_lib import utils 
 40   
 41   
 42  config.DeclareOption("--plugin", default=[], type="ArrayStringParser", 
 43                       help="Load user provided plugin bundle.") 
 44   
 45  config.DeclareOption( 
 46      "-h", "--help", default=False, type="Boolean", 
 47      help="Show help about global paramters.") 
 48   
 49   
50 -class RekallHelpFormatter(argparse.RawDescriptionHelpFormatter):
51 - def add_argument(self, action):
52 # Allow us to suppress an arg from the --help output for those options 53 # which do not make sense on the command line. 54 if action.dest != "SUPPRESS": 55 super(RekallHelpFormatter, self).add_argument(action)
56 57
58 -class RekallArgParser(argparse.ArgumentParser):
59 ignore_errors = False 60
61 - def __init__(self, session=None, **kwargs):
62 kwargs["formatter_class"] = RekallHelpFormatter 63 if session == None: 64 raise RuntimeError("Session must be set") 65 self.session = session 66 super(RekallArgParser, self).__init__(**kwargs)
67
68 - def error(self, message):
69 if self.ignore_errors: 70 return 71 72 # We trap this error especially since we launch the volshell. 73 if message == "too few arguments": 74 return 75 76 super(RekallArgParser, self).error(message)
77
78 - def parse_known_args(self, args=None, namespace=None, force=False, **_):
79 self.ignore_errors = force 80 81 result = super(RekallArgParser, self).parse_known_args( 82 args=args, namespace=namespace) 83 84 return result
85
86 - def print_help(self, file=None):
87 if self.ignore_errors: 88 return 89 90 return super(RekallArgParser, self).print_help(file=file)
91
92 - def exit(self, *args, **kwargs):
93 if self.ignore_errors: 94 return 95 96 return super(RekallArgParser, self).exit(*args, **kwargs)
97 98
99 -def LoadPlugins(paths=None):
100 PYTHON_EXTENSIONS = [".py", ".pyo", ".pyc"] 101 102 for path in paths: 103 if not os.access(path, os.R_OK): 104 logging.error("Unable to find %s", path) 105 continue 106 107 path = os.path.abspath(path) 108 directory, filename = os.path.split(path) 109 module_name, ext = os.path.splitext(filename) 110 111 # Its a python file. 112 if ext in PYTHON_EXTENSIONS: 113 # Make sure python can find the file. 114 sys.path.insert(0, directory) 115 116 try: 117 logging.info("Loading user plugin %s", path) 118 __import__(module_name) 119 except Exception as e: 120 logging.error("Error loading user plugin %s: %s", path, e) 121 finally: 122 sys.path.pop(0) 123 124 elif ext == ".zip": 125 zfile = zipfile.ZipFile(path) 126 127 # Make sure python can find the file. 128 sys.path.insert(0, path) 129 try: 130 logging.info("Loading user plugin archive %s", path) 131 for name in zfile.namelist(): 132 # Change from filename to python package name. 133 module_name, ext = os.path.splitext(name) 134 if ext in PYTHON_EXTENSIONS: 135 module_name = module_name.replace("/", ".").replace( 136 "\\", ".") 137 138 try: 139 __import__(module_name.strip("\\/")) 140 except Exception as e: 141 logging.error("Error loading user plugin %s: %s", 142 path, e) 143 144 finally: 145 sys.path.pop(0) 146 147 else: 148 logging.error("Plugin %s has incorrect extension.", path)
149 150
151 -def _TruncateARGV(argv):
152 """Truncate the argv list at the first sign of a plugin name. 153 154 At this stage we do not know which module is valid, or its options. The 155 syntax of the command line is: 156 157 rekal -x -y -z plugin_name -a -b -c 158 159 Where -x -y -z are global options, and -a -b -c are plugin option. We only 160 want to parse up to the plugin name. 161 """ 162 short_argv = [argv[0]] 163 for item in argv[1:]: 164 for plugin_cls in plugin.Command.classes.values(): 165 if plugin_cls.name == item: 166 return short_argv 167 168 short_argv.append(item) 169 170 return short_argv
171 172 173 # Argparser stupidly matches short options for options it does not know yet 174 # (with parse_known_args()). This list allows us to declare placeholders to 175 # avoid the partial option matching behaviour in some cases. 176 DISAMBIGUATE_OPTIONS = [ 177 "profile", 178 ] 179 180
181 -def ParseGlobalArgs(parser, argv, user_session):
182 """Parse some session wide args which must be done before anything else.""" 183 # Register global args. 184 ConfigureCommandLineParser(config.OPTIONS, parser) 185 186 # Parse the known args. 187 known_args, unknown_args = parser.parse_known_args(args=argv) 188 189 with user_session.state as state: 190 for arg, value in vars(known_args).items(): 191 # Argparser erroneously parses flags as str objects, but they are 192 # really unicode objects. 193 if isinstance(value, str): 194 value = utils.SmartUnicode(value) 195 196 # Argparse tries to interpolate defaults into the parsed data in the 197 # event that the args are not present - even when calling 198 # parse_known_args. Before we get to this point, the config system 199 # has already set the state from the config file, so if we allow 200 # argparse to set the default we would override the config file 201 # (with the defaults). We solve this by never allowing argparse 202 # itself to handle the defaults. We always set default=None, when 203 # configuring the parser, and rely on the 204 # config.MergeConfigOptions() to set the defaults. 205 if value is not None: 206 state.Set(arg, value) 207 208 # Enforce the appropriate logging level if user supplies the --verbose 209 # or --quiet command line flags. 210 verbose_flag = getattr(known_args, "verbose", None) 211 quiet_flag = getattr(known_args, "quiet", None) 212 213 if verbose_flag and quiet_flag: 214 raise ValueError("Cannot set both --verbose and --quiet!") 215 216 if verbose_flag: 217 state.Set("logging_level", "DEBUG") 218 elif quiet_flag: 219 state.Set("logging_level", "CRITICAL") 220 221 # Now load the third party user plugins. These may introduce additional 222 # plugins with args. 223 if user_session.state.plugin: 224 LoadPlugins(user_session.state.plugin) 225 226 # External files might have introduced new plugins - rebuild the plugin 227 # DB. 228 user_session.plugins.plugin_db.Rebuild() 229 230 return known_args, unknown_args
231 232
233 -def FindPlugin(argv=None, user_session=None):
234 """Search the argv for the first occurrence of a valid plugin name. 235 236 Returns a mutated argv where the plugin is moved to the front. If a plugin 237 is not found we assume the plugin is "shell" (i.e. the interactive session). 238 239 This maintains backwards compatibility with the old global/plugin specific 240 options. In the current implementation, the plugin name should probably come 241 first: 242 243 rekal pslist -v -f foo.elf --pid 4 244 245 but this still works: 246 247 rekal -v -f foo.elf pslist --pid 4 248 """ 249 result = argv[:] 250 for i, item in enumerate(argv): 251 if item in user_session.plugins.plugin_db.db: 252 result.pop(i) 253 return item, result 254 255 return "shell", result
256 257
258 -def ConfigureCommandLineParser(command_metadata, parser, critical=False):
259 """Apply the plugin configuration to an argparse parser. 260 261 This method is the essential glue between the abstract plugin metadata and 262 argparse. 263 264 The main intention is to de-couple the plugin's args definition from arg 265 parser's specific implementation. The plugin then conveys semantic meanings 266 about its arguments rather than argparse implementation specific 267 details. Note that args are parsed through other mechanisms in a number of 268 cases so this gives us flexibility to implement arbitrary parsing: 269 270 - Directly provided to the plugin in the constructor. 271 - Parsed from json from the web console. 272 """ 273 # This is used to allow the user to break the command line arbitrarily. 274 parser.add_argument('-', dest='__dummy', action="store_true", 275 help="A do nothing arg. Useful to separate options " 276 "which take multiple args from positional. Can be " 277 "specified many times.") 278 279 try: 280 groups = parser.groups 281 except AttributeError: 282 groups = parser.groups = { 283 "None": parser.add_argument_group("Global options") 284 } 285 286 if command_metadata.plugin_cls: 287 groups[command_metadata.plugin_cls.name] = parser.add_argument_group( 288 "Plugin %s options" % command_metadata.plugin_cls.name) 289 290 for name, options in command_metadata.args.iteritems(): 291 # We need to modify options to feed into argparse. 292 options = options.copy() 293 294 # Skip this option since it is hidden. 295 if options.pop("hidden", None): 296 options["help"] = argparse.SUPPRESS 297 298 # Prevent None getting into the kwargs because it upsets argparser. 299 kwargs = dict((k, v) for k, v in options.items() if v is not None) 300 name = kwargs.pop("name", None) or name 301 302 # If default is specified we assume the parameter is not required. 303 # However, defaults are not passed on to argparse in most cases, and 304 # instead applied separately through ApplyDefaults. For exceptions, 305 # see below. 306 default = kwargs.pop("default", None) 307 try: 308 required = kwargs.pop("required") 309 except KeyError: 310 required = default is None 311 312 group_name = kwargs.pop("group", None) 313 if group_name is None and command_metadata.plugin_cls: 314 group_name = command_metadata.plugin_cls.name 315 316 group = groups.get(group_name) 317 if group is None: 318 groups[group_name] = group = parser.add_argument_group(group_name) 319 320 positional_args = [] 321 322 short_opt = kwargs.pop("short_opt", None) 323 324 # A positional arg is allowed to be specified without a flag. 325 if kwargs.pop("positional", None): 326 positional_args.append(name) 327 328 # If a position arg is optional we need to specify nargs=? 329 if not required: 330 kwargs["nargs"] = "?" 331 332 # Otherwise argparse wants to have - in front of the arg. 333 else: 334 if short_opt: 335 positional_args.append("-" + short_opt) 336 337 positional_args.append("--" + name) 338 339 arg_type = kwargs.pop("type", None) 340 choices = kwargs.pop("choices", []) 341 if callable(choices): 342 choices = choices() 343 344 if arg_type == "ArrayIntParser": 345 kwargs["action"] = ArrayIntParser 346 kwargs["nargs"] = "+" if required else "*" 347 348 if arg_type in ["ArrayString", "ArrayStringParser"]: 349 kwargs["action"] = ArrayStringParser 350 kwargs["nargs"] = "+" if required else "*" 351 352 elif arg_type == "IntParser": 353 kwargs["action"] = IntParser 354 355 elif arg_type == "Float": 356 kwargs["type"] = float 357 358 elif arg_type == "Boolean" or arg_type == "Bool": 359 # Argparse will assume default False for flags and not return 360 # None, which is required by ApplyDefaults to recognize an unset 361 # argument. To solve this issue, we just pass the default on. 362 kwargs["default"] = default 363 kwargs["action"] = "store_true" 364 365 # Multiple entries of choices (requires a choices paramter). 366 elif arg_type == "ChoiceArray": 367 kwargs["nargs"] = "+" if required else "*" 368 kwargs["choices"] = list(choices) 369 370 elif arg_type == "Choices": 371 kwargs["choices"] = list(choices) 372 373 # Skip option if not critical. 374 critical_arg = kwargs.pop("critical", False) 375 if critical and critical_arg: 376 group.add_argument(*positional_args, **kwargs) 377 continue 378 379 if not (critical or critical_arg): 380 group.add_argument(*positional_args, **kwargs)
381 382
383 -def parse_args(argv=None, user_session=None, global_arg_cb=None):
384 """Parse the args from the command line argv. 385 386 Args: 387 argv: The args to process. 388 user_session: The session we work with. 389 global_arg_cb: A callback that will be used to process global 390 args. Global args are those which affect the state of the 391 Rekall framework and must be processed prior to any plugin 392 specific args. In essence these flags control which plugins 393 can be available. 394 """ 395 if argv is None: 396 argv = sys.argv[1:] 397 398 parser = RekallArgParser( 399 description=constants.BANNER, 400 conflict_handler='resolve', 401 add_help=True, 402 session=user_session, 403 epilog="When no module is provided, drops into interactive mode", 404 formatter_class=RekallHelpFormatter) 405 406 # Parse the global and critical args from the command line. 407 global_flags, unknown_flags = ParseGlobalArgs(parser, argv, user_session) 408 if global_arg_cb: 409 global_arg_cb(global_flags, unknown_flags) 410 411 # The plugin name is taken from the command line, but it is not enough to 412 # know which specific implementation will be used. For example there are 3 413 # classes implementing the pslist plugin WinPsList, LinPsList and OSXPsList. 414 plugin_name, argv = FindPlugin(argv, user_session) 415 416 # Add all critical parameters. Critical parameters are those which are 417 # common to all implementations of a certain plugin and are required in 418 # order to choose from these implementations. For example, the profile or 419 # filename are usually used to select the specific implementation of a 420 # plugin. 421 for metadata in user_session.plugins.plugin_db.MetadataByName(plugin_name): 422 ConfigureCommandLineParser(metadata, parser, critical=True) 423 424 # Parse the global and critical args from the command line. 425 ParseGlobalArgs(parser, argv, user_session) 426 427 # Find the specific implementation of the plugin that applies here. For 428 # example, we have 3 different pslist implementations depending on the 429 # specific profile loaded. 430 command_metadata = user_session.plugins.Metadata(plugin_name) 431 if not command_metadata: 432 raise plugin.PluginError( 433 "Plugin %s is not available for this configuration" % plugin_name) 434 435 # Configure the arg parser for this command's options. 436 plugin_cls = command_metadata.plugin_cls 437 ConfigureCommandLineParser(command_metadata, parser) 438 439 # We handle help especially. 440 if global_flags.help: 441 parser.print_help() 442 sys.exit(-1) 443 444 # Parse the final command line. 445 result = parser.parse_args(argv) 446 447 # Apply the defaults to the parsed args. 448 result = utils.AttributeDict(vars(result)) 449 result.pop("__dummy", None) 450 451 command_metadata.ApplyDefaults(result) 452 453 return plugin_cls, result
454 455 456 ## Parser for special args. 457
458 -class IntParser(argparse.Action):
459 """Class to parse ints either in hex or as ints."""
460 - def parse_int(self, value):
461 # Support suffixes 462 multiplier = 1 463 m = re.search("(.*)(Mb|mb|kb|m|M|k|g|G|Gb)", value) 464 if m: 465 value = m.group(1) 466 suffix = m.group(2).lower() 467 if suffix in ("gb", "g"): 468 multiplier = 1024 * 1024 * 1024 469 elif suffix in ("mb", "m"): 470 multiplier = 1024 * 1024 471 elif suffix in ("kb", "k"): 472 multiplier = 1024 473 474 try: 475 if value.startswith("0x"): 476 value = int(value, 16) * multiplier 477 else: 478 value = int(value) * multiplier 479 except ValueError: 480 raise argparse.ArgumentError(self, "Invalid integer value") 481 482 return value
483
484 - def __call__(self, parser, namespace, values, option_string=None):
485 if isinstance(values, basestring): 486 values = self.parse_int(values) 487 setattr(namespace, self.dest, values)
488 489
490 -class ArrayIntParser(IntParser):
491 """Parse input as a comma separated list of integers. 492 493 We support input in the following forms: 494 495 --pid 1,2,3,4,5 496 497 --pid 1 2 3 4 5 498 499 --pid 0x1 0x2 0x3 500 """ 501
502 - def Validate(self, value):
503 return self.parse_int(value)
504
505 - def __call__(self, parser, namespace, values, option_string=None):
506 result = [] 507 if isinstance(values, basestring): 508 values = [values] 509 510 for value in values: 511 result.extend([self.Validate(x) for x in value.split(",")]) 512 513 setattr(namespace, self.dest, result or None)
514 515
516 -class ArrayStringParser(argparse.Action):
517 - def __call__(self, parser, namespace, values, option_string=None):
518 result = [] 519 520 if isinstance(values, basestring): 521 values = [values] 522 523 for value in values: 524 result.extend([x for x in value.split(",")]) 525 526 setattr(namespace, self.dest, result)
527