本文会详细分析Windows即将推出的最大安全功能——智能应用控制(Smart App Control,SAC)。
“智能应用控制”功能是什么,为什么我认为它是 Windows 中最牛的安全功能之一。首先,SAC 会嵌入在操作系统中,启用后将阻止恶意或不受信任的应用程序。这与 AppLocker 非常相似。
Smart App Control 预计将与 Windows 22H2 一起发布,Windows 22H2 应该会在今年 9 月下旬发布。
SAC 具有三种可能的状态,其中只有一种会执行这些操作:
强制:将强制阻止恶意或不受信任的应用程序,我们将此状态标记为1。
评估:在此模式下,该功能将继续评估你的系统是否适合强制模式下的执行,我们将此状态标记为2。
关:该功能被禁用。一旦禁用,除非你重新安装操作系统,否则无法再次激活它,我们将此状态标记为0。
SAC 安装
了解如何安装它。此功能需要全新安装才能激活。如果我们为 Build 22621 安装 ISO 并通过 install.wim 导航到包含注册表配置单元的文件夹,那么我们可以将 SYSTEM Hive 加载到注册表编辑器中。在 CI\Policy 项中,我们可以找到值VerifiedAndReputablePolicyState设置为2(评估状态)。
同样在 CI 项中,我们有 SubKey Protected,我们可以在其中找到以下值 VerifiedAndReputablePolicyStateMinValueSeen 也设置为 2。
稍后我们将进一步了解如何使用这些项来控制SAC的实际状态,我们还将了解如何保护Protected SubKey下的值以避免被篡改。
为了在升级时强制执行此操作,我们可以看到安装 ISO 在 CI 的替换清单中有以下代码:[ISO]\sources\replacementmanifests\codeintegrity-repl.man。
升级操作系统时,这段代码将检查注册表值 HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy\VerifiedAndReputablePolicyState 是否存在,如果不存在,它将以 SAC 状态 0(关闭状态)创建。
除了这两个新的注册表值之外,操作系统还将在 System32\CodeIntegrity\CiPolicies 文件夹中附带两个新的系统完整性策略文件 (.cip)。
PolicyGUID:{0283AC0F-FFF1-49AE-ADA1-8A933130CAD6}强制SAC策略,当SAC状态设置为Enforce(1)时激活;
PolicyGUID:{1283AC0F-FFF1-49AE-ADA1-8A933130CAD6} 评估 SAC 策略,当 SAC 状态设置为evaluation (2)时激活;
使用 WDACTools 中的 CIPolicyParser 脚本,我们将两个 .cip 文件转换为它们的 .xml 表示形式。我们可以从 XML 中获取策略规则来了解这些策略的选项。设置了以下规则,两个 XML 文件都可以在附录中找到。
启用:UMCI;
启用:智能安全图授权;
启用:开发者模式动态代码信任;
启用:允许补充策略;
启用:已撤销已过期未签名;
启用:继承默认策略;
启用:未签名的系统完整性策略;
启用:高级启动选项菜单;
禁用:脚本执行;
启用:更新策略不重启;
启用:有条件的 Windows 锁定策略;
启用:审核模式(仅在 SAC 评估策略中);
最后,我们可以在 System32 文件夹中搜索使用前面提到的注册表值的二进制文件/模块。
SAC 初始化
我们将这个部分分为两个阶段。第一阶段我们将在Windows加载程序中讨论SAC。第二阶段我们将在OS初始化期间讨论SAC。重要的是要了解加载程序和操作系统都在启用 SAC 中发挥作用。最后,我将添加一个部分来解释 SubKey CI\Protected 下的值保护是如何工作的。
SAC 初始化流程图如下所示。
Winload 期间的 SAC
在本节中,我们将讨论如何选择活动 SAC 状态的 SAC 策略、Winload 如何强制执行 RegKey 之间的持久性和一致性以及如何将 SAC 策略传递给内核。
SAC初始化的第一步在操作系统加载程序过程中提前完成。更具体地说,在准备目标 (OslPrepareTarget) 期间加载 SystemHive 之后。为了处理系统完整性策略,将调用函数OslpProcessSIPolicy。在此函数中,将对条件策略(SKU、EMode、SAC Enforce、SAC Evaluation)进行评估,以查看是否应该忽略或解锁它们。 Microsoft 认为这四个策略是有条件的,因为它们可以被忽略/解锁,这与始终适用的“MS Windows 驱动程序策略”等其他策略不同。条件策略的策略GUID 存储在由符号 g_SiConditionalPolicies 定义的全局数组中。
忽略和解锁之间的区别非常微妙。解锁标志将一直被选中。另一方面,忽略标志只会在没有设置“Enabled:Unsigned System Integrity Policy”的策略中被选中。
要确定是否应为 Enforce 或 Evaluation 启用 SAC,使用以下两个函数。
OslpShouldIgnoreUnlockableNightsWatchDesktopEnforcePolicy OslpShouldIgnoreUnlockableNightsWatchDesktopEvalPolicy
这是我们第一次看到引用 Nights Watch 来表示 SAC,这似乎是微软的内部名称。
这两个函数的行为方式相同,唯一的区别是它们为内部评估函数提供了不同的 PolicyGUID:
此函数使用 PolicyGUID 参数来确定要检查的 SAC 状态。它调用 OslpGetNightsWatchDesktopRegKeyState,它返回设备中的实际 SAC 状态。如果实际 SAC 状态与正在评估的状态匹配,则认为该策略是活动的,这过于简化了。如果设备是 WinPE 或者是否需要签名策略,则需要进行更多检查。即使注册表指示 SAC 处于活动状态,这些检查也可以使函数返回 Ignore 和 Unlockable。
OslpGetNightsWatchDesktopRegKeyState 的行为值得一看。此例程负责在重新启动后保持启用 SAC 并保持两个注册表值之间的一致性。此例程有四种可能的情况:
VerifiedAndReputablePolicyState == VerifiedAndReputablePolicyStateMinValueSeen:值是相同的,所以直接返回值。
VerifiedAndReputablePolicyState < VerifiedAndReputablePolicyStateMinValueSeen:在上一个启动会话期间,SAC 状态被修改。我们从 VerifiedAndReputablePolicyState 返回值,并在Protected SubKey下更新该值。
VerifiedAndReputablePolicyState > VerifiedAndReputablePolicyStateMinValueSeen:这是一个极端情况,因为 VerifiedAndReputablePolicyState 永远不应大于受保护项下的值。如果有人手动编辑值 VerifiedAndReputablePolicyState,我相信这是为了保持两个值之间的一致性。
值为3或3以上:表示无效状态转换并且函数将失败。
伪代码总结如下。
当使用安全应用程序发生 SAC 状态更改时。操作系统将写入 VerifiedAndReputablePolicyState。用户重新启动后,此状态将持续存在于设备中。这意味着在 SAC 状态转换之后,仍然可以编辑 VerifiedAndReputablePolicyState,并且转换不会在下一次重新启动后持续存在。这让我认为微软只有在安装更新时才会触发评估模式的转换,或者他们会要求重新启动。显然,在会话期间发生SAC状态转换时,活动策略将被更新。
一旦检查了所有的条件策略,看看它们是可解锁的还是应该被忽略。从每个函数获得的值将写入以下两个全局变量:
g_SIPolicyConditionalPolicyConditionUnlockHasBeenMet
g_SIPolicyConditionalPolicyConditionIgnoreHasBeenMet
写入这些全局变量的值是一个四字节数组,可以用以下结构表示
在此之后,加载程序将尝试解析策略文件。首先将每个 .cip 文件中的序列化数据加载到内存中(参见 BlSIPolicyGetAllPolicyFiles)。然后从 SIPolicyParsePolicyData 中的每个文件解析数据,如果有人对细节感兴趣,请检查 SIPolicyInitialize 以了解如何将策略的每个部分解析为一个结构。
解析策略后,将检查忽略和解锁条件以查看它们是否满足。如果满足某个条件,则该策略将被放弃。如果不满足任何条件,则将使用函数 SIPolicySetAndUpdateActivePolicy 将策略设置为活动。
如果设置了策略选项“已启用:未签名的系统完整性策略”,则 PolicyVersion 和 PolicySignersData 将从 EFI SecureBoot 私有命名空间中删除。被删除的变量名将由PolicyGUID和PolicyVersion/ policyysignersdata字符串连接组成,只有当PolicyOptions禁用了“Enabled:Unsigned System Integrity Policy”时,才会创建这些EFI变量。
在下面的输出中,我们可以看到 SetVariable 是如何以大小 0 被调用的,这将导致如果找到该变量将被删除。
对于这两个 SAC 策略,任何 EFI 变量都将被清除。之后,将通过调用 SIPolicySetActivePolicy 将策略设置为活动。此调用会将策略添加到将链接到全局变量 g_SiPolicyCtx 的节点中。 g_NumberOfSiPolicies 将相应地递增,并且新策略的句柄将存储在 g_SiPolicyHandles 中,此变量是一个包含 32 个句柄的数组,因为 WDAC 一次在设备上支持多达 32 个活动策略。
保存在 g_SiPolicyCtx 中的 SI_POLICY_CTX 结构的原型如下:
下图显示了三个全局变量。在我的示例中,有三个活动策略,其中一个是 SAC 强制执行策略的补充策略,补充策略有助于扩展基本策略以增加策略的信任。
有了这些信息,加载程序将能够在加载程序参数块内构建 CI 结构。这是在函数 OslBuildCodeIntegrityLoaderBlock 内完成的。这个例程,除其他外,将在函数 SIPolicyGetSerializedPoliciesSize 的帮助下获得序列化 SI 策略的大小。该代码将使用全局变量 g_NumberOfSiPolicies 和 g_SiPolicyHandles,并且大小将存储在 LOADER_PARAMETER_CI_EXTENSION 的 CodeIntegrityPolicySize 字段中。之后,将通过函数 SIPolicyGetSerializedPolicies 复制序列化的数据。此数据的偏移量将存储在字段 CodeIntegrityPolicyOffset 中。此信息以及其他 CI 信息将存储在 LOADER_PARAMETER_EXTENSION 的 CodeIntegrityDataSize 和 CodeIntegrityData 字段中,当加载程序转换到操作系统时,加载程序参数块作为参数传递。
只有序列化的有效负载会被复制。我猜之前所做的所有策略解析主要是检查策略是否有效,如果无效则触发SYSTEM_INTEGRITY_POLICY错误。还可能使用来自认证或EFI变量策略的值。
这几乎就是我们将在 winload 期间看到的 SAC 初始化的全部内容。
下面的捕获显示了在转换到操作系统之前如何设置这些数据。
操作系统初始化期间的 SAC
看看内核如何初始化 CI。在此之后,我们将了解 CI 如何初始化 Winload 提供的策略。最后,再看看它如何从这些策略中确定 SAC 是否能够相应地采取行动。
在操作系统初始化期间,更具体地说是在阶段 1。内核将调用方法 CiInitialize(由 ci.dll 导出)。该函数主要用于内核和 CI 交换 API。内核接收 SeCiCallbacks,其中包含内核将用于与 CI 交互的函数指针。另一方面,CI DLL 接收 SeCiPrivateApis,其中包含 VSL HVCI 接口等内核函数,因此 CI 可以在进行任何 HVCI 验证时通过内核触发 Hypercall。内核还将传递初始的 CodeIntegrity 选项。这些选项由 Windows 加载程序构建并存储在 LOADER_PARAMETER_CI_EXTENSION 中。这些选项最初将包含诸如 CodeIntegrity BCD 选项(DisableIntegrityChecks、AllowPrereleaseSignatures、AllowFlightSignatures)和 WHQL 设置之类的内容。 CI 选项存储在全局变量 g_CiOptions 中,CI 还将根据从操作系统和策略中检索到的信息更新它们。
仍然在操作系统的第 1 阶段期间,内核将在整个 CI 回调中调用 CiInitializePolicy。该例程将接收 LOADER_PARAMETER_CI_EXTENSION 作为第一个参数。该例程将调用它的私有对应项 CipInitializeSiPolicy。该函数将调用 SIPolicyInitializeFromSerializedPolicies 来验证、解析和加载来自加载程序参数 CI 扩展的序列化策略。与 winload 相同,如果策略解析正确,则策略将添加到 g_SiPolicyHandles 和 g_SiPolicyCtx。更重要的是,如果正确解析了序列化策略,则将调用函数 CipUpdateCiSettingsFromPolicies。此方法根据每个策略的 PolicyRules 更新全局 CI 设置。在此函数中,CI 将通过调用 SIPolicyNightsWatchEnabled 检查是否启用了 SAC。
这个函数很有趣,我们终于可以开始研究 SI 策略结构了。该函数将调用 SIPolicyQueryOneSecurityPolicy。该例程具有以下原型:
这种方法在处理SI策略时经常出现。Since用于检查/获取策略中设置的SecureSettings。策略结构(我个人将该结构命名为SI_POLICY)有以下两个成员:SecureSettingsCount 和SecureSettingsData。
解析序列化策略时,所有安全设置所需的内存将被分配并存储在 SecureSettingsData 指针中。每当 CI 必须查询安全设置时,它会调用 SIPolicyQueryOneSecurityPolicy 并使用它需要查找的 Provider、Key 和 ValueName。在内部,该函数会将这三个值存储在一个结构中,该结构将用作 bsearch 函数中的 Key。搜索的基础将设置为策略的 SecureSettingsData。 CompareFunction 设置为 SIPolicySecureSettingSearchCompare。 CompareFunction 将尝试将 SECURE_SETTINGS_DATA 中的 Provider、Key 和 ValueName 正在查询的内容进行匹配。每个值的比较是使用 RtlCompareUnicodeString 完成的。
在我们的示例中,当查看是否启用了 SAC(在 SIPolicyNightsWatchEnabled 内部)时,传递给查询函数的值如下:
Provider: Microsoft Key: WindowsLockdownPolicySettings ValueName: VerifiedAndReputableTrustMode
如果在策略中找到安全设置,则认为 SAC 已启用,并将在 g_CiPolicyState 中设置值 NW_ENABLED (0x4000)。
这些值也以策略的 XML 格式显示。如果你检查附录中的实施和评估 XML,你将看到此安全设置在两者中都设置为 true。
仅仅为了完成,PolicyState是一个位字段,它可以取以下值(有些值没有),这些值大多取自函数CiInstrumentSiPolicyInfo的ETW事件元数据。
下图显示了在 SIPolicyNightsWatchEnabled 中调用 SIPolicyQueryOneSecurityPolicy 之前的状态,其中 SAC 强制策略用于查询。
回到 CiInitializePolicy,一个全局变量表示在这个启动会话中看到的 SAC 的最小值将以以下方式更新:
基本上,在启用 SAC 的情况下,将使用 SAC 强制策略的 PolicyGUID 设置本地变量 EnforceNW。然后将此 GUID 传递到函数 SIPolicyIsPolicyActive。如果此函数返回true(1),那么代码将减去“2-1”设置g_NightsWatchDesktopMinValueSeenDuringThisBootSession强制状态。在SAC Enforce策略未激活但启用SAC的情况下。该函数返回 false(0),然后存储在全局中的值会将“2-0”设置评估状态。最后,如果 SAC 未启用,则存储在全局中的值为 0(关闭状态)。
在第 2 部分中,我们将看到 CI 如何处理在 Windows 安全应用程序中更改状态时触发的 SAC 状态转换。
CI 保护子项(Protected SubKey)
接下来,我们将介绍RegKey CI\Protected下的值是如何被操作系统保护的。这对于该功能至关重要,因为能够控制 VerifiedAndReputablePolicyStateMinValueSeen 将允许我们在重新启动时更改 SAC 状态。
在 CiInitializePolicy 期间,将调用的第一个函数是 CipCheckLicensing。该例程将首先打开 SubKey \\CurrentControlSet\\Control\\CI\\Protected,这次打开是为了检查许可值,但这是不相关的。
一旦 CI 获得受保护的子项的句柄,它将使用内核在初始化期间在 SeCiPrivateApis 表中提供的方法之一。特别是SepZwLockRegistryKey方法。此方法将到达 NtLockRegistryKey(整个 Zw 版本)。 NtLockRegistryKey会使用key的Handle来获取Object的引用,key对象用CM_KEY_BODY结构表示。项对象将传递给 CmLockKeyForWrite,后者将获取 CM_KEY_CONTROL_BLOCK 并调用 CmpGlobalLockKeyForWrite。
在 CmpGlobalLockKeyForWrite 内部,ExtFlag CM_KCB_READ_ONLY_KEY (0x80) 将在 KCB 中为此 Key 对象设置。这很有趣,因为保护是在对象管理器级别发生的。通过NtSetValueKey,我们可以看到如何检查KCB ExtFlags对象是否为只读,从而拒绝或不操作。这将不会被用于用户权限或以前的模式。在尝试操作 VerifiedAndReputablePolicyStateMinValueSeen 时,请参阅下图以查看此操作,注意:将调用 CM 回调 RegNtSetValueKey,而RegNtPostSetValueKey不会被调用。
当然,winload 可以修改这个值,因为此时内核没有运行。如果我们在 System32 中搜索引用字符串 VerifiedAndReputablePolicyStateMinValueSeen 的二进制文件,我们只会找到:
winload.exe;
windload.efi;
tcbloader.dll;
我个人认为这是保护项的简单解决方案。因此可能无需为其添加代码。但我想知道为什么 MS 没有选择将这个值存储在像 TPM NV 存储这样的空间中。这不可能解决所有问题。但我觉得 RegKey 更容易操作,例如,使用 WinRE 打开注册表编辑器,然后加载 OS SYSTEM 配置单元将是修改值 VerifiedAndReputablePolicyStateMinValueSeen 的合理方法。可以肯定的是,如果有人能够加载 WinRE 并更改它,那么你就会遇到更大的问题。
这可能会遗漏一些东西,winload 可能会将值存储在其他地方,但是通过我刚才提到的使用 WinRE 的步骤,我已经能够成功地在我的 VM 上从 SAC 禁用变为 SAC 启用。
这就是是我们需要知道的关于如何初始化 SAC 的所有内容。
本文翻译自:https://n4r1b.com/posts/2022/08/smart-app-control-internals-part-1/如若转载,请注明原文地址