diff --git a/docs/vmcreation.rst b/docs/vmcreation.rst index da97e21..62a6876 100644 --- a/docs/vmcreation.rst +++ b/docs/vmcreation.rst @@ -114,7 +114,7 @@ See the example below for its help page. --network TEXT The network to use in CIDR notation. Example: 192.168.30.0/24. Uses VM platform default if not given. - --port INTEGER Port to run the Agent on. [default: 8000] + --port INTEGER Port to run the Agent on. [default: 8000] Define a port other than 8000 when creating a VM for CAPEv2 --ip TEXT Guest IP address to use --iso TEXT Specify install ISO to use. --vm TEXT Virtual Machinery. [default: qemu] @@ -129,6 +129,12 @@ The install might be frozen, etc. We will be making an image called 'win10base' This command can take a long time to complete depending on your system (20-60 minutes). +If you are using VMCloak to setup a QEMU VM for CAPEv2 define a port other than 8000. The port 8000 is going to be used by the CAPE agent. + +.. code-block:: bash + + vmcloak --debug init --port 8111 --win10x64 --hddsize 128 --cpus 2 --ramsize 4096 --network 192.168.30.0/24 --vm qemu --vrde --vrde-port 1 --ip 192.168.30.2 --iso-mount /mnt/win10x64 win10base qemubr0 + When the command finishes, the image should be available in the list of images. View the list of images and their attributes using: diff --git a/setup.py b/setup.py index 470c45e..b57fe96 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ install_requires=[ "click", "jinja2", - "pefile>=2019.4.18, <2019.5.0" + "pefile<2023.0.0", "pyyaml>=5.1", "sqlalchemy>=1.4, <1.5", "alembic>=1.7.4, <1.8", diff --git a/vmcloak/abstract.py b/vmcloak/abstract.py index b3bdfcc..1120397 100644 --- a/vmcloak/abstract.py +++ b/vmcloak/abstract.py @@ -162,9 +162,14 @@ def buildiso(self, mount, newiso, bootstrap, tmp_dir=None, env_vars={}): # Copy the agent binary to the tmp bootstrap folder with the extension # it should have, but using a normalized name. agent_name = f"{random_string(8, 16)}{file_ext}" + agent_name_cape = f"{random_string(8, 16)}.pyw" + agent_path_cape = os.path.splitext(agent_path)[0]+'.pyw' shutil.copy(agent_path, os.path.join(bootstrap_copy, agent_name)) + shutil.copy(agent_path_cape, os.path.join(bootstrap_copy, agent_name_cape)) env_vars["AGENT_FILE"] = agent_name + env_vars["AGENT_FILE_CAPE"] = agent_name_cape env_vars["AGENT_RUNKEY"] = random_string(8, 16) + env_vars["AGENT_RUNKEY_CAPE"] = random_string(8, 16) # Write the configuration values for bootstrap.bat. with open(os.path.join(bootstrap_copy, "settings.bat"), "wb") as f: diff --git a/vmcloak/data/bootstrap/windows/agent/agent_windows_amd64.pyw b/vmcloak/data/bootstrap/windows/agent/agent_windows_amd64.pyw new file mode 100644 index 0000000..f839e58 --- /dev/null +++ b/vmcloak/data/bootstrap/windows/agent/agent_windows_amd64.pyw @@ -0,0 +1,763 @@ +# Copyright (C) 2010-2019 Cuckoo Foundation. +# This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org +# See the file 'docs/LICENSE' for copying permission. + +import argparse +import base64 +import cgi +import enum +import http.server +import ipaddress +import json +import multiprocessing +import os +import platform +import shlex +import shutil +import socket +import socketserver +import stat +import subprocess +import sys +import tempfile +import traceback +from io import StringIO +from typing import Iterable +from zipfile import ZipFile + +try: + import re2 as re +except ImportError: + import re + +if sys.version_info[:2] < (3, 6): + sys.exit("You are running an incompatible version of Python, please use >= 3.6") + +# You must run x86 version not x64 +# The analysis process interacts with low-level Windows libraries that need a +# x86 Python to be running. +# (see https://github.com/kevoreilly/CAPEv2/issues/1680) +if sys.maxsize > 2**32 and sys.platform == "win32": + sys.exit("You should install python3 x86! not x64") + +AGENT_VERSION = "0.17" +AGENT_FEATURES = [ + "execpy", + "execute", + "pinning", + "logs", + "largefile", + "unicodepath", +] +BASE_64_ENCODING = "base64" + +if sys.platform == "win32": + AGENT_FEATURES.append("mutex") + MUTEX_TIMEOUT_MS = 500 + from ctypes import WinError, windll + + kernel32 = windll.kernel32 + SYNCHRONIZE = 0x100000 + ERROR_FILE_NOT_FOUND = 0x2 + WAIT_ABANDONED = 0x00000080 + WAIT_OBJECT_0 = 0x0 + WAIT_TIMEOUT = 0x102 + WAIT_FAILED = 0xFFFFFFFF + + +class Status(enum.IntEnum): + INIT = 1 + RUNNING = 2 + COMPLETE = 3 + FAILED = 4 + EXCEPTION = 5 + + def __str__(self): + return f"{self.name.lower()}" + + @classmethod + def _missing_(cls, value): + if not isinstance(value, str): + return None + value = value.lower() + for member in cls: + if str(member) == value: + return member + if value.isnumeric() and int(value) == member.value: + return member + return None + + +ANALYZER_FOLDER = "" +agent_mutexes = {} +"""Holds handles of mutexes held by the agent.""" +state = { + "status": Status.INIT, + "description": "", + "async_subprocess": None, + "mutexes": agent_mutexes, +} + + +class MiniHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + server_version = "CAPE Agent" + + def do_GET(self): + request.client_ip, request.client_port = self.client_address + request.form = {} + request.files = {} + request.method = "GET" + + self.httpd.handle(self) + + def do_POST(self): + environ = { + "REQUEST_METHOD": "POST", + "CONTENT_TYPE": self.headers.get("Content-Type"), + } + + form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=environ) + + request.client_ip, request.client_port = self.client_address + request.form = {} + request.files = {} + request.method = "POST" + + if form.list: + for key in form.keys(): + value = form[key] + if value.filename: + request.files[key] = value.file + else: + request.form[key] = value.value + self.httpd.handle(self) + + def do_DELETE(self): + environ = { + "REQUEST_METHOD": "DELETE", + "CONTENT_TYPE": self.headers.get("Content-Type"), + } + + form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=environ) + + request.client_ip, request.client_port = self.client_address + request.form = {} + request.files = {} + request.method = "DELETE" + + if form.list: + for key in form.keys(): + value = form[key] + if value.filename: + request.files[key] = value.file + else: + request.form[key] = value.value + self.httpd.handle(self) + + +class MiniHTTPServer: + def __init__(self): + self.handler = MiniHTTPRequestHandler + + # Reference back to the server. + self.handler.httpd = self + + self.routes = { + "GET": [], + "POST": [], + "DELETE": [], + } + + def run( + self, + host: ipaddress.IPv4Address = "0.0.0.0", + port: int = 8000, + event: multiprocessing.Event = None, + ): + socketserver.TCPServer.allow_reuse_address = True + self.s = socketserver.TCPServer((host, port), self.handler) + + # tell anyone waiting that they're good to go + if event: + event.set() + + self.s.serve_forever() + + def route(self, path: str, methods: Iterable[str] = ["GET"]): + def register(fn): + for method in methods: + self.routes[method].append((re.compile(f"{path}$"), fn)) + return fn + + return register + + def handle(self, obj): + if "client_ip" in state and request.client_ip != state["client_ip"]: + if request.client_ip != "127.0.0.1": + return + if obj.path != "/status" or request.method != "POST": + return + + for route, fn in self.routes[obj.command]: + if route.match(obj.path): + ret = fn() + break + else: + ret = json_error(404, message="Route not found") + + ret.init() + obj.send_response(ret.status_code) + ret.headers(obj) + obj.end_headers() + + if isinstance(ret, jsonify): + obj.wfile.write(ret.json().encode()) + elif isinstance(ret, send_file): + ret.write(obj, obj.wfile) + + if hasattr(self, "s") and self.s._BaseServer__shutdown_request: + self.close_connection = True + + def shutdown(self): + # BaseServer also features a .shutdown() method, but you can't use + # that from the same thread as that will deadlock the whole thing. + if hasattr(self, "s"): + self.s._BaseServer__shutdown_request = True + else: + # When running unit tests in Windows, the system would hang here, + # until this `exit(1)` was added. + print(f"{self} has no 's' attribute") + exit(1) + + +class jsonify: + """Wrapper that represents Flask.jsonify functionality.""" + + def __init__(self, status_code=200, **kwargs): + self.status_code = status_code + self.values = kwargs + + def init(self): + pass + + def json(self): + for valkey in self.values: + if isinstance(self.values[valkey], bytes): + self.values[valkey] = self.values[valkey].decode("utf8", "replace") + try: + retdata = json.dumps(self.values) + except Exception as ex: + retdata = json.dumps({"error": f"Error serializing json data: {ex.args[0]}"}) + + return retdata + + def headers(self, obj): + pass + + +class send_file: + """Wrapper that represents Flask.send_file functionality.""" + + def __init__(self, path, encoding): + self.length = None + self.path = path + self.status_code = 200 + self.encoding = encoding + + def okay_to_send(self): + return os.path.isfile(self.path) and os.access(self.path, os.R_OK) + + def init(self): + if self.okay_to_send(): + if self.encoding != BASE_64_ENCODING: + self.length = os.path.getsize(self.path) + else: + self.status_code = 404 + + def write(self, httplog, sock): + if not self.okay_to_send(): + return + + try: + with open(self.path, "rb") as f: + buf = f.read(1024 * 1024) + while buf: + if self.encoding == BASE_64_ENCODING: + buf = base64.b64encode(buf) + sock.write(buf) + buf = f.read(1024 * 1024) + except Exception as ex: + httplog.log_error(f"Error reading file {self.path}: {ex}") + + def headers(self, obj): + obj.send_header("Content-Length", self.length) + + +class request: + form = {} + files = {} + client_ip = None + client_port = None + method = None + environ = { + "werkzeug.server.shutdown": lambda: app.shutdown(), + } + + +app = MiniHTTPServer() + + +def isAdmin(): + is_admin = None + try: + if sys.platform == "win32": + import ctypes + + is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 + else: + is_admin = os.getuid() == 0 + except Exception as e: + print(e) + + return is_admin + + +def json_error(error_code: int, message: str, **kwargs) -> jsonify: + r = jsonify(message=message, error_code=error_code, **kwargs) + r.status_code = error_code + return r + + +def json_exception(message: str) -> jsonify: + r = jsonify(message=message, error_code=500, traceback=traceback.format_exc()) + r.status_code = 500 + return r + + +def json_success(message: str, status_code=200, **kwargs) -> jsonify: + return jsonify(message=message, status_code=status_code, **kwargs) + + +@app.route("/") +def get_index(): + is_admin = isAdmin() + return json_success("CAPE Agent!", version=AGENT_VERSION, features=AGENT_FEATURES, is_user_admin=bool(is_admin)) + + +def get_subprocess_status(): + """Return the subprocess status.""" + async_subprocess = state.get("async_subprocess") + message = "Analysis status" + exitcode = async_subprocess.exitcode + if exitcode is None or (sys.platform == "win32" and exitcode == 259): + # Process is still running. + state["status"] = Status.RUNNING + return json_success( + message=message, + status=str(state.get("status")), + description=state.get("description"), + process_id=async_subprocess.pid, + ) + # Process completed; reset async subprocess state. + state["async_subprocess"] = None + if exitcode == 0: + state["status"] = Status.COMPLETE + state["description"] = "" + else: + state["status"] = Status.FAILED + state["description"] = f"Exited with exit code {exitcode}" + return json_success( + message=message, + status=str(state.get("status")), + description=state.get("description"), + exitcode=exitcode, + ) + + +def open_mutex(mutex_name): + assert sys.platform == "win32" + access = SYNCHRONIZE # only flag the mutex for use + inherit_handle = False # don't pass the handle to children + hndl_mutex = kernel32.OpenMutexW(access, inherit_handle, mutex_name) + if not hndl_mutex: + winerr = WinError() + if winerr.errno == ERROR_FILE_NOT_FOUND: + return None, json_error(404, "mutex not found") + return None, json_error(500, f"error accessing mutex: {winerr}") + return hndl_mutex, None + + +def wait_mutex(hndl_mutex): + assert sys.platform == "win32" + ret = kernel32.WaitForSingleObject(hndl_mutex, MUTEX_TIMEOUT_MS) + if ret in (WAIT_ABANDONED, WAIT_OBJECT_0): + return True, None + elif ret == WAIT_TIMEOUT: + return False, json_error(408, "timeout waiting for mutex") + elif ret == WAIT_FAILED: + # get the extended error information + winerr = WinError() + return False, json_error(500, f"failed waiting for mutex: {winerr}") + else: + return False, json_error(500, f"failed waiting for mutex: {ret}") + + +def release_mutex(hndl_mutex): + assert sys.platform == "win32" + ret = kernel32.ReleaseMutex(hndl_mutex) + if not ret: + # get the extended error information + winerr = WinError() + return False, json_error(500, f"failed releasing mutex: {winerr}") + return True, None + + +@app.route("/status") +def get_status(): + if state["status"] != Status.COMPLETE and state.get("async_subprocess") is not None: + return get_subprocess_status() + return json_success("Analysis status", status=str(state.get("status")), description=state.get("description")) + + +@app.route("/mutex", methods=["POST"]) +def post_mutex(): + if sys.platform != "win32": + return json_error(400, f"mutex feature not supported on {sys.platform}") + mutex_name = request.form.get("mutex", "") + if not mutex_name: + return json_error(400, "no mutex provided") + if mutex_name in agent_mutexes: + return json_success(f"have mutex: {mutex_name}") + + # does the mutex exist? + hndl_mutex, error = open_mutex(mutex_name) + if error: + return error + + # try waiting on it + ok, error = wait_mutex(hndl_mutex) + if ok: + # save the mutex handle for future requests + agent_mutexes[mutex_name] = hndl_mutex + return json_success(f"got mutex: {mutex_name}", status_code=201) + return error + + +@app.route("/mutex", methods=["DELETE"]) +def delete_mutex(): + if sys.platform != "win32": + return json_error(400, f"mutex feature not supported on {sys.platform}") + mutex_name = request.form.get("mutex", "") + if not mutex_name: + return json_error(400, "no mutex provided") + if mutex_name not in agent_mutexes: + return json_error(404, f"mutex does not exist: {mutex_name}") + hndl_mutex = agent_mutexes.pop(mutex_name) + ok, error = release_mutex(hndl_mutex) + if ok: + return json_success(f"released mutex: {mutex_name}") + return error + + +@app.route("/status", methods=["POST"]) +def put_status(): + try: + status = Status(request.form.get("status")) + except ValueError: + return json_error(400, "No valid status has been provided") + + state["status"] = status + state["description"] = request.form.get("description") + return json_success("Analysis status updated") + + +@app.route("/logs") +def get_logs(): + if isinstance(sys.stdout, StringIO): + stdoutbuf = sys.stdout.getvalue() + stderrbuf = sys.stderr.getvalue() + else: + stdoutbuf = "verbose mode, stdout not saved" + stderrbuf = "verbose mode, stderr not saved" + return json_success("Agent logs", stdout=stdoutbuf, stderr=stderrbuf) + + +@app.route("/system") +def get_system(): + return json_success("System", system=platform.system()) + + +@app.route("/environ") +def get_environ(): + return json_success("Environment variables", environ=dict(os.environ)) + + +@app.route("/path") +def get_path(): + return json_success("Agent path", filepath=os.path.abspath(__file__)) + + +@app.route("/mkdir", methods=["POST"]) +def do_mkdir(): + if "dirpath" not in request.form: + return json_error(400, "No dirpath has been provided") + + try: + mode = int(request.form.get("mode", 0o777)) + + os.makedirs(request.form["dirpath"], mode=mode, exist_ok=True) + except Exception as ex: + print(f"error creating dir {ex}") + return json_exception("Error creating directory") + + return json_success("Successfully created directory") + + +@app.route("/mktemp", methods=("GET", "POST")) +def do_mktemp(): + suffix = request.form.get("suffix", "") + prefix = request.form.get("prefix", "tmp") + dirpath = request.form.get("dirpath") + + try: + fd, filepath = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dirpath) + except Exception: + return json_exception("Error creating temporary file") + + os.close(fd) + + return json_success("Successfully created temporary file", filepath=filepath) + + +@app.route("/mkdtemp", methods=("GET", "POST")) +def do_mkdtemp(): + suffix = request.form.get("suffix", "") + prefix = request.form.get("prefix", "tmp") + dirpath = request.form.get("dirpath") + + try: + dirpath = tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=dirpath) + except Exception: + return json_exception("Error creating temporary directory") + + return json_success("Successfully created temporary directory", dirpath=dirpath) + + +@app.route("/store", methods=["POST"]) +def do_store(): + if "filepath" not in request.form: + return json_error(400, "No filepath has been provided") + + if "file" not in request.files: + return json_error(400, "No file has been provided") + + try: + with open(request.form["filepath"], "wb") as f: + shutil.copyfileobj(request.files["file"], f, 10 * 1024 * 1024) + except Exception as ex: + return json_exception(f"Error storing file: {ex}") + + return json_success("Successfully stored file") + + +@app.route("/retrieve", methods=["POST"]) +def do_retrieve(): + if "filepath" not in request.form: + return json_error(400, "No filepath has been provided") + + return send_file(request.form["filepath"], request.form.get("encoding", "")) + + +@app.route("/extract", methods=["POST"]) +def do_extract(): + if "dirpath" not in request.form: + return json_error(400, "No dirpath has been provided") + + if "zipfile" not in request.files: + return json_error(400, "No zip file has been provided") + + try: + with ZipFile(request.files["zipfile"], "r") as archive: + archive.extractall(request.form["dirpath"]) + except Exception as ex: + return json_exception(f"Error extracting zip file {ex}") + + return json_success("Successfully extracted zip file") + + +@app.route("/remove", methods=["POST"]) +def do_remove(): + if "path" not in request.form: + return json_error(400, "No path has been provided") + + try: + if os.path.isdir(request.form["path"]): + # Mark all files as readable so they can be deleted. + for dirpath, _, filenames in os.walk(request.form["path"]): + for filename in filenames: + os.chmod(os.path.join(dirpath, filename), stat.S_IWRITE) + + shutil.rmtree(request.form["path"]) + message = "Successfully deleted directory" + elif os.path.isfile(request.form["path"]): + os.chmod(request.form["path"], stat.S_IWRITE) + os.remove(request.form["path"]) + message = "Successfully deleted file" + else: + return json_error(404, "Path provided does not exist") + except Exception: + return json_exception("Error removing file or directory") + + return json_success(message) + + +@app.route("/execute", methods=["POST"]) +def do_execute(): + local_ip = socket.gethostbyname(socket.gethostname()) + + if "command" not in request.form: + return json_error(400, "No command has been provided") + command_to_execute = shlex.split(request.form["command"]) + + # only allow date command from localhost. Even this is just to + # let it be tested + allowed_commands = ["date", "cmd /c date /t"] + if request.client_ip in ("127.0.0.1", local_ip) and request.form["command"] not in allowed_commands: + return json_error(500, "Not allowed to execute commands") + + # Execute the command asynchronously? As a shell command? + async_exec = "async" in request.form + shell = "shell" in request.form + + cwd = request.form.get("cwd") + stdout = stderr = None + + try: + if async_exec: + subprocess.Popen(command_to_execute, shell=shell, cwd=cwd) + else: + p = subprocess.Popen(command_to_execute, shell=shell, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if request.form.get("encoding", "") == BASE_64_ENCODING: + stdout = base64.b64encode(stdout) + stderr = base64.b64encode(stderr) + except Exception as ex: + state["status"] = Status.FAILED + state["description"] = "Error execute command" + return json_exception(f"Error executing command: {ex}") + + state["status"] = Status.RUNNING + state["description"] = "" + return json_success("Successfully executed command", stdout=stdout, stderr=stderr) + + +def run_subprocess(command_args, cwd, base64_encode, shell=False): + """Execute the subprocess, wait for completion. + + Return the exitcode (returncode), the stdout, and the stderr. + """ + p = subprocess.Popen( + args=command_args, + cwd=cwd, + shell=shell, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate() + if base64_encode: + stdout = base64.b64encode(stdout) + stderr = base64.b64encode(stderr) + return p.returncode, stdout, stderr + + +def background_subprocess(command_args, cwd, base64_encode, shell=False): + """Run subprocess, wait for completion, then exit. + + This process must exit, so the parent process (agent) can find the exit status.""" + # TODO: return the stdout/stderr to the parent process. + returncode, stdout, stderr = run_subprocess(command_args, cwd, base64_encode, shell) + sys.stdout.write(stdout.decode("ascii")) + sys.stderr.write(stderr.decode("ascii")) + sys.exit(returncode) + + +def spawn(args, cwd, base64_encode, shell=False): + """Kick off a subprocess in the background.""" + run_subprocess_args = [args, cwd, base64_encode, shell] + proc = multiprocessing.Process(target=background_subprocess, name=f"child process {args[1]}", args=run_subprocess_args) + proc.start() + state["status"] = Status.RUNNING + state["description"] = "" + state["async_subprocess"] = proc + return json_success("Successfully spawned command", process_id=proc.pid) + + +@app.route("/execpy", methods=["POST"]) +def do_execpy(): + if "filepath" not in request.form: + return json_error(400, "No Python file has been provided") + + # Execute the command asynchronously? As a shell command? + async_exec = "async" in request.form + base64_encode = request.form.get("encoding", "") == BASE_64_ENCODING + + cwd = request.form.get("cwd") + + args = ( + sys.executable, + request.form["filepath"], + ) + + if async_exec and state["status"] == Status.RUNNING and state["async_subprocess"]: + return json_error(400, "Async process already running.") + try: + if async_exec: + return spawn(args, cwd, base64_encode) + exitcode, stdout, stderr = run_subprocess(args, cwd, base64_encode) + if exitcode == 0: + state["status"] = Status.COMPLETE + state["description"] = "" + return json_success("Successfully executed command", stdout=stdout, stderr=stderr) + # Process exited with non-zero result. + state["status"] = Status.FAILED + message = "Error executing python command." + state["description"] = message + return json_error(400, message, stdout=stdout, stderr=stderr, exitcode=exitcode) + except Exception as ex: + state["status"] = Status.FAILED + state["description"] = "Error executing Python command" + return json_exception(f"Error executing Python command: {ex}") + + +@app.route("/pinning") +def do_pinning(): + if "client_ip" in state: + return json_error(500, "Agent has already been pinned to an IP!") + + state["client_ip"] = request.client_ip + return json_success("Successfully pinned Agent", client_ip=request.client_ip) + + +@app.route("/kill") +def do_kill(): + shutdown = request.environ.get("werkzeug.server.shutdown") + if shutdown is None: + return json_error(500, "Not running with the Werkzeug server") + + shutdown() + return json_success("Quit the CAPE Agent") + + +if __name__ == "__main__": + multiprocessing.set_start_method("spawn") + parser = argparse.ArgumentParser() + parser.add_argument("host", nargs="?", default="0.0.0.0") + parser.add_argument("port", type=int, nargs="?", default=8000) + parser.add_argument("-v", "--verbose", action="store_true") + args = parser.parse_args() + + if not args.verbose: + sys.stdout = StringIO() + sys.stderr = StringIO() + + app.run(host=args.host, port=args.port) \ No newline at end of file diff --git a/vmcloak/data/bootstrap/windows/bootstrap.bat b/vmcloak/data/bootstrap/windows/bootstrap.bat index 5615c4c..f1ab2fa 100644 --- a/vmcloak/data/bootstrap/windows/bootstrap.bat +++ b/vmcloak/data/bootstrap/windows/bootstrap.bat @@ -23,6 +23,7 @@ start /w C:\vmcloak\%PYTHONINSTALLER% PrependPath=1 TargetDir=%PYTHONPATH% /pass echo Copying agent file to c:\windows\system32\%AGENT_FILE% copy c:\vmcloak\%AGENT_FILE% c:\windows\system32\%AGENT_FILE% +copy c:\vmcloak\%AGENT_FILE_CAPE% c:\%AGENT_FILE_CAPE% echo Setting the resolution. %PYTHONPATH%\python.exe C:\vmcloak\resolution.py %RESO_WIDTH% %RESO_HEIGHT% @@ -32,6 +33,7 @@ reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\S echo Adding agent autorun key. Agent port: %AGENT_PORT% reg add HKLM\Software\Microsoft\Windows\CurrentVersion\Run /v %AGENT_RUNKEY% /t REG_SZ /d "c:\windows\system32\%AGENT_FILE% -host 0.0.0.0 -port %AGENT_PORT%" /f +reg add HKLM\Software\Microsoft\Windows\CurrentVersion\Run /v %AGENT_RUNKEY_CAPE% /t REG_SZ /d "c:\%AGENT_FILE_CAPE%" /f powershell -ExecutionPolicy bypass -File c:\vmcloak\genericsettings.ps1 diff --git a/vmcloak/data/wall.jpg b/vmcloak/data/wall.jpg new file mode 100644 index 0000000..58340f5 Binary files /dev/null and b/vmcloak/data/wall.jpg differ diff --git a/vmcloak/dependencies/dotnet.py b/vmcloak/dependencies/dotnet.py index 694d427..76083e6 100644 --- a/vmcloak/dependencies/dotnet.py +++ b/vmcloak/dependencies/dotnet.py @@ -68,15 +68,19 @@ class DotNet(Dependency): "https://cuckoo.sh/vmcloak/NDP47-KB3186497-x86-x64-AllOS-ENU.exe", ], "sha1": "76054141a492ba307595250bda05ad4e0694cdc3", - }, - { + }, { "version": "4.7.2", "urls": [ "https://hatching.io/hatchvm/NDP472-KB4054530-x86-x64-AllOS-ENU.exe", ], "sha1": "31fc0d305a6f651c9e892c98eb10997ae885eb1e", - } - ] + }, { + "version": "4.8", + "urls": [ + "https://download.visualstudio.microsoft.com/download/pr/2d6bb6b2-226a-4baa-bdec-798822606ff1/8494001c276a4b96804cde7829c04d7f/ndp48-x86-x64-allos-enu.exe", + ], + "sha1": "e322e2e0fb4c86172c38a97dc6c71982134f0570", + }] def run(self): self.upload_dependency("C:\\setup.exe") diff --git a/vmcloak/dependencies/firefox.py b/vmcloak/dependencies/firefox.py index 46420dc..a32c225 100644 --- a/vmcloak/dependencies/firefox.py +++ b/vmcloak/dependencies/firefox.py @@ -21,12 +21,16 @@ class Firefox(Dependency): "version": "63.0.3", "url": "https://cuckoo.sh/vmcloak/firefox_63_0_3.exe", "sha1": "c5f03fc93aebd2db9da14ba6eb1f01e98e18d95b", + }, { + "version": "latest", + "url": "https://download-installer.cdn.mozilla.net/pub/firefox/releases/123.0/win64/en-US/Firefox%20Setup%20123.0.exe", + "sha1": "75f85bbc5934c7f7ff4c4794a9c8d8ad45e79040", }] def run(self): - self.upload_dependency("C:\\Firefox_Setup_41.0.2.exe") - self.a.execute("C:\\Firefox_Setup_41.0.2.exe -ms") - self.a.remove("C:\\Firefox_Setup_41.0.2.exe") + self.upload_dependency("C:\\Firefox_Setup_123.0.exe") + self.a.execute("C:\\Firefox_Setup_123.0.exe -ms") + self.a.remove("C:\\Firefox_Setup_123.0.exe") class Firefox41(Firefox, Dependency): """Backwards compatibility""" diff --git a/vmcloak/dependencies/python.py b/vmcloak/dependencies/python.py index 4de9800..0c9c14b 100644 --- a/vmcloak/dependencies/python.py +++ b/vmcloak/dependencies/python.py @@ -41,19 +41,31 @@ class Python(Dependency): "window_name": "Python 3.7.3 (64-bit) Setup", "install_path": "C:\\Python3" }, + # { + # "arch": "amd64", + # "version": "3.10.0", + # "target": "win10x64", + # "urls": [ + # "https://www.python.org/ftp/python/3.10.0/python-3.10.0-amd64.exe", + # ], + # "sha1": "3ee4e92a8ef94c70fb56859503fdc805d217d689", + # "filename": "python-3.10.0-amd64.exe", + # "window_name": "Python 3.10.0 (64-bit) Setup", + # "install_path": "C:\\Python3" + # } { + #The 32 bits version of python is used for windows 10 because it's the one compatible with CAPE. "arch": "amd64", "version": "3.10.0", "target": "win10x64", "urls": [ - "https://www.python.org/ftp/python/3.10.0/python-3.10.0-amd64.exe", - ], - "sha1": "3ee4e92a8ef94c70fb56859503fdc805d217d689", - "filename": "python-3.10.0-amd64.exe", - "window_name": "Python 3.10.0 (64-bit) Setup", + "https://www.python.org/ftp/python/3.10.0/python-3.10.0.exe", + ], + "sha1": "91ee198775bf24ccf4dedfd3e83f7d7ef85a2b9f", + "filename": "python-3.10.0.exe", + "window_name": "Python 3.10.0 (32-bit) Setup", "install_path": "C:\\Python3" } - ] def run(self): diff --git a/vmcloak/dependencies/vcredist.py b/vmcloak/dependencies/vcredist.py index ab56dcc..38e2208 100644 --- a/vmcloak/dependencies/vcredist.py +++ b/vmcloak/dependencies/vcredist.py @@ -22,7 +22,8 @@ class VcRedist(Dependency): "2012": "/passive /norestart", "2013": "/passive /norestart", "2015": "/passive /norestart", - "2019": "/qb /quiet /norestart" + "2019": "/qb /quiet /norestart", + "2022": "/qb /quiet /norestart" } exes = [{ @@ -296,6 +297,13 @@ class VcRedist(Dependency): "https://hatching.dev/hatchvm/vcredist2019_x64.exe", ], "sha1": "de385d69864413400250f2f3fe9f4aec78eb997b" + },{ + "version": "2022", + "arch": "amd64", + "urls": [ + "https://aka.ms/vs/17/release/vc_redist.x64.exe", + ], + "sha1": "046f00c519900fcbf2e6e955fc155b11156a733b" }] # diff --git a/vmcloak/dependencies/wallpaper.py b/vmcloak/dependencies/wallpaper.py index 5c35ebe..16dea91 100644 --- a/vmcloak/dependencies/wallpaper.py +++ b/vmcloak/dependencies/wallpaper.py @@ -7,6 +7,7 @@ import os.path import random import requests +from pathlib import Path from vmcloak.abstract import Dependency @@ -22,7 +23,7 @@ class Wallpaper(Dependency): ] def init(self): - self.filepath = None + self.filepath = Path(Dependency.data_path, "wall.jpg") def run(self): uploadpath = os.path.join( diff --git a/vmcloak/platforms/qemu.py b/vmcloak/platforms/qemu.py index 0bd8a06..3fbc06c 100644 --- a/vmcloak/platforms/qemu.py +++ b/vmcloak/platforms/qemu.py @@ -36,9 +36,9 @@ def _create_image_disk(path, size): def _create_snapshot_disk(image_path, path): log.info("Creating snapshot %s with master %s", path, image_path) - subprocess.check_call(["qemu-img", "create", "-f", "qcow2", "-o", + subprocess.check_call(["qemu-img", "create", "-F", "qcow2", "-o", "lazy_refcounts=on,cluster_size=2M", "-b", - image_path, path]) + image_path, "-f", "qcow2", path]) def _make_pre_v41_args(attr): @@ -58,7 +58,8 @@ def _make_pre_v41_args(attr): "-device", "ide-cd,bus=ahci.1,unit=0,drive=cdrom,bootindex=1", "-device", "usb-ehci,id=ehci", "-device", "usb-tablet,bus=ehci.0", - "-soundhw", "hda", + "-device", "intel-hda", + "-device", "hda-duplex", "--enable-kvm" ] @@ -82,7 +83,8 @@ def _make_post_v41_args(attr): "-device", "ide-cd,bus=ahci.1,unit=0,drive=cdrom,bootindex=1", "-device", "usb-ehci,id=ehci", "-device", "usb-tablet,bus=ehci.0", - "-soundhw", "hda", + "-device", "intel-hda", + "-device", "hda-duplex", "-enable-kvm" ] @@ -227,12 +229,7 @@ def create_snapshot(name): # Stop the machine so the memory does not change while making the # memory snapshot. m.stdin.write(b"stop\n") - m.stdin.write(b"migrate_set_speed 1G\n") - # Send the actual memory snapshot command. The args helper tries to find - # lz4 of gzip binaries so we can compress the dump. - m.stdin.write( - f"migrate \"exec:{_get_exec_args(snapshot_path)}\"\n".encode() - ) + m.stdin.write(b"savevm myawesomesnapshot\n") m.stdin.write(b"quit\n") log.debug("Flushing snapshot commands to qemu.") m.stdin.flush()