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-2015 Cuckoo Foundation. 
  2  # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 
  3  # See the file 'docs/LICENSE' for copying permission. 
  4   
  5  import os 
  6  import json 
  7  import pkgutil 
  8  import inspect 
  9  import logging 
 10  from collections import defaultdict 
 11  from distutils.version import StrictVersion 
 12   
 13  from lib.cuckoo.common.abstracts import Auxiliary, Machinery, Processing 
 14  from lib.cuckoo.common.abstracts import Report, Signature 
 15  from lib.cuckoo.common.config import Config 
 16  from lib.cuckoo.common.constants import CUCKOO_ROOT, CUCKOO_VERSION 
 17  from lib.cuckoo.common.exceptions import CuckooCriticalError 
 18  from lib.cuckoo.common.exceptions import CuckooOperationalError 
 19  from lib.cuckoo.common.exceptions import CuckooProcessingError 
 20  from lib.cuckoo.common.exceptions import CuckooReportError 
 21  from lib.cuckoo.common.exceptions import CuckooDependencyError 
 22  from lib.cuckoo.core.database import Database 
 23   
 24  log = logging.getLogger(__name__) 
 25   
 26  _modules = defaultdict(dict) 
 27   
28 -def import_plugin(name):
29 try: 30 module = __import__(name, globals(), locals(), ["dummy"], -1) 31 except ImportError as e: 32 raise CuckooCriticalError("Unable to import plugin " 33 "\"{0}\": {1}".format(name, e)) 34 else: 35 load_plugins(module)
36
37 -def import_package(package):
38 prefix = package.__name__ + "." 39 for loader, name, ispkg in pkgutil.iter_modules(package.__path__, prefix): 40 if ispkg: 41 continue 42 43 import_plugin(name)
44
45 -def load_plugins(module):
46 for name, value in inspect.getmembers(module): 47 if inspect.isclass(value): 48 if issubclass(value, Auxiliary) and value is not Auxiliary: 49 register_plugin("auxiliary", value) 50 elif issubclass(value, Machinery) and value is not Machinery: 51 register_plugin("machinery", value) 52 elif issubclass(value, Processing) and value is not Processing: 53 register_plugin("processing", value) 54 elif issubclass(value, Report) and value is not Report: 55 register_plugin("reporting", value) 56 elif issubclass(value, Signature) and value is not Signature: 57 register_plugin("signatures", value)
58
59 -def register_plugin(group, name):
60 global _modules 61 group = _modules.setdefault(group, []) 62 group.append(name)
63
64 -def list_plugins(group=None):
65 if group: 66 return _modules[group] 67 else: 68 return _modules
69
70 -class RunAuxiliary(object):
71 """Auxiliary modules manager.""" 72
73 - def __init__(self, task, machine):
74 self.task = task 75 self.machine = machine 76 self.cfg = Config("auxiliary") 77 self.enabled = []
78
79 - def start(self):
80 auxiliary_list = list_plugins(group="auxiliary") 81 if auxiliary_list: 82 for module in auxiliary_list: 83 try: 84 current = module() 85 except: 86 log.exception("Failed to load the auxiliary module " 87 "\"{0}\":".format(module)) 88 return 89 90 module_name = inspect.getmodule(current).__name__ 91 if "." in module_name: 92 module_name = module_name.rsplit(".", 1)[1] 93 94 try: 95 options = self.cfg.get(module_name) 96 except CuckooOperationalError: 97 log.debug("Auxiliary module %s not found in " 98 "configuration file", module_name) 99 continue 100 101 if not options.enabled: 102 continue 103 104 current.set_task(self.task) 105 current.set_machine(self.machine) 106 current.set_options(options) 107 108 try: 109 current.start() 110 except NotImplementedError: 111 pass 112 except Exception as e: 113 log.warning("Unable to start auxiliary module %s: %s", 114 module_name, e) 115 else: 116 log.debug("Started auxiliary module: %s", 117 current.__class__.__name__) 118 self.enabled.append(current)
119
120 - def stop(self):
121 for module in self.enabled: 122 try: 123 module.stop() 124 except NotImplementedError: 125 pass 126 except Exception as e: 127 log.warning("Unable to stop auxiliary module: %s", e) 128 else: 129 log.debug("Stopped auxiliary module: %s", 130 module.__class__.__name__)
131
132 -class RunProcessing(object):
133 """Analysis Results Processing Engine. 134 135 This class handles the loading and execution of the processing modules. 136 It executes the enabled ones sequentially and generates a dictionary which 137 is then passed over the reporting engine. 138 """ 139
140 - def __init__(self, task_id):
141 """@param task_id: ID of the analyses to process.""" 142 self.task = Database().view_task(task_id).to_dict() 143 self.analysis_path = os.path.join(CUCKOO_ROOT, "storage", "analyses", str(task_id)) 144 self.cfg = Config("processing")
145
146 - def process(self, module):
147 """Run a processing module. 148 @param module: processing module to run. 149 @param results: results dict. 150 @return: results generated by module. 151 """ 152 # Initialize the specified processing module. 153 try: 154 current = module() 155 except: 156 log.exception("Failed to load the processing module " 157 "\"{0}\":".format(module)) 158 return 159 160 # Extract the module name. 161 module_name = inspect.getmodule(current).__name__ 162 if "." in module_name: 163 module_name = module_name.rsplit(".", 1)[1] 164 165 try: 166 options = self.cfg.get(module_name) 167 except CuckooOperationalError: 168 log.debug("Processing module %s not found in configuration file", 169 module_name) 170 return None 171 172 # If the processing module is disabled in the config, skip it. 173 if not options.enabled: 174 return None 175 176 # Give it path to the analysis results. 177 current.set_path(self.analysis_path) 178 # Give it the analysis task object. 179 current.set_task(self.task) 180 # Give it the options from the relevant processing.conf section. 181 current.set_options(options) 182 183 try: 184 # Run the processing module and retrieve the generated data to be 185 # appended to the general results container. 186 data = current.run() 187 188 log.debug("Executed processing module \"%s\" on analysis at " 189 "\"%s\"", current.__class__.__name__, self.analysis_path) 190 191 # If succeeded, return they module's key name and the data to be 192 # appended to it. 193 return {current.key: data} 194 except CuckooDependencyError as e: 195 log.warning("The processing module \"%s\" has missing dependencies: %s", current.__class__.__name__, e) 196 except CuckooProcessingError as e: 197 log.warning("The processing module \"%s\" returned the following " 198 "error: %s", current.__class__.__name__, e) 199 except: 200 log.exception("Failed to run the processing module \"%s\":", 201 current.__class__.__name__) 202 203 return None
204
205 - def run(self):
206 """Run all processing modules and all signatures. 207 @return: processing results. 208 """ 209 # This is the results container. It's what will be used by all the 210 # reporting modules to make it consumable by humans and machines. 211 # It will contain all the results generated by every processing 212 # module available. Its structure can be observed through the JSON 213 # dump in the analysis' reports folder. (If jsondump is enabled.) 214 # We friendly call this "fat dict". 215 results = {} 216 217 # Order modules using the user-defined sequence number. 218 # If none is specified for the modules, they are selected in 219 # alphabetical order. 220 processing_list = list_plugins(group="processing") 221 222 # If no modules are loaded, return an empty dictionary. 223 if processing_list: 224 processing_list.sort(key=lambda module: module.order) 225 226 # Run every loaded processing module. 227 for module in processing_list: 228 result = self.process(module) 229 # If it provided some results, append it to the big results 230 # container. 231 if result: 232 results.update(result) 233 else: 234 log.info("No processing modules loaded") 235 236 # Return the fat dict. 237 return results
238
239 -class RunSignatures(object):
240 """Run Signatures.""" 241
242 - def __init__(self, results):
243 self.results = results
244
245 - def _load_overlay(self):
246 """Loads overlay data from a json file. 247 See example in data/signature_overlay.json 248 """ 249 filename = os.path.join(CUCKOO_ROOT, "data", "signature_overlay.json") 250 251 try: 252 with open(filename) as fh: 253 odata = json.load(fh) 254 return odata 255 except IOError: 256 pass 257 258 return {}
259
260 - def _apply_overlay(self, signature, overlay):
261 """Applies the overlay attributes to the signature object.""" 262 if signature.name in overlay: 263 attrs = overlay[signature.name] 264 for attr, value in attrs.items(): 265 setattr(signature, attr, value)
266
267 - def _check_signature_version(self, current):
268 """Check signature version. 269 @param current: signature class/instance to check. 270 @return: check result. 271 """ 272 # Since signatures can hardcode some values or checks that might 273 # become obsolete in future versions or that might already be obsolete, 274 # I need to match its requirements with the running version of Cuckoo. 275 version = CUCKOO_VERSION.split("-")[0] 276 277 # If provided, check the minimum working Cuckoo version for this 278 # signature. 279 if current.minimum: 280 try: 281 # If the running Cuckoo is older than the required minimum 282 # version, skip this signature. 283 if StrictVersion(version) < StrictVersion(current.minimum.split("-")[0]): 284 log.debug("You are running an older incompatible version " 285 "of Cuckoo, the signature \"%s\" requires " 286 "minimum version %s", 287 current.name, current.minimum) 288 return None 289 except ValueError: 290 log.debug("Wrong minor version number in signature %s", 291 current.name) 292 return None 293 294 # If provided, check the maximum working Cuckoo version for this 295 # signature. 296 if current.maximum: 297 try: 298 # If the running Cuckoo is newer than the required maximum 299 # version, skip this signature. 300 if StrictVersion(version) > StrictVersion(current.maximum.split("-")[0]): 301 log.debug("You are running a newer incompatible version " 302 "of Cuckoo, the signature \"%s\" requires " 303 "maximum version %s", 304 current.name, current.maximum) 305 return None 306 except ValueError: 307 log.debug("Wrong major version number in signature %s", 308 current.name) 309 return None 310 311 return True
312
313 - def process(self, signature):
314 """Run a signature. 315 @param signature: signature to run. 316 @return: matched signature. 317 """ 318 # Skip signature processing if there are no results. 319 if not self.results: 320 return 321 322 # Initialize the current signature. 323 try: 324 current = signature(self.results) 325 except: 326 log.exception("Failed to load signature " 327 "\"{0}\":".format(signature)) 328 return 329 330 log.debug("Running signature \"%s\"", current.name) 331 332 # If the signature is disabled, skip it. 333 if not current.enabled: 334 return None 335 336 if not self._check_signature_version(current): 337 return None 338 339 try: 340 # Run the signature and if it gets matched, extract key information 341 # from it and append it to the results container. 342 if current.run(): 343 log.debug("Analysis matched signature \"%s\"", current.name) 344 # Return information on the matched signature. 345 return current.as_result() 346 except NotImplementedError: 347 return None 348 except: 349 log.exception("Failed to run signature \"%s\":", current.name) 350 351 return None
352
353 - def run(self):
354 """Run evented signatures.""" 355 # This will contain all the matched signatures. 356 matched = [] 357 358 complete_list = list_plugins(group="signatures") 359 evented_list = [sig(self.results) 360 for sig in complete_list 361 if sig.enabled and sig.evented and 362 self._check_signature_version(sig)] 363 364 overlay = self._load_overlay() 365 log.debug("Applying signature overlays for signatures: %s", ", ".join(overlay.keys())) 366 for signature in complete_list + evented_list: 367 self._apply_overlay(signature, overlay) 368 369 if evented_list: 370 log.debug("Running %u evented signatures", len(evented_list)) 371 for sig in evented_list: 372 if sig == evented_list[-1]: 373 log.debug("\t `-- %s", sig.name) 374 else: 375 log.debug("\t |-- %s", sig.name) 376 377 # Iterate calls and tell interested signatures about them. 378 for proc in self.results["behavior"]["processes"]: 379 for call in proc["calls"]: 380 # Loop through active evented signatures. 381 for sig in evented_list: 382 # Skip current call if it doesn't match the filters (if any). 383 if sig.filter_processnames and not proc["process_name"] in sig.filter_processnames: 384 continue 385 if sig.filter_apinames and not call["api"] in sig.filter_apinames: 386 continue 387 if sig.filter_categories and not call["category"] in sig.filter_categories: 388 continue 389 390 result = None 391 try: 392 result = sig.on_call(call, proc) 393 except NotImplementedError: 394 result = False 395 except: 396 log.exception("Failed to run signature \"%s\":", sig.name) 397 result = False 398 399 # If the signature returns None we can carry on, the 400 # condition was not matched. 401 if result is None: 402 continue 403 404 # On True, the signature is matched. 405 if result is True: 406 log.debug("Analysis matched signature \"%s\"", sig.name) 407 matched.append(sig.as_result()) 408 if sig in complete_list: 409 complete_list.remove(sig) 410 411 # Either True or False, we don't need to check this sig anymore. 412 evented_list.remove(sig) 413 del sig 414 415 # Call the stop method on all remaining instances. 416 for sig in evented_list: 417 try: 418 result = sig.on_complete() 419 except NotImplementedError: 420 continue 421 except: 422 log.exception("Failed run on_complete() method for signature \"%s\":", sig.name) 423 continue 424 else: 425 if result is True: 426 log.debug("Analysis matched signature \"%s\"", sig.name) 427 matched.append(sig.as_result()) 428 if sig in complete_list: 429 complete_list.remove(sig) 430 431 # Link this into the results already at this point, so non-evented signatures can use it 432 self.results["signatures"] = matched 433 434 # Compat loop for old-style (non evented) signatures. 435 if complete_list: 436 complete_list.sort(key=lambda sig: sig.order) 437 log.debug("Running non-evented signatures") 438 439 for signature in complete_list: 440 match = self.process(signature) 441 # If the signature is matched, add it to the list. 442 if match: 443 matched.append(match) 444 445 # Reset the ParseProcessLog instances after each signature 446 if "behavior" in self.results: 447 for process in self.results["behavior"]["processes"]: 448 process["calls"].reset() 449 450 # Sort the matched signatures by their severity level. 451 matched.sort(key=lambda key: key["severity"])
452
453 -class RunReporting:
454 """Reporting Engine. 455 456 This class handles the loading and execution of the enabled reporting 457 modules. It receives the analysis results dictionary from the Processing 458 Engine and pass it over to the reporting modules before executing them. 459 """ 460
461 - def __init__(self, task_id, results):
462 """@param analysis_path: analysis folder path.""" 463 self.task = Database().view_task(task_id).to_dict() 464 self.results = results 465 self.analysis_path = os.path.join(CUCKOO_ROOT, "storage", "analyses", str(task_id)) 466 self.cfg = Config("reporting")
467
468 - def process(self, module):
469 """Run a single reporting module. 470 @param module: reporting module. 471 @param results: results results from analysis. 472 """ 473 # Initialize current reporting module. 474 try: 475 current = module() 476 except: 477 log.exception("Failed to load the reporting module \"{0}\":".format(module)) 478 return 479 480 # Extract the module name. 481 module_name = inspect.getmodule(current).__name__ 482 if "." in module_name: 483 module_name = module_name.rsplit(".", 1)[1] 484 485 try: 486 options = self.cfg.get(module_name) 487 except CuckooOperationalError: 488 log.debug("Reporting module %s not found in configuration file", module_name) 489 return 490 491 # If the reporting module is disabled in the config, skip it. 492 if not options.enabled: 493 return 494 495 # Give it the path to the analysis results folder. 496 current.set_path(self.analysis_path) 497 # Give it the analysis task object. 498 current.set_task(self.task) 499 # Give it the the relevant reporting.conf section. 500 current.set_options(options) 501 # Load the content of the analysis.conf file. 502 current.cfg = Config(cfg=current.conf_path) 503 504 try: 505 current.run(self.results) 506 log.debug("Executed reporting module \"%s\"", current.__class__.__name__) 507 except CuckooDependencyError as e: 508 log.warning("The reporting module \"%s\" has missing dependencies: %s", current.__class__.__name__, e) 509 except CuckooReportError as e: 510 log.warning("The reporting module \"%s\" returned the following error: %s", current.__class__.__name__, e) 511 except: 512 log.exception("Failed to run the reporting module \"%s\":", current.__class__.__name__)
513
514 - def run(self):
515 """Generates all reports. 516 @raise CuckooReportError: if a report module fails. 517 """ 518 # In every reporting module you can specify a numeric value that 519 # represents at which position that module should be executed among 520 # all the available ones. It can be used in the case where a 521 # module requires another one to be already executed beforehand. 522 reporting_list = list_plugins(group="reporting") 523 524 # Return if no reporting modules are loaded. 525 if reporting_list: 526 reporting_list.sort(key=lambda module: module.order) 527 528 # Run every loaded reporting module. 529 for module in reporting_list: 530 self.process(module) 531 else: 532 log.info("No reporting modules loaded")
533