本帖最后由 ANY.LNK 于 2024-12-5 01:09 编辑
原文地址:https://n4r1b.com/posts/2022/08/ ... l-internals-part-1/
引言
所以什么是“智能应用控制(SAC)”以及为何我认为这是Windows的最大的安全特性之一?基本上,SAC是OS固有的特性——系统自带,启用时阻止恶意或不受信任的应用。很简单,与AppLocker如出一辙,但微软成为了写下这些规则/策略的作者。
SAC同Win11 22H2一同发布,本次我们以Beta通道22621版本中的SAC为分析对象
SAC有三种状态,只有其一可以强制策略执行:
·打开(强制):强制拦截恶意和不受信任的应用执行。State = 1
·评估:此模式SAC会持续评估你的系统是否适合开启强制模式。State = 2
·关闭:SAC功能已禁用。无法在不全新安装系统的情况下重新开启。State = 0
声明:本文不会讨论此功能是如何决定终端是否适合开启强制模式的,这是我仍需要研究的。在此,我先搁置
微软认为,“不经常阻碍其正常工作”的人是好的候选者。但不幸的是微软并未提供更多关于评估模式的信息。评估模式可以切换到强制打开或关闭。但用户可以随时禁用此功能(选择模式)——如上所述,这意味着除非重新安装操作系统,否则无法再次启用此功能。
如果我们参考微软关于SAC的文档。我们可以注意到此功能目前不允许排除。在强制模式下,不允许任何应用绕过SAC。这意味着不受信任的和恶意应用没有任何条件执行。换句话说,这也会阻止一些未签名或微软云安全没有信誉评分的正常应用程序.
SAC安装
进入到SAC的安装流程。此功能需要全新安装以激活。如果我们加载Win11 22621版本的ISO,并在install.wim导航到注册表文件的目录,接着就可以加载SYSTEM注册表文件到注册表编辑器中了。在CI\Policy项中,我们可以找到设置为2(评估模式)的VerifiedAndReputablePolicyState值。
同样在CI项中可以找到子项Protected并找到同样设置为2的VerifiedAndReputablePolicyStateMinValueSeen值
稍后我们会看到这些键值是如何控制SAC的实际状态的。也可以看到Protected子项下的值是如何保护避免篡改的
在结束此节前我们先看看在操作系统升级时发生了什么。我们可以看到,为了在升级时执行这点,安装镜像在CI的替换列表([ISO]\sources\replacementmanifests\codeintegrity-repl.man)中拥有如下代码:
- <addObjects>
- <conditions>
- <condition negation="Yes">MigXmlHelper.DoesObjectExist("Registry", "HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy [VerifiedAndReputablePolicyState]")</condition>
- </conditions>
- <object>
- <location type="Registry">HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy [VerifiedAndReputablePolicyState]</location>
- <attributes>dword</attributes>
- <bytes>00000000</bytes>
- </object>
- </addObjects>
复制代码
当升级系统时这段代码会检查注册表值HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy\VerifiedAndReputablePolicyState是否存在。若不存在将创建状态0(关闭)的此值。
除了这两个新注册表值,操作系统同样会在System32\CodeIntegrity\CiPolicies中随附两个新的系统完整性策略。
·策略GUID:{0283AC0F-FFF1-49AE-ADA1-8A933130CAD6},强制模式SAC策略,当SAC状态被设置为强制打开(1)时激活
·策略GUID:{1283AC0F-FFF1-49AE-ADA1-8A933130CAD6},评估模式SAC策略,当SAC状态被设置为评估模式(2)时激活
使用WDACTools中的CIPolicyParser脚本,我们可以将.cip文件转为.xml的表达形式。从这些XML中我们可以获取策略规则进而了解这些策略的选项。已设置了如下规则(所有XML文件可以在Appendix中获得。
·启用:用户模式代码完整性(UMCI)
·启用:智能安全图表授权(Intelligent Security Graph Authorization)
·启用:开发者模式动态代码信任(Developer Mode Dynamic Code Trust)
·启用:允许附加策略(Allow Supplemental Policies)
·启用:吊销、过期签名视为未签名(Revoked Expired As Unsigned)
·启用:默认策略继承(Inherit Default Policy)
·启用:未签名的系统完整性策略(Unsigned System Integrity Policy)
·启用:高级启动选项菜单(Advanced Boot Options Menu)
·禁用:脚本控制策略强制实施(Script Enforcement)
·启用:无重启更新策略(Update Policy No Reboot)
·启用:有条件的Windows锁定策略(Conditional Windows Lockdown Policy)
·启用:审核模式(仅在SAC评估模式下)(Audit Mode (Only in the SAC Evaluation Policy))
最终,我们可以在System32文件夹中搜索到使用上述注册表值的二进制文件/模块
SAC初始化
我们将此节分为两个部分。第一部分我们讨论Windows加载时的SAC。第二阶段我们讨论操作系统初始化时的SAC。理解加载器和OS在启用SAC的作用都很重要。最后,我会加一节用于解释子项CI\Protected的保护值的工作原理。下面的图表展示了上述SAC的初始化流程。
Winload中的SAC
此节我们讨论如何激活状态下的SAC策略如何选择,Winload如何确保注册表键值的持久性和一致性以及SAC策略如何传入内核。下图展示了本节中的内容:
SAC初始化的第一步在OS加载过程早期即开始。确切的说,就在准备目标(OslPrepareTarget)过程中的加载系统注册表之后。为处理系统完整性策略,将调用函数OslpProcessSIPolicy。此函数中会评估条件策略(SKU,EMode,强制打开SAC,评估模式SAC)应被忽略还是解锁——微软认为此四项可以选择性的忽略或解锁,而不像“微软Windows驱动策略”那样强制执行。这些策略的GUID存储于由标志g_SiConditionalPolicies定义的全局数组中。
忽略和解锁间的差别很小。总是会检查解锁Unlock标志。换句话说,忽略Ignore标志只会在未设置Enabled:Unsigned System Integrity Policy(启用:未签名的系统完整性策略)时检查。此时Ignore和Unlock匹配1比1
为决定SAC应启用到强制打开模式还是评估模式,会使用如下二函数:
·OslpShouldIgnoreUnlockableNightsWatchDesktopEnforcePolicy
·OslpShouldIgnoreUnlockableNightsWatchDesktopEvalPolicy
这似乎是我们首次看到用Nights Watch用于表示SAC,这似乎是微软的内部名称
两函数以类似的方式执行,唯一的不同在于它们向内部评估函数提供了不同的策略GUID:
- bool // 返回值表示是否可解锁
- OslpShouldIgnoreUnlockableNightsWatchDesktopPolicy(
- PGUID PolicyGUID, // 此 PolicyGUID 匹配前文 `.cip` 文件的名称
- HANDLE SystemHive,
- PBOOL Active, // 若为真则激活策略
- PBOOL Ignore // 若为真忽略策略
- );
复制代码
函数使用PolicyGUID参数用于决定检查哪个SAC状态。它调用OslpGetNightsWatchDesktopRegKeyState,这个函数会返回此设备上的实际SAC状态。若实际SAC状态匹配正在评估的状态,则认为该策略被激活(此流程已被过度简化)。此外还有一些其他的检查如设备是否为WinPE或是否需要签名策略等。这些检查可让函数返回忽略(Ignore)或无法解锁(Unlockable)即使注册表表示SAC被激活。
OslpGetNightsWatchDesktopRegKeyState的行为值得一看。此例程负责在重启时保持SAC的状态开启和两注册表值的一致性。此例程拥有4种可能的情况。
1.VerifiedAndReputablePolicyState == VerifiedAndReputablePolicyStateMinValueSeen:值相同,直接返回值。
2.VerifiedAndReputablePolicyState < VerifiedAndReputablePolicyStateMinValueSeen:在先前的加载过程中SAC状态被修改,从VerifiedAndReputablePolicyState返回值,并更新Protected子键下的值。
3.VerifiedAndReputablePolicyState > VerifiedAndReputablePolicyStateMinValueSeen:这是种极端情况,因为VerifiedAndReputablePolicyState不应大于Protected键下的值。我认为此处是为了在有人手动编辑VerifiedAndReputablePolicyState值时保持两个值之间的一致性。
4.任意值大于3:这指示无效的状态更改,函数执行失败。
如下伪代码概括了上述行为:
- ...
- Status = OslGetDWordValue(SystemHive, PolicySubkey, L"VerifiedAndReputablePolicyState", &NWState);
- Status = OslGetDWordValue(SystemHive, ProtectedSubkey, L"VerifiedAndReputablePolicyStateMinValueSeen", &NWMinValSeen);
- if ( NT_SUCCESS( Status ) ) {
- if ( NWState <= NWMinValSeen ) {
- *SACState = NWState;
- if ( NWState < 3 ) {
- if ( NWState >= NWMinValSeen )
- return STATUS_SUCCESS;
- return OslHiveReadWriteDword( SystemHive, 1, ProtectedSubkey, L"VerifiedAndReputablePolicyStateMinValueSeen", SACState );
- }
- }
- else {
- *SACState = NWMinValSeen;
- if ( NWMinValSeen <= 2 )
- return OslHiveReadWriteDword( SystemHive, 1, PolicySubkey, L"VerifiedAndReputablePolicyState", SACState );
- }
- return STATUS_INVALID_STATE_TRANSITION;
- }
复制代码
当使用安全应用更改SAC状态时,系统将写入VerifiedAndReputablePolicyState。用户重启后,状态将保存在设备中。这意味着在SAC状态更改后在重启之前仍可以编辑VerifiedAndReputablePolicyState且状态不会被保存。这使我认为微软只会在安装更新或要求重启的状况下触发从评估模式的更改。显然,在会话过程中SAC状态更改时,活动策略会被更新。
检查完所有条件策略是否应被解锁或忽略后,从两个函数中获取的值会被写入如下两个全局变量:
·g_SIPolicyConditionalPolicyConditionUnlockHasBeenMet
·g_SIPolicyConditionalPolicyConditionIgnoreHasBeenMet
写入这些全局变量的是一个四字节数组,如下:
- typedef struct _SI_POLICY_MODES {
- BOOLEAN SkuPolicy; // S模式Windows应用的策略
- BOOLEAN EModePolicy; // 我无法找到任何关于EMode的信息,如果有人知道的话请告诉我
- BOOLEAN NightsWatchDesktopEnforce;
- BOOLEAN NightsWatchDesktopEval;
- } SI_POLICY_MODES, *PSI_POLICY_MODES;
复制代码
此后,加载器将尝试解析策略文件。首先加载每个.cip文件中的序列化数据到内存中(参见BlSIPolicyGetAllPolicyFiles),随后在SIPolicyParsePolicyData中解析数据(对细节感兴趣的话可以检查SIPolicyInitialize查看每部分的策略是如何解析入结构的)。我们会在第二部分详细讲解此结构和它的数据。
策略解析完成,将检查忽略和解锁的条件是否匹配。若条件匹配,策略将被舍弃。若无条件匹配,策略将用函数SIPolicySetAndUpdateActivePolicy设置为激活状态。
若设置了策略选项“启用:未签名系统完整性策略”(Enabled:Unsigned System Integrity Policy),将从EFI安全启动命名空间删除策略版本PolicyVersion和策略签名者数据PolicySignersData。删除的变量名由PolicyGUID和PolicyVersion/PolicySignersData串接而成——这些EFI变量仅会在策略选项“Enabled:Unsigned System Integrity Policy”禁用时创建。
在下面的输出中我们可以看到是如何用大小0调用SetVariable删除找到的变量的。
- [SetVariable][VendorGUID: 77FA9ABD-0359-4D32-BD60-28F4E78F784B] Variable: "{0283ac0f-fff1-49ae-ada1-8a933130cad6}PolicyVersion" Size: "0x00000000" Attributes: "0x00000000" Status: EFI_NOT_FOUND
- [SetVariable][VendorGUID: 77FA9ABD-0359-4D32-BD60-28F4E78F784B] Variable: "{0283ac0f-fff1-49ae-ada1-8a933130cad6}PolicyUpdateSigners" Size: "0x00000000" Attributes: "0x00000000" Status: EFI_NOT_FOUND
复制代码
对于两种SAC策略而言,所有EFI变量都会被清除。随后,调用SIPolicySetActivePolicy激活策略。此调用将会把策略添加到链入全局变量g_SiPolicyCtx的节点。g_NumberOfSiPolicies会据此增加且新策略的句柄将会存储于g_SiPolicyHandles——此变量是一个包含32个句柄的数组,因为WDAC可以在一台设备上同时支持激活的最多策略的数目为32.
保存SI_POLICY_CTX结构的g_SiPolicyCtx原型如下:
下图展示了这3个全局变量。在我这次的测试环境中,总共有三条激活的策略。其中之一是SAC强制策略的补充策略——补充策略有助于扩展基础策略增加策略信任圈。
获取到这些信息后,加载器就能在加载器参数块中建立CI结构了。此过程由函数OslBuildCodeIntegrityLoaderBlock完成。函数使用全局变量g_NumberOfSiPolicies和g_SiPolicyHandles,大小存储在LOADER_PARAMETER_CI_EXTENSION的CodeIntegrityPolicySize区域中。之后,函数SIPolicyGetSerializedPolicies将复制序列化的数据。此数据的偏移量存储在CodeIntegrityPolicyOffset区域。此信息和其他CI信息存储在CodeIntegrityDataSize区域和LOADER_PARAMETER_EXTENSION的CodeIntegrityData——加载器参数块作为参数由加载器传递到系统。
是的,只有序列化的有效载荷会被复制。我猜之前对策略的所有解析是用于检验策略是否有效,若无效则触发SYSTEM_INTEGRITY_POLICY错误。还有可能将策略中的值用于认证或EFI变量
这已经是在winload过程中足够多的有关SAC初始化的信息了,将来我们或许还会讨论winload过程中的Si策略、测量启动、PCR……等等
下图展示了这些数据是如何在转入操作系统时设置的
操作系统初始化时的SAC
本节中我们主要介绍内核初始化CI。此后,将重点关注CI是如何初始化Winload提供的策略的。最终,介绍CI如何依据这些策略决定对应的SAC行为。
操作系统初始化过程,确切的说,第一阶段,内核调用CiInitialize方法(由ci.dll导出)。此函数主要用于内核和CI交换API。内核接收(包含内核与CI交互的函数指针的)SeCiCallbacks;另一边,CI DLL接收包含VSL和HVCI接口的内核函数的SeCiPrivateApis,以便CI在内核进行HVCI验证时触发Hypercall。内核同样会传递初始化的代码完整性设置。这些设置由Windows Loader建立并存储于LOADER_PARAMETER_CI_EXTENSION。它们包含代码完整性BCD设置(DisableIntegrityChecks, AllowPrereleaseSignatures, AllowFlightSignatures)以及WHQL设置。CI设置存储于全局变量g_CiOptions中且CI会基于从系统和策略获取到的信息更新它们。
声明:CI是一个整体,但此处我们重点关注SAC的运作原理的关键部分,后续可能会写完整的CI
同样是在操作系统初始化的第一阶段,内核会通过CI回调调用CiInitializePolicy。此例程的首个参数是LOADER_PARAMETER_CI_EXTENSION,例程调用其私有补充函数CipInitializeSiPolicy。这个函数会调用SIPolicyInitializeFromSerializedPolicies,用于将加载器CI扩展中序列化的策略验证、解析并载入内存。和winload时一样,若策略解析成功,则策略将被添加到g_SiPolicyHandles和g_SiPolicyCtx;更重要的是,序列化的策略解析成功意味着CipUpdateCiSettingsFromPolicies函数将被调用。此方法会基于每条策略的策略规则更新全局CI设置,在此函数中,CI将通过SIPolicyNightsWatchEnabled检查SAC是否开启。
此函数很有趣,我们也终于可以开始查看SI策略的结构了。函数会调用SIPolicyQueryOneSecurityPolicy,例程有如下原型:
这种方法在处理SI策略时经常使用,用于检查/获取策略中的安全设置SecureSetting。策略结构(我命名为结构SI_POLICY)拥有如下二组件SecureSettingsCount和SecureSettingsData
解析完序列化策略后,将继续为安全设置分配必要的内存并存储于SecureSettingsData指针。当CI请求安全设置时,它将使用需要查询的目标提供者,键和值名称(Provider, Key and ValueName)调用SIPolicyQueryOneSecurityPolicy。函数会将这三个值存储在一个结构中,此结构会在bsearch函数中作为键值使用。搜索库设置为策略的SecureSettingsData。比较函数CompareFunction设置为SIPolicySecureSettingSearchCompare。CompareFunction会尝试使用RtlCompareUnicodeString匹配比较SECURE_SETTINGS_DATA中的和正在查询的提供者,键和值名称。
在我们测试的情况中,在SIPolicyNightsWatchEnabled内检查SAC是否启用时,传入查询函数的值如下:
·提供者:Microsoft
·键:WindowsLockdownPolicySettings
·值名称:VerifiedAndReputableTrustMode
若在策略中找到安全设置,SAC会被认为处于启用状态,将会在g_CiPolicyState设置值NW_ENABLED (0x4000)。
这些值同样可以在XML格式的策略中找到。如果在附录中检查强制打开和评估模式的XML,可以看到二者的安全设置Secure Setting均被设置为真(true)
补充说明,PolicyState是一个位字段,可以取以下值(部分缺失,未在此处列出):
- typedef enum _CI_POLICY_STATE {
- NEED_TO_APPLY_TO_CI = 0x1,
- NEED_TO_APPLY_TO_UMCI = 0x2,
- AUDIT_MODE_ENABLED = 0x4,
- REQUIRES_WHQL = 0x8,
- REQUIRES_EV_WHQL = 0x10,
- INVALIDATE_EA_ON_REBOOT = 0x20,
- PER_PROCESS_VALIDATION = 0x40,
- FORCE_IMAGE_REVALIDATION = 0x80,
- FULL_IMAGE_PATH_AND_MACROS = 0x400,
- UMCI_AUDIT_ONLY = 0x800,
- UMCI_OPT_FOR_EXPIRED = 0x1000,
- AUTH_ROOT_AUTHORIZED = 0x2000,
- NIGHTS_WATCH = 0x4000,
- SMART_LOCKER = 0x8000,
- REQUEST_AUTH_ATTRS = 0x10000,
- APPID_TAGGING = 0x20000,
- } CI_POLICY_STATE, *PCI_POLICY_STATE;
复制代码 这些内容多数源自函数CiInstrumentSiPolicyInfo的ETW事件元数据。
下面的截图展示了在SAC强制打开模式下SIPolicyNightsWatchEnabled中调用SIPolicyQueryOneSecurityPolicy前查询的策略。
回到CiInitializePolicy——一个用于指示此次启动会话SAC查询的最小值的全局变量将按如下方式更新:
基本上,在启用SAC的状态下,本地变量EnforceNW将被设置为SAC强制执行的策略GUID,随后该GUID传入函数SIPolicyIsPolicyActive。若此函数返回真(1)则代码将g_NightsWatchDesktopMinValueSeenDuringThisBootSession设为“2-1”Enforce状态。若SAC强制执行策略未激活但SAC处于启用状态,函数返回假(0),存储于全局变量的值将设为“2-0”Evaluation状态。最后若SAC未启用则存储于全局变量的值设为0(关闭状态)
在第二部分中我们将看到CI是如何在Windows安全中心中更改状态时处理触发的SAC状态转换的。小小剧透一下:这涉及到如何处理Defender发送的信号。
CI Protected 子键
在这里本文的最后一节中我们将讲述系统是如何保证注册表键CI\Protected下的值的安全的。这对于此功能来说很重要,因为能够控制VerifiedAndReputablePolicyStateMinValueSeen就能随意在重启时更改SAC状态。
在CiInitializePolicy过程中首个被调用的函数是CipCheckLicensing。这也将会是首个打开子键\\CurrentControlSet\\Control\\CI\\Protected的例程——为了检查Licensed值,但并不相干。
一旦CI获取到了Protected的句柄,它将使用内核初始化时SeCiPrivateApis表提供的方法之一,特别是SepZwLockRegistryKey方法。此方法会访问NtLockRegistryKey(在Zw版本中),而NtLockRegistryKey会使用键的句柄获取对象的引用(键对象在CM_KEY_BODY结构中)。该键对象会被传入CmLockKeyForWrite,它将继续获取CM_KEY_CONTROL_BLOCK并调用CmpGlobalLockKeyForWrite,参见如下栈:
- 1: kd> k
- # Child-SP RetAddr Call Site
- 00 fffff882`b2a06500 fffff805`3f7a6189 nt!CmpGlobalLockKeyForWrite+0xbe
- 01 fffff882`b2a06540 fffff805`3f7a6020 nt!CmLockKeyForWrite+0x11d
- 02 fffff882`b2a06590 fffff805`3f432465 nt!NtLockRegistryKey+0x70
- 03 fffff882`b2a065e0 fffff805`3f424380 nt!KiSystemServiceCopyEnd+0x25
- 04 fffff882`b2a06778 fffff805`3f86cab9 nt!KiServiceLinkage
- 05 fffff882`b2a06780 fffff805`43ff4e87 nt!SepZwLockRegistryKey+0x9
- 06 fffff882`b2a067b0 fffff805`43ff24bf CI!CipCheckLicensing+0x1fb
- 07 fffff882`b2a068a0 fffff805`3fb5766c CI!CiInitializePolicy+0x4f
- 08 fffff882`b2a069d0 fffff805`3fb2a59b nt!SeCodeIntegrityInitializePolicy+0x70
- 09 fffff882`b2a06a00 fffff805`3f825d43 nt!Phase1InitializationDiscard+0xb0f
- 0a fffff882`b2a06bb0 fffff805`3f2c3977 nt!Phase1Initialization+0x23
- 0b fffff882`b2a06bf0 fffff805`3f423bb4 nt!PspSystemThreadStartup+0x57
- 0c fffff882`b2a06c40 00000000`00000000 nt!KiStartSystemThread+0x34
复制代码
在CmpGlobalLockKeyForWrite内将在KCB中为此键对象设置扩展标志CM_KCB_READ_ONLY_KEY (0x80)。有趣的是,保护时在对象管理器级别的。观察NtSetValueKey,可以看出是如何检查KCB扩展标志以确认对象是否为只读属性进而对操作拒绝与否的。不论用户权限或先前模式如何,这都将实行。参见下图以了解试图操作更改VerifiedAndReputablePolicyStateMinValueSeen时的实际效果 - 备注:CM回调RegNtSetValueKey会被调用,但RegNtPostSetValueKey不会。
当然,winload可以修改此值,因为此时内核尚未运行。若我们在System32目录下的二进制文件中搜索VerifiedAndReputablePolicyStateMinValueSeen字符串的引用,我们只会找到:
·winload.exe
·winload.efi
·tcbloader.efi
我个人认为这是个保护键的简单解决方案。它已被用于保护Licensed值不被添加任何代码。但我还是想知道,为何微软不选择将值存储在TPM NV这样的区域内。这并不能解决所有问题,注册表键值更容易进行操作——例如,在WinRE中使用注册表编辑器,加载系统的SYSTEM注册表并修改VerifiedAndReputablePolicyStateMinValueSeen值。当然,如果真有人能加载WinRE并更改你的系统的值,你就有大问题了^_^
我或许遗漏了一些东西,winload也可能会将值存储到其他地方,但通过上述WinRE步骤,我确实成功将我虚拟机上的SAC从禁用状态切换到了启用状态
这就是这篇文章的结束了!在下一篇文章中我们就可以开始讲解SAC是如何工作的了。若有错误,欢迎指正!
附录
感谢Matt Graeber和WDACTools(https://github.com/mattifestation/WDACTools)
https://gist.github.com/n4r1b/f1c44d573f055ee2194a16ae10a61611
https://gist.github.com/n4r1b/a2c026def8fb77e01ab231c43430bc90
抱歉这一篇拖了比较久,我最近真的很忙……
|