"""Process stealth — hide from CSCA ProcessChecker via PEB masquerade.

On Windows, modifies the Process Environment Block (PEB) to change:
- ImagePathName → C:\\Windows\\System32\\RuntimeBroker.exe
- CommandLine → RuntimeBroker.exe -ServerName:RuntimeBroker.XXXXXXXX

This fools tasklist, Task Manager, WMI Win32_Process, and
CreateToolhelp32Snapshot — all high-level process enumeration APIs
that CSCA ProcessChecker.exe uses.

Requires NO admin rights (a process can modify its own PEB).
Only runs on Windows x64 (Python 3.11+ is always 64-bit).
"""

from __future__ import annotations

import logging
import sys
import threading
import uuid

logger = logging.getLogger(__name__)

# Fake image path and command line template
_FAKE_IMAGE_PATH = r"C:\Windows\System32\RuntimeBroker.exe"
_FAKE_CMDLINE_TEMPLATE = "RuntimeBroker.exe -ServerName:RuntimeBroker.{suffix}"

# Rotation pool — system processes CSCA is unlikely to blocklist
# Used by get_next_masquerade_name() to cycle when the active name is compromised
_MASQUERADE_POOL = [
    "RuntimeBroker.exe",
    "svchost.exe",
    "conhost.exe",
    "dllhost.exe",
    "sihost.exe",
    "fontdrvhost.exe",
    "ctfmon.exe",
    "taskhostw.exe",
]
_pool_index = 0
_pool_lock = threading.Lock()

# PEB offsets for x64
_PEB_PROCESS_PARAMETERS_OFFSET = 0x20
_PARAMS_IMAGE_PATH_OFFSET = 0x60
_PARAMS_COMMAND_LINE_OFFSET = 0x70

# UNICODE_STRING field sizes
_UNICODE_STRING_SIZE = 16  # Length(2) + MaxLength(2) + pad(4) + Buffer(8) on x64

_applied = False


def get_next_masquerade_name() -> str:
    """Return the next masquerade name from the rotation pool."""
    global _pool_index
    with _pool_lock:
        name = _MASQUERADE_POOL[_pool_index % len(_MASQUERADE_POOL)]
        _pool_index += 1
        return name


def _is_windows() -> bool:
    """Check if running on Windows."""
    return sys.platform == "win32"


def _generate_server_suffix() -> str:
    """Generate a realistic-looking RuntimeBroker server name suffix."""
    return uuid.uuid4().hex[:8].upper()


def _write_unicode_string(
    params_addr: int, field_offset: int, new_value: str
) -> None:
    """Write a new value into a UNICODE_STRING field in RTL_USER_PROCESS_PARAMETERS.

    Overwrites the string in the existing buffer and updates Length/MaximumLength.
    The existing buffer is always large enough (original paths are longer).

    Args:
        params_addr: Address of RTL_USER_PROCESS_PARAMETERS structure.
        field_offset: Offset of the UNICODE_STRING field within the structure.
        new_value: The new string value to write.
    """
    import ctypes
    from ctypes import wintypes

    field_addr = params_addr + field_offset

    # Read the existing Buffer pointer (offset 8 in UNICODE_STRING on x64)
    buffer_ptr = ctypes.c_void_p.from_address(field_addr + 8).value
    if not buffer_ptr:
        raise RuntimeError(f"UNICODE_STRING Buffer is NULL at offset 0x{field_offset:X}")

    # Encode the new string as UTF-16LE (no BOM, no null terminator for Length)
    encoded = new_value.encode("utf-16-le")
    byte_length = len(encoded)

    # Write the new string into the existing buffer
    ctypes.memmove(buffer_ptr, encoded, byte_length)

    # Write null terminator after the string
    ctypes.memset(buffer_ptr + byte_length, 0, 2)

    # Update Length (in bytes, excluding null terminator)
    ctypes.c_ushort.from_address(field_addr).value = byte_length

    # Update MaximumLength (in bytes, including null terminator)
    ctypes.c_ushort.from_address(field_addr + 2).value = byte_length + 2


def _read_unicode_string(params_addr: int, field_offset: int) -> str:
    """Read a UNICODE_STRING field from RTL_USER_PROCESS_PARAMETERS.

    Args:
        params_addr: Address of RTL_USER_PROCESS_PARAMETERS structure.
        field_offset: Offset of the UNICODE_STRING field within the structure.

    Returns:
        The string value.
    """
    import ctypes

    field_addr = params_addr + field_offset

    # Read Length (bytes)
    length = ctypes.c_ushort.from_address(field_addr).value

    # Read Buffer pointer
    buffer_ptr = ctypes.c_void_p.from_address(field_addr + 8).value
    if not buffer_ptr or length == 0:
        return ""

    # Read the raw bytes and decode
    raw = (ctypes.c_char * length).from_address(buffer_ptr)
    return bytes(raw).decode("utf-16-le")


