lib.cuckoo.core.guest

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

# Source Code for Module lib.cuckoo.core.guest

      1  # Copyright (C) 2010-2013 Claudio Guarnieri.
      2  # Copyright (C) 2014-2016 Cuckoo Foundation.
      3  # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
      4  # See the file 'docs/LICENSE' for copying permission.
      5
      6  import datetime
      7  import io
      8  import json
      9  import os
     10  import time
     11  import socket
     12  import logging
     13  import requests
     14  import xmlrpclib
     15
     16  from zipfile import ZipFile, ZIP_STORED
     17
     18  from lib.cuckoo.common.config import Config
     19  from lib.cuckoo.common.constants import CUCKOO_ROOT
     20  from lib.cuckoo.common.constants import CUCKOO_GUEST_PORT, CUCKOO_GUEST_INIT
     21  from lib.cuckoo.common.constants import CUCKOO_GUEST_COMPLETED
     22  from lib.cuckoo.common.constants import CUCKOO_GUEST_FAILED
     23  from lib.cuckoo.common.exceptions import CuckooGuestError
     24  from lib.cuckoo.common.utils import TimeoutServer
     25  from lib.cuckoo.core.database import Database
     26
     27  log = logging.getLogger(__name__)
     28  db = Database()
     29
     30 -def analyzer_zipfile(platform, monitor):
     31      """Creates the Zip file that is sent to the Guest."""
     32      t = time.time()
     33
     34      zip_data = io.BytesIO()
     35      zip_file = ZipFile(zip_data, "w", ZIP_STORED)
     36
     37      # Select the proper analyzer's folder according to the operating
     38      # system associated with the current machine.
     39      root = os.path.join(CUCKOO_ROOT, "analyzer", platform)
     40      root_len = len(os.path.abspath(root))
     41
     42      if not os.path.exists(root):
     43          log.error("No valid analyzer found at path: %s", root)
     44          raise CuckooGuestError(
     45              "No valid analyzer found for %s platform!" % platform
     46          )
     47
     48      # Walk through everything inside the analyzer's folder and write
     49      # them to the zip archive.
     50      for root, dirs, files in os.walk(root):
     51          archive_root = os.path.abspath(root)[root_len:]
     52          for name in files:
     53              path = os.path.join(root, name)
     54              archive_name = os.path.join(archive_root, name)
     55              zip_file.write(path, archive_name)
     56
     57      # Include the chosen monitoring component.
     58      if platform == "windows":
     59          dirpath = os.path.join(CUCKOO_ROOT, "data", "monitor", monitor)
     60
     61          # Sometimes we might get a file instead of a symbolic link, in that
     62          # case we follow the semi-"symbolic link" manually.
     63          if os.path.isfile(dirpath):
     64              monitor = os.path.basename(open(dirpath, "rb").read().strip())
     65              dirpath = os.path.join(CUCKOO_ROOT, "data", "monitor", monitor)
     66
     67          for name in os.listdir(dirpath):
     68              path = os.path.join(dirpath, name)
     69              archive_name = os.path.join("/bin", name)
     70              zip_file.write(path, archive_name)
     71
     72      zip_file.close()
     73      data = zip_data.getvalue()
     74
     75      if time.time() - t > 10:
     76          log.warning(
     77              "It took more than 10 seconds to build the Analyzer Zip for the "
     78              "Guest. This might be a serious performance penalty. Is your "
     79              "analyzer/windows/ directory bloated with unnecessary files?"
     80          )
     81
     82      return data
     83
     84 -class OldGuestManager(object):
     85      """Old and deprecated Guest Manager.
     86
     87      This class handles the communications with the old agent running in the
     88      virtual machine.
     89      """
     90
     91 -    def __init__(self, vm_id, ip, platform, task_id):
     92          """@param ip: guest's IP address.
     93          @param platform: guest's operating system type.
     94          """
     95          self.id = vm_id
     96          self.ip = ip
     97          self.platform = platform
     98          self.task_id = task_id
     99
    100          self.cfg = Config()
    101
    102          # initialized in start_analysis so we can update the critical timeout
    103          # TODO, pull options parameter into __init__ so we can do this here
    104          self.timeout = None
    105          self.server = None
    106
    107 -    def wait(self, status):
    108          """Waiting for status.
    109          @param status: status.
    110          @return: always True.
    111          """
    112          log.debug("%s: waiting for status 0x%.04x", self.id, status)
    113
    114          end = time.time() + self.timeout
    115          self.server._set_timeout(self.timeout)
    116
    117          while db.guest_get_status(self.task_id) == "starting":
    118              # Check if we've passed the timeout.
    119              if time.time() > end:
    120                  raise CuckooGuestError("{0}: the guest initialization hit the "
    121                                         "critical timeout, analysis "
    122                                         "aborted.".format(self.id))
    123
    124              try:
    125                  # If the server returns the given status, break the loop
    126                  # and return.
    127                  if self.server.get_status() == status:
    128                      log.debug("%s: status ready", self.id)
    129                      break
    130              except:
    131                  pass
    132
    133              log.debug("%s: not ready yet", self.id)
    134              time.sleep(1)
    135
    136          self.server._set_timeout(None)
    137          return True
    138
    139 -    def upload_analyzer(self, monitor):
    140          """Upload analyzer to guest.
    141          @return: operation status.
    142          """
    143          zip_data = analyzer_zipfile(self.platform, monitor)
    144
    145          log.debug(
    146              "Uploading analyzer to guest (id=%s, ip=%s, monitor=%s, size=%d)",
    147              self.id, self.ip, monitor, len(zip_data)
    148          )
    149
    150          # Send the zip containing the analyzer to the agent running inside
    151          # the guest.
    152          try:
    153              self.server.add_analyzer(xmlrpclib.Binary(zip_data))
    154          except socket.timeout:
    155              raise CuckooGuestError("{0}: guest communication timeout: unable "
    156                                     "to upload agent, check networking or try "
    157                                     "to increase timeout".format(self.id))
    158
    159 -    def start_analysis(self, options, monitor):
    160          """Start analysis.
    161          @param options: options.
    162          @return: operation status.
    163          """
    164          # TODO Deal with unicode URLs, should probably try URL encoding.
    165          # Unicode files are being taken care of.
    166
    167          self.timeout = options["timeout"] + self.cfg.timeouts.critical
    168
    169          url = "http://{0}:{1}".format(self.ip, CUCKOO_GUEST_PORT)
    170          self.server = TimeoutServer(url, allow_none=True,
    171                                      timeout=self.timeout)
    172
    173          try:
    174              # Wait for the agent to respond. This is done to check the
    175              # availability of the agent and verify that it's ready to receive
    176              # data.
    177              self.wait(CUCKOO_GUEST_INIT)
    178
    179              # Invoke the upload of the analyzer to the guest.
    180              self.upload_analyzer(monitor)
    181
    182              # Give the analysis options to the guest, so it can generate the
    183              # analysis.conf inside the guest.
    184              try:
    185                  self.server.add_config(options)
    186              except:
    187                  raise CuckooGuestError("{0}: unable to upload config to "
    188                                         "analysis machine".format(self.id))
    189
    190              # If the target of the analysis is a file, upload it to the guest.
    191              if options["category"] == "file":
    192                  try:
    193                      file_data = open(options["target"], "rb").read()
    194                  except (IOError, OSError) as e:
    195                      raise CuckooGuestError("Unable to read {0}, error: "
    196                                             "{1}".format(options["target"], e))
    197
    198                  data = xmlrpclib.Binary(file_data)
    199
    200                  try:
    201                      self.server.add_malware(data, options["file_name"])
    202                  except Exception as e:
    203                      raise CuckooGuestError("{0}: unable to upload malware to "
    204                                             "analysis machine: {1}".format(self.id, e))
    205
    206              # Launch the analyzer.
    207              pid = self.server.execute()
    208              log.debug("%s: analyzer started with PID %d", self.id, pid)
    209          # If something goes wrong when establishing the connection, raise an
    210          # exception and abort the analysis.
    211          except (socket.timeout, socket.error):
    212              raise CuckooGuestError("{0}: guest communication timeout, check "
    213                                     "networking or try to increase "
    214                                     "timeout".format(self.id))
    215
    216 -    def wait_for_completion(self):
    217          """Wait for analysis completion.
    218          @return: operation status.
    219          """
    220          log.debug("%s: waiting for completion", self.id)
    221
    222          end = time.time() + self.timeout
    223          self.server._set_timeout(self.timeout)
    224
    225          while db.guest_get_status(self.task_id) == "running":
    226              time.sleep(1)
    227
    228              # If the analysis hits the critical timeout, just return straight
    229              # away and try to recover the analysis results from the guest.
    230              if time.time() > end:
    231                  raise CuckooGuestError("The analysis hit the critical timeout, terminating.")
    232
    233              try:
    234                  status = self.server.get_status()
    235              except Exception as e:
    236                  log.debug("%s: error retrieving status: %s", self.id, e)
    237                  continue
    238
    239              # React according to the returned status.
    240              if status == CUCKOO_GUEST_COMPLETED:
    241                  log.info("%s: analysis completed successfully", self.id)
    242                  break
    243              elif status == CUCKOO_GUEST_FAILED:
    244                  error = self.server.get_error()
    245                  raise CuckooGuestError(
    246                      "Analysis failed: %s" % (error or "unknown error")
    247                  )
    248              else:
    249                  log.debug("%s: analysis not completed yet (status=%s)",
    250                            self.id, status)
    251
    252          self.server._set_timeout(None)
    253
    254 -class GuestManager(object):
    255      """This class represents the new Guest Manager. It operates on the new
    256      Cuckoo Agent which features a more abstract but more feature-rich API."""
    257
    258 -    def __init__(self, vmid, ipaddr, platform, task_id, analysis_manager):
    259          self.vmid = vmid
    260          self.ipaddr = ipaddr
    261          self.port = CUCKOO_GUEST_PORT
    262          self.platform = platform
    263          self.task_id = task_id
    264          self.analysis_manager = analysis_manager
    265
    266          self.cfg = Config()
    267          self.timeout = None
    268
    269          # Just in case we have an old agent inside the Virtual Machine. This
    270          # allows us to remain backwards compatible (for now).
    271          self.old = OldGuestManager(vmid, ipaddr, platform, task_id)
    272          self.is_old = False
    273
    274          # We maintain the path of the Cuckoo Analyzer on the host.
    275          self.analyzer_path = None
    276          self.environ = {}
    277
    278          self.options = {}
    279
    280      @property
    281 -    def aux(self):
    282          return self.analysis_manager.aux
    283
    284 -    def get(self, method, *args, **kwargs):
    285          """Simple wrapper around requests.get()."""
    286          url = "http://%s:%s%s" % (self.ipaddr, self.port, method)
    287          session = requests.Session()
    288          session.trust_env = False
    289          session.proxies = None
    290          return session.get(url, *args, **kwargs)
    291
    292 -    def post(self, method, *args, **kwargs):
    293          """Simple wrapper around requests.post()."""
    294          url = "http://%s:%s%s" % (self.ipaddr, self.port, method)
    295          session = requests.Session()
    296          session.trust_env = False
    297          session.proxies = None
    298          return session.post(url, *args, **kwargs)
    299
    300 -    def wait_available(self):
    301          """Wait until the Virtual Machine is available for usage."""
    302          end = time.time() + self.timeout
    303
    304          while db.guest_get_status(self.task_id) == "starting":
    305              try:
    306                  socket.create_connection((self.ipaddr, self.port), 1).close()
    307                  break
    308              except socket.timeout:
    309                  log.debug("%s: not ready yet", self.vmid)
    310              except socket.error:
    311                  log.debug("%s: not ready yet", self.vmid)
    312                  time.sleep(1)
    313
    314              if time.time() > end:
    315                  raise CuckooGuestError(
    316                      "%s: the guest initialization hit the critical timeout, "
    317                      "analysis aborted." % self.vmid
    318                  )
    319
    320 -    def query_environ(self):
    321          """Query the environment of the Agent in the Virtual Machine."""
    322          self.environ = self.get("/environ").json()["environ"]
    323
    324 -    def determine_analyzer_path(self):
    325          """Determine the path of the analyzer. Basically creating a temporary
    326          directory in the systemdrive, i.e., C:\\."""
    327          systemdrive = "%s\\" % self.environ["SYSTEMDRIVE"]
    328
    329          r = self.post("/mkdtemp", data={"dirpath": systemdrive})
    330          self.analyzer_path = r.json()["dirpath"]
    331
    332 -    def upload_analyzer(self, monitor):
    333          """Upload the analyzer to the Virtual Machine."""
    334          zip_data = analyzer_zipfile(self.platform, monitor)
    335
    336          log.debug("Uploading analyzer to guest (id=%s, ip=%s, monitor=%s)",
    337                    self.vmid, self.ipaddr, monitor)
    338
    339          self.determine_analyzer_path()
    340          data = {
    341              "dirpath": self.analyzer_path,
    342          }
    343          self.post("/extract", files={"zipfile": zip_data}, data=data)
    344
    345 -    def add_config(self, options):
    346          """Upload the analysis.conf for this task to the Virtual Machine."""
    347          config = [
    348              "[analysis]",
    349          ]
    350          for key, value in options.items():
    351              # Encode datetime objects the way xmlrpc encodes them.
    352              if isinstance(value, datetime.datetime):
    353                  config.append("%s = %s" % (key, value.strftime("%Y%m%dT%H:%M:%S")))
    354              else:
    355                  config.append("%s = %s" % (key, value))
    356
    357          data = {
    358              "filepath": os.path.join(self.analyzer_path, "analysis.conf"),
    359          }
    360          self.post("/store", files={"file": "\n".join(config)}, data=data)
    361
    362 -    def start_analysis(self, options, monitor):
    363          """Start the analysis by uploading all required files.
    364
    365          @param options: the task options
    366          @param monitor: identifier of the monitor to be used.
    367          """
    368          log.info("Starting analysis on guest (id=%s, ip=%s)",
    369                   self.vmid, self.ipaddr)
    370
    371          self.options = options
    372          self.timeout = options["timeout"] + self.cfg.timeouts.critical
    373
    374          # Wait for the agent to come alive.
    375          self.wait_available()
    376
    377          # Could be beautified a bit, but basically we have to perform the
    378          # same check here as we did in wait_available().
    379          if db.guest_get_status(self.task_id) != "starting":
    380              return
    381
    382          # Check whether this is the new Agent or the old one (by looking at
    383          # the status code of the index page).
    384          r = self.get("/")
    385          if r.status_code == 501:
    386              # log.info("Cuckoo 2.0 features a new Agent which is more "
    387              #          "feature-rich. It is recommended to make new Virtual "
    388              #          "Machines with the new Agent, but for now falling back "
    389              #          "to backwards compatibility with the old agent.")
    390              self.is_old = True
    391              self.aux.callback("legacy_agent")
    392              self.old.start_analysis(options, monitor)
    393              return
    394
    395          if r.status_code != 200:
    396              log.critical(
    397                  "While trying to determine the Agent version that your VM is "
    398                  "running we retrieved an unexpected HTTP status code: %s. If "
    399                  "this is a false positive, please report this issue to the "
    400                  "Cuckoo Developers. HTTP response headers: %s",
    401                  r.status_code, json.dumps(dict(r.headers)),
    402              )
    403              db.guest_set_status(self.task_id, "failed")
    404              return
    405
    406          try:
    407              status = r.json()
    408              version = status.get("version")
    409              features = status.get("features", [])
    410          except:
    411              log.critical(
    412                  "We were unable to detect either the Old or New Agent in the "
    413                  "Guest VM, are you sure you have set it up correctly? Please "
    414                  "go through the documentation once more and otherwise inform "
    415                  "the Cuckoo Developers of your issue."
    416              )
    417              db.guest_set_status(self.task_id, "failed")
    418              return
    419
    420          log.info("Guest is running Cuckoo Agent %s (id=%s, ip=%s)",
    421                   version, self.vmid, self.ipaddr)
    422
    423          # Pin the Agent to our IP address so that it is not accessible by
    424          # other Virtual Machines etc.
    425          if "pinning" in features:
    426              self.get("/pinning")
    427
    428          # Obtain the environment variables.
    429          self.query_environ()
    430
    431          # Upload the analyzer.
    432          self.upload_analyzer(monitor)
    433
    434          # Pass along the analysis.conf file.
    435          self.add_config(options)
    436
    437          # Allow Auxiliary modules to prepare the Guest.
    438          self.aux.callback("prepare_guest")
    439
    440          # If the target is a file, upload it to the guest.
    441          if options["category"] == "file":
    442              data = {
    443                  "filepath": os.path.join(self.environ["TEMP"], options["file_name"]),
    444              }
    445              files = {
    446                  "file": open(options["target"], "rb"),
    447              }
    448              self.post("/store", files=files, data=data)
    449
    450          if "execpy" in features:
    451              data = {
    452                  "filepath": "%s\\analyzer.py" % self.analyzer_path,
    453                  "async": "yes",
    454                  "cwd": self.analyzer_path,
    455              }
    456              self.post("/execpy", data=data)
    457          else:
    458              # Execute the analyzer that we just uploaded.
    459              data = {
    460                  "command": "C:\\Python27\\pythonw.exe %s\\analyzer.py" % self.analyzer_path,
    461                  "async": "yes",
    462                  "cwd": self.analyzer_path,
    463              }
    464              self.post("/execute", data=data)
    465
    466 -    def wait_for_completion(self):
    467          if self.is_old:
    468              self.old.wait_for_completion()
    469              return
    470
    471          end = time.time() + self.timeout
    472
    473          while db.guest_get_status(self.task_id) == "running":
    474              log.debug("%s: analysis still processing", self.vmid)
    475
    476              time.sleep(1)
    477
    478              # If the analysis hits the critical timeout, just return straight
    479              # away and try to recover the analysis results from the guest.
    480              if time.time() > end:
    481                  raise CuckooGuestError("The analysis hit the critical timeout, terminating.")
    482
    483              try:
    484                  status = self.get("/status", timeout=5).json()
    485              except Exception as e:
    486                  log.info("Virtual Machine /status failed (%r)", e)
    487                  # this might fail due to timeouts or just temporary network issues
    488                  # thus we don't want to abort the analysis just yet and wait for things to
    489                  # recover
    490                  continue
    491
    492              if status["status"] == "complete":
    493                  log.info("%s: analysis completed successfully", self.vmid)
    494                  return
    495              elif status["status"] == "exception":
    496                  log.info("%s: analysis caught an exception\n%s",
    497                           self.vmid, status["description"])
    498                  return
    499
    500      @property
    501 -    def server(self):
    502          """Currently the Physical machine manager is using GuestManager in
    503          an incorrect way. This should be fixed up later but for now this
    504          workaround will do."""
    505          return self.old.server
    506