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