SEBWIN-313: Started implementing blacklist monitoring.

This commit is contained in:
dbuechel 2019-10-08 16:11:19 +02:00
parent 8d0d1832a9
commit d3d98c7df7
10 changed files with 136 additions and 102 deletions

View file

@ -94,7 +94,7 @@ namespace SafeExamBrowser.Client
taskbar = BuildTaskbar(); taskbar = BuildTaskbar();
terminationActivator = new TerminationActivator(ModuleLogger(nameof(TerminationActivator))); 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 displayMonitor = new DisplayMonitor(ModuleLogger(nameof(DisplayMonitor)), nativeMethods, systemInfo);
var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods); var explorerShell = new ExplorerShell(ModuleLogger(nameof(ExplorerShell)), nativeMethods);
var hashAlgorithm = new HashAlgorithm(); var hashAlgorithm = new HashAlgorithm();

View file

@ -14,7 +14,6 @@ using SafeExamBrowser.Core.Contracts.OperationModel.Events;
using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Settings;
using SafeExamBrowser.Settings.Applications; using SafeExamBrowser.Settings.Applications;
namespace SafeExamBrowser.Client.Operations namespace SafeExamBrowser.Client.Operations
@ -110,18 +109,18 @@ namespace SafeExamBrowser.Client.Operations
private void StartMonitor() private void StartMonitor()
{ {
if (Context.Settings.KioskMode != KioskMode.None) //TODO: if (Context.Settings.KioskMode != KioskMode.None)
{ //{
applicationMonitor.Start(); applicationMonitor.Start();
} //}
} }
private void StopMonitor() private void StopMonitor()
{ {
if (Context.Settings.KioskMode != KioskMode.None) //TODO: if (Context.Settings.KioskMode != KioskMode.None)
{ //{
applicationMonitor.Stop(); applicationMonitor.Stop();
} //}
} }
private OperationResult TryTerminate(IEnumerable<RunningApplication> runningApplications) private OperationResult TryTerminate(IEnumerable<RunningApplication> runningApplications)

View file

