From 607a1562351ba599d8ce474da681f612df4f4314 Mon Sep 17 00:00:00 2001 From: Violet Hansen Date: Sun, 12 Jan 2025 22:13:00 +0200 Subject: [PATCH] Another protection when removing signed policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This new protection mechanism ensures the safe removal of signed policies. To complete the process securely, a system reboot is required after the first stage. The newly implemented protection verifies that the reboot has been performed before allowing the process to proceed to the final stage. If the user forgets to reboot or is unsure whether it’s necessary, a prompt will appear to guide them through the process. This safeguard prevents accidental errors that could lead to boot failures, making the AppControl Manager even safer and more reliable when managing Signed App Control policies. --- .../Logic/Main/UserConfiguration.cs | 117 +++++++++++- .../ViewCurrentPolicies.xaml.cs | 177 ++++++++++++------ 2 files changed, 236 insertions(+), 58 deletions(-) diff --git a/AppControl Manager/Logic/Main/UserConfiguration.cs b/AppControl Manager/Logic/Main/UserConfiguration.cs index a07c41d8b..a00e289e9 100644 --- a/AppControl Manager/Logic/Main/UserConfiguration.cs +++ b/AppControl Manager/Logic/Main/UserConfiguration.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using AppControlManager.Logging; @@ -33,7 +34,8 @@ public sealed partial class UserConfiguration( Guid? strictKernelNoFlightRootsPolicyGUID, DateTime? lastUpdateCheck, DateTime? strictKernelModePolicyTimeOfDeployment, - bool? autoUpdateCheck + bool? autoUpdateCheck, + Dictionary? signedPolicyStage1RemovalTimes = null ) { [JsonPropertyOrder(1)] @@ -66,6 +68,9 @@ public sealed partial class UserConfiguration( [JsonPropertyOrder(10)] public bool? AutoUpdateCheck { get; set; } = autoUpdateCheck; + [JsonPropertyOrder(11)] + public Dictionary? SignedPolicyStage1RemovalTimes { get; set; } = signedPolicyStage1RemovalTimes; + /// @@ -82,6 +87,7 @@ public sealed partial class UserConfiguration( /// /// /// + /// /// /// internal static UserConfiguration Set( @@ -94,7 +100,8 @@ internal static UserConfiguration Set( Guid? StrictKernelNoFlightRootsPolicyGUID = null, DateTime? LastUpdateCheck = null, DateTime? StrictKernelModePolicyTimeOfDeployment = null, - bool? AutoUpdateCheck = null + bool? AutoUpdateCheck = null, + Dictionary? SignedPolicyStage1RemovalTimes = null ) { // Validate certificateCommonName @@ -154,6 +161,11 @@ internal static UserConfiguration Set( if (StrictKernelModePolicyTimeOfDeployment.HasValue) UserConfiguration.StrictKernelModePolicyTimeOfDeployment = StrictKernelModePolicyTimeOfDeployment; if (AutoUpdateCheck.HasValue) UserConfiguration.AutoUpdateCheck = AutoUpdateCheck; + if (SignedPolicyStage1RemovalTimes is not null) + { + UserConfiguration.SignedPolicyStage1RemovalTimes = SignedPolicyStage1RemovalTimes; + } + // Write the updated properties back to the JSON file WriteUserConfiguration(UserConfiguration); @@ -186,6 +198,7 @@ internal static UserConfiguration Get() /// /// /// + /// internal static void Remove( bool SignedPolicyPath = false, bool UnsignedPolicyPath = false, @@ -196,7 +209,8 @@ internal static void Remove( bool StrictKernelNoFlightRootsPolicyGUID = false, bool LastUpdateCheck = false, bool StrictKernelModePolicyTimeOfDeployment = false, - bool AutoUpdateCheck = false + bool AutoUpdateCheck = false, + bool SignedPolicyStage1RemovalTimes = false ) { // Read the current configuration @@ -213,6 +227,7 @@ internal static void Remove( if (LastUpdateCheck) currentConfig.LastUpdateCheck = null; if (StrictKernelModePolicyTimeOfDeployment) currentConfig.StrictKernelModePolicyTimeOfDeployment = null; if (AutoUpdateCheck) currentConfig.AutoUpdateCheck = null; + if (SignedPolicyStage1RemovalTimes) currentConfig.SignedPolicyStage1RemovalTimes = null; // Write the updated configuration back to the JSON file WriteUserConfiguration(currentConfig); @@ -220,6 +235,7 @@ internal static void Remove( Logger.Write("The specified properties have been removed and set to null in the UserConfigurations.json file."); } + private static UserConfiguration ReadUserConfiguration() { try @@ -276,7 +292,8 @@ private static UserConfiguration ParseJson(string json) TryGetGuidProperty(root, nameof(StrictKernelNoFlightRootsPolicyGUID)), TryGetDateTimeProperty(root, nameof(LastUpdateCheck)), TryGetDateTimeProperty(root, nameof(StrictKernelModePolicyTimeOfDeployment)), - TryGetBoolProperty(root, nameof(AutoUpdateCheck)) + TryGetBoolProperty(root, nameof(AutoUpdateCheck)), + TryGetKeyValuePairsProperty(root, nameof(SignedPolicyStage1RemovalTimes)) ); static string? TryGetStringProperty(JsonElement root, string propertyName) @@ -326,6 +343,21 @@ private static UserConfiguration ParseJson(string json) return null; } } + + static Dictionary? TryGetKeyValuePairsProperty(JsonElement root, string propertyName) + { + try + { + return root.TryGetProperty(propertyName, out var propertyValue) && propertyValue.ValueKind == JsonValueKind.Object + ? propertyValue.EnumerateObject().ToDictionary(e => e.Name, e => e.Value.GetDateTime().ToUniversalTime()) + : null; + } + catch + { + return null; + } + } + } @@ -341,4 +373,81 @@ private static void WriteUserConfiguration(UserConfiguration userConfiguration) File.WriteAllText(GlobalVars.UserConfigJson, jsonString); Logger.Write("The UserConfigurations.json file has been updated successfully."); } + + + + + /// + /// Adds a new key-value pair to the SignedPolicyStage1RemovalTimes dictionary. + /// + /// The key to add. + /// The value to associate with the key. + internal static void Add(string key, DateTime value) + { + // Get the current user configuration + UserConfiguration currentConfig = ReadUserConfiguration(); + + // Initialize the dictionary if it doesn't exist + currentConfig.SignedPolicyStage1RemovalTimes ??= []; + + // Add the key-value pair to the dictionary + currentConfig.SignedPolicyStage1RemovalTimes[key] = value; // This will add or update the value for the key + + // Write the updated configuration back to the JSON file + WriteUserConfiguration(currentConfig); + + Logger.Write($"Key-value pair added to the SignedPolicyStage1RemovalTimes: {key} = {value}"); + } + + + + /// + /// Queries the SignedPolicyStage1RemovalTimes dictionary by key and returns the corresponding value. + /// + /// The key to query. + /// The value associated with the key, or null if the key does not exist. + internal static DateTime? Query(string key) + { + // Get the current user configuration + UserConfiguration currentConfig = ReadUserConfiguration(); + + // Return the value if the key exists, otherwise return null + if (currentConfig.SignedPolicyStage1RemovalTimes is not null && currentConfig.SignedPolicyStage1RemovalTimes.TryGetValue(key, out DateTime value)) + { + return value; + } + + // Return null if the key doesn't exist + return null; + } + + + + /// + /// Removes a key-value pair from the SignedPolicyStage1RemovalTimes dictionary by key. + /// + /// The key to remove. + /// True if the key was successfully removed; false if the key was not found. + internal static void RemoveKey(string key) + { + // Get the current user configuration + UserConfiguration currentConfig = ReadUserConfiguration(); + + // Check if the dictionary exists and contains the key + if (currentConfig.SignedPolicyStage1RemovalTimes is not null && currentConfig.SignedPolicyStage1RemovalTimes.ContainsKey(key)) + { + // Remove the key-value pair + _ = currentConfig.SignedPolicyStage1RemovalTimes.Remove(key); + + // Write the updated configuration back to the JSON file + WriteUserConfiguration(currentConfig); + + Logger.Write($"Key '{key}' removed from the SignedPolicyStage1RemovalTimes dictionary."); + } + else + { + Logger.Write($"Key '{key}' not found in the SignedPolicyStage1RemovalTimes dictionary."); + } + } + } diff --git a/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs b/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs index 05d22bb2a..93ade2cb3 100644 --- a/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs +++ b/AppControl Manager/Pages/SystemInformation/ViewCurrentPolicies.xaml.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using AppControlManager.CustomUIElements; +using AppControlManager.Logging; using AppControlManager.SiPolicyIntel; using CommunityToolkit.WinUI.UI.Controls; using Microsoft.UI; @@ -322,14 +323,8 @@ await Task.Run(() => foreach (CiPolicyInfo policy in policiesToRemove) { - // Remove the policy directly from the system if it's unsigned - if (!policy.IsSignedPolicy || - // or supplemental - !string.Equals(policy.PolicyID, policy.BasePolicyID, StringComparison.OrdinalIgnoreCase) || - // or signed base policy with the EnabledUnsignedSystemIntegrityPolicy policy rule option - (policy.IsSignedPolicy && string.Equals(policy.PolicyID, policy.BasePolicyID, StringComparison.OrdinalIgnoreCase) && - policy.PolicyOptions is not null && policy.PolicyOptionsDisplay.Contains("Enabled:Unsigned System Integrity Policy", StringComparison.OrdinalIgnoreCase) - )) + // Remove the policy directly from the system if it's unsigned or supplemental + if (!policy.IsSignedPolicy || !string.Equals(policy.PolicyID, policy.BasePolicyID, StringComparison.OrdinalIgnoreCase)) { await Task.Run(() => { @@ -337,78 +332,120 @@ await Task.Run(() => }); } - // If the policy is base and signed + // At this point the policy is definitely a Signed Base policy else { - #region Signing Details acquisition + // If the EnabledUnsignedSystemIntegrityPolicy policy rule option exists + // Which means 1st stage already happened + if (policy.PolicyOptions is not null && policy.PolicyOptionsDisplay.Contains("Enabled:Unsigned System Integrity Policy", StringComparison.OrdinalIgnoreCase)) + { + // And if system was rebooted once after performing the 1st removal stage + if (VerifyRemovalEligibility(policy.PolicyID!)) + { + CiToolHelper.RemovePolicy(policy.PolicyID!); + + // Remove the PolicyID from the SignedPolicyStage1RemovalTimes dictionary + UserConfiguration.RemoveKey(policy.PolicyID!); + } + else + { + // Create and display a ContentDialog + ContentDialog dialog = new() + { + Title = "WARNING", + Content = $"Before you can safely remove the signed policy named '{policy.FriendlyName}' with the ID '{policy.PolicyID}', you must restart your system.", + PrimaryButtonText = "I Understand", + BorderBrush = Application.Current.Resources["AccentFillColorDefaultBrush"] as Brush ?? new SolidColorBrush(Colors.Transparent), + BorderThickness = new Thickness(1), + XamlRoot = this.XamlRoot // Set XamlRoot to the current page's XamlRoot + }; + + // Show the dialog and wait for user response + _ = await dialog.ShowAsync(); + + // Exit the method, nothing more can be done about the selected policy + return; + } + } + else + { - string CertCN; - string CertPath; - string SignToolPath; - string XMLPolicyPath; - // Instantiate the Content Dialog - SigningDetailsDialogForRemoval customDialog = new(currentlyDeployedBasePolicyIDs, policy.PolicyID!); + #region Signing Details acquisition - // Show the dialog and await its result - ContentDialogResult result = await customDialog.ShowAsync(); + string CertCN; + string CertPath; + string SignToolPath; + string XMLPolicyPath; - // Ensure primary button was selected - if (result is ContentDialogResult.Primary) - { - SignToolPath = customDialog.SignToolPath!; - CertPath = customDialog.CertificatePath!; - CertCN = customDialog.CertificateCommonName!; - XMLPolicyPath = customDialog.XMLPolicyPath!; + // Instantiate the Content Dialog + SigningDetailsDialogForRemoval customDialog = new(currentlyDeployedBasePolicyIDs, policy.PolicyID!); - // Sometimes the content dialog lingers on or re-appears so making sure it hides - customDialog.Hide(); + // Show the dialog and await its result + ContentDialogResult result = await customDialog.ShowAsync(); - } - else - { - return; - } + // Ensure primary button was selected + if (result is ContentDialogResult.Primary) + { + SignToolPath = customDialog.SignToolPath!; + CertPath = customDialog.CertificatePath!; + CertCN = customDialog.CertificateCommonName!; + XMLPolicyPath = customDialog.XMLPolicyPath!; - #endregion + // Sometimes the content dialog lingers on or re-appears so making sure it hides + customDialog.Hide(); - // Add the unsigned policy rule option to the policy - CiRuleOptions.Set(filePath: XMLPolicyPath, rulesToAdd: [CiRuleOptions.PolicyRuleOptions.EnabledUnsignedSystemIntegrityPolicy]); + } + else + { + return; + } - // Making sure SupplementalPolicySigners do not exist in the XML policy - CiPolicyHandler.RemoveSupplementalSigners(XMLPolicyPath); + #endregion - // Define the path for the CIP file - string randomString = GUIDGenerator.GenerateUniqueGUID(); - string xmlFileName = Path.GetFileName(XMLPolicyPath); - string CIPFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip"); + // Add the unsigned policy rule option to the policy + CiRuleOptions.Set(filePath: XMLPolicyPath, rulesToAdd: [CiRuleOptions.PolicyRuleOptions.EnabledUnsignedSystemIntegrityPolicy]); - string CIPp7SignedFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip.p7"); + // Making sure SupplementalPolicySigners do not exist in the XML policy + CiPolicyHandler.RemoveSupplementalSigners(XMLPolicyPath); - // Convert the XML file to CIP, overwriting the unsigned one - PolicyToCIPConverter.Convert(XMLPolicyPath, CIPFilePath); + // Define the path for the CIP file + string randomString = GUIDGenerator.GenerateUniqueGUID(); + string xmlFileName = Path.GetFileName(XMLPolicyPath); + string CIPFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip"); - // Sign the CIP - SignToolHelper.Sign(new FileInfo(CIPFilePath), new FileInfo(SignToolPath), CertCN); + string CIPp7SignedFilePath = Path.Combine(stagingArea.FullName, $"{xmlFileName}-{randomString}.cip.p7"); - // Rename the .p7 signed file to .cip - File.Move(CIPp7SignedFilePath, CIPFilePath, true); + // Convert the XML file to CIP, overwriting the unsigned one + PolicyToCIPConverter.Convert(XMLPolicyPath, CIPFilePath); - // Deploy the signed CIP file - CiToolHelper.UpdatePolicy(CIPFilePath); + // Sign the CIP + SignToolHelper.Sign(new FileInfo(CIPFilePath), new FileInfo(SignToolPath), CertCN); - } - } + // Rename the .p7 signed file to .cip + File.Move(CIPp7SignedFilePath, CIPFilePath, true); + // Deploy the signed CIP file + CiToolHelper.UpdatePolicy(CIPFilePath); - // Refresh the DataGrid's policies and their count - RetrievePolicies(); + SiPolicy.SiPolicy policyObj = SiPolicy.Management.Initialize(XMLPolicyPath); + + // The time of first stage of the signed policy removal + // Since policy object has the full ID, in upper case with curly brackets, + // We need to normalize them to match what the CiPolicyInfo class uses + UserConfiguration.Add(policyObj.PolicyID.Trim('{', '}').ToLowerInvariant(), DateTime.UtcNow); + } + } + } } } } finally { + // Refresh the DataGrid's policies and their count + RetrievePolicies(); + DeployedPolicies.IsHitTestVisible = true; RetrievePoliciesButton.IsEnabled = true; SearchBox.IsEnabled = true; @@ -659,4 +696,36 @@ private void MenuFlyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs ar #pragma warning restore CA1822 + + /// + /// If returns true, the signed policy can be removed + /// + /// + /// + private static bool VerifyRemovalEligibility(string policyID) + { + // When system was last reboot + DateTime lastRebootTimeUtc = DateTime.UtcNow - TimeSpan.FromMilliseconds(Environment.TickCount64); + + Logger.Write($"System's last reboot was {lastRebootTimeUtc} (UTC)"); + + // When the policy's 1st stage was completed + DateTime? stage1RemovalTime = UserConfiguration.Query(policyID); + + if (stage1RemovalTime is not null) + { + Logger.Write($"Signed policy with the ID '{policyID}' completed its 1st state at {stage1RemovalTime} (UTC)"); + + if (stage1RemovalTime < lastRebootTimeUtc) + { + Logger.Write("Signed policy is safe to be removed because system was restarted after 1st stage"); + + return true; + } + } + + return false; + } + + }