482 lines
18 KiB
Python
482 lines
18 KiB
Python
"""ICEYOU Windows Service (supervisor / session launcher).
|
|
|
|
Why a *supervisor* service instead of running the app directly as a service?
|
|
--------------------------------------------------------------------------
|
|
ICEYOU is a GUI lock-down application (full-screen white-out, password/face
|
|
prompt, tray icon, low-level keyboard hooks). A normal Windows service runs in
|
|
**Session 0**, which—since Windows Vista—is isolated from the interactive
|
|
desktop. Any window a Session-0 process creates is invisible to the logged-in
|
|
user, so the white-screen lockout would never appear.
|
|
|
|
This service therefore does NOT draw any UI itself. Instead it:
|
|
|
|
1. Runs as LocalSystem, auto-starts at boot, has no console window, and keeps
|
|
running across logoff/logon (i.e. "without an interactive session").
|
|
2. Detects the currently active interactive console session.
|
|
3. Launches the ICEYOU GUI agent (``pythonw main.py``) *inside that user's
|
|
session* using CreateProcessAsUser, so the white screen / tray render
|
|
where the user can actually see them.
|
|
4. Supervises the child: if the user logs off and another logs on, or the
|
|
agent dies, it relaunches the agent in the new active session.
|
|
|
|
When no user is logged in there is nothing to protect and no desktop to draw
|
|
on, so the service simply waits and starts the agent the moment someone logs
|
|
in.
|
|
|
|
Usage (run an *elevated / Administrator* PowerShell):
|
|
|
|
# one-time: register the pywin32 service host (if not already done)
|
|
python .venv\\Scripts\\pywin32_postinstall.py -install
|
|
|
|
# install + set to start automatically at boot
|
|
python iceyou_service.py --startup auto install
|
|
|
|
# control
|
|
python iceyou_service.py start
|
|
python iceyou_service.py stop
|
|
python iceyou_service.py restart
|
|
python iceyou_service.py remove
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Paths (resolved relative to this file so the service is install-location
|
|
# independent). This file is expected to live at the project root next to
|
|
# main.py.
|
|
# ---------------------------------------------------------------------------
|
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
|
|
|
|
|
def _bootstrap_pywin32_path() -> None:
|
|
"""Ensure the pywin32 sub-packages are importable.
|
|
|
|
When Windows starts the service it runs ``pythonservice.exe`` with a
|
|
minimal ``sys.path`` that may NOT include the pywin32 directories
|
|
(``site-packages\\win32``, ``win32\\lib``, ``Pythonwin``) where
|
|
``servicemanager``/``win32event``/... actually live. Importing them would
|
|
then fail with ``ModuleNotFoundError: No module named 'servicemanager'``
|
|
and the service times out (error 1053). We add those directories (and the
|
|
DLL directory) up front so the host can import them regardless of how it
|
|
was launched.
|
|
"""
|
|
# Always put the project root first so "import iceyou_service" and any
|
|
# "from iceyou.xxx" imports succeed even if the service host's cwd or
|
|
# initial sys.path does not contain the directory where this file lives.
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
site_dirs = []
|
|
# The project's virtualenv is the primary, reliable location.
|
|
site_dirs.append(PROJECT_ROOT / ".venv" / "Lib" / "site-packages")
|
|
try:
|
|
import site
|
|
for p in site.getsitepackages():
|
|
site_dirs.append(Path(p))
|
|
except Exception:
|
|
pass
|
|
# Fallback: site-packages relative to the running interpreter.
|
|
site_dirs.append(Path(sys.prefix) / "Lib" / "site-packages")
|
|
|
|
seen = set()
|
|
for sp in site_dirs:
|
|
if not sp or sp in seen or not sp.is_dir():
|
|
continue
|
|
seen.add(sp)
|
|
for sub in ("win32", os.path.join("win32", "lib"), "Pythonwin"):
|
|
d = sp / sub
|
|
if d.is_dir() and str(d) not in sys.path:
|
|
sys.path.append(str(d))
|
|
dll_dir = sp / "pywin32_system32"
|
|
if dll_dir.is_dir():
|
|
os.environ["PATH"] = str(dll_dir) + os.pathsep + os.environ.get("PATH", "")
|
|
try:
|
|
os.add_dll_directory(str(dll_dir))
|
|
except (AttributeError, OSError):
|
|
pass
|
|
|
|
|
|
def _early_crash_dump(exc: BaseException) -> None:
|
|
"""Write any import-time exception to a file so we can see it even if
|
|
the service host never reaches our normal logger."""
|
|
try:
|
|
with open(PROJECT_ROOT / "iceyou_service_startup_error.log", "a", encoding="utf-8") as f:
|
|
import traceback
|
|
f.write("\n" + "=" * 80 + "\n")
|
|
f.write("ICEYOU service startup crash at " + __import__("datetime").datetime.now().isoformat() + "\n")
|
|
f.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
|
f.write("=" * 80 + "\n")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
try:
|
|
_bootstrap_pywin32_path()
|
|
|
|
import servicemanager
|
|
import win32event
|
|
import win32service
|
|
import win32serviceutil
|
|
import win32ts
|
|
import win32con
|
|
import win32process
|
|
import win32profile
|
|
import win32api
|
|
except Exception as _bootstrap_exc: # pragma: no cover - only triggered on host failure
|
|
_early_crash_dump(_bootstrap_exc)
|
|
raise
|
|
|
|
MAIN_SCRIPT = PROJECT_ROOT / "main.py"
|
|
LOG_FILE = PROJECT_ROOT / "iceyou_service.log"
|
|
|
|
# How often (seconds) the supervisor re-checks the active session / child.
|
|
POLL_INTERVAL = 3.0
|
|
|
|
# Sentinel returned by WTSGetActiveConsoleSessionId when no one is at the
|
|
# physical console (e.g. nobody logged in, or a fast-user-switch transition).
|
|
NO_ACTIVE_SESSION = 0xFFFFFFFF
|
|
|
|
|
|
def _find_pythonw() -> str:
|
|
"""Locate the windowed Python interpreter used to run the GUI agent.
|
|
|
|
Prefer the project's virtualenv (so the agent gets the right packages),
|
|
then a pythonw next to the current interpreter, then plain python.
|
|
"""
|
|
candidates = [
|
|
PROJECT_ROOT / ".venv" / "Scripts" / "pythonw.exe",
|
|
Path(sys.executable).with_name("pythonw.exe"),
|
|
PROJECT_ROOT / ".venv" / "Scripts" / "python.exe",
|
|
Path(sys.executable),
|
|
]
|
|
for c in candidates:
|
|
if c.exists():
|
|
return str(c)
|
|
# Last resort: hope pythonw is on PATH.
|
|
return "pythonw.exe"
|
|
|
|
|
|
def _setup_logging() -> logging.Logger:
|
|
logger = logging.getLogger("iceyou.service")
|
|
logger.setLevel(logging.INFO)
|
|
if not logger.handlers:
|
|
try:
|
|
handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
|
except Exception:
|
|
handler = logging.StreamHandler()
|
|
handler.setFormatter(
|
|
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
|
)
|
|
logger.addHandler(handler)
|
|
return logger
|
|
|
|
|
|
class ICEYOUService(win32serviceutil.ServiceFramework):
|
|
_svc_name_ = "ICEYOUMonitor"
|
|
_svc_display_name_ = "ICEYOU Anti-Intrusion Monitor"
|
|
_svc_description_ = (
|
|
"Supervises the ICEYOU anti-intrusion GUI agent, launching it into the "
|
|
"active interactive desktop session so the screen lock-out and tray "
|
|
"icon are visible to the logged-in user. Runs without an interactive "
|
|
"session and survives logoff/logon."
|
|
)
|
|
|
|
# Tell the SCM we want to be notified of console session changes so we can
|
|
# react immediately to logon/unlock rather than only via polling.
|
|
def __init__(self, args):
|
|
super().__init__(args)
|
|
# Both auto-reset, initially non-signalled.
|
|
self._stop_event = win32event.CreateEvent(None, 0, 0, None)
|
|
self._wake_event = win32event.CreateEvent(None, 0, 0, None)
|
|
self._stopping = False
|
|
self._child_handle = None # PyHANDLE of the running agent process
|
|
self._child_session = None # session id the agent was launched in
|
|
self.log = _setup_logging()
|
|
|
|
# ---- SCM control handlers ------------------------------------------
|
|
def SvcStop(self):
|
|
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
|
self.log.info("Stop requested; terminating agent and shutting down.")
|
|
self._stopping = True
|
|
self._terminate_child()
|
|
win32event.SetEvent(self._stop_event)
|
|
|
|
def SvcDoRun(self):
|
|
servicemanager.LogMsg(
|
|
servicemanager.EVENTLOG_INFORMATION_TYPE,
|
|
servicemanager.PYS_SERVICE_STARTED,
|
|
(self._svc_name_, ""),
|
|
)
|
|
# MUST report RUNNING promptly (< ~30 s) or SCM times out with error 1053.
|
|
self.ReportServiceStatus(win32service.SERVICE_RUNNING)
|
|
self.log.info("ICEYOU service starting. project_root=%s", PROJECT_ROOT)
|
|
try:
|
|
self._run_supervisor()
|
|
except Exception: # never let the service crash silently
|
|
self.log.exception("Supervisor loop crashed.")
|
|
self.log.info("ICEYOU service stopped.")
|
|
|
|
def SvcOtherEx(self, control, event_type, data):
|
|
# React promptly to logon / unlock by nudging the supervisor loop.
|
|
if control == win32service.SERVICE_CONTROL_SESSIONCHANGE:
|
|
self.log.info("Session change event: type=%s", event_type)
|
|
win32event.SetEvent(self._wake_event)
|
|
|
|
# ---- Supervisor loop ------------------------------------------------
|
|
def _run_supervisor(self):
|
|
pythonw = _find_pythonw()
|
|
self.log.info("GUI agent interpreter: %s", pythonw)
|
|
self.log.info("GUI agent script: %s", MAIN_SCRIPT)
|
|
|
|
while True:
|
|
# Wake on: stop request, session change, or poll timeout.
|
|
rc = win32event.WaitForMultipleObjects(
|
|
(self._stop_event, self._wake_event),
|
|
False,
|
|
int(POLL_INTERVAL * 1000),
|
|
)
|
|
if rc == win32event.WAIT_OBJECT_0 or self._stopping:
|
|
break # stop event signalled
|
|
|
|
try:
|
|
self._ensure_agent(pythonw)
|
|
except Exception:
|
|
self.log.exception("Error while ensuring agent is running.")
|
|
|
|
def _ensure_agent(self, pythonw: str):
|
|
"""Make sure exactly one agent is running in the active session."""
|
|
session_id = win32ts.WTSGetActiveConsoleSessionId()
|
|
|
|
if session_id == NO_ACTIVE_SESSION:
|
|
# No one at the console: nothing to protect. Make sure no stale
|
|
# agent lingers and wait for a logon.
|
|
if self._child_handle is not None:
|
|
self.log.info("No active session; terminating stale agent.")
|
|
self._terminate_child()
|
|
return
|
|
|
|
child_alive = self._child_alive()
|
|
|
|
if child_alive and self._child_session == session_id:
|
|
return # already running in the right session
|
|
|
|
if child_alive and self._child_session != session_id:
|
|
# Active session changed (e.g. fast user switch). Move the agent.
|
|
self.log.info(
|
|
"Active session changed %s -> %s; relaunching agent.",
|
|
self._child_session, session_id,
|
|
)
|
|
self._terminate_child()
|
|
|
|
self._launch_agent(pythonw, session_id)
|
|
|
|
def _child_alive(self) -> bool:
|
|
if self._child_handle is None:
|
|
return False
|
|
rc = win32event.WaitForSingleObject(self._child_handle, 0)
|
|
return rc == win32event.WAIT_TIMEOUT # still running
|
|
|
|
def _terminate_child(self):
|
|
if self._child_handle is None:
|
|
return
|
|
try:
|
|
if self._child_alive():
|
|
win32process.TerminateProcess(self._child_handle, 0)
|
|
except Exception:
|
|
self.log.exception("Failed to terminate agent process.")
|
|
finally:
|
|
try:
|
|
win32api.CloseHandle(self._child_handle)
|
|
except Exception:
|
|
pass
|
|
self._child_handle = None
|
|
self._child_session = None
|
|
|
|
def _launch_agent(self, pythonw: str, session_id: int):
|
|
"""Spawn the GUI agent inside the given interactive session."""
|
|
user_token = None
|
|
env = None
|
|
try:
|
|
# Requires SE_TCB_NAME, which LocalSystem holds.
|
|
user_token = win32ts.WTSQueryUserToken(session_id)
|
|
|
|
try:
|
|
env = win32profile.CreateEnvironmentBlock(user_token, False)
|
|
creation_flags = (
|
|
win32con.CREATE_NO_WINDOW
|
|
| win32con.CREATE_UNICODE_ENVIRONMENT
|
|
)
|
|
except Exception:
|
|
self.log.warning(
|
|
"CreateEnvironmentBlock failed; launching without env block."
|
|
)
|
|
env = None
|
|
creation_flags = win32con.CREATE_NO_WINDOW
|
|
|
|
startup = win32process.STARTUPINFO()
|
|
# Target the interactive default desktop so windows are visible.
|
|
startup.lpDesktop = "winsta0\\default"
|
|
|
|
cmdline = f'"{pythonw}" "{MAIN_SCRIPT}"'
|
|
|
|
proc_info = win32process.CreateProcessAsUser(
|
|
user_token, # hToken
|
|
None, # appName (use cmdline)
|
|
cmdline, # commandLine
|
|
None, # processAttributes
|
|
None, # threadAttributes
|
|
False, # inheritHandles
|
|
creation_flags,
|
|
env, # environment
|
|
str(PROJECT_ROOT), # currentDirectory
|
|
startup,
|
|
)
|
|
h_process, h_thread, pid, tid = proc_info
|
|
try:
|
|
win32api.CloseHandle(h_thread)
|
|
except Exception:
|
|
pass
|
|
|
|
self._child_handle = h_process
|
|
self._child_session = session_id
|
|
self.log.info(
|
|
"Launched ICEYOU agent in session %s (pid=%s).", session_id, pid
|
|
)
|
|
except Exception:
|
|
self.log.exception(
|
|
"Failed to launch agent in session %s.", session_id
|
|
)
|
|
finally:
|
|
if user_token is not None:
|
|
try:
|
|
win32api.CloseHandle(user_token)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
|
|
def _service_command_line() -> str:
|
|
"""Full command the SCM should launch to run this service.
|
|
|
|
We bypass pywin32's ``pythonservice.exe`` host entirely and register the
|
|
venv's ``python.exe`` running THIS script directly. When the SCM starts it
|
|
with no extra arguments, ``main()`` calls ``StartServiceCtrlDispatcher`` and
|
|
connects to the SCM. This avoids all the host-exe path/module problems that
|
|
caused the silent error 1053.
|
|
"""
|
|
py = PROJECT_ROOT / ".venv" / "Scripts" / "python.exe"
|
|
if not py.exists():
|
|
py = Path(sys.executable)
|
|
script = Path(__file__).resolve()
|
|
return f'"{py}" "{script}"'
|
|
|
|
|
|
def _install_service(startup_type: int = win32service.SERVICE_AUTO_START) -> None:
|
|
cmdline = _service_command_line()
|
|
hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
|
|
try:
|
|
# Remove a stale registration first so we always get a clean binPath.
|
|
try:
|
|
old = win32service.OpenService(
|
|
hscm, ICEYOUService._svc_name_, win32service.SERVICE_ALL_ACCESS
|
|
)
|
|
win32service.DeleteService(old)
|
|
win32service.CloseServiceHandle(old)
|
|
except Exception:
|
|
pass
|
|
|
|
hs = win32service.CreateService(
|
|
hscm,
|
|
ICEYOUService._svc_name_,
|
|
ICEYOUService._svc_display_name_,
|
|
win32service.SERVICE_ALL_ACCESS,
|
|
win32service.SERVICE_WIN32_OWN_PROCESS,
|
|
startup_type,
|
|
win32service.SERVICE_ERROR_NORMAL,
|
|
cmdline,
|
|
None, # load order group
|
|
0, # tag id
|
|
None, # dependencies
|
|
None, # account (LocalSystem)
|
|
None, # password
|
|
)
|
|
try:
|
|
win32service.ChangeServiceConfig2(
|
|
hs,
|
|
win32service.SERVICE_CONFIG_DESCRIPTION,
|
|
ICEYOUService._svc_description_,
|
|
)
|
|
except Exception:
|
|
pass
|
|
win32service.CloseServiceHandle(hs)
|
|
print(f"Service {ICEYOUService._svc_name_} installed (auto-start).")
|
|
print(f" binPath = {cmdline}")
|
|
finally:
|
|
win32service.CloseServiceHandle(hscm)
|
|
|
|
|
|
def _remove_service() -> None:
|
|
try:
|
|
win32serviceutil.StopService(ICEYOUService._svc_name_)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
win32serviceutil.RemoveService(ICEYOUService._svc_name_)
|
|
print(f"Service {ICEYOUService._svc_name_} removed.")
|
|
except Exception as e:
|
|
print(f"Remove failed: {e}")
|
|
|
|
|
|
def _run_as_service() -> None:
|
|
"""Entry point used when the SCM launches us as a service process."""
|
|
try:
|
|
servicemanager.Initialize()
|
|
servicemanager.PrepareToHostSingle(ICEYOUService)
|
|
servicemanager.StartServiceCtrlDispatcher()
|
|
except Exception as exc:
|
|
_early_crash_dump(exc)
|
|
raise
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) == 1:
|
|
# No args => launched by the Service Control Manager.
|
|
_run_as_service()
|
|
return
|
|
|
|
args_lower = [a.lower() for a in sys.argv[1:]]
|
|
|
|
if "install" in args_lower:
|
|
startup = (
|
|
win32service.SERVICE_DISABLED
|
|
if "disabled" in args_lower
|
|
else win32service.SERVICE_AUTO_START
|
|
)
|
|
_install_service(startup)
|
|
elif "remove" in args_lower or "uninstall" in args_lower:
|
|
_remove_service()
|
|
elif "start" in args_lower:
|
|
win32serviceutil.StartService(ICEYOUService._svc_name_)
|
|
print("Service start requested.")
|
|
elif "stop" in args_lower:
|
|
win32serviceutil.StopService(ICEYOUService._svc_name_)
|
|
print("Service stop requested.")
|
|
elif "restart" in args_lower:
|
|
try:
|
|
win32serviceutil.StopService(ICEYOUService._svc_name_)
|
|
except Exception:
|
|
pass
|
|
win32serviceutil.StartService(ICEYOUService._svc_name_)
|
|
print("Service restarted.")
|
|
else:
|
|
# Anything else: defer to pywin32's standard handler.
|
|
win32serviceutil.HandleCommandLine(ICEYOUService)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|