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