diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 1cf952a3..43beafeb 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -83,7 +83,7 @@ namespace SafeExamBrowser.Runtime var serviceProxy = new ServiceProxy(appConfig.ServiceAddress, new ProxyObjectFactory(), ModuleLogger(nameof(ServiceProxy)), Interlocutor.Runtime); var sessionContext = new SessionContext(); var splashScreen = uiFactory.CreateSplashScreen(appConfig); - var vmDetector = new VirtualMachineDetector(ModuleLogger(nameof(VirtualMachineDetector)), systemInfo); + var vmDetector = new VirtualMachineDetector(ModuleLogger(nameof(VirtualMachineDetector)), registry, systemInfo); var bootstrapOperations = new Queue(); var sessionOperations = new Queue(); diff --git a/SafeExamBrowser.SystemComponents.Contracts/Registry/IRegistry.cs b/SafeExamBrowser.SystemComponents.Contracts/Registry/IRegistry.cs index 25db0c17..da6aabcd 100644 --- a/SafeExamBrowser.SystemComponents.Contracts/Registry/IRegistry.cs +++ b/SafeExamBrowser.SystemComponents.Contracts/Registry/IRegistry.cs @@ -7,6 +7,7 @@ */ using SafeExamBrowser.SystemComponents.Contracts.Registry.Events; +using System.Collections.Generic; namespace SafeExamBrowser.SystemComponents.Contracts.Registry { @@ -34,5 +35,15 @@ namespace SafeExamBrowser.SystemComponents.Contracts.Registry /// Attempts to read the value of the given name under the specified registry key. /// bool TryRead(string key, string name, out object value); + + /// + /// Attempts to read the value names of the given registry key. + /// + bool TryGetNames(string key, out IEnumerable names); + + /// + /// Attempts to read the subkey names of the given registry key. + /// + bool TryGetSubKeys(string key, out IEnumerable subKeys); } } diff --git a/SafeExamBrowser.SystemComponents/Registry/Registry.cs b/SafeExamBrowser.SystemComponents/Registry/Registry.cs index cbbf958b..43b67ddd 100644 --- a/SafeExamBrowser.SystemComponents/Registry/Registry.cs +++ b/SafeExamBrowser.SystemComponents/Registry/Registry.cs @@ -8,7 +8,10 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Security.Cryptography; using System.Timers; +using Microsoft.Win32; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.SystemComponents.Contracts.Registry; using SafeExamBrowser.SystemComponents.Contracts.Registry.Events; @@ -86,6 +89,59 @@ namespace SafeExamBrowser.SystemComponents.Registry return success; } + public bool TryGetNames(string keyName, out IEnumerable names) + { + names = default; + + if (!TryOpenKey(keyName, out var key)) + { + return false; + } + + var success = true; + using (key) + { + try + { + names = key.GetValueNames(); + } + catch (Exception e) + { + logger.Error($"Failed to get registry value names '{keyName}'!", e); + success = false; + } + + } + + return success; + } + + public bool TryGetSubKeys(string keyName, out IEnumerable subKeys) + { + subKeys = default; + + if (!TryOpenKey(keyName, out var key)) + { + return false; + } + + var success = true; + using (key) + { + try + { + subKeys = key.GetSubKeyNames(); + } + catch (Exception e) + { + logger.Error($"Failed to get registry value names '{keyName}'!", e); + success = false; + } + } + + return success; + } + private void Timer_Elapsed(object sender, ElapsedEventArgs e) { foreach (var item in values) @@ -104,5 +160,89 @@ namespace SafeExamBrowser.SystemComponents.Registry } } } + + private bool TryGetBaseKeyFromKeyName(string keyName, out RegistryKey baseKey, out string subKeyName) + { + baseKey = default; + subKeyName = default; + + string basekeyName; + var baseKeyLength = keyName.IndexOf('\\'); + if (baseKeyLength != -1) + { + basekeyName = keyName.Substring(0, baseKeyLength).ToUpper(System.Globalization.CultureInfo.InvariantCulture); + } + else + { + basekeyName = keyName.ToUpper(System.Globalization.CultureInfo.InvariantCulture); + } + + switch (basekeyName) + { + case "HKEY_CURRENT_USER": + case "HKCU": + baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64); + break; + case "HKEY_LOCAL_MACHINE": + case "HKLM": + baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + break; + case "HKEY_CLASSES_ROOT": + case "HKCR": + baseKey = RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64); + break; + case "HKEY_USERS": + case "HKU": + baseKey = RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Registry64); + break; + case "HKEY_PERFORMANCE_DATA": + case "HKPD": + baseKey = RegistryKey.OpenBaseKey(RegistryHive.PerformanceData, RegistryView.Registry64); + break; + case "HKEY_CURRENT_CONFIG": + case "HKCC": + baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentConfig, RegistryView.Registry64); + break; + case "HKEY_DYN_DATA": + case "HKDD": + baseKey = RegistryKey.OpenBaseKey(RegistryHive.DynData, RegistryView.Registry64); + break; + default: + return false; + } + + if (baseKeyLength == -1 || baseKeyLength == keyName.Length) + { + subKeyName = string.Empty; + } + else + { + subKeyName = keyName.Substring(baseKeyLength + 1, keyName.Length - baseKeyLength - 1); + } + + return true; + } + + private bool TryOpenKey(string keyName, out RegistryKey key) + { + key = default; + + try + { + logger.Info($"default(RegistryKey) == null: {key == null}"); + if (TryGetBaseKeyFromKeyName(keyName, out var baseKey, out var subKey)) + { + key = baseKey.OpenSubKey(subKey); + } + + } + catch (Exception e) + { + logger.Error($"Failed to open registry key '{keyName}'!", e); + return false; + } + + return key != default; + } } } diff --git a/SafeExamBrowser.SystemComponents/VirtualMachineDetector.cs b/SafeExamBrowser.SystemComponents/VirtualMachineDetector.cs index 51d628d6..f6b7a271 100644 --- a/SafeExamBrowser.SystemComponents/VirtualMachineDetector.cs +++ b/SafeExamBrowser.SystemComponents/VirtualMachineDetector.cs @@ -7,8 +7,14 @@ */ using System.Linq; +using System.Management; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.SystemComponents.Contracts; +using SafeExamBrowser.SystemComponents.Contracts.Registry; +using Microsoft.Win32; +using System.Collections; +using System.Collections.Generic; +using System; namespace SafeExamBrowser.SystemComponents { @@ -38,35 +44,36 @@ namespace SafeExamBrowser.SystemComponents }; private readonly ILogger logger; + private readonly IRegistry registry; private readonly ISystemInfo systemInfo; - public VirtualMachineDetector(ILogger logger, ISystemInfo systemInfo) + public VirtualMachineDetector(ILogger logger, IRegistry registry, ISystemInfo systemInfo) { this.logger = logger; + this.registry = registry; this.systemInfo = systemInfo; } public bool IsVirtualMachine() { - var biosInfo = systemInfo.BiosInfo.ToLower(); var isVirtualMachine = false; + + var biosInfo = systemInfo.BiosInfo; var macAddress = systemInfo.MacAddress; - var manufacturer = systemInfo.Manufacturer.ToLower(); - var model = systemInfo.Model.ToLower(); + var manufacturer = systemInfo.Manufacturer; + var model = systemInfo.Model; var devices = systemInfo.PlugAndPlayDeviceIds; - isVirtualMachine |= biosInfo.Contains("hyper-v"); - isVirtualMachine |= biosInfo.Contains("virtualbox"); - isVirtualMachine |= biosInfo.Contains("vmware"); - isVirtualMachine |= manufacturer.Contains("microsoft corporation") && !model.Contains("surface"); - isVirtualMachine |= manufacturer.Contains("parallels software"); - isVirtualMachine |= manufacturer.Contains("qemu"); - isVirtualMachine |= manufacturer.Contains("vmware"); - isVirtualMachine |= model.Contains("virtualbox"); + // redundancy: registry check does this aswell (systemInfo may be using different methods) + isVirtualMachine |= IsVirtualSystemInfo(biosInfo, manufacturer, model); + isVirtualMachine |= IsVirtualWmi(); + isVirtualMachine |= IsVirtualRegistry(); if (macAddress != null && macAddress.Count() > 2) { - isVirtualMachine |= macAddress.StartsWith(QEMU_MAC_PREFIX) || macAddress.StartsWith(VIRTUALBOX_MAC_PREFIX); + isVirtualMachine |= macAddress.StartsWith(QEMU_MAC_PREFIX); + isVirtualMachine |= macAddress.StartsWith(VIRTUALBOX_MAC_PREFIX); + isVirtualMachine |= macAddress.StartsWith("000000000000"); // indicates tampering } foreach (var device in devices) @@ -78,5 +85,154 @@ namespace SafeExamBrowser.SystemComponents return isVirtualMachine; } + + private bool IsVirtualSystemInfo(string biosInfo, string manufacturer, string model) + { + var isVirtualMachine = false; + + biosInfo = biosInfo.ToLower(); + manufacturer = manufacturer.ToLower(); + model = model.ToLower(); + + isVirtualMachine |= biosInfo.Contains("hyper-v"); + isVirtualMachine |= biosInfo.Contains("virtualbox"); + isVirtualMachine |= biosInfo.Contains("vmware"); + isVirtualMachine |= biosInfo.Contains("ovmf"); + isVirtualMachine |= biosInfo.Contains("edk ii unknown"); // qemu + isVirtualMachine |= manufacturer.Contains("microsoft corporation") && !model.Contains("surface"); + isVirtualMachine |= manufacturer.Contains("parallels software"); + isVirtualMachine |= manufacturer.Contains("qemu"); + isVirtualMachine |= manufacturer.Contains("vmware"); + isVirtualMachine |= model.Contains("virtualbox"); + isVirtualMachine |= model.Contains("Q35 +"); // qemu + + return isVirtualMachine; + } + + private bool IsVirtualRegistry() + { + var isVirtualMachine = false; + + // the resulting IsVirtualRegistry() would be massive so split it + isVirtualMachine |= HasHistoricVirtualMachineHardwareConfiguration(); + isVirtualMachine |= HasLocalVirtualMachineDeviceCache(); + + return isVirtualMachine; + } + + private bool HasHistoricVirtualMachineHardwareConfiguration() + { + var isVirtualMachine = false; + + /** + * scanned registry format: + * + * HKLM\SYSTEM\HardwareConfig\{configId=uuid} + * - BIOSVendor + * - SystemManufacturer + * - ... + * \ComputerIds + * - {computerId=uuid}: {computerSummary=hardwareInfo} + * + */ + const string hardwareRootKey = "HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig"; + if (!registry.TryGetSubKeys(hardwareRootKey, out var hardwareConfigSubkeys)) + { + return false; + } + + foreach (var configId in hardwareConfigSubkeys) + { + var hardwareConfigKey = $"{hardwareRootKey}\\{configId}"; + var didReadKeys = true; + + // collect system values for IsVirtualSystemInfo() + didReadKeys &= registry.TryRead(hardwareConfigKey, "BIOSVendor", out var biosVendor); + didReadKeys &= registry.TryRead(hardwareConfigKey, "BIOSVersion", out var biosVersion); + didReadKeys &= registry.TryRead(hardwareConfigKey, "SystemManufacturer", out var systemManufacturer); + didReadKeys &= registry.TryRead(hardwareConfigKey, "SystemProductName", out var systemProductName); + if (!didReadKeys) + { + continue; + } + + // reconstruct the systemInfo.biosInfo string + var biosInfo = $"{(string) biosVendor} {(string) biosVersion}"; + + isVirtualMachine |= IsVirtualSystemInfo(biosInfo, (string) systemManufacturer, (string) systemProductName); + + // check even more hardware information + var computerIdsKey = $"{hardwareConfigKey}\\ComputerIds"; + if (!registry.TryGetNames(computerIdsKey, out var computerIdNames)) + { + continue; + } + + foreach (var computerIdName in computerIdNames) + { + // collect computer hardware summary (e.g. manufacturer&version&sku&...) + if (!registry.TryRead(computerIdsKey, computerIdName, out var computerSummary)) + { + continue; + } + + isVirtualMachine |= IsVirtualSystemInfo((string) computerSummary, (string) systemManufacturer, (string) systemProductName); + } + } + + return isVirtualMachine; + } + + private bool HasLocalVirtualMachineDeviceCache() + { + var isVirtualMachine = false; + + // device cache contains hardware about other devices logged into as well, so lock onto this device in case an innocent VM was logged into. + // in the future, try to improve this check somehow since DeviceCache only gives ComputerName + var deviceName = System.Environment.GetEnvironmentVariable("COMPUTERNAME"); + + // check Windows timeline caches for current hardware config + const string deviceCacheParentKey = "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\TaskFlow\\DeviceCache"; + var hasDeviceCacheKeys = registry.TryGetSubKeys(deviceCacheParentKey, out var deviceCacheKeys); + + if (deviceName != null && hasDeviceCacheKeys) + { + foreach (var cacheId in deviceCacheKeys) + { + var cacheIdKey = $"{deviceCacheParentKey}\\{cacheId}"; + var didReadKeys = true; + + didReadKeys &= registry.TryRead(cacheIdKey, "DeviceName", out var cacheDeviceName); + if (!didReadKeys || deviceName.ToLower() != ((string) cacheDeviceName).ToLower()) + { + continue; + } + + didReadKeys &= registry.TryRead(cacheIdKey, "DeviceMake", out var cacheDeviceManufacturer); + didReadKeys &= registry.TryRead(cacheIdKey, "DeviceModel", out var cacheDeviceModel); + if (!didReadKeys) + { + continue; + } + + isVirtualMachine |= IsVirtualSystemInfo("", (string) cacheDeviceManufacturer, (string) cacheDeviceModel); + } + } + + return isVirtualMachine; + } + + private bool IsVirtualWmi() + { + var isVirtualMachine = false; + var cpuObjSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"); + + foreach (var cpuObj in cpuObjSearcher.Get()) + { + isVirtualMachine |= ((string) cpuObj["Name"]).ToLower().Contains(" kvm "); // qemu (KVM specifically) + } + + return isVirtualMachine; + } } }