1   
   2   
   3   
   4   
   5   
   6   
   7   
   8   
   9   
  10   
  11   
  12   
  13   
  14   
  15   
  16   
  17   
  18   
  19  """This module implements plugins related to forensic artifacts. 
  20   
  21  https://github.com/ForensicArtifacts 
  22  """ 
  23   
  24  __author__ = "Michael Cohen <scudette@google.com>" 
  25  import csv 
  26  import datetime 
  27  import json 
  28  import platform 
  29  import os 
  30  import StringIO 
  31  import sys 
  32  import zipfile 
  33   
  34  import yaml 
  35   
  36  from artifacts import definitions 
  37  from artifacts import errors 
  38   
  39  from rekall import plugin 
  40  from rekall import obj 
  41  from rekall_lib import yaml_utils 
  42  from rekall.ui import text 
  43  from rekall.ui import json_renderer 
  44  from rekall.plugins.response import common 
  45   
  46  from rekall_lib import registry 
  50      """Bundle all the results from an artifact.""" 
  51 -    def __init__(self, artifact_name=None, result_type=None, fields=None): 
   52          self.artifact_name = artifact_name 
  53          self.result_type = result_type 
  54          self.results = [] 
  55          self.fields = fields or [] 
   56   
  58          return iter(self.results) 
   59   
  61          if data: 
  62              self.results.append(data) 
   63   
  65          self.results.extend(other) 
   66   
  68          return dict(fields=self.fields, 
  69                      results=self.results, 
  70                      artifact_name=self.artifact_name, 
  71                      result_type=self.result_type) 
    72   
  76      """Writes the results of artifacts.""" 
  77      __abstract = True 
  78   
  79      __metaclass__ = registry.MetaclassRegistry 
  80   
  81 -    def __init__(self, session=None, copy_files=False, 
  82                   create_timeline=False): 
   86   
  88          """Writes the artifact result.""" 
   89   
  91          """Create a new timeline result from the given result. 
  92   
  93          We use the output format suitable for the timesketch tool: 
  94          https://github.com/google/timesketch/wiki/UserGuideTimelineFromFile 
  95          """ 
  96          artifact_fields = artifact_result.fields 
  97          fields = [ 
  98              dict(name="message", type="unicode"), 
  99              dict(name="timestamp", type="int"), 
 100              dict(name="datetime", type="unicode"), 
 101              dict(name="timestamp_desc", type="unicode"), 
 102          ] + artifact_fields 
 103   
 104          new_result = ArtifactResult( 
 105              artifact_name=artifact_result.artifact_name, 
 106              result_type="timeline", 
 107              fields=fields) 
 108   
 109          for field in artifact_fields: 
 110               
 111              if field["type"] == "epoch": 
 112                  for row in artifact_result.results: 
 113                      new_row = row.copy() 
 114                      timestamp = row.get(field["name"]) 
 115                      if timestamp is None: 
 116                          continue 
 117   
 118                      new_row["timestamp"] = int(timestamp) 
 119                      new_row["datetime"] = datetime.datetime.utcfromtimestamp( 
 120                          timestamp).strftime("%Y-%m-%dT%H:%M:%S+00:00") 
 121                      new_row["timestamp_desc"] = artifact_result.artifact_name 
 122                      new_row["message"] = " ".join( 
 123                          unicode(row[field["name"]]) for field in artifact_fields 
 124                          if field["name"] in row) 
 125                      new_result.add_result(**new_row) 
 126   
 127          return new_result 
  128   
 131   
 132 -    def __exit__(self, unused_type, unused_value, unused_traceback): 
   134   
 137      name = "Directory" 
 138   
 139 -    def __init__(self, output=None, **kwargs): 
  146   
 161   
 163          fieldnames = [x["name"] for x in result.fields] 
 164          writer = csv.DictWriter( 
 165              out_fd, dialect="excel", 
 166              fieldnames=fieldnames) 
 167          writer.writeheader() 
 168          for row in result.results: 
 169              writer.writerow(row) 
  170   
 172          """Writes the artifact result.""" 
 173          if self.copy_files and result.result_type == "file_information": 
 174              try: 
 175                  self.write_file(result) 
 176              except (IOError, OSError) as e: 
 177                  self.session.logging.warn("Unable to copy file: %s", e) 
 178   
 179          with self.session.GetRenderer().open( 
 180                  directory=self.dump_dir, 
 181                  filename="artifacts/%s.json" % result.artifact_name, 
 182                  mode="wb") as out_fd: 
 183              out_fd.write(json.dumps(result.as_dict(), sort_keys=True)) 
 184   
 185          with self.session.GetRenderer().open( 
 186                  directory=self.dump_dir, 
 187                  filename="artifacts/%s.csv" % result.artifact_name, 
 188                  mode="wb") as out_fd: 
 189              self._write_csv_file(out_fd, result) 
 190   
 191          if self.create_timeline: 
 192              with self.session.GetRenderer().open( 
 193                      directory=self.dump_dir, 
 194                      filename="artifacts/%s.timeline.csv" % 
 195                      result.artifact_name, 
 196                      mode="wb") as out_fd: 
 197                  self._write_csv_file(out_fd, self._create_timeline(result)) 
   198   
 201      name = "Zip" 
 202   
 203 -    def __init__(self, output=None, **kwargs): 
  206   
 215   
 219   
 221          fieldnames = [x["name"] for x in result.fields] 
 222          writer = csv.DictWriter( 
 223              out_fd, dialect="excel", 
 224              fieldnames=fieldnames) 
 225          writer.writeheader() 
 226          for row in result.results: 
 227              writer.writerow(row) 
  228   
 233   
 235          """Writes the artifact result.""" 
 236          if self.copy_files and result.result_type == "file_information": 
 237              try: 
 238                  self.write_file(result) 
 239              except (IOError, OSError) as e: 
 240                  self.session.logging.warn( 
 241                      "Unable to copy file %s into output: %s", 
 242                      result["filename"], e) 
 243   
 244          self.outzip.writestr("artifacts/%s.json" % result.artifact_name, 
 245                               json.dumps(result.as_dict(), sort_keys=True), 
 246                               zipfile.ZIP_DEFLATED) 
 247   
 248          tmp_fd = StringIO.StringIO() 
 249          self._write_csv_file(tmp_fd, result) 
 250          self.outzip.writestr("artifacts/%s.csv" % result.artifact_name, 
 251                               tmp_fd.getvalue(), 
 252                               zipfile.ZIP_DEFLATED) 
 253   
 254   
 255          if self.create_timeline: 
 256              tmp_fd = StringIO.StringIO() 
 257              self._write_csv_file(tmp_fd, self._create_timeline(result)) 
 258              self.outzip.writestr("artifacts/%s.timeline.csv" % 
 259                                   result.artifact_name, 
 260                                   tmp_fd.getvalue(), 
 261                                   zipfile.ZIP_DEFLATED) 
   262   
 263   
 264   
 265  TYPE_INDICATOR_REKALL = "REKALL_EFILTER" 
 269      """Loads and validates fields in a dict. 
 270   
 271      We check their name, types and if they are optional according to a template 
 272      in _field_definitions. 
 273      """ 
 274      _field_definitions = [] 
 275   
 277          for field in field_definitions: 
 278              name = field["name"] 
 279   
 280              default = field.get("default") 
 281              required_type = field.get("type") 
 282   
 283              if required_type in (str, unicode): 
 284                  required_type = basestring 
 285   
 286              if default is None and required_type is not None: 
 287                   
 288                  if required_type is basestring: 
 289                      default = "" 
 290                  else: 
 291                      default = required_type() 
 292   
 293              if required_type is None and default is not None: 
 294                  required_type = type(default) 
 295   
 296              if not field.get("optional"): 
 297                  if name not in data: 
 298                      raise errors.FormatError( 
 299                          u'Missing fields {}.'.format(name)) 
 300   
 301              value = data.get(name, default) 
 302              if default is not None and not isinstance(value, required_type): 
 303                  raise errors.FormatError( 
 304                      u'field {} has type {} should be {}.'.format( 
 305                          name, type(data[name]), required_type)) 
 306   
 307              if field.get("checker"): 
 308                  value = field["checker"](self, data) 
 309   
 310              setattr(self, name, value) 
   311   
 314      """All sources inherit from this.""" 
 315   
 316       
 317      _common_fields = [ 
 318          dict(name="type", optional=False), 
 319          dict(name="supported_os", optional=True, type=list, 
 320               default=list(definitions.SUPPORTED_OS)), 
 321      ] 
 322   
 323 -    def __init__(self, source_definition, artifact=None): 
  324          attributes = source_definition["attributes"] 
 325           
 326          self.artifact = artifact 
 327          self.source_definition = source_definition 
 328          self.type_indicator = source_definition["type"] 
 329          self._LoadFieldDefinitions(attributes, self._field_definitions) 
 330          self._LoadFieldDefinitions(source_definition, self._common_fields) 
  331   
 333          """Indicates if the source is applicable to the environment.""" 
 334          return True 
  335   
 336 -    def apply(self, artifact_name=None, fields=None, result_type=None, **_): 
  337          """Generate ArtifactResult instances.""" 
 338          return ArtifactResult(artifact_name=artifact_name, 
 339                                result_type=result_type, 
 340                                fields=fields) 
   341   
 342   
 343   
 344   
 345  REKALL_IMAGE_TYPES = [ 
 346      "Windows", "WindowsAPI", 
 347      "Linux", "LinuxAPI", 
 348      "Darwin", "DarwinAPI" 
 349  ] 
 353      """Class to support Rekall Efilter artifact types.""" 
 354   
 355      allowed_types = { 
 356          "int": int, 
 357          "unicode": unicode,   
 358          "str": str,  
 359          "float": float, 
 360          "epoch": float,  
 361          "any": str   
 362      } 
 363   
 364      _field_definitions = [ 
 365          dict(name="query", type=basestring), 
 366          dict(name="query_parameters", default=[], optional=True), 
 367          dict(name="fields", type=list), 
 368          dict(name="type_name", type=basestring), 
 369          dict(name="image_type", type=list, optional=True, 
 370               default=REKALL_IMAGE_TYPES), 
 371      ] 
 372   
 373 -    def __init__(self, source_definition, **kw): 
  374          super(RekallEFilterArtifacts, self).__init__(source_definition, **kw) 
 375          for column in self.fields: 
 376              if "name" not in column or "type" not in column: 
 377                  raise errors.FormatError( 
 378                      u"Field definition should have both name and type.") 
 379   
 380              mapped_type = column["type"] 
 381              if mapped_type not in self.allowed_types: 
 382                  raise errors.FormatError( 
 383                      u"Unsupported type %s." % mapped_type) 
  384   
 386          """Returns one of the standard image types based on the session.""" 
 387          result = session.profile.metadata("os").capitalize() 
 388   
 389          if session.GetParameter("live_mode") == "API": 
 390              result += "API" 
 391   
 392          return result 
  393   
 395          """Determine if this source is active.""" 
 396          return (self.image_type and 
 397                  self.GetImageType(session) in self.image_type) 
  398   
 399 -    def apply(self, session=None, **kwargs): 
   425   
 434   
 437      _field_definitions = [ 
 438          dict(name="paths", default=[]), 
 439          dict(name="separator", default="/", type=basestring, 
 440               optional=True), 
 441      ] 
 442   
 443       
 444      _FIELDS = [ 
 445          dict(name="st_mode", type="unicode"), 
 446          dict(name="st_nlink", type="int"), 
 447          dict(name="st_uid", type="unicode"), 
 448          dict(name="st_gid", type="unicode"), 
 449          dict(name="st_size", type="int"), 
 450          dict(name="st_mtime", type="epoch"), 
 451          dict(name="filename", type="unicode"), 
 452      ] 
 453   
 454 -    def apply(self, session=None, **kwargs): 
   473   
 486   
 488      _field_definitions = [ 
 489          dict(name="query", type=basestring), 
 490          dict(name="fields", type=list, optional=True, default=[]), 
 491          dict(name="type_name", type=basestring, optional=True), 
 492          dict(name="supported_os", optional=True, 
 493               default=definitions.SUPPORTED_OS), 
 494      ] 
 495   
 496      fields = None 
 497   
 499          result = [] 
 500          for key, value in sample.iteritems(): 
 501              field_type = type(value) 
 502              if field_type is int: 
 503                  field_type = "int" 
 504              elif field_type is str: 
 505                  field_type = "unicode" 
 506              else: 
 507                  field_type = "unicode" 
 508   
 509              result.append(dict(name=key, type=field_type)) 
 510          return result 
  511   
 512 -    def apply(self, session=None, **kwargs): 
   544   
 547      _field_definitions = [ 
 548          dict(name="keys", default=[]), 
 549          dict(name="supported_os", optional=True, 
 550               default=["Windows"]), 
 551      ] 
 552   
 553      _FIELDS = [ 
 554          dict(name="st_mtime", type="epoch"), 
 555          dict(name="hive", type="unicode"), 
 556          dict(name="key_name", type="unicode"), 
 557          dict(name="value", type="str"), 
 558          dict(name="value_type", type="str"), 
 559      ] 
 560   
 561 -    def apply(self, session=None, **kwargs): 
   582   
 586          key_value_pairs = source["key_value_pairs"] 
 587          for pair in key_value_pairs: 
 588              if (not isinstance(pair, dict) or "key" not in pair or 
 589                  "value" not in pair): 
 590                  raise errors.FormatError( 
 591                      u"key_value_pairs should consist of dicts with key and " 
 592                      "value items.") 
 593   
 594          return key_value_pairs 
  595   
 596      _field_definitions = [ 
 597          dict(name="key_value_pairs", default=[], 
 598               checker=CheckKeyValuePairs), 
 599          dict(name="supported_os", optional=True, 
 600               default=["Windows"]), 
 601      ] 
 602   
 603      _FIELDS = [ 
 604          dict(name="st_mtime", type="epoch"), 
 605          dict(name="hive", type="unicode"), 
 606          dict(name="key_name", type="unicode"), 
 607          dict(name="value_name", type="unicode"), 
 608          dict(name="value_type", type="str"), 
 609          dict(name="value", type="str"), 
 610      ] 
 611   
 612 -    def apply(self, session=None, **kwargs): 
  649      """The main artifact class.""" 
 650   
 652          """Ensure labels are defined.""" 
 653          labels = art_definition.get("labels", []) 
 654           
 655           
 656           
 657           
 658          self.undefined_labels = set(labels).difference(definitions.LABELS) 
 659          return labels 
  660   
 662          sources = art_definition["sources"] 
 663          result = [] 
 664          self.unsupported_source_types = [] 
 665          for source in sources: 
 666              if not isinstance(source, dict): 
 667                  raise errors.FormatError("Source is not a dict.") 
 668   
 669              source_type_name = source.get("type") 
 670              if source_type_name is None: 
 671                  raise errors.FormatError("Source has no type.") 
 672   
 673              source_cls = self.source_types.get(source_type_name) 
 674              if source_cls: 
 675                  result.append(source_cls(source, artifact=self)) 
 676              else: 
 677                  self.unsupported_source_types.append(source_type_name) 
 678   
 679          if not result: 
 680              if self.unsupported_source_types: 
 681                  raise errors.FormatError( 
 682                      "No supported sources: %s" % ( 
 683                          self.unsupported_source_types,)) 
 684   
 685              raise errors.FormatError("No available sources.") 
 686   
 687          return result 
  688   
 690          supported_os = art_definition.get( 
 691              "supported_os", definitions.SUPPORTED_OS) 
 692   
 693          undefined_supported_os = set(supported_os).difference( 
 694              definitions.SUPPORTED_OS) 
 695   
 696          if undefined_supported_os: 
 697              raise errors.FormatError( 
 698                  u'supported operating system: {} ' 
 699                  u'not defined.'.format( 
 700                      u', '.join(undefined_supported_os))) 
 701   
 702          return supported_os 
  703   
 704      _field_definitions = [ 
 705          dict(name="name", type=basestring), 
 706          dict(name="doc", type=basestring), 
 707          dict(name="labels", default=[], 
 708               checker=CheckLabels, optional=True), 
 709          dict(name="sources", default=[], 
 710               checker=BuildSources), 
 711          dict(name="supported_os", 
 712               checker=SupportedOS, optional=True), 
 713          dict(name="conditions", default=[], optional=True), 
 714          dict(name="returned_types", default=[], optional=True), 
 715          dict(name="provides", type=list, optional=True), 
 716          dict(name="urls", type=list, optional=True) 
 717      ] 
 718   
 719      name = "unknown" 
 720      source_types = SOURCE_TYPES 
 721   
 722 -    def __init__(self, data, source_types=None): 
  731   
 734   
 736          if not isinstance(data, dict): 
 737              raise errors.FormatError( 
 738                  "Artifact definition must be a dict.") 
 739   
 740          different_keys = set(data) - definitions.TOP_LEVEL_KEYS 
 741          if different_keys: 
 742              raise errors.FormatError(u'Undefined keys: {}'.format( 
 743                  different_keys)) 
 744   
 745          self._LoadFieldDefinitions(data, self._field_definitions) 
   746   
 749      """Loads artifacts from the artifact profiles.""" 
 750      name = "$ARTIFACTS" 
 751   
 753          for definition in art_definitions: 
 754              try: 
 755                  profile.AddDefinition(definition) 
 756              except errors.FormatError as e: 
 757                  session.logging.debug( 
 758                      "Skipping Artifact %s: %s", definition.get("name"), e) 
 759   
 760          return profile 
   761   
 764      """A profile containing artifact definitions.""" 
 765   
 766       
 771   
 773          """Add a new definition from a dict.""" 
 774          self.definitions.append(definition) 
 775          self.definitions_by_name[definition["name"]] = definition 
  776   
 783   
  793   
 797      """Collects artifacts.""" 
 798   
 799      name = "artifact_collector" 
 800   
 801      __args = [ 
 802          dict(name="artifacts", positional=True, required=True, 
 803               type="ArrayStringParser", 
 804               help="A list of artifact names to collect."), 
 805   
 806          dict(name="artifact_files", type="ArrayStringParser", 
 807               help="A list of additional yaml files to load which contain " 
 808               "artifact definitions."), 
 809   
 810          dict(name="definitions", type="ArrayStringParser", 
 811               help="An inline artifact definition in yaml format."), 
 812   
 813          dict(name="create_timeline", type="Bool", default=False, 
 814               help="Also generate a timeline file."), 
 815   
 816          dict(name="copy_files", type="Bool", default=False, 
 817               help="Copy files into the output."), 
 818   
 819          dict(name="writer", type="Choices", 
 820               choices=lambda: ( 
 821                   x.name for x in BaseArtifactResultWriter.classes.values()), 
 822               help="Writer for artifact results."), 
 823   
 824          dict(name="output_path", 
 825               help="Path suitable for dumping files."), 
 826      ] 
 827   
 828      table_header = [ 
 829          dict(name="divider", type="Divider"), 
 830          dict(name="result"), 
 831      ] 
 832   
 833      table_options = dict( 
 834          suppress_headers=True 
 835      ) 
 836   
 839   
 871   
 872      @classmethod 
 887   
 892   
 894          if artifact_name in self.seen: 
 895              return 
 896   
 897          self.seen.add(artifact_name) 
 898   
 899          try: 
 900              definition = self.artifact_profile.GetDefinitionByName( 
 901                  artifact_name) 
 902          except KeyError: 
 903              self.session.logging.error("Unknown artifact %s" % artifact_name) 
 904              return 
 905   
 906           
 907          if self.supported_os not in definition.supported_os: 
 908              self.session.logging.debug( 
 909                  "Skipping artifact %s: Supported OS: %s, but we are %s", 
 910                  definition.name, definition.supported_os, 
 911                  self.supported_os) 
 912              return 
 913   
 914          if not self._evaluate_conditions(definition.conditions): 
 915              return 
 916   
 917          yield dict(divider="Artifact: %s" % definition.name) 
 918   
 919          for source in definition.sources: 
 920               
 921              if not source.is_active(session=self.session): 
 922                  continue 
 923   
 924              for result in source.apply( 
 925                      artifact_name=definition.name, 
 926                      session=self.session, 
 927                      collector=self): 
 928                  if isinstance(result, dict): 
 929                      yield result 
 930                  else: 
 931                      yield dict(result=result) 
  932   
 955   
  962   
 963 -class ArtifactsView(plugin.TypedProfileCommand, 
 964                      plugin.Command): 
  965      name = "artifact_view" 
 966   
 967      __args = [ 
 968          dict(name="artifacts", type="ArrayStringParser", positional=True, 
 969               help="A list of artifacts to display") 
 970      ] 
 971   
 972      table_header = [ 
 973          dict(name="divider", type="Divider"), 
 974          dict(name="Message") 
 975      ] 
 976   
 978          artifact_profile = self.session.LoadProfile("artifacts") 
 979          for artifact in self.plugin_args.artifacts: 
 980              definition = artifact_profile.definitions_by_name.get(artifact) 
 981              if definition: 
 982                  yield dict(divider=artifact) 
 983                  yield dict(Message=yaml_utils.safe_dump(definition)) 
   984   
 985   
 986 -class ArtifactsList(plugin.TypedProfileCommand, 
 987                      plugin.Command): 
  988      """List details about all known artifacts.""" 
 989   
 990      name = "artifact_list" 
 991   
 992      __args = [ 
 993          dict(name="regex", type="RegEx", 
 994               default=".", 
 995               help="Filter the artifact name."), 
 996          dict(name="supported_os", type="ArrayStringParser", required=False, 
 997               help="If specified show for these OSs, otherwise autodetect " 
 998               "based on the current image."), 
 999          dict(name="labels", type="ArrayStringParser", 
1000               help="Filter by these labels."), 
1001          dict(name="all", type="Bool", 
1002               help="Show all artifacts."), 
1003      ] 
1004   
1005      table_header = [ 
1006          dict(name="Name", width=30), 
1007          dict(name="OS", width=8), 
1008          dict(name="Labels", width=20), 
1009          dict(name="Types", width=20), 
1010          dict(name="Description", width=50), 
1011      ] 
1012   
 1037   
1038   
1039 -class ArtifactResult_TextObjectRenderer(text.TextObjectRenderer): 
 1040      renders_type = "ArtifactResult" 
1041   
1042 -    def render_row(self, target, **_): 
 1043          column_names = [x["name"] for x in target.fields] 
1044          table = text.TextTable( 
1045              columns=target.fields, 
1046              renderer=self.renderer, 
1047              session=self.session) 
1048   
1049          if not target.results: 
1050              return text.Cell("") 
1051   
1052          result = [ 
1053              text.JoinedCell(*[text.Cell(x) for x in column_names]), 
1054              text.JoinedCell(*[text.Cell("-" * len(x)) for x in column_names])] 
1055   
1056          for row in target.results: 
1057              ordered_row = [] 
1058              for column in column_names: 
1059                  ordered_row.append(row.get(column)) 
1060   
1061              result.append(table.get_row(*ordered_row)) 
1062   
1063          result = text.StackedCell(*result) 
1064          return result 
  1065   
1069      renders_type = "ArtifactResult" 
1070      renderers = ["DataExportRenderer"] 
1071   
1073          return dict(artifact_name=item.artifact_name, 
1074                      result_type=item.result_type, 
1075                      fields=item.fields, 
1076                      results=item.results) 
  1077