@ -9,8 +9,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Management; using System.Timers;
using System.Threading;
using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Monitoring.Contracts.Applications; using SafeExamBrowser.Monitoring.Contracts.Applications;
using SafeExamBrowser.Monitoring.Contracts.Applications.Events; using SafeExamBrowser.Monitoring.Contracts.Applications.Events;
@ -24,21 +23,24 @@ namespace SafeExamBrowser.Monitoring.Applications
private IntPtr activeWindow; private IntPtr activeWindow;
private IList<BlacklistApplication> blacklist; private IList<BlacklistApplication> blacklist;
private Guid? captureHookId; private Guid? captureHookId;
private ManagementEventWatcher explorerWatcher;
private Guid? foregroundHookId; private Guid? foregroundHookId;
private ILogger logger; private ILogger logger;
private INativeMethods nativeMethods; private INativeMethods nativeMethods;
private IList<IProcess> processes;
private IProcessFactory processFactory; private IProcessFactory processFactory;
private Timer timer;
private IList<WhitelistApplication> whitelist; private IList<WhitelistApplication> whitelist;
public event ExplorerStartedEventHandler ExplorerStarted; 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<BlacklistApplication>(); this.blacklist = new List<BlacklistApplication>();
this.logger = logger; this.logger = logger;
this.nativeMethods = nativeMethods; this.nativeMethods = nativeMethods;
this.processes = new List<IProcess>();
this.processFactory = processFactory; this.processFactory = processFactory;
this.timer = new Timer(interval_ms);
this.whitelist = new List<WhitelistApplication>(); this.whitelist = new List<WhitelistApplication>();
} }
@ -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 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))}" : ".")}"); 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) 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); AddForTermination(application.ExecutableName, process, result);
} }
else if (isMatch && application.AutoTerminate && !TryTerminate(process)) else if (isBlacklisted && application.AutoTerminate && !TryTerminate(process))
{ {
AddFailed(application.ExecutableName, process, result); AddFailed(application.ExecutableName, process, result);
} }
@ -86,13 +90,10 @@ namespace SafeExamBrowser.Monitoring.Applications
public void Start() public void Start()
{ {
// TODO: Start monitoring blacklist... timer.AutoReset = false;
timer.Elapsed += Timer_Elapsed;
// TODO: Remove WMI event and use timer mechanism! timer.Start();
explorerWatcher = new ManagementEventWatcher(@"\\.\root\CIMV2", GetQueryFor("explorer.exe")); logger.Info("Started monitoring applications.");
explorerWatcher.EventArrived += new EventArrivedEventHandler(ExplorerWatcher_EventArrived);
explorerWatcher.Start();
logger.Info("Started monitoring process 'explorer.exe'.");
captureHookId = nativeMethods.RegisterSystemCaptureStartEvent(SystemEvent_WindowChanged); captureHookId = nativeMethods.RegisterSystemCaptureStartEvent(SystemEvent_WindowChanged);
logger.Info($"Registered system capture start event with ID = {captureHookId}."); logger.Info($"Registered system capture start event with ID = {captureHookId}.");
@ -103,8 +104,9 @@ namespace SafeExamBrowser.Monitoring.Applications
public void Stop() public void Stop()
{ {
explorerWatcher?.Stop(); timer.Stop();
logger.Info("Stopped monitoring 'explorer.exe'."); timer.Elapsed -= Timer_Elapsed;
logger.Info("Stopped monitoring applications.");
if (captureHookId.HasValue) if (captureHookId.HasValue)
{ {
@ -142,7 +144,7 @@ namespace SafeExamBrowser.Monitoring.Applications
} }
application.Processes.Add(process); 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) private void AddForTermination(string name, IProcess process, InitializationResult result)
@ -156,7 +158,7 @@ namespace SafeExamBrowser.Monitoring.Applications
} }
application.Processes.Add(process); 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) private bool BelongsToApplication(IProcess process, BlacklistApplication application)
@ -231,63 +233,73 @@ namespace SafeExamBrowser.Monitoring.Applications
private bool TryTerminate(IProcess process) private bool TryTerminate(IProcess process)
{ {
const int MAX_ATTEMPTS = 5; 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++) for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++)
{ {
if (process.TryClose()) if (process.TryKill(TIMEOUT))
{ {
break; 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; 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!"); logger.Debug($"Process {process} has been started.");
ExplorerStarted?.Invoke(); 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) private void SystemEvent_WindowChanged(IntPtr window)
@ -299,15 +311,5 @@ namespace SafeExamBrowser.Monitoring.Applications
Check(window); Check(window);
} }
} }
private string GetQueryFor(string processName)
{
return $@"
SELECT *
FROM __InstanceOperationEvent
WITHIN 2
WHERE TargetInstance ISA 'Win32_Process'
AND TargetInstance.Name = '{processName}'";
}
} }
} }

View file

@ -50,7 +50,6 @@
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Management" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
<Reference Include="WindowsBase" /> <Reference Include="WindowsBase" />

View file

@ -172,7 +172,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Once); proxy.Verify(p => p.InitiateShutdown(), Times.Once);
proxy.Verify(p => p.Disconnect(), 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.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy); Assert.IsNull(sessionContext.ClientProxy);
@ -181,12 +181,12 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
[TestMethod] [TestMethod]
public void Revert_MustKillClientIfStoppingFailed() 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(); PerformNormally();
sut.Revert(); 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.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy); Assert.IsNull(sessionContext.ClientProxy);
@ -198,7 +198,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
PerformNormally(); PerformNormally();
sut.Revert(); 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.ClientProcess);
Assert.IsNotNull(sessionContext.ClientProxy); Assert.IsNotNull(sessionContext.ClientProxy);
@ -213,7 +213,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Never); proxy.Verify(p => p.InitiateShutdown(), Times.Never);
proxy.Verify(p => p.Disconnect(), 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.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy); Assert.IsNull(sessionContext.ClientProxy);

View file

