/* * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Timers; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Monitoring.Contracts.Applications.Events; using SafeExamBrowser.Settings.Applications; using SafeExamBrowser.WindowsApi.Contracts; namespace SafeExamBrowser.Monitoring.Applications { public class ApplicationMonitor : IApplicationMonitor { private IList blacklist; private Guid? captureHookId; private Guid? foregroundHookId; private ILogger logger; private INativeMethods nativeMethods; private IList processes; private IProcessFactory processFactory; private Timer timer; private IList whitelist; private Window activeWindow; public event ExplorerStartedEventHandler ExplorerStarted; public event InstanceStartedEventHandler InstanceStarted; public event TerminationFailedEventHandler TerminationFailed; public ApplicationMonitor(int interval_ms, ILogger logger, INativeMethods nativeMethods, IProcessFactory processFactory) { this.blacklist = new List(); this.logger = logger; this.nativeMethods = nativeMethods; this.processes = new List(); this.processFactory = processFactory; this.timer = new Timer(interval_ms); this.whitelist = new List(); } public InitializationResult Initialize(ApplicationSettings settings) { var result = new InitializationResult(); InitializeProcesses(); InitializeBlacklist(settings, result); InitializeWhitelist(settings, result); return result; } public void Start() { timer.AutoReset = false; timer.Elapsed += Timer_Elapsed; timer.Start(); logger.Info("Started monitoring applications."); captureHookId = nativeMethods.RegisterSystemCaptureStartEvent(SystemEvent_WindowChanged); logger.Info($"Registered system capture start event with ID = {captureHookId}."); foregroundHookId = nativeMethods.RegisterSystemForegroundEvent(SystemEvent_WindowChanged); logger.Info($"Registered system foreground event with ID = {foregroundHookId}."); } public void Stop() { timer.Stop(); timer.Elapsed -= Timer_Elapsed; logger.Info("Stopped monitoring applications."); if (captureHookId.HasValue) { nativeMethods.DeregisterSystemEventHook(captureHookId.Value); logger.Info($"Unregistered system capture start event with ID = {captureHookId}."); } if (foregroundHookId.HasValue) { nativeMethods.DeregisterSystemEventHook(foregroundHookId.Value); logger.Info($"Unregistered system foreground event with ID = {foregroundHookId}."); } } public bool TryTerminate(RunningApplication application) { var success = true; foreach (var process in application.Processes) { success &= TryTerminate(process); } return success; } private void SystemEvent_WindowChanged(IntPtr handle) { if (handle != IntPtr.Zero && activeWindow?.Handle != handle) { var title = nativeMethods.GetWindowTitle(handle); var window = new Window { Handle = handle, Title = title }; logger.Debug($"Window has changed from {activeWindow} to {window}."); activeWindow = window; Task.Run(() => { if (!IsAllowed(window) && !TryHide(window)) { Close(window); } }); } } private void Timer_Elapsed(object sender, ElapsedEventArgs e) { var failed = new List(); var running = processFactory.GetAllRunning(); var started = running.Where(r => processes.All(p => p.Id != r.Id)).ToList(); var terminated = processes.Where(p => running.All(r => r.Id != p.Id)).ToList(); foreach (var process in started) { logger.Debug($"Process {process} has been started."); processes.Add(process); if (process.Name == "explorer.exe") { HandleExplorerStart(process); } else if (!IsAllowed(process) && !TryTerminate(process)) { AddFailed(process, failed); } else if (IsWhitelisted(process, out var applicationId)) { HandleInstanceStart(applicationId.Value, process); } } foreach (var process in terminated) { logger.Debug($"Process {process} has been terminated."); processes.Remove(process); } if (failed.Any()) { logger.Warn($"Failed to terminate these blacklisted applications: {string.Join(", ", failed.Select(a => a.Name))}."); TerminationFailed?.Invoke(failed); } timer.Start(); } private void AddFailed(IProcess process, List failed) { var name = blacklist.First(a => BelongsToApplication(process, a)).ExecutableName; var application = failed.FirstOrDefault(a => a.Name == name); if (application == default(RunningApplication)) { application = new RunningApplication(name); failed.Add(application); } application.Processes.Add(process); } private void AddFailed(string name, IProcess process, InitializationResult result) { var application = result.FailedAutoTerminations.FirstOrDefault(a => a.Name == name); if (application == default(RunningApplication)) { application = new RunningApplication(name); result.FailedAutoTerminations.Add(application); } application.Processes.Add(process); logger.Error($"Process {process} belongs to application '{application.Name}' and could not be terminated automatically!"); } private void AddForTermination(string name, IProcess process, InitializationResult result) { var application = result.RunningApplications.FirstOrDefault(a => a.Name == name); if (application == default(RunningApplication)) { application = new RunningApplication(name); result.RunningApplications.Add(application); } application.Processes.Add(process); logger.Debug($"Process {process} belongs to application '{application.Name}' and needs to be terminated."); } private bool BelongsToApplication(IProcess process, BlacklistApplication application) { var sameName = process.Name.Equals(application.ExecutableName, StringComparison.OrdinalIgnoreCase); var sameOriginalName = process.OriginalName?.Equals(application.OriginalName, StringComparison.OrdinalIgnoreCase) == true; return sameName || sameOriginalName; } private bool BelongsToApplication(IProcess process, WhitelistApplication application) { var ignoreOriginalName = string.IsNullOrWhiteSpace(application.OriginalName); var sameName = process.Name.Equals(application.ExecutableName, StringComparison.OrdinalIgnoreCase); var sameOriginalName = process.OriginalName?.Equals(application.OriginalName, StringComparison.OrdinalIgnoreCase) == true; return sameName && (ignoreOriginalName || sameOriginalName); } private bool BelongsToSafeExamBrowser(IProcess process) { var isRuntime = process.Name == "SafeExamBrowser.exe" && process.OriginalName == "SafeExamBrowser.exe"; var isClient = process.Name == "SafeExamBrowser.Client.exe" && process.OriginalName == "SafeExamBrowser.Client.exe"; return isRuntime || isClient; } private void Close(Window window) { nativeMethods.SendCloseMessageTo(window.Handle); logger.Info($"Sent close message to window {window}."); } private void HandleExplorerStart(IProcess process) { logger.Warn($"A new instance of Windows Explorer {process} has been started!"); Task.Run(() => ExplorerStarted?.Invoke()); } private void HandleInstanceStart(Guid applicationId, IProcess process) { logger.Debug($"Detected start of whitelisted application instance {process}."); Task.Run(() => InstanceStarted?.Invoke(applicationId, process)); } private void InitializeProcesses() { processes = processFactory.GetAllRunning(); logger.Debug($"Initialized {processes.Count} currently running processes."); } private void InitializeBlacklist(ApplicationSettings settings, InitializationResult result) { foreach (var application in settings.Blacklist) { blacklist.Add(application); } logger.Debug($"Initialized blacklist with {blacklist.Count} applications{(blacklist.Any() ? $": {string.Join(", ", blacklist.Select(a => a.ExecutableName))}" : ".")}"); foreach (var process in processes) { foreach (var application in blacklist) { var isBlacklisted = BelongsToApplication(process, application); if (isBlacklisted) { if (!application.AutoTerminate) { AddForTermination(application.ExecutableName, process, result); } else if (application.AutoTerminate && !TryTerminate(process)) { AddFailed(application.ExecutableName, process, result); } break; } } } } private void InitializeWhitelist(ApplicationSettings settings, InitializationResult result) { foreach (var application in settings.Whitelist) { whitelist.Add(application); } logger.Debug($"Initialized whitelist with {whitelist.Count} applications{(whitelist.Any() ? $": {string.Join(", ", whitelist.Select(a => a.ExecutableName))}" : ".")}"); foreach (var process in processes) { foreach (var application in whitelist) { var isWhitelisted = BelongsToApplication(process, application); if (isWhitelisted) { if (!application.AllowRunning && !application.AutoTerminate) { AddForTermination(application.ExecutableName, process, result); } else if (!application.AllowRunning && application.AutoTerminate && !TryTerminate(process)) { AddFailed(application.ExecutableName, process, result); } break; } } } } private bool IsAllowed(IProcess process) { foreach (var application in blacklist) { if (BelongsToApplication(process, application)) { logger.Warn($"Process {process} belongs to blacklisted application '{application.ExecutableName}'!"); return false; } } return true; } private bool IsAllowed(Window window) { var processId = Convert.ToInt32(nativeMethods.GetProcessIdFor(window.Handle)); if (processFactory.TryGetById(processId, out var process)) { if (BelongsToSafeExamBrowser(process) || IsWhitelisted(process, out _)) { return true; } logger.Warn($"Window {window} belongs to not whitelisted process '{process.Name}'!"); } else { logger.Error($"Could not find process for window {window} and process ID = {processId}!"); } return false; } private bool IsWhitelisted(IProcess process, out Guid? applicationId) { applicationId = default(Guid?); foreach (var application in whitelist) { if (BelongsToApplication(process, application)) { applicationId = application.Id; return true; } } return false; } private bool TryHide(Window window) { var success = nativeMethods.HideWindow(window.Handle); if (success) { logger.Info($"Successfully hid window {window}."); } else { logger.Warn($"Failed to hide window {window}!"); } return success; } private bool TryTerminate(IProcess process) { const int MAX_ATTEMPTS = 5; const int TIMEOUT = 500; for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { if (process.TryClose(TIMEOUT)) { break; } } if (!process.HasTerminated) { for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { if (process.TryKill(TIMEOUT)) { break; } } } if (process.HasTerminated) { logger.Info($"Successfully terminated process {process}."); } else { logger.Warn($"Failed to terminate process {process}!"); } return process.HasTerminated; } } }