Module analyzer
[hide private]
[frames] | no frames]

Source Code for Module analyzer

  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 sys 
  7  import socket 
  8  import struct 
  9  import random 
 10  import pkgutil 
 11  import logging 
 12  import hashlib 
 13  import xmlrpclib 
 14  import traceback 
 15  from ctypes import create_unicode_buffer, create_string_buffer 
 16  from ctypes import c_wchar_p, byref, c_int, sizeof 
 17  from threading import Lock, Thread 
 18  from datetime import datetime 
 19   
 20  from lib.api.process import Process 
 21  from lib.common.abstracts import Package, Auxiliary 
 22  from lib.common.constants import PATHS, PIPE 
 23  from lib.common.defines import KERNEL32 
 24  from lib.common.defines import ERROR_MORE_DATA, ERROR_PIPE_CONNECTED 
 25  from lib.common.defines import PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE 
 26  from lib.common.defines import PIPE_READMODE_MESSAGE, PIPE_WAIT 
 27  from lib.common.defines import PIPE_UNLIMITED_INSTANCES, INVALID_HANDLE_VALUE 
 28  from lib.common.exceptions import CuckooError, CuckooPackageError 
 29  from lib.common.results import upload_to_host 
 30  from lib.core.config import Config 
 31  from lib.core.packages import choose_package 
 32  from lib.core.privileges import grant_debug_privilege 
 33  from lib.core.startup import create_folders, init_logging 
 34  from modules import auxiliary 
 35   
 36  log = logging.getLogger() 
 37   
 38  BUFSIZE = 512 
 39  FILES_LIST = [] 
 40  DUMPED_LIST = [] 
 41  PROCESS_LIST = [] 
 42  PROCESS_LOCK = Lock() 
 43  DEFAULT_DLL = None 
 44   
 45  PID = os.getpid() 
 46  PPID = Process(pid=PID).get_parent_pid() 
 47   
 48  # this is still preparation status - needs finalizing 