@ -81,7 +81,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Operations
proxy.Verify(p => p.InitiateShutdown(), Times.Once); proxy.Verify(p => p.InitiateShutdown(), Times.Once);
proxy.Verify(p => p.Disconnect(), 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.ClientProcess);
Assert.IsNull(sessionContext.ClientProxy); Assert.IsNull(sessionContext.ClientProxy);

View file

@ -41,13 +41,16 @@ namespace SafeExamBrowser.WindowsApi.Contracts
event ProcessTerminatedEventHandler Terminated; event ProcessTerminatedEventHandler Terminated;
/// <summary> /// <summary>
/// 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 <c>true</c> if the process has terminated,
/// otherwise <c>false</c>.
/// </summary> /// </summary>
bool TryClose(); bool TryClose(int timeout_ms = 0);
/// <summary> /// <summary>
/// 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 <c>true</c>
/// if the process has terminated, otherwise <c>false</c>.
/// </summary> /// </summary>
bool TryKill(); bool TryKill(int timeout_ms = 0);
} }
} }

View file

@ -24,7 +24,7 @@ namespace SafeExamBrowser.WindowsApi.Contracts
/// <summary> /// <summary>
/// Retrieves all currently running processes. /// Retrieves all currently running processes.
/// </summary> /// </summary>
IEnumerable<IProcess> GetAllRunning(); IList<IProcess> GetAllRunning();
/// <summary> /// <summary>
/// Starts a new process with the given command-line arguments. /// Starts a new process with the given command-line arguments.

View file

@ -62,18 +62,25 @@ namespace SafeExamBrowser.WindowsApi
this.originalNameInitialized = true; this.originalNameInitialized = true;
} }
public bool TryClose() public bool TryClose(int timeout_ms = 0)
{ {
try try
{ {
logger.Debug("Attempting to close process...");
process.Refresh(); 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) catch (Exception e)
{ {
@ -83,18 +90,16 @@ namespace SafeExamBrowser.WindowsApi
return false; return false;
} }
public bool TryKill() public bool TryKill(int timeout_ms = 0)
{ {
try try
{ {
logger.Debug("Attempting to kill process...");
process.Refresh(); process.Refresh();
process.Kill();
if (!process.HasExited) return WaitForTermination(timeout_ms);
{
process.Kill();
}
return process.HasExited;
} }
catch (Exception e) catch (Exception e)
{ {
@ -104,6 +109,11 @@ namespace SafeExamBrowser.WindowsApi
return false; return false;
} }
public override string ToString()
{
return $"'{Name}' ({Id})";
}
private bool IsTerminated() private bool IsTerminated()
{ {
try try
@ -127,6 +137,7 @@ namespace SafeExamBrowser.WindowsApi
eventInitialized = true; eventInitialized = true;
process.Exited += Process_Exited; process.Exited += Process_Exited;
process.EnableRaisingEvents = true; process.EnableRaisingEvents = true;
logger.Debug("Initialized termination event.");
} }
} }
@ -165,9 +176,26 @@ namespace SafeExamBrowser.WindowsApi
return originalName; 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) private void Process_Exited(object sender, EventArgs e)
{ {
TerminatedEvent?.Invoke(process.ExitCode); TerminatedEvent?.Invoke(process.ExitCode);
logger.Debug("Process has terminated.");
} }
} }
} }

View file

@ -32,17 +32,20 @@ namespace SafeExamBrowser.WindowsApi
this.logger = logger; this.logger = logger;
} }
public IEnumerable<IProcess> GetAllRunning() public IList<IProcess> GetAllRunning()
{ {
var processes = System.Diagnostics.Process.GetProcesses(); var processes = new List<IProcess>();
var running = System.Diagnostics.Process.GetProcesses();
var originalNames = LoadOriginalNames(); var originalNames = LoadOriginalNames();
foreach (var process in processes) foreach (var process in running)
{ {
var originalName = originalNames.FirstOrDefault(n => n.processId == process.Id).originalName; 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) public IProcess StartNew(string path, params string[] args)