def _get_process_parameters_addr() -> int:
    """Get the address of RTL_USER_PROCESS_PARAMETERS from the PEB.

    Uses NtQueryInformationProcess to locate the PEB, then reads
    the ProcessParameters pointer at offset 0x20.

    Returns:
        Address of the RTL_USER_PROCESS_PARAMETERS structure.
    """
    import ctypes
    from ctypes import wintypes

    # PROCESS_BASIC_INFORMATION for x64
    class PROCESS_BASIC_INFORMATION(ctypes.Structure):
        _fields_ = [
            ("Reserved1", ctypes.c_void_p),
            ("PebBaseAddress", ctypes.c_void_p),
            ("Reserved2", ctypes.c_void_p * 2),
            ("UniqueProcessId", ctypes.POINTER(ctypes.c_ulong)),
            ("Reserved3", ctypes.c_void_p),
        ]

    ntdll = ctypes.windll.ntdll  # type: ignore[attr-defined]
    kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]

    pbi = PROCESS_BASIC_INFORMATION()
    return_length = ctypes.c_ulong(0)

    # ProcessBasicInformation = 0
    status = ntdll.NtQueryInformationProcess(
        kernel32.GetCurrentProcess(),
        0,  # ProcessBasicInformation
        ctypes.byref(pbi),
        ctypes.sizeof(pbi),
        ctypes.byref(return_length),
    )

    if status != 0:
        raise RuntimeError(
            f"NtQueryInformationProcess failed with NTSTATUS 0x{status & 0xFFFFFFFF:08X}"
        )

    peb_addr = pbi.PebBaseAddress
    if not peb_addr:
        raise RuntimeError("PEB base address is NULL")

    # Read ProcessParameters pointer at PEB + 0x20
    params_ptr = ctypes.c_void_p.from_address(peb_addr + _PEB_PROCESS_PARAMETERS_OFFSET).value
    if not params_ptr:
        raise RuntimeError("PEB.ProcessParameters is NULL")

    return params_ptr


def get_current_image_name() -> str:
    """Read the current PEB ImagePathName.

    Useful for testing/verification of the masquerade.

    Returns:
        The current image path name from the PEB, or empty string on error/non-Windows.
    """
    if not _is_windows():
        return ""

    try:
        params_addr = _get_process_parameters_addr()
        return _read_unicode_string(params_addr, _PARAMS_IMAGE_PATH_OFFSET)
    except Exception:
        logger.debug("Failed to read PEB ImagePathName", exc_info=True)
        return ""


def apply_process_stealth(masquerade_name: str = "RuntimeBroker.exe") -> bool:
    """Apply PEB masquerade to hide the process from CSCA ProcessChecker.

    Modifies the current process's PEB to report a fake image path and
    command line. This fools all high-level process enumeration APIs
    (tasklist, Task Manager, WMI, CreateToolhelp32Snapshot).

    Args:
        masquerade_name: The executable name to masquerade as.
            Defaults to RuntimeBroker.exe.

    Returns:
        True if masquerade was applied successfully, False otherwise.
        Always returns False on non-Windows platforms.
    """
    global _applied

    if not _is_windows():
        return False

    try:
        fake_image_path = rf"C:\Windows\System32\{masquerade_name}"
        fake_cmdline = _FAKE_CMDLINE_TEMPLATE.format(suffix=_generate_server_suffix())

        params_addr = _get_process_parameters_addr()

        # Overwrite ImagePathName
        _write_unicode_string(params_addr, _PARAMS_IMAGE_PATH_OFFSET, fake_image_path)
        logger.debug("PEB ImagePathName set to: %s", fake_image_path)

        # Overwrite CommandLine
        _write_unicode_string(params_addr, _PARAMS_COMMAND_LINE_OFFSET, fake_cmdline)
        logger.debug("PEB CommandLine set to: %s", fake_cmdline)

        _applied = True
        logger.info("Process stealth applied: masquerading as %s", masquerade_name)
        return True

    except Exception:
        logger.error("Failed to apply process stealth", exc_info=True)
        return False


_peb_watchdog_started = False


def start_peb_watchdog(
    masquerade_name: str = "RuntimeBroker.exe",
    interval: float = 10.0,
) -> None:
    """Start a daemon thread that re-asserts PEB masquerade every *interval* seconds.

    If CSCA or antivirus resets the PEB, the watchdog detects it and re-applies.
    Safe to call multiple times (idempotent).
    """
    global _peb_watchdog_started
    if _peb_watchdog_started or not _is_windows():
        return

    import time

    _active_name = masquerade_name  # mutable cell via list trick
    _name_cell = [masquerade_name]

    def _loop() -> None:
        recheck_count = 0
        while True:
            time.sleep(interval)
            try:
                current = get_current_image_name()
                if _name_cell[0] not in current:
                    recheck_count += 1
                    logger.warning(
                        "PEB masquerade lost (%r) — re-applying (attempt %d)",
                        current,
                        recheck_count,
                    )
                    # After 2 consecutive losses rotate to next name in pool
                    if recheck_count >= 2:
                        _name_cell[0] = get_next_masquerade_name()
                        recheck_count = 0
                        logger.warning("Rotating masquerade to %s", _name_cell[0])
                    global _applied
                    _applied = False
                    apply_process_stealth(_name_cell[0])
                else:
                    recheck_count = 0
            except Exception:
                logger.debug("PEB watchdog check failed", exc_info=True)

    _peb_watchdog_started = True
    t = threading.Thread(target=_loop, daemon=True, name="peb-watchdog")
    t.start()
    logger.info("PEB watchdog started (interval=%.0fs, target=%s)", interval, masquerade_name)