49 -def protected_filename(fname):
50 """Checks file name against some protected names.""" 51 if not fname: 52 return False 53 54 protected_names = [] 55 for name in protected_names: 56 if name in fname: 57 return True 58 59 return False
60
61 -def add_pid(pid):
62 """Add a process to process list.""" 63 if type(pid) == long or type(pid) == int or type(pid) == str: 64 log.info("Added new process to list with pid: %s", pid) 65 PROCESS_LIST.append(pid)
66
67 -def add_pids(pids):
68 """Add PID.""" 69 if type(pids) == list: 70 for pid in pids: 71 add_pid(pid) 72 else: 73 add_pid(pids)
74
75 -def add_file(file_path):
76 """Add a file to file list.""" 77 if file_path not in FILES_LIST: 78 log.info("Added new file to list with path: %s", 79 unicode(file_path).encode("utf-8", "replace")) 80 FILES_LIST.append(file_path)
81
82 -def dump_file(file_path):
83 """Create a copy of the give file path.""" 84 try: 85 if os.path.exists(file_path): 86 sha256 = hashlib.sha256(open(file_path, "rb").read()).hexdigest() 87 if sha256 in DUMPED_LIST: 88 # The file was already dumped, just skip. 89 return 90 else: 91 log.warning("File at path \"%s\" does not exist, skip", file_path) 92 return 93 except IOError as e: 94 log.warning("Unable to access file at path \"%s\": %s", file_path, e) 95 return 96 97 # 32k is the maximum length for a filename 98 path = create_unicode_buffer(32 * 1024) 99 name = c_wchar_p() 100 KERNEL32.GetFullPathNameW(file_path, 32 * 1024, path, byref(name)) 101 file_path = path.value 102 103 # Check if the path has a valid file name, otherwise it's a directory 104 # and we should abort the dump. 105 if name.value: 106 # Should be able to extract Alternate Data Streams names too. 107 file_name = name.value[name.value.find(":")+1:] 108 else: 109 return 110 111 upload_path = os.path.join("files", 112 str(random.randint(100000000, 9999999999)), 113 file_name) 114 try: 115 upload_to_host(file_path, upload_path) 116 DUMPED_LIST.append(sha256) 117 except (IOError, socket.error) as e: 118 log.error("Unable to upload dropped file at path \"%s\": %s", 119 file_path, e)
120 121
122 -def del_file(fname):
123 dump_file(fname) 124 125 # Filenames are case-insenstive in windows. 126 fnames = [x.lower() for x in FILES_LIST] 127 128 # If this filename exists in the FILES_LIST, then delete it, because it 129 # doesn't exist anymore anyway. 130 if fname.lower() in fnames: 131 FILES_LIST.pop(fnames.index(fname.lower()))
132
133 -def move_file(old_fname, new_fname):
134 # Filenames are case-insenstive in windows. 135 fnames = [x.lower() for x in FILES_LIST] 136 137 # Check whether the old filename is in the FILES_LIST 138 if old_fname.lower() in fnames: 139 140 # Get the index of the old filename 141 idx = fnames.index(old_fname.lower()) 142 143 # Replace the old filename by the new filename 144 FILES_LIST[idx] = new_fname
145
146 -def dump_files():
147 """Dump all the dropped files.""" 148 for file_path in FILES_LIST: 149 dump_file(file_path)
150
151 -class PipeHandler(Thread):
152 """Pipe Handler. 153 154 This class handles the notifications received through the Pipe Server and 155 decides what to do with them. 156 """ 157
158 - def __init__(self, h_pipe):
159 """@param h_pipe: PIPE to read.""" 160 Thread.__init__(self) 161 self.h_pipe = h_pipe
162
163 - def run(self):
164 """Run handler. 165 @return: operation status. 166 """ 167 data = "" 168 response = "OK" 169 wait = False 170 proc = None 171 172 # Read the data submitted to the Pipe Server. 173 while True: 174 bytes_read = c_int(0) 175 176 buf = create_string_buffer(BUFSIZE) 177 success = KERNEL32.ReadFile(self.h_pipe, 178 buf, 179 sizeof(buf), 180 byref(bytes_read), 181 None) 182 183 data += buf.value 184 185 if not success and KERNEL32.GetLastError() == ERROR_MORE_DATA: 186 continue 187 #elif not success or bytes_read.value == 0: 188 # if KERNEL32.GetLastError() == ERROR_BROKEN_PIPE: 189 # pass 190 191 break 192 193 if data: 194 command = data.strip() 195 196 # Parse the prefix for the received notification. 197 # In case of GETPIDS we're gonna return the current process ID 198 # and the process ID of our parent process (agent.py). 199 if command == "GETPIDS": 200 response = struct.pack("II", PID, PPID) 201 202 # When analyzing we don't want to hook all functions, as we're 203 # having some stability issues with regards to webbrowsers. 204 elif command == "HOOKDLLS": 205 is_url = Config(cfg="analysis.conf").category != "file" 206 207 url_dlls = "ntdll", "kernel32" 208 209 def hookdll_encode(names): 210 # We have to encode each dll name as unicode string 211 # with length 16. 212 names = [name + "\x00" * (16-len(name)) for name in names] 213 f = lambda s: "".join(ch + "\x00" for ch in s) 214 return "".join(f(name) for name in names)
215 216 # If this sample is not a URL, then we don't want to limit 217 # any API hooks (at least for now), so we write a null-byte 218 # which indicates that all DLLs should be hooked. 219 if not is_url: 220 response = "\x00" 221 else: 222 response = hookdll_encode(url_dlls) 223 224 # In case of PID, the client is trying to notify the creation of 225 # a new process to be injected and monitored. 226 elif command.startswith("PROCESS:"): 227 # We acquire the process lock in order to prevent the analyzer 228 # to terminate the analysis while we are operating on the new 229 # process. 230 PROCESS_LOCK.acquire() 231 232 # Set the current DLL to the default one provided 233 # at submission. 234 dll = DEFAULT_DLL 235 236 # We parse the process ID. 237 data = command[8:] 238 process_id = thread_id = None 239 if not "," in data: 240 if data.isdigit(): 241 process_id = int(data) 242 elif len(data.split(",")) == 2: 243 process_id, param = data.split(",") 244 thread_id = None 245 if process_id.isdigit(): 246 process_id = int(process_id) 247 else: 248 process_id = None 249 250 if param.isdigit(): 251 thread_id = int(param) 252 else: 253 # XXX: Expect a new DLL as a message parameter? 254 if isinstance(param, str): 255 dll = param 256 257 if process_id: 258 if process_id not in (PID, PPID): 259 # We inject the process only if it's not being 260 # monitored already, otherwise we would generated 261 # polluted logs. 262 if process_id not in PROCESS_LIST: 263 # Open the process and inject the DLL. 264 # Hope it enjoys it. 265 proc = Process(pid=process_id, 266 thread_id=thread_id) 267 268 filepath = proc.get_filepath() 269 filename = os.path.basename(filepath) 270 271 log.info("Announced process name: %s", filename) 272 273 if not protected_filename(filename): 274 # Add the new process ID to the list of 275 # monitored processes. 276 add_pids(process_id) 277 278 # If we have both pid and tid, then we can use 279 # apc to inject 280 if process_id and thread_id: 281 proc.inject(dll, apc=True) 282 else: 283 # we inject using CreateRemoteThread, this 284 # needs the waiting in order to make sure 285 # no race conditions occur 286 proc.inject(dll) 287 wait = True 288 289 log.info("Successfully injected process with " 290 "pid %s", proc.pid) 291 else: 292 log.warning("Received request to inject Cuckoo " 293 "processes, skip") 294 295 # Once we're done operating on the processes list, we release 296 # the lock. 297 PROCESS_LOCK.release() 298 # In case of FILE_NEW, the client is trying to notify the creation 299 # of a new file. 300 elif command.startswith("FILE_NEW:"): 301 # We extract the file path. 302 file_path = command[9:].decode("utf-8") 303 # We add the file to the list. 304 add_file(file_path) 305 # In case of FILE_DEL, the client is trying to notify an ongoing 306 # deletion of an existing file, therefore we need to dump it 307 # straight away. 308 elif command.startswith("FILE_DEL:"): 309 # Extract the file path. 310 file_path = command[9:].decode("utf-8") 311 # Dump the file straight away. 312 del_file(file_path) 313 elif command.startswith("FILE_MOVE:"): 314 # syntax = FILE_MOVE:old_file_path::new_file_path 315 if "::" in command[10:]: 316 old_fname, new_fname = command[10:].split("::", 1) 317 move_file(old_fname.decode("utf-8"), 318 new_fname.decode("utf-8")) 319 320 KERNEL32.WriteFile(self.h_pipe, 321 create_string_buffer(response), 322 len(response), 323 byref(bytes_read), 324 None) 325 326 KERNEL32.CloseHandle(self.h_pipe) 327 328 # We wait until cuckoomon reports back. 329 if wait: 330 proc.wait() 331 332 if proc: 333 proc.close() 334 335 return True
336
337 -class PipeServer(Thread):
338 """Cuckoo PIPE server. 339 340 This Pipe Server receives notifications from the injected processes for 341 new processes being spawned and for files being created or deleted. 342 """ 343
344 - def __init__(self, pipe_name=PIPE):
345 """@param pipe_name: Cuckoo PIPE server name.""" 346 Thread.__init__(self) 347 self.pipe_name = pipe_name 348 self.do_run = True
349
350 - def stop(self):
351 """Stop PIPE server.""" 352 self.do_run = False
353
354 - def run(self):
355 """Create and run PIPE server. 356 @return: operation status. 357 """ 358 while self.do_run: 359 # Create the Named Pipe. 360 h_pipe = KERNEL32.CreateNamedPipeA(self.pipe_name, 361 PIPE_ACCESS_DUPLEX, 362 PIPE_TYPE_MESSAGE | 363 PIPE_READMODE_MESSAGE | 364 PIPE_WAIT, 365 PIPE_UNLIMITED_INSTANCES, 366 BUFSIZE, 367 BUFSIZE, 368 0, 369 None) 370 371 if h_pipe == INVALID_HANDLE_VALUE: 372 return False 373 374 # If we receive a connection to the pipe, we invoke the handler. 375 if KERNEL32.ConnectNamedPipe(h_pipe, None) or KERNEL32.GetLastError() == ERROR_PIPE_CONNECTED: 376 handler = PipeHandler(h_pipe) 377 handler.daemon = True 378 handler.start() 379 else: 380 KERNEL32.CloseHandle(h_pipe) 381 382 return True
383
384 -class Analyzer:
385 """Cuckoo Windows Analyzer. 386 387 This class handles the initialization and execution of the analysis 388 procedure, including handling of the pipe server, the auxiliary modules and 389 the analysis packages. 390 """ 391 PIPE_SERVER_COUNT = 4 392
393 - def __init__(self):
394 self.pipes = [None]*self.PIPE_SERVER_COUNT 395 self.config = None 396 self.target = None
397
398 - def prepare(self):
399 """Prepare env for analysis.""" 400 global DEFAULT_DLL 401 402 # Get SeDebugPrivilege for the Python process. It will be needed in 403 # order to perform the injections. 404 grant_debug_privilege() 405 406 # Create the folders used for storing the results. 407 create_folders() 408 409 # Initialize logging. 410 init_logging() 411 412 # Parse the analysis configuration file generated by the agent. 413 self.config = Config(cfg="analysis.conf") 414 415 # Set virtual machine clock. 416 clock = datetime.strptime(self.config.clock, "%Y%m%dT%H:%M:%S") 417 # Setting date and time. 418 # NOTE: Windows system has only localized commands with date format 419 # following localization settings, so these commands for english date 420 # format cannot work in other localizations. 421 # In addition DATE and TIME commands are blocking if an incorrect 422 # syntax is provided, so an echo trick is used to bypass the input 423 # request and not block analysis. 424 os.system("echo:|date {0}".format(clock.strftime("%m-%d-%y"))) 425 os.system("echo:|time {0}".format(clock.strftime("%H:%M:%S"))) 426 427 # Set the default DLL to be used by the PipeHandler. 428 DEFAULT_DLL = self.get_options().get("dll", None) 429 430 # Initialize and start the Pipe Servers. This is going to be used for 431 # communicating with the injected and monitored processes. 432 for x in xrange(self.PIPE_SERVER_COUNT): 433 self.pipes[x] = PipeServer() 434 self.pipes[x].daemon = True 435 self.pipes[x].start() 436 437 # We update the target according to its category. If it's a file, then 438 # we store the path. 439 if self.config.category == "file": 440 self.target = os.path.join(os.environ["TEMP"] + os.sep, 441 str(self.config.file_name)) 442 # If it's a URL, well.. we store the URL. 443 else: 444 self.target = self.config.target
445
446 - def get_options(self):
447 """Get analysis options. 448 @return: options dict. 449 """ 450 # The analysis package can be provided with some options in the 451 # following format: 452 # option1=value1,option2=value2,option3=value3 453 # 454 # Here we parse such options and provide a dictionary that will be made 455 # accessible to the analysis package. 456 options = {} 457 if self.config.options: 458 try: 459 # Split the options by comma. 460 fields = self.config.options.strip().split(",") 461 except ValueError as e: 462 log.warning("Failed parsing the options: %s", e) 463 else: 464 for field in fields: 465 # Split the name and the value of the option. 466 try: 467 key, value = field.strip().split("=") 468 except ValueError as e: 469 log.warning("Failed parsing option (%s): %s", field, e) 470 else: 471 # If the parsing went good, we add the option to the 472 # dictionary. 473 options[key.strip()] = value.strip() 474 475 return options
476
477 - def complete(self):
478 """End analysis.""" 479 # Stop the Pipe Servers. 480 for x in xrange(self.PIPE_SERVER_COUNT): 481 self.pipes[x].stop() 482 # Dump all the notified files. 483 dump_files() 484 # Hell yeah. 485 log.info("Analysis completed")
486
487 - def run(self):
488 """Run analysis. 489 @return: operation status. 490 """ 491 self.prepare() 492 493 log.info("Starting analyzer from: %s", os.getcwd()) 494 log.info("Storing results at: %s", PATHS["root"]) 495 log.info("Pipe server name: %s", PIPE) 496 497 # If no analysis package was specified at submission, we try to select 498 # one automatically. 499 if not self.config.package: 500 log.info("No analysis package specified, trying to detect " 501 "it automagically") 502 # If the analysis target is a file, we choose the package according 503 # to the file format. 504 if self.config.category == "file": 505 package = choose_package(self.config.file_type, self.config.file_name) 506 # If it's an URL, we'll just use the default Internet Explorer 507 # package. 508 else: 509 package = "ie" 510 511 # If we weren't able to automatically determine the proper package, 512 # we need to abort the analysis. 513 if not package: 514 raise CuckooError("No valid package available for file " 515 "type: {0}".format(self.config.file_type)) 516 517 log.info("Automatically selected analysis package \"%s\"", package) 518 # Otherwise just select the specified package. 519 else: 520 package = self.config.package 521 522 # Generate the package path. 523 package_name = "modules.packages.%s" % package 524 525 # Try to import the analysis package. 526 try: 527 __import__(package_name, globals(), locals(), ["dummy"], -1) 528 # If it fails, we need to abort the analysis. 529 except ImportError: 530 raise CuckooError("Unable to import package \"{0}\", does " 531 "not exist.".format(package_name)) 532 533 # Initialize the package parent abstract. 534 Package() 535 536 # Enumerate the abstract's subclasses. 537 try: 538 package_class = Package.__subclasses__()[0] 539 except IndexError as e: 540 raise CuckooError("Unable to select package class " 541 "(package={0}): {1}".format(package_name, e)) 542 543 # Initialize the analysis package. 544 pack = package_class(self.get_options()) 545 546 # Initialize Auxiliary modules 547 Auxiliary() 548 prefix = auxiliary.__name__ + "." 549 for loader, name, ispkg in pkgutil.iter_modules(auxiliary.__path__, prefix): 550 if ispkg: 551 continue 552 553 # Import the auxiliary module. 554 try: 555 __import__(name, globals(), locals(), ["dummy"], -1) 556 except ImportError as e: 557 log.warning("Unable to import the auxiliary module " 558 "\"%s\": %s", name, e) 559 560 # Walk through the available auxiliary modules. 561 aux_enabled = [] 562 for module in Auxiliary.__subclasses__(): 563 # Try to start the auxiliary module. 564 try: 565 aux = module() 566 aux.start() 567 except (NotImplementedError, AttributeError): 568 log.warning("Auxiliary module %s was not implemented", 569 aux.__class__.__name__) 570 continue 571 except Exception as e: 572 log.warning("Cannot execute auxiliary module %s: %s", 573 aux.__class__.__name__, e) 574 continue 575 finally: 576 log.info("Started auxiliary module %s", 577 aux.__class__.__name__) 578 aux_enabled.append(aux) 579 580 # Start analysis package. If for any reason, the execution of the 581 # analysis package fails, we have to abort the analysis. 582 try: 583 pids = pack.start(self.target) 584 except NotImplementedError: 585 raise CuckooError("The package \"{0}\" doesn't contain a run " 586 "function.".format(package_name)) 587 except CuckooPackageError as e: 588 raise CuckooError("The package \"{0}\" start function raised an " 589 "error: {1}".format(package_name, e)) 590 except Exception as e: 591 raise CuckooError("The package \"{0}\" start function encountered " 592 "an unhandled exception: " 593 "{1}".format(package_name, e)) 594 595 # If the analysis package returned a list of process IDs, we add them 596 # to the list of monitored processes and enable the process monitor. 597 if pids: 598 add_pids(pids) 599 pid_check = True 600 # If the package didn't return any process ID (for example in the case 601 # where the package isn't enabling any behavioral analysis), we don't 602 # enable the process monitor. 603 else: 604 log.info("No process IDs returned by the package, running " 605 "for the full timeout") 606 pid_check = False 607 608 # Check in the options if the user toggled the timeout enforce. If so, 609 # we need to override pid_check and disable process monitor. 610 if self.config.enforce_timeout: 611 log.info("Enabled timeout enforce, running for the full timeout") 612 pid_check = False 613 614 time_counter = 0 615 616 while True: 617 time_counter += 1 618 if time_counter == int(self.config.timeout): 619 log.info("Analysis timeout hit, terminating analysis") 620 break 621 622 # If the process lock is locked, it means that something is 623 # operating on the list of monitored processes. Therefore we cannot 624 # proceed with the checks until the lock is released. 625 if PROCESS_LOCK.locked(): 626 KERNEL32.Sleep(1000) 627 continue 628 629 try: 630 # If the process monitor is enabled we start checking whether 631 # the monitored processes are still alive. 632 if pid_check: 633 for pid in PROCESS_LIST: 634 if not Process(pid=pid).is_alive(): 635 log.info("Process with pid %s has terminated", pid) 636 PROCESS_LIST.remove(pid) 637 638 # If none of the monitored processes are still alive, we 639 # can terminate the analysis. 640 if len(PROCESS_LIST) == 0: 641 log.info("Process list is empty, " 642 "terminating analysis...") 643 break 644 645 # Update the list of monitored processes available to the 646 # analysis package. It could be used for internal 647 # operations within the module. 648 pack.set_pids(PROCESS_LIST) 649 650 try: 651 # The analysis packages are provided with a function that 652 # is executed at every loop's iteration. If such function 653 # returns False, it means that it requested the analysis 654 # to be terminate. 655 if not pack.check(): 656 log.info("The analysis package requested the " 657 "termination of the analysis...") 658 break 659 660 # If the check() function of the package raised some exception 661 # we don't care, we can still proceed with the analysis but we 662 # throw a warning. 663 except Exception as e: 664 log.warning("The package \"%s\" check function raised " 665 "an exception: %s", package_name, e) 666 finally: 667 # Zzz. 668 KERNEL32.Sleep(1000) 669 670 try: 671 # Before shutting down the analysis, the package can perform some 672 # final operations through the finish() function. 673 pack.finish() 674 except Exception as e: 675 log.warning("The package \"%s\" finish function raised an " 676 "exception: %s", package_name, e) 677 678 # Terminate the Auxiliary modules. 679 for aux in aux_enabled: 680 try: 681 aux.stop() 682 except (NotImplementedError, AttributeError): 683 continue 684 except Exception as e: 685 log.warning("Cannot terminate auxiliary module %s: %s", 686 aux.__class__.__name__, e) 687 688 # Try to terminate remaining active processes. We do this to make sure 689 # that we clean up remaining open handles (sockets, files, etc.). 690 log.info("Terminating remaining processes before shutdown...") 691 692 for pid in PROCESS_LIST: 693 proc = Process(pid=pid) 694 if proc.is_alive(): 695 try: 696 proc.terminate() 697 except: 698 continue 699 700 # Let's invoke the completion procedure. 701 self.complete() 702 703 return True
704 705 if __name__ == "__main__": 706 success = False 707 error = "" 708 709 try: 710 # Initialize the main analyzer class. 711 analyzer = Analyzer() 712 # Run it and wait for the response. 713 success = analyzer.run() 714 # This is not likely to happen. 715 except KeyboardInterrupt: 716 error = "Keyboard Interrupt" 717 # If the analysis process encountered a critical error, it will raise a 718 # CuckooError exception, which will force the termination of the analysis 719 # weill notify the agent of the failure. Also catched unexpected 720 # exceptions. 721 except Exception as e: 722 # Store the error. 723 error_exc = traceback.format_exc() 724 error = str(e) 725 726 # Just to be paranoid. 727 if len(log.handlers) > 0: 728 log.exception(error_exc) 729 else: 730 sys.stderr.write("{0}\n".format(error_exc)) 731 # Once the analysis is completed or terminated for any reason, we report 732 # back to the agent, notifying that it can report back to the host. 733 finally: 734 # Establish connection with the agent XMLRPC server. 735 server = xmlrpclib.Server("http://127.0.0.1:8000") 736 server.complete(success, error, PATHS["root"]) 737