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

Source Code for Module lib.cuckoo.core.scheduler

  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 time 
  7  import shutil 
  8  import logging 
  9  import Queue 
 10  from threading import Thread, Lock 
 11   
 12  from lib.cuckoo.common.config import Config 
 13  from lib.cuckoo.common.constants import CUCKOO_ROOT 
 14  from lib.cuckoo.common.exceptions import CuckooMachineError, CuckooGuestError 
 15  from lib.cuckoo.common.exceptions import CuckooOperationalError 
 16  from lib.cuckoo.common.exceptions import CuckooCriticalError 
 17  from lib.cuckoo.common.objects import File 
 18  from lib.cuckoo.common.utils import create_folder 
 19  from lib.cuckoo.core.database import Database, TASK_COMPLETED, TASK_REPORTED 
 20  from lib.cuckoo.core.guest import GuestManager 
 21  from lib.cuckoo.core.plugins import list_plugins, RunAuxiliary, RunProcessing 
 22  from lib.cuckoo.core.plugins import RunSignatures, RunReporting 
 23  from lib.cuckoo.core.resultserver import ResultServer 
 24   
 25  log = logging.getLogger(__name__) 
 26   
 27  machinery = None 
 28  machine_lock = Lock() 
 29  latest_symlink_lock = Lock() 
 30   
 31  active_analysis_count = 0 
 32   
 33   
