lib.cuckoo.core.plugins

Package lib :: Package cuckoo :: Package core :: Module plugins |
[hide private]
[frames] | no frames]

# Source Code for Module lib.cuckoo.core.plugins

      1  # Copyright (C) 2010-2013 Claudio Guarnieri.
      2  # Copyright (C) 2014-2016 Cuckoo Foundation.
      3  # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
      4  # See the file 'docs/LICENSE' for copying permission.
      5
      6  import os
      7  import pkgutil
      8  import importlib
      9  import inspect
     10  import logging
     11  from collections import defaultdict
     12  from distutils.version import StrictVersion
     13
     14  from lib.cuckoo.common.abstracts import Auxiliary, Machinery, LibVirtMachinery, Processing
     15  from lib.cuckoo.common.abstracts import Report, Signature
     16  from lib.cuckoo.common.config import Config
     17  from lib.cuckoo.common.constants import CUCKOO_ROOT, CUCKOO_VERSION
     18  from lib.cuckoo.common.exceptions import CuckooCriticalError
     19  from lib.cuckoo.common.exceptions import CuckooOperationalError
     20  from lib.cuckoo.common.exceptions import CuckooProcessingError
     21  from lib.cuckoo.common.exceptions import CuckooReportError
     22  from lib.cuckoo.common.exceptions import CuckooDependencyError
     23  from lib.cuckoo.common.exceptions import CuckooDisableModule
     24
     25  log = logging.getLogger(__name__)
     26
     27  _modules = defaultdict(list)
     28
     29 -def enumerate_plugins(dirpath, module_prefix, namespace, class_,
     30                        attributes={}):
     31      """Import plugins of type `class` located at `dirpath` into the
     32      `namespace` that starts with `module_prefix`. If `dirpath` represents a
     33      filepath then it is converted into its containing directory. The
     34      `attributes` dictionary allows one to set extra fields for all imported
     35      plugins."""
     36      if os.path.isfile(dirpath):
     37          dirpath = os.path.dirname(dirpath)
     38
     39      for fname in os.listdir(dirpath):
     40          if fname.endswith(".py") and not fname.startswith("__init__"):
     41              module_name, _ = os.path.splitext(fname)
     42              importlib.import_module("%s.%s" % (module_prefix, module_name))
     43
     44      plugins = []
     45      for subclass in class_.__subclasses__():
     46          # Check whether this subclass belongs to the module namespace that
     47          # we're currently importing. It should be noted that parent and child
     48          # namespaces should fail the following if-statement.
     49          if module_prefix != ".".join(subclass.__module__.split(".")[:-1]):
     50              continue
     51
     52          namespace[subclass.__name__] = subclass
     53          for key, value in attributes.items():
     54              setattr(subclass, key, value)
     55          plugins.append(subclass)
     56      return plugins
     57
     58 -def import_plugin(name):
     59      try:
     60          module = __import__(name, globals(), locals(), ["dummy"], -1)
     61      except ImportError as e:
     62          raise CuckooCriticalError("Unable to import plugin "
     63                                    "\"{0}\": {1}".format(name, e))
     64      else:
     65          load_plugins(module)
     66
     67 -def import_package(package):
     68      prefix = package.__name__ + "."
     69      for loader, name, ispkg in pkgutil.iter_modules(package.__path__, prefix):
     70          import_plugin(name)
     71
     72 -def load_plugins(module):
     73      for name, value in inspect.getmembers(module):
     74          if inspect.isclass(value):
     75              if issubclass(value, Auxiliary) and value is not Auxiliary:
     76                  register_plugin("auxiliary", value)
     77              elif issubclass(value, Machinery) and value is not Machinery and value is not LibVirtMachinery:
     78                  register_plugin("machinery", value)
     79              elif issubclass(value, Processing) and value is not Processing:
     80                  register_plugin("processing", value)
     81              elif issubclass(value, Report) and value is not Report:
     82                  register_plugin("reporting", value)
     83              elif issubclass(value, Signature) and value is not Signature:
     84                  register_plugin("signatures", value)
     85
     86 -def register_plugin(group, name):
     87      global _modules
     88      group = _modules.setdefault(group, [])
     89      group.append(name)
     90
     91 -def list_plugins(group=None):
     92      if group:
     93          return _modules[group]
     94      else:
     95          return _modules
     96
     97 -class RunAuxiliary(object):
     98      """Auxiliary modules manager."""
     99
    100 -    def __init__(self, task, machine, guest_manager):
    101          self.task = task
    102          self.machine = machine
    103          self.guest_manager = guest_manager
    104
    105          self.cfg = Config("auxiliary")
    106          self.enabled = []
    107
    108 -    def start(self):
    109          for module in list_plugins(group="auxiliary"):
    110              try:
    111                  current = module()
    112              except:
    113                  log.exception("Failed to load the auxiliary module "
    114                                "\"{0}\":".format(module))
    115                  return
    116
    117              module_name = inspect.getmodule(current).__name__
    118              if "." in module_name:
    119                  module_name = module_name.rsplit(".", 1)[1]
    120
    121              try:
    122                  options = self.cfg.get(module_name)
    123              except CuckooOperationalError:
    124                  log.debug("Auxiliary module %s not found in "
    125                            "configuration file", module_name)
    126                  continue
    127
    128              if not options.enabled:
    129                  continue
    130
    131              current.set_task(self.task)
    132              current.set_machine(self.machine)
    133              current.set_guest_manager(self.guest_manager)
    134              current.set_options(options)
    135
    136              try:
    137                  current.start()
    138              except NotImplementedError:
    139                  pass
    140              except CuckooDisableModule:
    141                  continue
    142              except Exception as e:
    143                  log.warning("Unable to start auxiliary module %s: %s",
    144                              module_name, e)
    145              else:
    146                  log.debug("Started auxiliary module: %s",
    147                            current.__class__.__name__)
    148                  self.enabled.append(current)
    149
    150 -    def callback(self, name, *args, **kwargs):
    151          def default(*args, **kwargs):
    152              pass
    153
    154          enabled = []
    155          for module in self.enabled:
    156              try:
    157                  getattr(module, "cb_%s" % name, default)(*args, **kwargs)
    158              except NotImplementedError:
    159                  pass
    160              except CuckooDisableModule:
    161                  continue
    162              except Exception as e:
    163                  log.warning(
    164                      "Error performing callback %r on auxiliary module %r: %s",
    165                      name, module.__class__.__name__, e
    166                  )
    167
    168              enabled.append(module)
    169          self.enabled = enabled
    170
    171 -    def stop(self):
    172          for module in self.enabled:
    173              try:
    174                  module.stop()
    175              except NotImplementedError:
    176                  pass
    177              except Exception as e:
    178                  log.warning("Unable to stop auxiliary module: %s", e)
    179              else:
    180                  log.debug("Stopped auxiliary module: %s",
    181                            module.__class__.__name__)
    182
    183 -class RunProcessing(object):
    184      """Analysis Results Processing Engine.
    185
    186      This class handles the loading and execution of the processing modules.
    187      It executes the enabled ones sequentially and generates a dictionary which
    188      is then passed over the reporting engine.
    189      """
    190
    191 -    def __init__(self, task):
    192          """@param task: task dictionary of the analysis to process."""
    193          self.task = task
    194          self.analysis_path = os.path.join(CUCKOO_ROOT, "storage", "analyses", str(task["id"]))
    195          self.baseline_path = os.path.join(CUCKOO_ROOT, "storage", "baseline")
    196          self.cfg = Config("processing")
    197
    198 -    def process(self, module, results):
    199          """Run a processing module.
    200          @param module: processing module to run.
    201          @param results: results dict.
    202          @return: results generated by module.
    203          """
    204          # Initialize the specified processing module.
    205          try:
    206              current = module()
    207          except:
    208              log.exception("Failed to load the processing module "
    209                            "\"{0}\":".format(module))
    210              return None, None
    211
    212          # Extract the module name.
    213          module_name = inspect.getmodule(current).__name__
    214          if "." in module_name:
    215              module_name = module_name.rsplit(".", 1)[1]
    216
    217          try:
    218              options = self.cfg.get(module_name)
    219          except CuckooOperationalError:
    220              log.debug("Processing module %s not found in configuration file",
    221                        module_name)
    222              return None, None
    223
    224          # If the processing module is disabled in the config, skip it.
    225          if not options.enabled:
    226              return None, None
    227
    228          # Give it the path to the baseline directory.
    229          current.set_baseline(self.baseline_path)
    230          # Give it the path to the analysis results.
    231          current.set_path(self.analysis_path)
    232          # Give it the analysis task object.
    233          current.set_task(self.task)
    234          # Give it the options from the relevant processing.conf section.
    235          current.set_options(options)
    236          # Give the results that we have obtained so far.
    237          current.set_results(results)
    238
    239          try:
    240              # Run the processing module and retrieve the generated data to be
    241              # appended to the general results container.
    242              data = current.run()
    243
    244              log.debug("Executed processing module \"%s\" on analysis at "
    245                        "\"%s\"", current.__class__.__name__, self.analysis_path)
    246
    247              # If succeeded, return they module's key name and the data.
    248              return current.key, data
    249          except CuckooDependencyError as e:
    250              log.warning("The processing module \"%s\" has missing dependencies: %s", current.__class__.__name__, e)
    251          except CuckooProcessingError as e:
    252              log.warning("The processing module \"%s\" returned the following "
    253                          "error: %s", current.__class__.__name__, e)
    254          except:
    255              log.exception("Failed to run the processing module \"%s\" for task #%d:",
    256                            current.__class__.__name__, self.task["id"])
    257
    258          return None, None
    259
    260 -    def run(self):
    261          """Run all processing modules and all signatures.
    262          @return: processing results.
    263          """
    264          # This is the results container. It's what will be used by all the
    265          # reporting modules to make it consumable by humans and machines.
    266          # It will contain all the results generated by every processing
    267          # module available. Its structure can be observed through the JSON
    268          # dump in the analysis' reports folder. (If jsondump is enabled.)
    269          # We friendly call this "fat dict".
    270          results = {
    271              "_temp": {},
    272          }
    273
    274          # Order modules using the user-defined sequence number.
    275          # If none is specified for the modules, they are selected in
    276          # alphabetical order.
    277          processing_list = list_plugins(group="processing")
    278
    279          # If no modules are loaded, return an empty dictionary.
    280          if processing_list:
    281              processing_list.sort(key=lambda module: module.order)
    282
    283              # Run every loaded processing module.
    284              for module in processing_list:
    285                  key, result = self.process(module, results)
    286
    287                  # If the module provided results, append it to the fat dict.
    288                  if key and result:
    289                      results[key] = result
    290          else:
    291              log.info("No processing modules loaded")
    292
    293          results.pop("_temp", None)
    294
    295          # Return the fat dict.
    296          return results
    297
    298 -class RunSignatures(object):
    299      """Run Signatures."""
    300
    301 -    def __init__(self, results):
    302          self.results = results
    303          self.matched = []
    304
    305          # While developing our version is generally something along the lines
    306          # of "2.0-dev" whereas StrictVersion() does not handle "-dev", so we
    307          # strip that part off.
    308          self.version = CUCKOO_VERSION.split("-")[0]
    309
    310          # Gather all enabled, up-to-date, and applicable signatures.
    311          self.signatures = []
    312          for signature in list_plugins(group="signatures"):
    313              if self._should_enable_signature(signature):
    314                  self.signatures.append(signature(self))
    315
    316          # Signatures to call per API name.
    317          self.api_sigs = {}
    318
    319 -    def _should_enable_signature(self, signature):
    320          """Should the given signature be enabled for this analysis?"""
    321          if not signature.enabled:
    322              return False
    323
    324          if not self.check_signature_version(signature):
    325              return False
    326
    327          # Network and/or cross-platform signatures.
    328          if not signature.platform:
    329              return True
    330
    331          task_platform = self.results.get("info", {}).get("platform")
    332
    333          # Windows is implied when a platform has not been specified during the
    334          # submission of a sample, but for other platforms the platform has to
    335          # be explicitly stated.
    336          if not task_platform and signature.platform == "windows":
    337              return True
    338
    339          return task_platform == signature.platform
    340
    341 -    def check_signature_version(self, signature):
    342          """Check signature version.
    343          @param current: signature class/instance to check.
    344          @return: check result.
    345          """
    346          # Check the minimum Cuckoo version for this signature, if provided.
    347          if signature.minimum:
    348              try:
    349                  # If the running Cuckoo is older than the required minimum
    350                  # version, skip this signature.
    351                  if StrictVersion(self.version) < StrictVersion(signature.minimum):
    352                      log.debug("You are running an older incompatible version "
    353                                "of Cuckoo, the signature \"%s\" requires "
    354                                "minimum version %s.",
    355                                signature.name, signature.minimum)
    356                      return False
    357
    358                  if StrictVersion("1.2") > StrictVersion(signature.minimum):
    359                      log.warn("Cuckoo signature style has been redesigned in "
    360                               "cuckoo 1.2. This signature is not "
    361                               "compatible: %s.", signature.name)
    362                      return False
    363
    364                  if StrictVersion("2.0") > StrictVersion(signature.minimum):
    365                      log.warn("Cuckoo version 2.0 features a lot of changes that "
    366                               "render old signatures ineffective as they are not "
    367                               "backwards-compatible. Please upgrade this "
    368                               "signature: %s.", signature.name)
    369                      return False
    370
    371                  if hasattr(signature, "run"):
    372                      log.warn("This signatures features one or more deprecated "
    373                               "functions which indicates that it is very likely "
    374                               "an old-style signature. Please upgrade this "
    375                               "signature: %s.", signature.name)
    376                      return False
    377
    378              except ValueError:
    379                  log.debug("Wrong minor version number in signature %s",
    380                            signature.name)
    381                  return False
    382
    383          # Check the maximum version of Cuckoo for this signature, if provided.
    384          if signature.maximum:
    385              try:
    386                  # If the running Cuckoo is newer than the required maximum
    387                  # version, skip this signature.
    388                  if StrictVersion(self.version) > StrictVersion(signature.maximum):
    389                      log.debug("You are running a newer incompatible version "
    390                                "of Cuckoo, the signature \"%s\" requires "
    391                                "maximum version %s.",
    392                                signature.name, signature.maximum)
    393                      return False
    394              except ValueError:
    395                  log.debug("Wrong major version number in signature %s",
    396                            signature.name)
    397                  return False
    398
    399          return True
    400
    401 -    def call_signature(self, signature, handler, *args, **kwargs):
    402          """Wrapper to call into 3rd party signatures. This wrapper yields the
    403          event to the signature and handles matched signatures recursively."""
    404          try:
    405              if handler(*args, **kwargs):
    406                  signature.matched = True
    407                  for sig in self.signatures:
    408                      self.call_signature(sig, sig.on_signature, signature)
    409          except NotImplementedError:
    410              return False
    411          except:
    412              log.exception("Failed to run '%s' of the %s signature",
    413                            handler.__name__, signature.name)
    414          return True
    415
    416 -    def init_api_sigs(self, apiname, category):
    417          """Initialize a list of signatures for which we should trigger its
    418          on_call method for this particular API name and category."""
    419          self.api_sigs[apiname] = []
    420
    421          for sig in self.signatures:
    422              if sig.filter_apinames and apiname not in sig.filter_apinames:
    423                  continue
    424
    425              if sig.filter_categories and category not in sig.filter_categories:
    426                  continue
    427
    428              self.api_sigs[apiname].append(sig)
    429
    430 -    def yield_calls(self, proc):
    431          """Yield calls of interest to each interested signature."""
    432          for idx, call in enumerate(proc.get("calls", [])):
    433
    434              # Initialize a list of signatures to call for this API call.
    435              if call["api"] not in self.api_sigs:
    436                  self.init_api_sigs(call["api"], call.get("category"))
    437
    438              # See the following SO answer on why we're using reversed() here.
    439              # http://stackoverflow.com/a/10665800
    440              for sig in reversed(self.api_sigs[call["api"]]):
    441                  sig.cid, sig.call = idx, call
    442                  if self.call_signature(sig, sig.on_call, call, proc) is False:
    443                      self.api_sigs[call["api"]].remove(sig)
    444
    445 -    def run(self):
    446          """Run signatures."""
    447          # Allow signatures to initialize themselves.
    448          for signature in self.signatures:
    449              signature.init()
    450
    451          log.debug("Running %d signatures", len(self.signatures))
    452
    453          # Iterate calls and tell interested signatures about them.
    454          for proc in self.results.get("behavior", {}).get("processes", []):
    455
    456              # Yield the new process event.
    457              for sig in self.signatures:
    458                  sig.pid = proc["pid"]
    459                  self.call_signature(sig, sig.on_process, proc)
    460
    461              self.yield_calls(proc)
    462
    463          # Yield completion events to each signature.
    464          for sig in self.signatures:
    465              self.call_signature(sig, sig.on_complete)
    466
    467          score = 0
    468          for signature in self.signatures:
    469              if signature.matched:
    470                  log.debug("Analysis matched signature: %s", signature.name)
    471                  self.matched.append(signature.results())
    472                  score += signature.severity
    473
    474          # Sort the matched signatures by their severity level and put them
    475          # into the results dictionary.
    476          self.matched.sort(key=lambda key: key["severity"])
    477          self.results["signatures"] = self.matched
    478          if "info" in self.results:
    479              self.results["info"]["score"] = score / 5.0
    480
    481 -class RunReporting(object):
    482      """Reporting Engine.
    483
    484      This class handles the loading and execution of the enabled reporting
    485      modules. It receives the analysis results dictionary from the Processing
    486      Engine and pass it over to the reporting modules before executing them.
    487      """
    488
    489 -    def __init__(self, task, results):
    490          """@param analysis_path: analysis folder path."""
    491          self.task = task
    492          self.results = results
    493          self.analysis_path = os.path.join(CUCKOO_ROOT, "storage", "analyses", str(task["id"]))
    494          self.cfg = Config("reporting")
    495
    496 -    def process(self, module):
    497          """Run a single reporting module.
    498          @param module: reporting module.
    499          @param results: results results from analysis.
    500          """
    501          # Initialize current reporting module.
    502          try:
    503              current = module()
    504          except:
    505              log.exception("Failed to load the reporting module \"{0}\":".format(module))
    506              return
    507
    508          # Extract the module name.
    509          module_name = inspect.getmodule(current).__name__
    510          if "." in module_name:
    511              module_name = module_name.rsplit(".", 1)[1]
    512
    513          try:
    514              options = self.cfg.get(module_name)
    515          except CuckooOperationalError:
    516              log.debug("Reporting module %s not found in configuration file", module_name)
    517              return
    518
    519          # If the reporting module is disabled in the config, skip it.
    520          if not options.enabled:
    521              return
    522
    523          # Give it the path to the analysis results folder.
    524          current.set_path(self.analysis_path)
    525          # Give it the analysis task object.
    526          current.set_task(self.task)
    527          # Give it the the relevant reporting.conf section.
    528          current.set_options(options)
    529          # Load the content of the analysis.conf file.
    530          current.cfg = Config(cfg=current.conf_path)
    531
    532          try:
    533              current.run(self.results)
    534              log.debug("Executed reporting module \"%s\"", current.__class__.__name__)
    535          except CuckooDependencyError as e:
    536              log.warning("The reporting module \"%s\" has missing dependencies: %s", current.__class__.__name__, e)
    537          except CuckooReportError as e:
    538              log.warning("The reporting module \"%s\" returned the following error: %s", current.__class__.__name__, e)
    539          except:
    540              log.exception("Failed to run the reporting module \"%s\":", current.__class__.__name__)
    541
    542 -    def run(self):
    543          """Generates all reports.
    544          @raise CuckooReportError: if a report module fails.
    545          """
    546          # In every reporting module you can specify a numeric value that
    547          # represents at which position that module should be executed among
    548          # all the available ones. It can be used in the case where a
    549          # module requires another one to be already executed beforehand.
    550          reporting_list = list_plugins(group="reporting")
    551
    552          # Return if no reporting modules are loaded.
    553          if reporting_list:
    554              reporting_list.sort(key=lambda module: module.order)
    555
    556              # Run every loaded reporting module.
    557              for module in reporting_list:
    558                  self.process(module)
    559          else:
    560              log.info("No reporting modules loaded")
    561