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