34 -class CuckooDeadMachine(Exception):
35 """Exception thrown when a machine turns dead. 36 37 When this exception has been thrown, the analysis task will start again, 38 and will try to use another machine, when available. 39 """ 40 pass
41 42
43 -class AnalysisManager(Thread):
44 """Analysis Manager. 45 46 This class handles the full analysis process for a given task. It takes 47 care of selecting the analysis machine, preparing the configuration and 48 interacting with the guest agent and analyzer components to launch and 49 complete the analysis and store, process and report its results. 50 """ 51
52 - def __init__(self, task, error_queue):
53 """@param task: task object containing the details for the analysis.""" 54 Thread.__init__(self) 55 Thread.daemon = True 56 57 self.task = task 58 self.errors = error_queue 59 self.cfg = Config() 60 self.storage = "" 61 self.binary = "" 62 self.machine = None
63
64 - def init_storage(self):
65 """Initialize analysis storage folder.""" 66 self.storage = os.path.join(CUCKOO_ROOT, 67 "storage", 68 "analyses", 69 str(self.task.id)) 70 71 # If the analysis storage folder already exists, we need to abort the 72 # analysis or previous results will be overwritten and lost. 73 if os.path.exists(self.storage): 74 log.error("Analysis results folder already exists at path \"%s\"," 75 " analysis aborted", self.storage) 76 return False 77 78 # If we're not able to create the analysis storage folder, we have to 79 # abort the analysis. 80 try: 81 create_folder(folder=self.storage) 82 except CuckooOperationalError: 83 log.error("Unable to create analysis folder %s", self.storage) 84 return False 85 86 return True
87
88 - def check_file(self):
89 """Checks the integrity of the file to be analyzed.""" 90 sample = Database().view_sample(self.task.sample_id) 91 92 sha256 = File(self.task.target).get_sha256() 93 if sha256 != sample.sha256: 94 log.error("Target file has been modified after submission: \"%s\"", self.task.target) 95 return False 96 97 return True
98
99 - def store_file(self):
100 """Store a copy of the file being analyzed.""" 101 if not os.path.exists(self.task.target): 102 log.error("The file to analyze does not exist at path \"%s\", " 103 "analysis aborted", self.task.target) 104 return False 105 106 sha256 = File(self.task.target).get_sha256() 107 self.binary = os.path.join(CUCKOO_ROOT, "storage", "binaries", sha256) 108 109 if os.path.exists(self.binary): 110 log.info("File already exists at \"%s\"", self.binary) 111 else: 112 # TODO: do we really need to abort the analysis in case we are not 113 # able to store a copy of the file? 114 try: 115 shutil.copy(self.task.target, self.binary) 116 except (IOError, shutil.Error) as e: 117 log.error("Unable to store file from \"%s\" to \"%s\", " 118 "analysis aborted", self.task.target, self.binary) 119 return False 120 121 try: 122 new_binary_path = os.path.join(self.storage, "binary") 123 124 if hasattr(os, "symlink"): 125 os.symlink(self.binary, new_binary_path) 126 else: 127 shutil.copy(self.binary, new_binary_path) 128 except (AttributeError, OSError) as e: 129 log.error("Unable to create symlink/copy from \"%s\" to " 130 "\"%s\": %s", self.binary, self.storage, e) 131 132 return True
133
134 - def acquire_machine(self):
135 """Acquire an analysis machine from the pool of available ones.""" 136 machine = None 137 138 # Start a loop to acquire the a machine to run the analysis on. 139 while True: 140 machine_lock.acquire() 141 142 # In some cases it's possible that we enter this loop without 143 # having any available machines. We should make sure this is not 144 # such case, or the analysis task will fail completely. 145 if not machinery.availables(): 146 machine_lock.release() 147 time.sleep(1) 148 continue 149 150 # If the user specified a specific machine ID, a platform to be 151 # used or machine tags acquire the machine accordingly. 152 try: 153 machine = machinery.acquire(machine_id=self.task.machine, 154 platform=self.task.platform, 155 tags=self.task.tags) 156 finally: 157 machine_lock.release() 158 159 # If no machine is available at this moment, wait for one second 160 # and try again. 161 if not machine: 162 log.debug("Task #%d: no machine available yet", self.task.id) 163 time.sleep(1) 164 else: 165 log.info("Task #%d: acquired machine %s (label=%s)", 166 self.task.id, machine.name, machine.label) 167 break 168 169 self.machine = machine
170
171 - def build_options(self):
172 """Generate analysis options. 173 @return: options dict. 174 """ 175 options = {} 176 177 options["id"] = self.task.id 178 options["ip"] = self.machine.resultserver_ip 179 options["port"] = self.machine.resultserver_port 180 options["category"] = self.task.category 181 options["target"] = self.task.target 182 options["package"] = self.task.package 183 options["options"] = self.task.options 184 options["enforce_timeout"] = self.task.enforce_timeout 185 options["clock"] = self.task.clock 186 options["terminate_processes"] = self.cfg.cuckoo.terminate_processes 187 188 if not self.task.timeout or self.task.timeout == 0: 189 options["timeout"] = self.cfg.timeouts.default 190 else: 191 options["timeout"] = self.task.timeout 192 193 if self.task.category == "file": 194 options["file_name"] = File(self.task.target).get_name() 195 options["file_type"] = File(self.task.target).get_type() 196 197 return options
198
199 - def launch_analysis(self):
200 """Start analysis.""" 201 succeeded = False 202 dead_machine = False 203 204 log.info("Starting analysis of %s \"%s\" (task=%d)", 205 self.task.category.upper(), self.task.target, self.task.id) 206 207 # Initialize the analysis folders. 208 if not self.init_storage(): 209 return False 210 211 if self.task.category == "file": 212 # Check whether the file has been changed for some unknown reason. 213 # And fail this analysis if it has been modified. 214 if not self.check_file(): 215 return False 216 217 # Store a copy of the original file. 218 if not self.store_file(): 219 return False 220 221 # Acquire analysis machine. 222 try: 223 self.acquire_machine() 224 except CuckooOperationalError as e: 225 log.error("Cannot acquire machine: {0}".format(e)) 226 return False 227 228 # Generate the analysis configuration file. 229 options = self.build_options() 230 231 # At this point we can tell the ResultServer about it. 232 try: 233 ResultServer().add_task(self.task, self.machine) 234 except Exception as e: 235 machinery.release(self.machine.label) 236 self.errors.put(e) 237 238 aux = RunAuxiliary(task=self.task, machine=self.machine) 239 aux.start() 240 241 try: 242 # Mark the selected analysis machine in the database as started. 243 guest_log = Database().guest_start(self.task.id, 244 self.machine.name, 245 self.machine.label, 246 machinery.__class__.__name__) 247 # Start the machine. 248 machinery.start(self.machine.label) 249 250 # Initialize the guest manager. 251 guest = GuestManager(self.machine.name, self.machine.ip, 252 self.machine.platform) 253 254 # Start the analysis. 255 guest.start_analysis(options) 256 257 guest.wait_for_completion() 258 succeeded = True 259 except CuckooMachineError as e: 260 log.error(str(e), extra={"task_id": self.task.id}) 261 dead_machine = True 262 except CuckooGuestError as e: 263 log.error(str(e), extra={"task_id": self.task.id}) 264 finally: 265 # Stop Auxiliary modules. 266 aux.stop() 267 268 # Take a memory dump of the machine before shutting it off. 269 if self.cfg.cuckoo.memory_dump or self.task.memory: 270 try: 271 dump_path = os.path.join(self.storage, "memory.dmp") 272 machinery.dump_memory(self.machine.label, dump_path) 273 except NotImplementedError: 274 log.error("The memory dump functionality is not available " 275 "for the current machine manager.") 276 except CuckooMachineError as e: 277 log.error(e) 278 279 try: 280 # Stop the analysis machine. 281 machinery.stop(self.machine.label) 282 except CuckooMachineError as e: 283 log.warning("Unable to stop machine %s: %s", 284 self.machine.label, e) 285 286 # Mark the machine in the database as stopped. Unless this machine 287 # has been marked as dead, we just keep it as "started" in the 288 # database so it'll not be used later on in this session. 289 Database().guest_stop(guest_log) 290 291 # After all this, we can make the ResultServer forget about the 292 # internal state for this analysis task. 293 ResultServer().del_task(self.task, self.machine) 294 295 if dead_machine: 296 # Remove the guest from the database, so that we can assign a 297 # new guest when the task is being analyzed with another 298 # machine. 299 Database().guest_remove(guest_log) 300 301 # Remove the analysis directory that has been created so 302 # far, as launch_analysis() is going to be doing that again. 303 shutil.rmtree(self.storage) 304 305 # This machine has turned dead, so we throw an exception here 306 # which informs the AnalysisManager that it should analyze 307 # this task again with another available machine. 308 raise CuckooDeadMachine() 309 310 try: 311 # Release the analysis machine. But only if the machine has 312 # not turned dead yet. 313 machinery.release(self.machine.label) 314 except CuckooMachineError as e: 315 log.error("Unable to release machine %s, reason %s. " 316 "You might need to restore it manually.", 317 self.machine.label, e) 318 319 return succeeded
320
321 - def process_results(self):
322 """Process the analysis results and generate the enabled reports.""" 323 results = RunProcessing(task_id=self.task.id).run() 324 RunSignatures(results=results).run() 325 RunReporting(task_id=self.task.id, results=results).run() 326 327 # If the target is a file and the user enabled the option, 328 # delete the original copy. 329 if self.task.category == "file" and self.cfg.cuckoo.delete_original: 330 if not os.path.exists(self.task.target): 331 log.warning("Original file does not exist anymore: \"%s\": " 332 "File not found.", self.task.target) 333 else: 334 try: 335 os.remove(self.task.target) 336 except OSError as e: 337 log.error("Unable to delete original file at path " 338 "\"%s\": %s", self.task.target, e) 339 340 # If the target is a file and the user enabled the delete copy of 341 # the binary option, then delete the copy. 342 if self.task.category == "file" and self.cfg.cuckoo.delete_bin_copy: 343 if not os.path.exists(self.binary): 344 log.warning("Copy of the original file does not exist anymore: \"%s\": File not found", self.binary) 345 else: 346 try: 347 os.remove(self.binary) 348 except OSError as e: 349 log.error("Unable to delete the copy of the original file at path \"%s\": %s", self.binary, e) 350 351 log.info("Task #%d: reports generation completed (path=%s)", 352 self.task.id, self.storage) 353 354 return True
355
356 - def run(self):
357 """Run manager thread.""" 358 global active_analysis_count 359 active_analysis_count += 1 360 try: 361 while True: 362 try: 363 success = self.launch_analysis() 364 except CuckooDeadMachine: 365 continue 366 367 break 368 369 Database().set_status(self.task.id, TASK_COMPLETED) 370 371 log.debug("Released database task #%d with status %s", 372 self.task.id, success) 373 374 if self.cfg.cuckoo.process_results: 375 self.process_results() 376 Database().set_status(self.task.id, TASK_REPORTED) 377 378 # We make a symbolic link ("latest") which links to the latest 379 # analysis - this is useful for debugging purposes. This is only 380 # supported under systems that support symbolic links. 381 if hasattr(os, "symlink"): 382 latest = os.path.join(CUCKOO_ROOT, "storage", 383 "analyses", "latest") 384 385 # First we have to remove the existing symbolic link, then we 386 # have to create the new one. 387 # Deal with race conditions using a lock. 388 latest_symlink_lock.acquire() 389 try: 390 if os.path.exists(latest): 391 os.remove(latest) 392 393 os.symlink(self.storage, latest) 394 except OSError as e: 395 log.warning("Error pointing latest analysis symlink: %s" % e) 396 finally: 397 latest_symlink_lock.release() 398 399 log.info("Task #%d: analysis procedure completed", self.task.id) 400 except: 401 log.exception("Failure in AnalysisManager.run") 402 403 active_analysis_count -= 1
404
405 -class Scheduler:
406 """Tasks Scheduler. 407 408 This class is responsible for the main execution loop of the tool. It 409 prepares the analysis machines and keep waiting and loading for new 410 analysis tasks. 411 Whenever a new task is available, it launches AnalysisManager which will 412 take care of running the full analysis process and operating with the 413 assigned analysis machine. 414 """
415 - def __init__(self, maxcount=None):
416 self.running = True 417 self.cfg = Config() 418 self.db = Database() 419 self.maxcount = maxcount 420 self.total_analysis_count = 0
421
422 - def initialize(self):
423 """Initialize the machine manager.""" 424 global machinery 425 426 machinery_name = self.cfg.cuckoo.machinery 427 428 log.info("Using \"%s\" machine manager", machinery_name) 429 430 # Get registered class name. Only one machine manager is imported, 431 # therefore there should be only one class in the list. 432 plugin = list_plugins("machinery")[0] 433 # Initialize the machine manager. 434 machinery = plugin() 435 436 # Find its configuration file. 437 conf = os.path.join(CUCKOO_ROOT, "conf", "%s.conf" % machinery_name) 438 439 if not os.path.exists(conf): 440 raise CuckooCriticalError("The configuration file for machine " 441 "manager \"{0}\" does not exist at path:" 442 " {1}".format(machinery_name, conf)) 443 444 # Provide a dictionary with the configuration options to the 445 # machine manager instance. 446 machinery.set_options(Config(machinery_name)) 447 448 # Initialize the machine manager. 449 try: 450 machinery.initialize(machinery_name) 451 except CuckooMachineError as e: 452 raise CuckooCriticalError("Error initializing machines: %s" % e) 453 454 # At this point all the available machines should have been identified 455 # and added to the list. If none were found, Cuckoo needs to abort the 456 # execution. 457 if not len(machinery.machines()): 458 raise CuckooCriticalError("No machines available.") 459 else: 460 log.info("Loaded %s machine/s", len(machinery.machines())) 461 462 if len(machinery.machines()) > 1 and self.db.engine.name == "sqlite": 463 log.warning("As you've configured Cuckoo to execute parallel " 464 "analyses, we recommend you to switch to a MySQL " 465 "a PostgreSQL database as SQLite might cause some " 466 "issues.") 467 468 if len(machinery.machines()) > 4 and self.cfg.cuckoo.process_results: 469 log.warning("When running many virtual machines it is recommended " 470 "to process the results in a separate process.py to " 471 "increase throughput and stability. Please read the " 472 "documentation about the `Processing Utility`.")
473
474 - def stop(self):
475 """Stop scheduler.""" 476 self.running = False 477 # Shutdown machine manager (used to kill machines that still alive). 478 machinery.shutdown()
479
480 - def start(self):
481 """Start scheduler.""" 482 self.initialize() 483 484 log.info("Waiting for analysis tasks.") 485 486 # Message queue with threads to transmit exceptions (used as IPC). 487 errors = Queue.Queue() 488 489 # Command-line overrides the configuration file. 490 if self.maxcount is None: 491 self.maxcount = self.cfg.cuckoo.max_analysis_count 492 493 # This loop runs forever. 494 while self.running: 495 time.sleep(1) 496 497 # If not enough free disk space is available, then we print an 498 # error message and wait another round (this check is ignored 499 # when the freespace configuration variable is set to zero). 500 if self.cfg.cuckoo.freespace: 501 # Resolve the full base path to the analysis folder, just in 502 # case somebody decides to make a symbolic link out of it. 503 dir_path = os.path.join(CUCKOO_ROOT, "storage", "analyses") 504 505 # TODO: Windows support 506 if hasattr(os, "statvfs"): 507 dir_stats = os.statvfs(dir_path) 508 509 # Calculate the free disk space in megabytes. 510 space_available = dir_stats.f_bavail * dir_stats.f_frsize 511 space_available /= 1024 * 1024 512 513 if space_available < self.cfg.cuckoo.freespace: 514 log.error("Not enough free disk space! (Only %d MB!)", 515 space_available) 516 continue 517 518 # Have we limited the number of concurrently executing machines? 519 if self.cfg.cuckoo.max_machines_count > 0: 520 # Are too many running? 521 if len(machinery.running()) >= self.cfg.cuckoo.max_machines_count: 522 continue 523 524 # If no machines are available, it's pointless to fetch for 525 # pending tasks. Loop over. 526 if not machinery.availables(): 527 continue 528 529 # Exits if max_analysis_count is defined in the configuration 530 # file and has been reached. 531 if self.maxcount and self.total_analysis_count >= self.maxcount: 532 if active_analysis_count <= 0: 533 self.stop() 534 else: 535 # Fetch a pending analysis task. 536 #TODO: this fixes only submissions by --machine, need to add other attributes (tags etc.) 537 for machine in self.db.get_available_machines(): 538 539 task = self.db.fetch(machine=machine.name) 540 if task: 541 log.debug("Processing task #%s", task.id) 542 self.total_analysis_count += 1 543 544 # Initialize and start the analysis manager. 545 analysis = AnalysisManager(task, errors) 546 analysis.start() 547 548 # Deal with errors. 549 try: 550 raise errors.get(block=False) 551 except Queue.Empty: 552 pass
553