lib.api.process

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