Package lib :: Package api :: Module process
[hide private]
[frames] | no frames]

Source Code for Module lib.api.process

  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 os 
  7  import logging 
  8  import random 
  9  import subprocess 
 10  import tempfile 
 11   
 12  from ctypes import byref, c_ulong, create_string_buffer, c_int, sizeof 
 13  from ctypes import c_uint, c_wchar_p, create_unicode_buffer 
 14   
 15  from lib.common.constants import SHUTDOWN_MUTEX 
 16  from lib.common.defines import KERNEL32, NTDLL, SYSTEM_INFO, STILL_ACTIVE 
 17  from lib.common.defines import THREAD_ALL_ACCESS, PROCESS_ALL_ACCESS 
 18  from lib.common.errors import get_error_string 
 19  from lib.common.exceptions import CuckooError 
 20  from lib.common.results import upload_to_host 
 21   
 22  log = logging.getLogger(__name__) 
23 24 -def subprocess_checkcall(args, env=None):
25 return subprocess.check_call( 26 args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, 27 stderr=subprocess.PIPE, env=env, 28 )
29
30 -def subprocess_checkoutput(args, env=None):
31 return subprocess.check_output( 32 args, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=env, 33 )
34
35 -class Process(object):
36 """Windows process.""" 37 first_process = True 38 config = None 39 40 # Keeps track of the dump memory index for a particular process as in 41 # theory, and will be useful later, we may want to dump one process 42 # multiple times. 43 dumpmem = {} 44
45 - def __init__(self, pid=None, tid=None, process_name=None):
46 """ 47 @param pid: process identifier. 48 @param tid: thread identifier. 49 @param process_name: process name. 50 """ 51 self.pid = pid 52 self.tid = tid 53 self.process_name = process_name
54 55 @staticmethod
56 - def set_config(config):
57 """Sets the analyzer configuration once.""" 58 Process.config = config
59
60 - def get_system_info(self):
61 """Get system information.""" 62 self.system_info = SYSTEM_INFO() 63 KERNEL32.GetSystemInfo(byref(self.system_info))
64
65 - def open_process(self):
66 """Open a process handle.""" 67 return KERNEL32.OpenProcess(PROCESS_ALL_ACCESS, False, self.pid)
68
69 - def open_thread(self):
70 """Open a thread handle.""" 71 return KERNEL32.OpenThread(THREAD_ALL_ACCESS, False, self.tid)
72
73 - def exit_code(self):
74 """Get process exit code. 75 @return: exit code value. 76 """ 77 process_handle = self.open_process() 78 79 exit_code = c_ulong(0) 80 KERNEL32.GetExitCodeProcess(process_handle, byref(exit_code)) 81 KERNEL32.CloseHandle(process_handle) 82 83 return exit_code.value
84
85 - def get_filepath(self):
86 """Get process image file path. 87 @return: decoded file path. 88 """ 89 process_handle = self.open_process() 90 91 NT_SUCCESS = lambda val: val >= 0 92 93 pbi = create_string_buffer(200) 94 size = c_int() 95 96 # Set return value to signed 32bit integer. 97 NTDLL.NtQueryInformationProcess.restype = c_int 98 99 ret = NTDLL.NtQueryInformationProcess(process_handle, 100 27, 101 byref(pbi), 102 sizeof(pbi), 103 byref(size)) 104 105 KERNEL32.CloseHandle(process_handle) 106 107 if NT_SUCCESS(ret) and size.value > 8: 108 try: 109 fbuf = pbi.raw[8:] 110 fbuf = fbuf[:fbuf.find("\x00\x00")+1] 111 return fbuf.decode("utf16", errors="ignore") 112 except: 113 return "" 114 115 return ""
116
117 - def is_alive(self):
118 """Process is alive? 119 @return: process status. 120 """ 121 return self.exit_code() == STILL_ACTIVE
122
123 - def get_parent_pid(self):
124 """Get the Parent Process ID.""" 125 process_handle = self.open_process() 126 127 NT_SUCCESS = lambda val: val >= 0 128 129 pbi = (c_int * 6)() 130 size = c_int() 131 132 # Set return value to signed 32bit integer. 133 NTDLL.NtQueryInformationProcess.restype = c_int 134 135 ret = NTDLL.NtQueryInformationProcess(process_handle, 136 0, 137 byref(pbi), 138 sizeof(pbi), 139 byref(size)) 140 141 KERNEL32.CloseHandle(process_handle) 142 143 if NT_SUCCESS(ret) and size.value == sizeof(pbi): 144 return pbi[5] 145 146 return None
147
148 - def shortpath(self, path):
149 """Returns the shortpath for a file. 150 151 As Python 2.7 does not support passing along unicode strings in 152 subprocess.Popen() and alike this will have to do. See also: 153 http://stackoverflow.com/questions/2595448/unicode-filename-to-python-subprocess-call 154 """ 155 KERNEL32.GetShortPathNameW.restype = c_uint 156 KERNEL32.GetShortPathNameW.argtypes = c_wchar_p, c_wchar_p, c_uint 157 158 buf = create_unicode_buffer(0x8000) 159 KERNEL32.GetShortPathNameW(path, buf, len(buf)) 160 return buf.value
161
162 - def _encode_args(self, args):
163 """Convert a list of arguments to a string that can be passed along 164 on the command-line. 165 @param args: list of arguments 166 @return: the command-line equivalent 167 """ 168 ret = [] 169 for line in args: 170 if " " in line: 171 ret.append('"%s"' % line) 172 else: 173 ret.append(line) 174 return " ".join(ret)
175
176 - def is32bit(self, pid=None, process_name=None, path=None):
177 """Is a PE file 32-bit or does a process identifier belong to a 178 32-bit process. 179 @param pid: process identifier. 180 @param process_name: process name. 181 @param path: path to a PE file. 182 @return: boolean or exception. 183 """ 184 count = (pid is None) + (process_name is None) + (path is None) 185 if count != 2: 186 raise CuckooError("Invalid usage of is32bit, only one identifier " 187 "should be specified") 188 189 is32bit_exe = os.path.join("bin", "is32bit.exe") 190 191 if pid: 192 args = [is32bit_exe, "-p", "%s" % pid] 193 elif process_name: 194 args = [is32bit_exe, "-n", process_name] 195 196 # If we're running a 32-bit Python in a 64-bit Windows system and the 197 # path points to System32, then we hardcode it as being a 64-bit 198 # binary. (To be fair, a 64-bit Python on 64-bit Windows would also 199 # make the System32 binary 64-bit). 200 elif os.path.isdir("C:\\Windows\\Sysnative") and \ 201 path.lower().startswith("c:\\windows\\system32"): 202 return False 203 else: 204 args = [is32bit_exe, "-f", self.shortpath(path)] 205 206 try: 207 bitsize = int(subprocess_checkoutput(args)) 208 except subprocess.CalledProcessError as e: 209 raise CuckooError("Error returned by is32bit: %s" % e) 210 211 return bitsize == 32
212
213 - def execute(self, path, args=None, dll=None, free=False, curdir=None, 214 source=None, mode=None, maximize=False, env=None, 215 trigger=None):
216 """Execute sample process. 217 @param path: sample path. 218 @param args: process args. 219 @param dll: dll path. 220 @param free: do not inject our monitor. 221 @param curdir: current working directory. 222 @param source: process identifier or process name which will 223 become the parent process for the new process. 224 @param mode: monitor mode - which functions to instrument. 225 @param maximize: whether the GUI should be maximized. 226 @param env: environment variables. 227 @param trigger: trigger to indicate analysis start 228 @return: operation status. 229 """ 230 if not os.access(path, os.X_OK): 231 log.error("Unable to access file at path \"%s\", " 232 "execution aborted", path) 233 return False 234 235 is32bit = self.is32bit(path=path) 236 237 if not dll: 238 if is32bit: 239 dll = "monitor-x86.dll" 240 else: 241 dll = "monitor-x64.dll" 242 243 dllpath = os.path.abspath(os.path.join("bin", dll)) 244 245 if not os.path.exists(dllpath): 246 log.warning("No valid DLL specified to be injected, " 247 "injection aborted.") 248 return False 249 250 if source: 251 if isinstance(source, (int, long)) or source.isdigit(): 252 inject_is32bit = self.is32bit(pid=int(source)) 253 else: 254 inject_is32bit = self.is32bit(process_name=source) 255 else: 256 inject_is32bit = is32bit 257 258 if inject_is32bit: 259 inject_exe = os.path.join("bin", "inject-x86.exe") 260 else: 261 inject_exe = os.path.join("bin", "inject-x64.exe") 262 263 argv = [ 264 inject_exe, 265 "--app", self.shortpath(path), 266 "--only-start", 267 ] 268 269 if args: 270 argv += ["--args", self._encode_args(args)] 271 272 if curdir: 273 argv += ["--curdir", self.shortpath(curdir)] 274 275 if source: 276 if isinstance(source, (int, long)) or source.isdigit(): 277 argv += ["--from", "%s" % source] 278 else: 279 argv += ["--from-process", source] 280 281 if maximize: 282 argv += ["--maximize"] 283 284 try: 285 output = subprocess_checkoutput(argv, env) 286 self.pid, self.tid = map(int, output.split()) 287 except Exception: 288 log.error("Failed to execute process from path %r with " 289 "arguments %r (Error: %s)", path, argv, 290 get_error_string(KERNEL32.GetLastError())) 291 return False 292 293 if is32bit: 294 inject_exe = os.path.join("bin", "inject-x86.exe") 295 else: 296 inject_exe = os.path.join("bin", "inject-x64.exe") 297 298 argv = [ 299 inject_exe, 300 "--resume-thread", 301 "--pid", "%s" % self.pid, 302 "--tid", "%s" % self.tid, 303 ] 304 305 if free: 306 argv.append("--free") 307 else: 308 argv += [ 309 "--apc", 310 "--dll", dllpath, 311 "--config", self.drop_config(mode=mode, trigger=trigger), 312 ] 313 314 try: 315 subprocess_checkoutput(argv, env) 316 except Exception: 317 log.error("Failed to execute process from path %r with " 318 "arguments %r (Error: %s)", path, argv, 319 get_error_string(KERNEL32.GetLastError())) 320 return False 321 322 log.info("Successfully executed process from path %r with " 323 "arguments %r and pid %d", path, args or "", self.pid) 324 return True
325
326 - def terminate(self):
327 """Terminate process. 328 @return: operation status. 329 """ 330 process_handle = self.open_process() 331 332 ret = KERNEL32.TerminateProcess(process_handle, 1) 333 KERNEL32.CloseHandle(process_handle) 334 335 if ret: 336 log.info("Successfully terminated process with pid %d.", self.pid) 337 return True 338 else: 339 log.error("Failed to terminate process with pid %d.", self.pid) 340 return False
341
342 - def inject(self, dll=None, apc=False, track=True, mode=None):
343 """Inject our monitor into the specified process. 344 @param dll: Cuckoo DLL path. 345 @param apc: Use APC injection. 346 @param track: Track this process in the analyzer. 347 @param mode: Monitor mode - which functions to instrument. 348 """ 349 if not self.pid and not self.process_name: 350 log.warning("No valid pid or process name specified, " 351 "injection aborted.") 352 return False 353 354 # Only check whether the process is still alive when it's identified 355 # by a process identifier. Not when it's identified by a process name. 356 if not self.process_name and not self.is_alive(): 357 log.warning("The process with pid %s is not alive, " 358 "injection aborted", self.pid) 359 return False 360 361 if self.process_name: 362 is32bit = self.is32bit(process_name=self.process_name) 363 elif self.pid: 364 is32bit = self.is32bit(pid=self.pid) 365 366 if not dll: 367 if is32bit: 368 dll = "monitor-x86.dll" 369 else: 370 dll = "monitor-x64.dll" 371 372 dllpath = os.path.abspath(os.path.join("bin", dll)) 373 if not os.path.exists(dllpath): 374 log.warning("No valid DLL specified to be injected in process " 375 "with pid %s / process name %s, injection aborted.", 376 self.pid, self.process_name) 377 return False 378 379 if is32bit: 380 inject_exe = os.path.join("bin", "inject-x86.exe") 381 else: 382 inject_exe = os.path.join("bin", "inject-x64.exe") 383 384 args = [ 385 inject_exe, 386 "--dll", dllpath, 387 "--config", self.drop_config(track=track, mode=mode), 388 ] 389 390 if self.pid: 391 args += ["--pid", "%s" % self.pid] 392 elif self.process_name: 393 args += ["--process-name", self.process_name] 394 395 if apc: 396 args += ["--apc", "--tid", "%s" % self.tid] 397 else: 398 args += ["--crt"] 399 400 try: 401 subprocess_checkcall(args) 402 except Exception: 403 log.error("Failed to inject %s-bit process with pid %s and " 404 "process name %s", 32 if is32bit else 64, self.pid, 405 self.process_name) 406 return False 407 408 return True
409
410 - def drop_config(self, track=True, mode=None, trigger=None):
411 """Helper function to drop the configuration for a new process.""" 412 fd, config_path = tempfile.mkstemp() 413 414 # The first time we come up with a random startup-time. 415 if Process.first_process: 416 # This adds 1 up to 30 times of 20 minutes to the startup 417 # time of the process, therefore bypassing anti-vm checks 418 # which check whether the VM has only been up for <10 minutes. 419 Process.startup_time = random.randint(1, 30) * 20 * 60 * 1000 420 421 lines = { 422 "pipe": self.config.pipe, 423 "logpipe": self.config.logpipe, 424 "analyzer": os.getcwd(), 425 "first-process": "1" if Process.first_process else "0", 426 "startup-time": Process.startup_time, 427 "shutdown-mutex": SHUTDOWN_MUTEX, 428 "force-sleepskip": self.config.options.get("force-sleepskip", "0"), 429 "track": "1" if track else "0", 430 "mode": mode or "", 431 "disguise": self.config.options.get("disguise", "0"), 432 "pipe-pid": "1", 433 "trigger": trigger or "", 434 } 435 436 for key, value in lines.items(): 437 os.write(fd, "%s=%s\n" % (key, value)) 438 439 os.close(fd) 440 Process.first_process = False 441 return config_path
442
443 - def dump_memory(self, addr=None, length=None):
444 """Dump process memory, optionally target only a certain memory range. 445 @return: operation status. 446 """ 447 if not self.pid: 448 log.warning("No valid pid specified, memory dump aborted") 449 return False 450 451 if not self.is_alive(): 452 log.warning("The process with pid %d is not alive, memory " 453 "dump aborted", self.pid) 454 return False 455 456 if self.is32bit(pid=self.pid): 457 inject_exe = os.path.join("bin", "inject-x86.exe") 458 else: 459 inject_exe = os.path.join("bin", "inject-x64.exe") 460 461 # Take the memory dump. 462 dump_path = tempfile.mktemp() 463 464 try: 465 args = [ 466 inject_exe, 467 "--pid", "%s" % self.pid, 468 "--dump", dump_path, 469 ] 470 471 # Restrict to a certain memory block. 472 if addr and length: 473 args += [ 474 "--dump-block", 475 "0x%x" % addr, 476 "%s" % length, 477 ] 478 479 subprocess_checkcall(args) 480 except subprocess.CalledProcessError: 481 log.error("Failed to dump memory of %d-bit process with pid %d.", 482 32 if self.is32bit(pid=self.pid) else 64, self.pid) 483 return 484 485 # Calculate the next index and send the process memory dump over to 486 # the host. Keep in mind that one process may have multiple process 487 # memory dumps in the future. 488 idx = self.dumpmem[self.pid] = self.dumpmem.get(self.pid, 0) + 1 489 490 if addr and length: 491 file_name = os.path.join( 492 "memory", "block-%s-0x%x-%s.dmp" % (self.pid, addr, idx) 493 ) 494 else: 495 file_name = os.path.join("memory", "%s-%s.dmp" % (self.pid, idx)) 496 497 upload_to_host(dump_path, file_name) 498 os.unlink(dump_path) 499 500 log.info("Memory dump of process with pid %d completed", self.pid) 501 return True
502 503 # The dump_memory_block functionality has been integrated with the 504 # dump_memory function, this alias is just for backwards compatibility. 505 dump_memory_block = dump_memory
506