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