From d3d98c7df706592c7b38c9589bd8bc5db0b58efb Mon Sep 17 00:00:00 2001 From: dbuechel Date: Tue, 8 Oct 2019 16:11:19 +0200 Subject: [PATCH] SEBWIN-313: Started implementing blacklist monitoring. --- SafeExamBrowser.Client/CompositionRoot.cs | 2 +- .../Operations/ApplicationOperation.cs | 13 +- .../Applications/ApplicationMonitor.cs | 136 +++++++++--------- .../SafeExamBrowser.Monitoring.csproj | 1 - .../Operations/ClientOperationTests.cs | 10 +- .../ClientTerminationOperationTests.cs | 2 +- .../IProcess.cs | 11 +- .../IProcessFactory.cs | 2 +- SafeExamBrowser.WindowsApi/Process.cs | 50 +++++-- SafeExamBrowser.WindowsApi/ProcessFactory.cs | 11 +- 10 files changed, 136 insertions(+), 102 deletions(-) diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 1d478834..4277044b 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -94,7 +94,7 @@ namespace SafeExamBrowser.Client taskbar = BuildTaskbar(); terminationActivator = new TerminationActivator(ModuleLogger(nameof(TerminationActivator))); - var applicationMonitor = new ApplicationMonitor(ModuleLogger(nameof(ApplicationMonitor)), nativeMethods, new ProcessFactory(ModuleLogger(nameof(ProcessFactory)))); + var applicationMonitor = new ApplicationMonitor(FIVE_SECONDS, ModuleLogger(nameof(ApplicationMonitor)), nativeMethods, new ProcessFactory(ModuleLogger(nameof(ProcessFactory)))); var displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo); var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods); var hashAlgorithm = new HashAlgorithm(); diff --git a/SafeExamBrowser.Client/Operations/ApplicationOperation.cs b/SafeExamBrowser.Client/Operations/ApplicationOperation.cs index eba09ab2..46b1baf8 100644 --- a/SafeExamBrowser.Client/Operations/ApplicationOperation.cs +++ b/SafeExamBrowser.Client/Operations/ApplicationOperation.cs @@ -14,7 +14,6 @@ using SafeExamBrowser.Core.Contracts.OperationModel.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Monitoring.Contracts.Applications; -using SafeExamBrowser.Settings; using SafeExamBrowser.Settings.Applications; namespace SafeExamBrowser.Client.Operations @@ -110,18 +109,18 @@ namespace SafeExamBrowser.Client.Operations private void StartMonitor() { - if (Context.Settings.KioskMode != KioskMode.None) - { + //TODO: if (Context.Settings.KioskMode != KioskMode.None) + //{ applicationMonitor.Start(); - } + //} } private void StopMonitor() { - if (Context.Settings.KioskMode != KioskMode.None) - { + //TODO: if (Context.Settings.KioskMode != KioskMode.None) + //{ applicationMonitor.Stop(); - } + //} } private OperationResult TryTerminate(IEnumerable runningApplications) diff --git a/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs b/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs index 1c7c7de3..71fbbf78 100644 --- a/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs +++ b/SafeExamBrowser.Monitoring/Applications/ApplicationMonitor.cs @@ -9,8 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Management; -using System.Threading; +using System.Timers; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Monitoring.Contracts.Applications.Events; @@ -24,21 +23,24 @@ namespace SafeExamBrowser.Monitoring.Applications private IntPtr activeWindow; private IList blacklist; private Guid? captureHookId; - private ManagementEventWatcher explorerWatcher; private Guid? foregroundHookId; private ILogger logger; private INativeMethods nativeMethods; + private IList processes; private IProcessFactory processFactory; + private Timer timer; private IList whitelist; public event ExplorerStartedEventHandler ExplorerStarted; - public ApplicationMonitor(ILogger logger, INativeMethods nativeMethods, IProcessFactory processFactory) + 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(); } @@ -59,17 +61,19 @@ namespace SafeExamBrowser.Monitoring.Applications logger.Debug($"Initialized blacklist with {blacklist.Count} applications{(blacklist.Any() ? $": {string.Join(", ", blacklist.Select(a => a.ExecutableName))}" : ".")}"); logger.Debug($"Initialized whitelist with {whitelist.Count} applications{(whitelist.Any() ? $": {string.Join(", ", whitelist.Select(a => a.ExecutableName))}" : ".")}"); - foreach (var process in processFactory.GetAllRunning()) + processes = processFactory.GetAllRunning(); + + foreach (var process in processes) { foreach (var application in blacklist) { - var isMatch = BelongsToApplication(process, application); + var isBlacklisted = BelongsToApplication(process, application); - if (isMatch && !application.AutoTerminate) + if (isBlacklisted && !application.AutoTerminate) { AddForTermination(application.ExecutableName, process, result); } - else if (isMatch && application.AutoTerminate && !TryTerminate(process)) + else if (isBlacklisted && application.AutoTerminate && !TryTerminate(process)) { AddFailed(application.ExecutableName, process, result); } @@ -86,13 +90,10 @@ namespace SafeExamBrowser.Monitoring.Applications public void Start() { - // TODO: Start monitoring blacklist... - - // TODO: Remove WMI event and use timer mechanism! - explorerWatcher = new ManagementEventWatcher(@"\\.\root\CIMV2", GetQueryFor("explorer.exe")); - explorerWatcher.EventArrived += new EventArrivedEventHandler(ExplorerWatcher_EventArrived); - explorerWatcher.Start(); - logger.Info("Started monitoring process 'explorer.exe'."); + 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}."); @@ -103,8 +104,9 @@ namespace SafeExamBrowser.Monitoring.Applications public void Stop() { - explorerWatcher?.Stop(); - logger.Info("Stopped monitoring 'explorer.exe'."); + timer.Stop(); + timer.Elapsed -= Timer_Elapsed; + logger.Info("Stopped monitoring applications."); if (captureHookId.HasValue) { @@ -142,7 +144,7 @@ namespace SafeExamBrowser.Monitoring.Applications } application.Processes.Add(process); - logger.Error($"Process '{process.Name}' belongs to application '{application.Name}' and could not be terminated automatically!"); + logger.Error($"Process {process} belongs to application '{application.Name}' and could not be terminated automatically!"); } private void AddForTermination(string name, IProcess process, InitializationResult result) @@ -156,7 +158,7 @@ namespace SafeExamBrowser.Monitoring.Applications } application.Processes.Add(process); - logger.Debug($"Process '{process.Name}' belongs to application '{application.Name}' and needs to be terminated."); + logger.Debug($"Process {process} belongs to application '{application.Name}' and needs to be terminated."); } private bool BelongsToApplication(IProcess process, BlacklistApplication application) @@ -231,63 +233,73 @@ namespace SafeExamBrowser.Monitoring.Applications private bool TryTerminate(IProcess process) { const int MAX_ATTEMPTS = 5; - const int TIMEOUT = 100; + const int TIMEOUT = 500; - try + 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.TryClose()) + if (process.TryKill(TIMEOUT)) { break; } - else - { - Thread.Sleep(TIMEOUT); - } - } - - if (!process.HasTerminated) - { - for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++) - { - if (process.TryKill()) - { - break; - } - else - { - Thread.Sleep(TIMEOUT); - } - } - } - - if (process.HasTerminated) - { - logger.Info($"Successfully terminated process '{process.Name}'."); - } - else - { - logger.Warn($"Failed to terminate process '{process.Name}'!"); } } - catch (Exception e) + + if (process.HasTerminated) { - logger.Error($"An error occurred while attempting to terminate process '{process.Name}'!", e); + logger.Info($"Successfully terminated process {process}."); + } + else + { + logger.Warn($"Failed to terminate process {process}!"); } return process.HasTerminated; } - private void ExplorerWatcher_EventArrived(object sender, EventArrivedEventArgs e) + private void Timer_Elapsed(object sender, ElapsedEventArgs e) { - var eventName = e.NewEvent.ClassPath.ClassName; + 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(); - if (eventName == "__InstanceCreationEvent") + foreach (var process in started) { - logger.Warn("A new instance of Windows explorer has been started!"); - ExplorerStarted?.Invoke(); + logger.Debug($"Process {process} has been started."); + processes.Add(process); + + foreach (var application in blacklist) + { + if (BelongsToApplication(process, application)) + { + logger.Warn($"Process {process} belongs to blacklisted application '{application.ExecutableName}'! Attempting termination..."); + + var success = TryTerminate(process); + + if (!success) + { + // TODO: Invoke event -> Show lock screen! + } + } + } } + + foreach (var process in terminated) + { + logger.Debug($"Process {process} has been terminated."); + processes.Remove(process); + } + + timer.Start(); } private void SystemEvent_WindowChanged(IntPtr window) @@ -299,15 +311,5 @@ namespace SafeExamBrowser.Monitoring.Applications Check(window); } } - - private string GetQueryFor(string processName) - { - return $@" - SELECT * - FROM __InstanceOperationEvent - WITHIN 2 - WHERE TargetInstance ISA 'Win32_Process' - AND TargetInstance.Name = '{processName}'"; - } } } diff --git a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj index 01f4c292..d24a251b 100644 --- a/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj +++ b/SafeExamBrowser.Monitoring/SafeExamBrowser.Monitoring.csproj @@ -50,7 +50,6 @@ - diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs index f8426b7e..86e6de2c 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ClientOperationTests.cs @@ -172,7 +172,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations proxy.Verify(p => p.InitiateShutdown(), Times.Once); proxy.Verify(p => p.Disconnect(), Times.Once); - process.Verify(p => p.TryKill(), Times.Never); + process.Verify(p => p.TryKill(default(int)), Times.Never); Assert.IsNull(sessionContext.ClientProcess); Assert.IsNull(sessionContext.ClientProxy); @@ -181,12 +181,12 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations [TestMethod] public void Revert_MustKillClientIfStoppingFailed() { - process.Setup(p => p.TryKill()).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true)); + process.Setup(p => p.TryKill(default(int))).Callback(() => process.SetupGet(p => p.HasTerminated).Returns(true)); PerformNormally(); sut.Revert(); - process.Verify(p => p.TryKill(), Times.AtLeastOnce); + process.Verify(p => p.TryKill(default(int)), Times.AtLeastOnce); Assert.IsNull(sessionContext.ClientProcess); Assert.IsNull(sessionContext.ClientProxy); @@ -198,7 +198,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations PerformNormally(); sut.Revert(); - process.Verify(p => p.TryKill(), Times.Exactly(5)); + process.Verify(p => p.TryKill(default(int)), Times.Exactly(5)); Assert.IsNotNull(sessionContext.ClientProcess); Assert.IsNotNull(sessionContext.ClientProxy); @@ -213,7 +213,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations proxy.Verify(p => p.InitiateShutdown(), Times.Never); proxy.Verify(p => p.Disconnect(), Times.Never); - process.Verify(p => p.TryKill(), Times.Never); + process.Verify(p => p.TryKill(default(int)), Times.Never); Assert.IsNull(sessionContext.ClientProcess); Assert.IsNull(sessionContext.ClientProxy); diff --git a/SafeExamBrowser.Runtime.UnitTests/Operations/ClientTerminationOperationTests.cs b/SafeExamBrowser.Runtime.UnitTests/Operations/ClientTerminationOperationTests.cs index 66565b85..0c5a5d91 100644 --- a/SafeExamBrowser.Runtime.UnitTests/Operations/ClientTerminationOperationTests.cs +++ b/SafeExamBrowser.Runtime.UnitTests/Operations/ClientTerminationOperationTests.cs @@ -81,7 +81,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations proxy.Verify(p => p.InitiateShutdown(), Times.Once); proxy.Verify(p => p.Disconnect(), Times.Once); - process.Verify(p => p.TryKill(), Times.Never); + process.Verify(p => p.TryKill(default(int)), Times.Never); Assert.IsNull(sessionContext.ClientProcess); Assert.IsNull(sessionContext.ClientProxy); diff --git a/SafeExamBrowser.WindowsApi.Contracts/IProcess.cs b/SafeExamBrowser.WindowsApi.Contracts/IProcess.cs index fb1660ac..5e5e6b30 100644 --- a/SafeExamBrowser.WindowsApi.Contracts/IProcess.cs +++ b/SafeExamBrowser.WindowsApi.Contracts/IProcess.cs @@ -41,13 +41,16 @@ namespace SafeExamBrowser.WindowsApi.Contracts event ProcessTerminatedEventHandler Terminated; /// - /// Attempts to gracefully terminate the process by closing its main window. This will only work for interactive processes which have a main window. + /// Attempts to gracefully terminate the process by closing its main window. This will only work for interactive processes which have a main + /// window. Optionally waits the specified amount of time for the process to terminate. Returns true if the process has terminated, + /// otherwise false. /// - bool TryClose(); + bool TryClose(int timeout_ms = 0); /// - /// Attempts to immediately kill the process. + /// Attempts to immediately kill the process. Optionally waits the specified amount of time for the process to terminate. Returns true + /// if the process has terminated, otherwise false. /// - bool TryKill(); + bool TryKill(int timeout_ms = 0); } } diff --git a/SafeExamBrowser.WindowsApi.Contracts/IProcessFactory.cs b/SafeExamBrowser.WindowsApi.Contracts/IProcessFactory.cs index 20db160f..c5eb65c1 100644 --- a/SafeExamBrowser.WindowsApi.Contracts/IProcessFactory.cs +++ b/SafeExamBrowser.WindowsApi.Contracts/IProcessFactory.cs @@ -24,7 +24,7 @@ namespace SafeExamBrowser.WindowsApi.Contracts /// /// Retrieves all currently running processes. /// - IEnumerable GetAllRunning(); + IList GetAllRunning(); /// /// Starts a new process with the given command-line arguments. diff --git a/SafeExamBrowser.WindowsApi/Process.cs b/SafeExamBrowser.WindowsApi/Process.cs index 6808ccbc..aab763e1 100644 --- a/SafeExamBrowser.WindowsApi/Process.cs +++ b/SafeExamBrowser.WindowsApi/Process.cs @@ -62,18 +62,25 @@ namespace SafeExamBrowser.WindowsApi this.originalNameInitialized = true; } - public bool TryClose() + public bool TryClose(int timeout_ms = 0) { try { + logger.Debug("Attempting to close process..."); process.Refresh(); - if (!process.HasExited) + var success = process.CloseMainWindow(); + + if (success) { - process.CloseMainWindow(); + logger.Debug("Successfully sent close message to main window."); + } + else + { + logger.Warn("Failed to send close message to main window!"); } - return process.HasExited; + return success && WaitForTermination(timeout_ms); } catch (Exception e) { @@ -83,18 +90,16 @@ namespace SafeExamBrowser.WindowsApi return false; } - public bool TryKill() + public bool TryKill(int timeout_ms = 0) { try { + logger.Debug("Attempting to kill process..."); + process.Refresh(); + process.Kill(); - if (!process.HasExited) - { - process.Kill(); - } - - return process.HasExited; + return WaitForTermination(timeout_ms); } catch (Exception e) { @@ -104,6 +109,11 @@ namespace SafeExamBrowser.WindowsApi return false; } + public override string ToString() + { + return $"'{Name}' ({Id})"; + } + private bool IsTerminated() { try @@ -127,6 +137,7 @@ namespace SafeExamBrowser.WindowsApi eventInitialized = true; process.Exited += Process_Exited; process.EnableRaisingEvents = true; + logger.Debug("Initialized termination event."); } } @@ -165,9 +176,26 @@ namespace SafeExamBrowser.WindowsApi return originalName; } + private bool WaitForTermination(int timeout_ms) + { + var terminated = process.WaitForExit(timeout_ms); + + if (terminated) + { + logger.Debug($"Process has terminated within {timeout_ms}ms."); + } + else + { + logger.Warn($"Process failed to terminate within {timeout_ms}ms!"); + } + + return terminated; + } + private void Process_Exited(object sender, EventArgs e) { TerminatedEvent?.Invoke(process.ExitCode); + logger.Debug("Process has terminated."); } } } diff --git a/SafeExamBrowser.WindowsApi/ProcessFactory.cs b/SafeExamBrowser.WindowsApi/ProcessFactory.cs index 8d5fda46..bd7be07d 100644 --- a/SafeExamBrowser.WindowsApi/ProcessFactory.cs +++ b/SafeExamBrowser.WindowsApi/ProcessFactory.cs @@ -32,17 +32,20 @@ namespace SafeExamBrowser.WindowsApi this.logger = logger; } - public IEnumerable GetAllRunning() + public IList GetAllRunning() { - var processes = System.Diagnostics.Process.GetProcesses(); + var processes = new List(); + var running = System.Diagnostics.Process.GetProcesses(); var originalNames = LoadOriginalNames(); - foreach (var process in processes) + foreach (var process in running) { var originalName = originalNames.FirstOrDefault(n => n.processId == process.Id).originalName; - yield return new Process(process, originalName, LoggerFor(process)); + processes.Add(new Process(process, originalName, LoggerFor(process))); } + + return processes; } public IProcess StartNew(string path, params string[] args)