mirror of
https://github.com/hanzoui/custom-nodes-security-scan.git
synced 2026-10-30 20:56:54 +00:00
587 lines
19 KiB
Python
587 lines
19 KiB
Python
# Copyright (C) 2012-2013 Claudio Guarnieri.
|
|
# Copyright (C) 2014-2018 Cuckoo Foundation.
|
|
# This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
|
|
# See the file 'docs/LICENSE' for copying permission.
|
|
|
|
import os
|
|
import logging
|
|
import random
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from ctypes import (
|
|
c_ulong, create_string_buffer, c_int, c_uint16, c_uint, c_wchar_p,
|
|
c_void_p, sizeof, byref, Structure, cast
|
|
)
|
|
from _subprocess import STARTF_USESTDHANDLES
|
|
|
|
from lib.common.constants import SHUTDOWN_MUTEX
|
|
from lib.common.defines import KERNEL32, NTDLL, SYSTEM_INFO, STILL_ACTIVE
|
|
from lib.common.defines import THREAD_ALL_ACCESS, PROCESS_ALL_ACCESS
|
|
from lib.common.exceptions import CuckooError
|
|
from lib.common.results import upload_to_host
|
|
from lib.core.ioctl import zer0m0n
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
def spCreateProcessW(application_name, command_line, process_attributes,
|
|
thread_attributes, inherit_handles, creation_flags,
|
|
environment, current_directory, startup_info):
|
|
class STARTUPINFO(Structure):
|
|
_fields_ = [
|
|
("cb", c_uint),
|
|
("reserved1", c_void_p),
|
|
("desktop", c_void_p),
|
|
("title", c_void_p),
|
|
("unused1", c_uint * 7),
|
|
("flags", c_uint),
|
|
("show_window", c_uint16),
|
|
("reserved2", c_uint16),
|
|
("reserved3", c_void_p),
|
|
("std_input", c_void_p),
|
|
("std_output", c_void_p),
|
|
("std_error", c_void_p),
|
|
]
|
|
|
|
class PROCESS_INFORMATION(Structure):
|
|
_fields_ = [
|
|
("process_handle", c_void_p),
|
|
("thread_handle", c_void_p),
|
|
("process_identifier", c_uint),
|
|
("thread_identifier", c_uint),
|
|
]
|
|
|
|
class Handle(int):
|
|
def Close(self):
|
|
KERNEL32.CloseHandle(self)
|
|
|
|
if environment:
|
|
environment = "\x00".join(
|
|
"%s=%s" % (k, v) for k, v in environment.items()
|
|
) + "\x00\x00"
|
|
|
|
si = STARTUPINFO()
|
|
si.cb = sizeof(STARTUPINFO)
|
|
|
|
if startup_info:
|
|
si.flags = startup_info.dwFlags
|
|
si.show_window = startup_info.wShowWindow
|
|
|
|
if si.flags & STARTF_USESTDHANDLES:
|
|
si.std_input = cast(int(startup_info.hStdInput), c_void_p)
|
|
si.std_output = cast(int(startup_info.hStdOutput), c_void_p)
|
|
si.std_error = cast(int(startup_info.hStdError), c_void_p)
|
|
|
|
pi = PROCESS_INFORMATION()
|
|
|
|
result = KERNEL32.CreateProcessW(
|
|
application_name, command_line, None, None, inherit_handles,
|
|
creation_flags, environment, current_directory, byref(si), byref(pi)
|
|
)
|
|
if not result:
|
|
# TODO We'll just assume this is correct for now.
|
|
raise WindowsError(KERNEL32.GetLastError())
|
|
|
|
return (
|
|
Handle(pi.process_handle), Handle(pi.thread_handle),
|
|
pi.process_identifier, pi.thread_identifier
|
|
)
|
|
|
|
# We patch Python 2.7's native .CreateProcess method to be unicode-aware.
|
|
subprocess._subprocess.CreateProcess = spCreateProcessW
|
|
KERNEL32.CreateProcessW.argtypes = (
|
|
c_wchar_p, c_wchar_p, c_void_p, c_void_p, c_uint, c_uint, c_void_p,
|
|
c_wchar_p, c_void_p, c_void_p
|
|
)
|
|
|
|
def subprocess_checkcall(args, env=None):
|
|
return subprocess.check_call(
|
|
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, env=env,
|
|
)
|
|
|
|
def subprocess_checkoutput(args, env=None):
|
|
return subprocess.check_output(
|
|
args, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=env,
|
|
)
|
|
|
|
class Process(object):
|
|
"""Windows process."""
|
|
first_process = True
|
|
config = None
|
|
|
|
# Keeps track of the dump memory index for a particular process as in
|
|
# theory, and will be useful later, we may want to dump one process
|
|
# multiple times.
|
|
dumpmem = {}
|
|
|
|
def __init__(self, pid=None, tid=None, process_name=None):
|
|
"""
|
|
@param pid: process identifier.
|
|
@param tid: thread identifier.
|
|
@param process_name: process name.
|
|
"""
|
|
self.pid = pid
|
|
self.tid = tid
|
|
self.process_name = process_name
|
|
|
|
@staticmethod
|
|
def set_config(config):
|
|
"""Set the analyzer configuration once."""
|
|
Process.config = config
|
|
|
|
def get_system_info(self):
|
|
"""Get system information."""
|
|
self.system_info = SYSTEM_INFO()
|
|
KERNEL32.GetSystemInfo(byref(self.system_info))
|
|
|
|
def open_process(self):
|
|
"""Open a process handle."""
|
|
return KERNEL32.OpenProcess(PROCESS_ALL_ACCESS, False, self.pid)
|
|
|
|
def open_thread(self):
|
|
"""Open a thread handle."""
|
|
return KERNEL32.OpenThread(THREAD_ALL_ACCESS, False, self.tid)
|
|
|
|
def exit_code(self):
|
|
"""Get process exit code.
|
|
@return: exit code value.
|
|
"""
|
|
process_handle = self.open_process()
|
|
|
|
exit_code = c_ulong(0)
|
|
KERNEL32.GetExitCodeProcess(process_handle, byref(exit_code))
|
|
KERNEL32.CloseHandle(process_handle)
|
|
|
|
return exit_code.value
|
|
|
|
def get_filepath(self):
|
|
"""Get process image file path.
|
|
@return: decoded file path.
|
|
"""
|
|
process_handle = self.open_process()
|
|
|
|
NT_SUCCESS = lambda val: val >= 0
|
|
|
|
pbi = create_string_buffer(200)
|
|
size = c_int()
|
|
|
|
# Set return value to signed 32bit integer.
|
|
NTDLL.NtQueryInformationProcess.restype = c_int
|
|
|
|
ret = NTDLL.NtQueryInformationProcess(process_handle,
|
|
27,
|
|
byref(pbi),
|
|
sizeof(pbi),
|
|
byref(size))
|
|
|
|
KERNEL32.CloseHandle(process_handle)
|
|
|
|
if NT_SUCCESS(ret) and size.value > 8:
|
|
try:
|
|
fbuf = pbi.raw[8:]
|
|
fbuf = fbuf[:fbuf.find("\x00\x00")+1]
|
|
return fbuf.decode("utf16", errors="ignore")
|
|
except:
|
|
return ""
|
|
|
|
return ""
|
|
|
|
def is_alive(self):
|
|
"""Process is alive?
|
|
@return: process status.
|
|
"""
|
|
return self.exit_code() == STILL_ACTIVE
|
|
|
|
def get_parent_pid(self):
|
|
"""Get the Parent Process ID."""
|
|
class PROCESS_BASIC_INFORMATION(Structure):
|
|
_fields_ = [
|
|
("ExitStatus", c_void_p),
|
|
("PebBaseAddress", c_void_p),
|
|
("AffinityMask", c_void_p),
|
|
("BasePriority", c_void_p),
|
|
("UniqueProcessId", c_void_p),
|
|
("InheritedFromUniqueProcessId", c_void_p),
|
|
]
|
|
|
|
NT_SUCCESS = lambda val: val >= 0
|
|
|
|
pbi = PROCESS_BASIC_INFORMATION()
|
|
size = c_int()
|
|
|
|
# Set return value to signed 32bit integer.
|
|
NTDLL.NtQueryInformationProcess.restype = c_int
|
|
|
|
process_handle = self.open_process()
|
|
ret = NTDLL.NtQueryInformationProcess(
|
|
process_handle, 0, byref(pbi), sizeof(pbi), byref(size)
|
|
)
|
|
KERNEL32.CloseHandle(process_handle)
|
|
|
|
if NT_SUCCESS(ret) and size.value == sizeof(pbi):
|
|
return pbi.InheritedFromUniqueProcessId
|
|
|
|
def _encode_args(self, args):
|
|
"""Convert a list of arguments to a string that can be passed along
|
|
on the command-line.
|
|
@param args: list of arguments
|
|
@return: the command-line equivalent
|
|
"""
|
|
ret = []
|
|
for line in args:
|
|
if " " in line:
|
|
ret.append('"%s"' % line)
|
|
else:
|
|
ret.append(line)
|
|
return " ".join(ret)
|
|
|
|
def is32bit(self, pid=None, process_name=None, path=None):
|
|
"""Is a PE file 32-bit or does a process identifier belong to a
|
|
32-bit process.
|
|
@param pid: process identifier.
|
|
@param process_name: process name.
|
|
@param path: path to a PE file.
|
|
@return: boolean or exception.
|
|
"""
|
|
count = (pid is None) + (process_name is None) + (path is None)
|
|
if count != 2:
|
|
raise CuckooError("Invalid usage of is32bit, only one identifier "
|
|
"should be specified")
|
|
|
|
is32bit_exe = os.path.join("bin", "is32bit.exe")
|
|
|
|
if pid:
|
|
args = [is32bit_exe, "-p", "%s" % pid]
|
|
elif process_name:
|
|
args = [is32bit_exe, "-n", process_name]
|
|
|
|
# If we're running a 32-bit Python in a 64-bit Windows system and the
|
|
# path points to System32, then we hardcode it as being a 64-bit
|
|
# binary. (To be fair, a 64-bit Python on 64-bit Windows would also
|
|
# make the System32 binary 64-bit).
|
|
elif os.path.isdir("C:\\Windows\\Sysnative") and \
|
|
path.lower().startswith("c:\\windows\\system32"):
|
|
return False
|
|
elif not os.path.exists(path):
|
|
raise CuckooError("File not found: %s" % path)
|
|
else:
|
|
args = [is32bit_exe, "-f", path]
|
|
|
|
try:
|
|
bitsize = int(subprocess_checkoutput(args))
|
|
except subprocess.CalledProcessError as e:
|
|
raise CuckooError("Error returned by is32bit: %s" % e.output)
|
|
|
|
return bitsize == 32
|
|
|
|
def execute(self, path, args=None, dll=None, free=False, curdir=None,
|
|
source=None, mode=None, maximize=False, env=None,
|
|
trigger=None):
|
|
"""Execute sample process.
|
|
@param path: sample path.
|
|
@param args: process args.
|
|
@param dll: dll path.
|
|
@param free: do not inject our monitor.
|
|
@param curdir: current working directory.
|
|
@param source: process identifier or process name which will
|
|
become the parent process for the new process.
|
|
@param mode: monitor mode - which functions to instrument.
|
|
@param maximize: whether the GUI should be maximized.
|
|
@param env: environment variables.
|
|
@param trigger: trigger to indicate analysis start
|
|
@return: operation status.
|
|
"""
|
|
if not os.access(path, os.X_OK):
|
|
log.error(
|
|
"Unable to access file at path %r, execution aborted!", path
|
|
)
|
|
return False
|
|
|
|
is32bit = self.is32bit(path=path)
|
|
|
|
if source:
|
|
if isinstance(source, (int, long)) or source.isdigit():
|
|
inject_is32bit = self.is32bit(pid=int(source))
|
|
else:
|
|
inject_is32bit = self.is32bit(process_name=source)
|
|
else:
|
|
inject_is32bit = is32bit
|
|
|
|
if inject_is32bit:
|
|
inject_exe = os.path.join("bin", "inject-x86.exe")
|
|
else:
|
|
inject_exe = os.path.join("bin", "inject-x64.exe")
|
|
|
|
argv = [
|
|
inject_exe,
|
|
"--app", path,
|
|
"--only-start",
|
|
]
|
|
|
|
if args:
|
|
argv += ["--args", self._encode_args(args)]
|
|
|
|
if curdir:
|
|
argv += ["--curdir", curdir]
|
|
|
|
if source:
|
|
if isinstance(source, (int, long)) or source.isdigit():
|
|
argv += ["--from", "%s" % source]
|
|
else:
|
|
argv += ["--from-process", source]
|
|
|
|
if maximize:
|
|
argv += ["--maximize"]
|
|
|
|
try:
|
|
output = subprocess_checkoutput(argv, env)
|
|
self.pid, self.tid = map(int, output.split())
|
|
except subprocess.CalledProcessError as e:
|
|
log.error(
|
|
"Failed to execute process from path %r with "
|
|
"arguments %r (Error: %s)", path, argv, e
|
|
)
|
|
return False
|
|
|
|
# Report this PID to the kernel driver (if present).
|
|
zer0m0n.addpid(self.pid)
|
|
|
|
# With .NET for AnyCPU target, a 32-bit PE file can start a 64-bit
|
|
# process. Recheck the process bitness here after startup to make sure
|
|
# injection works.
|
|
is32bit = self.is32bit(self.pid)
|
|
|
|
if not dll:
|
|
if is32bit:
|
|
dll = "monitor-x86.dll"
|
|
else:
|
|
dll = "monitor-x64.dll"
|
|
|
|
dllpath = os.path.abspath(os.path.join("bin", dll))
|
|
|
|
if not os.path.exists(dllpath):
|
|
log.warning("No valid DLL specified to be injected, "
|
|
"injection aborted.")
|
|
return False
|
|
|
|
if is32bit:
|
|
inject_exe = os.path.join("bin", "inject-x86.exe")
|
|
else:
|
|
inject_exe = os.path.join("bin", "inject-x64.exe")
|
|
|
|
argv = [
|
|
inject_exe,
|
|
"--resume-thread",
|
|
"--pid", "%s" % self.pid,
|
|
"--tid", "%s" % self.tid,
|
|
]
|
|
|
|
if free:
|
|
argv.append("--free")
|
|
else:
|
|
argv += [
|
|
"--apc",
|
|
"--dll", dllpath,
|
|
"--config", self.drop_config(mode=mode, trigger=trigger),
|
|
]
|
|
|
|
try:
|
|
subprocess_checkoutput(argv, env)
|
|
except subprocess.CalledProcessError as e:
|
|
log.error(
|
|
"Failed to execute process from path %r with "
|
|
"arguments %r (Error: %s)", path, argv, e
|
|
)
|
|
return False
|
|
|
|
log.info("Successfully executed process from path %r with "
|
|
"arguments %r and pid %d", path, args or "", self.pid)
|
|
return True
|
|
|
|
def terminate(self):
|
|
"""Terminate process.
|
|
@return: operation status.
|
|
"""
|
|
process_handle = self.open_process()
|
|
|
|
ret = KERNEL32.TerminateProcess(process_handle, 1)
|
|
KERNEL32.CloseHandle(process_handle)
|
|
|
|
if ret:
|
|
log.info("Successfully terminated process with pid %d.", self.pid)
|
|
return True
|
|
else:
|
|
log.error("Failed to terminate process with pid %d.", self.pid)
|
|
return False
|
|
|
|
def inject(self, dll=None, apc=False, track=True, mode=None):
|
|
"""Inject our monitor into the specified process.
|
|
@param dll: Cuckoo DLL path.
|
|
@param apc: Use APC injection.
|
|
@param track: Track this process in the analyzer.
|
|
@param mode: Monitor mode - which functions to instrument.
|
|
"""
|
|
if not self.pid and not self.process_name:
|
|
log.warning("No valid pid or process name specified, "
|
|
"injection aborted.")
|
|
return False
|
|
|
|
# Only check whether the process is still alive when it's identified
|
|
# by a process identifier. Not when it's identified by a process name.
|
|
if not self.process_name and not self.is_alive():
|
|
log.warning("The process with pid %s is not alive, "
|
|
"injection aborted", self.pid)
|
|
return False
|
|
|
|
if self.process_name:
|
|
is32bit = self.is32bit(process_name=self.process_name)
|
|
elif self.pid:
|
|
is32bit = self.is32bit(pid=self.pid)
|
|
|
|
if not dll:
|
|
if is32bit:
|
|
dll = "monitor-x86.dll"
|
|
else:
|
|
dll = "monitor-x64.dll"
|
|
|
|
dllpath = os.path.abspath(os.path.join("bin", dll))
|
|
if not os.path.exists(dllpath):
|
|
log.warning("No valid DLL specified to be injected in process "
|
|
"with pid %s / process name %s, injection aborted.",
|
|
self.pid, self.process_name)
|
|
return False
|
|
|
|
if is32bit:
|
|
inject_exe = os.path.join("bin", "inject-x86.exe")
|
|
else:
|
|
inject_exe = os.path.join("bin", "inject-x64.exe")
|
|
|
|
args = [
|
|
inject_exe,
|
|
"--dll", dllpath,
|
|
"--config", self.drop_config(track=track, mode=mode),
|
|
]
|
|
|
|
if self.pid:
|
|
args += ["--pid", "%s" % self.pid]
|
|
elif self.process_name:
|
|
args += ["--process-name", self.process_name]
|
|
|
|
if apc:
|
|
args += ["--apc", "--tid", "%s" % self.tid]
|
|
else:
|
|
args += ["--crt"]
|
|
|
|
try:
|
|
subprocess_checkcall(args)
|
|
except Exception:
|
|
log.error("Failed to inject %s-bit process with pid %s and "
|
|
"process name %s", 32 if is32bit else 64, self.pid,
|
|
self.process_name)
|
|
return False
|
|
|
|
return True
|
|
|
|
def drop_config(self, track=True, mode=None, trigger=None):
|
|
"""Helper function to drop the configuration for a new process."""
|
|
fd, config_path = tempfile.mkstemp()
|
|
|
|
# The first time we come up with a random startup-time.
|
|
if Process.first_process:
|
|
# This adds 1 up to 30 times of 20 minutes to the startup
|
|
# time of the process, therefore bypassing anti-vm checks
|
|
# which check whether the VM has only been up for <10 minutes.
|
|
Process.startup_time = random.randint(1, 30) * 20 * 60 * 1000
|
|
|
|
lines = {
|
|
"pipe": self.config.pipe,
|
|
"logpipe": self.config.logpipe,
|
|
"analyzer": os.getcwd(),
|
|
"first-process": "1" if Process.first_process else "0",
|
|
"startup-time": Process.startup_time,
|
|
"shutdown-mutex": SHUTDOWN_MUTEX,
|
|
"force-sleepskip": self.config.options.get("force-sleepskip", "0"),
|
|
"track": "1" if track else "0",
|
|
"mode": mode or "",
|
|
"disguise": self.config.options.get("disguise", "0"),
|
|
"pipe-pid": "1",
|
|
"trigger": (trigger or "").encode("utf8"),
|
|
}
|
|
|
|
for key, value in lines.items():
|
|
os.write(fd, "%s=%s\n" % (key, value))
|
|
|
|
os.close(fd)
|
|
|
|
# Only change the first_process attribute for processes that we
|
|
# "track", i.e., the lsass.exe injection doesn't count.
|
|
if track:
|
|
Process.first_process = False
|
|
|
|
return config_path
|
|
|
|
def dump_memory(self, addr=None, length=None):
|
|
"""Dump process memory, optionally target only a certain memory range.
|
|
@return: operation status.
|
|
"""
|
|
if not self.pid:
|
|
log.warning("No valid pid specified, memory dump aborted")
|
|
return False
|
|
|
|
if not self.is_alive():
|
|
log.warning("The process with pid %d is not alive, memory "
|
|
"dump aborted", self.pid)
|
|
return False
|
|
|
|
if self.is32bit(pid=self.pid):
|
|
inject_exe = os.path.join("bin", "inject-x86.exe")
|
|
else:
|
|
inject_exe = os.path.join("bin", "inject-x64.exe")
|
|
|
|
# Take the memory dump.
|
|
dump_path = tempfile.mktemp()
|
|
|
|
try:
|
|
args = [
|
|
inject_exe,
|
|
"--pid", "%s" % self.pid,
|
|
"--dump", dump_path,
|
|
]
|
|
|
|
# Restrict to a certain memory block.
|
|
if addr and length:
|
|
args += [
|
|
"--dump-block",
|
|
"0x%x" % addr,
|
|
"%s" % length,
|
|
]
|
|
|
|
subprocess_checkcall(args)
|
|
except subprocess.CalledProcessError:
|
|
log.error("Failed to dump memory of %d-bit process with pid %d.",
|
|
32 if self.is32bit(pid=self.pid) else 64, self.pid)
|
|
return
|
|
|
|
# Calculate the next index and send the process memory dump over to
|
|
# the host. Keep in mind that one process may have multiple process
|
|
# memory dumps in the future.
|
|
idx = self.dumpmem[self.pid] = self.dumpmem.get(self.pid, 0) + 1
|
|
|
|
if addr and length:
|
|
file_name = os.path.join(
|
|
"memory", "block-%s-0x%x-%s.dmp" % (self.pid, addr, idx)
|
|
)
|
|
else:
|
|
file_name = os.path.join("memory", "%s-%s.dmp" % (self.pid, idx))
|
|
|
|
upload_to_host(dump_path, file_name)
|
|
os.unlink(dump_path)
|
|
|
|
log.info("Memory dump of process with pid %d completed", self.pid)
|
|
return True
|
|
|
|
# The dump_memory_block functionality has been integrated with the
|
|
# dump_memory function, this alias is just for backwards compatibility.
|
|
dump_memory_block = dump_memory
|