本帖最后由 ANY.LNK 于 2024-12-15 03:29 编辑
原文地址:https://n4r1b.com/posts/2022/09/ ... l-internals-part-2/
(续上篇)
在本篇文章中,我们将讨论SAC的运作。在那之前,需要澄清的是,尽管SAC是一项新功能,但其使用的代码早已在操作系统中存在。我的意思是说,在22H2之前的版本中,只需配置适当的策略,就能取得类似的效果。可以说,SAC带来的最大的改变在于微软将自动激活特定的WDAC策略,类似于在启用HVCI时自动激活的“易受攻击的驱动程序阻止列表”策略。
在本文中,许多内容早已存在于之前版本的操作系统中,并且被AppLocker或AppID之类的功能使用。当然,也有一些方面仅适用于SAC,我会对这些方面加以说明。好的一面是,这篇文章的大部分内容可以推广到评估其他WDAC策略上。
话不多说,我们切入正题。首先,我们将先通过一个高级图表说明一个对象是如何被CI验证的。
SAC的操作
此节我们主要关注CI为处理源自内核的验证请求而采取的步骤。我们将深入此过程的主要例程,并探讨部分CI所使用的主要结构。正如我刚刚所说,许多东西并不是SAC的独占且不论哪些策略被激活都会被采用。参见先前的图表,共有三个评估的主要资源。据我所知,这些点主要与如下功能/策略规则有关——基于策略规则选择使用一种或多种评估。
·声明源(EA或令牌):托管安装程序、AppLocker,SmartScreen以及SAC;
·查询Defender:智能安全图(ISG)和SAC;
·策略文件规则:所有拥有策略的文件规则通用
下面的图表更好的展示了CI验证映像的步骤的细节:
所以开始我们的旅程吧……我们已经在第一篇中知道全局变量g_CiPolicyState拥有指示SAC已经启用的NW_ENABLED位,且SAC策略(打开或评估)已被激活并存储于g_SiPolicyCtx。现在我们看看CI向内核提供的回调以找出内核用于验证对象的方式。下列函数支持了执行一些类型的验证的选项:
·CiValidateImageHeader
·CiValidateImageData
·CiValidateFileAsImageType
·CiRevalidateImage
在本篇文章中我们只会关注CiValidateImageHeader
CiValidateImageHeader
此函数可能是大多数CI验证的主要入口点。内核从MiValidateSectionCreate引用的SeValidateImageHeader调用此函数。CiValidateImageHeader将处理CI初始化的第二阶段(主要为minCrypt,ETW,后备缓冲区等),一旦这些完成(只有一次),第一步就是获取指定映像的操作(CiGetActionsForImage)。此函数将依据诸如请求的签名级别或对象是否源自受保护的/系统进程等条件决定验证采取的操作。说实话,我只知道这些操作是一个位域枚举,但我不知道大部分的值的意义是什么。
一旦操作获取完成,函数就准备好启动映像验证了。若操作变量设置了位0(ACTION_FILE_IN_CACHE (0x1))则CI会尝试获取任何先前获取的已为此FO设置的验证数据,并重新验证。
本文我们不讨论CI缓存及其验证工作。必要的话,它将尝试获取内核EA:$Kernel.Purge.CIpCache或$Kernel.Purge.ESBCache(参见函数CipGetFileCache),并在CiApplyPolicyToSyntheticEa中向这些属性应用策略。此例程结束于调用CipApplySiPolicyEx,它的细节我们将在稍后讨论
若“缓存文件”属性未设置,则会分配进程验证的主要结构(CipAllocateValidationContext)。此结构用于所有类型的验证,同样的上下文也会用于HVCI验证(参见CiHvciSetValidationContextForHvci)。此上下文分配后,我观察到为UMCI验证采取了如下二操作:
·若设置了位2(ACTION_PAGE_HASH (0x4)),验证函数→CipValidatePageHash
·若设置了位8(ACTION_FILE_HASH (0x100)),验证函数→CipValidateFileHash
CipValidateImageHash将以函数指针的形式接收执行操作的验证函数。不论传递的是PageHash或FileHash指针,CipValidateImageHash最后都会调用它。在两个验证函数中,CI都会用正在验证的对象的信息更新验证上下文,诸如文件版本(CipUpdateValidationContextWithFileInfo),嵌入式签名(CipImageGetCertInfo),或对象哈希(页CipCalculateHeaderHash,文件CipCalculateImageHash)等。拥有这些信息后代码将继续通过函数CipApplySiPolicyEx应用策略。
对于未签名的映像,验证函数将返回STATUS_INVALID_IMAGE_HASH,代码将继续进入CipApplySIPolicyUMCI,并最终调用CipApplySiPolicyEx。相反,对于已签名映像的验证,将从CiVerifyPageHashSignedFile或CiVerifyFileHashSignedFile途径到达此函数。(笔记,此二函数拥有HVCI的异时空同位体CiHvciXxx)
CipApplySiPolicyEx
如其名所述,该函数将对正在验证的对象应用策略。函数首先会设立2个结构,它们将在稍后传入验证引擎。其一将存有验证的映像文件ImageFile的信息,另一个则包含了“额外(external)”的授权过程所需的信息(称之为此是因为微软在验证回调所用的函数名中使用了这个词)。
这两个结构将会存储于验证上下文中并且之后都会被其中的消息填充。包含我命名为CI_VALIDATE_IMAGE_DATA的映像数据,以及下列内容:
- typedef struct _SI_CERT_CHAIN {
- UINT32 AlgId;
- INT32 Size;
- PVOID Hash;
- UNICODE_STRING IssuedTo;
- UNICODE_STRING IssuedBy;
- } SI_CERT_CHAIN, *PSI_CERT_CHAIN;
- typedef struct _SI_EKU {
- INT32 Type;
- PVOID Oid;
- } SI_EKU, PSI_EKU;
- typedef struct _SI_CHAIN_INFO {
- INT32 Size;
- PSI_EKU Ekus[];
- INT32 EkusCount;
- PSI_CERT_CHAIN CertChain[];
- INT32 CertChainCount;
- INT32 Type;
- UNICODE_STRING field_28;
- } SI_CHAIN_INFO, *PSI_CHAIN_INFO;
- typedef struct _CI_VALIDATE_IMAGE_DATA {
- PSI_CHAIN_INFO ChainInfo; // 若映像已签名,在函数SIPolicyConvertChainInfo中解析
- PVOID ValidationHash; // 我认为这里会依据正在进行的验证(PageHash, FileHash, 等等)
- // 使用不同的值
- INT32 ValidationHashSize;
- PUNICODE_STRING OriginalFileName;
- PUNICODE_STRING InternalName;
- PUNICODE_STRING FileDescription;
- PUNICODE_STRING ProductName;
- UINT64 ProductVersion;
- ...
- } CI_VALIDATE_IMAGE_DATA, *PCI_VALIDATE_IMAGE_DATA;
复制代码
另一方面,我命名为CI_EXTERNAL_AUTH的额外授权结构包含以下有趣的值:
- typedef struct _CI_EXTERNAL_AUTH {
- INT32 SiSigningScenario; // 同Matt Graeber的 "Threat Detection using WDAC (Device Guard)" 相关
- ...
- bool RunFullTrustFlag
- bool IsSignedScript
- CI_VALIDATION_CTX **pValidationCtx;
- NTSTATUS (__fastcall *pCipExternalAuthorizationCallback)(SI_POLICY *Policy, CI_VALIDATION_CTX **);
- ...
- } CI_EXTERNAL_AUTH, *PCI_EXTERNAL_AUTH;
复制代码
在调用验证引擎例程前,会设立一个包含每条策略结果的结构数组,数组的大小等于激活的策略的数目。我命名此结构为CI_VALIDATION_RESULT,它拥有如下结构:
- typedef struct _CI_VALIDATION_RESULT {
- SI_POLICY_CTX *PolicyCtx;
- bool ReprieveResult;
- bool FileRuleMatchFound;
- INT32 ValidateImageStatus;
- NTSTATUS AuthorizationCbStatus;
- VALIDATE_SCORE ValidateImageScore;
- } CI_VALIDATION_RESULT, *PCI_VALIDATION_RESULT;
复制代码
最终,我们已经准备好调用SIPolicyObjectValidationEngine了,它拥有如下原型:
- void
- SIPolicyObjectValidationEngine(
- PCI_EXTERNAL_AUTH ExternalAuthData,
- PCI_VALIDATE_IMAGE_DATA ValidateImageData,
- PSI_POLICY_CTX PolicyCtx,
- PCI_VALIDATION_RESULT ResultPerPolicy
- );
复制代码
此例程会调用内部例程SIPolicyValidateImageInternal遍历每条策略和补充策略。
内部验证例程拥有调用外部授权回调从“外部资源”获取验证分数的任务。基于此分数它将决定是否继续评估映像匹配策略内的规则。我们首先关注外部回调——CipExternalAuthorizationCallback——并在稍后讨论如何评估策略规则。
CipExternalAuthorizationCallback
此函数包含SAC的核心功能,且在21H1到21H2变化不大。但当SAC开启时的一点小细节的变化就足以形成很大的不同。尽管如此,大部分我们要讨论的内容都是已经被AppLocker和ISG使用过的或正在其被使用的。位了解我们是如何进行到这一步的,下面给出了验证未签名映像时的外部授权回调的堆栈:
函数以检查策略选项Intelligent Security Graph Authorization或Managed Installer为始,若均未设置,函数将退出,SIPolicyValidateImageInternal将继续处理策略文件规则FileRules——我们将在后文看到这点。
若设置了这些中的任何选项,则会基于签名级别决定是否应该信任映像。此过程通过将获取到的映像ValidatedSigningLevel值与全局变量g_CipWhichLevelComparisons中拥有索引0xC的位掩码进行比较进而决定。
快速笔记:全局变量g_CipWhichLevelComparisons存储了指向数组ULONGs的指针。每个值表示应用到此签名级别的比较级别。通常与验证的签名级别共同使用以判断应对映像使用的不同操作/选项。举个例子,对于验证的签名级别为“文件未签名”数组中的索引1,位掩码为0xFFFFFFFE,大多数情况下对比此位掩码的结果都将为正。其他情况下如上所述,索引硬编码在代码中,仅在验证的签名级别匹配该索引的位掩码时才会被激活。希望下表有助于理解g_CipWhichLevelComparisons和ValidatedSigningLevel的相关性。
[td] ValidatedSigningLevel 值
| ValidatedSigningLevel 掩码
| g_CipWhichLevelComparisons 位掩码
| 未检查签名级别
| 2^0 = 0x1
| 0xFFFFFFFF
| 文件未签名
| 2^1 = 0x2
| 0xFFFFFFFE
| 被WDAC策略信任
| 2^2 = 0x4
| 0x5994
| 开发者签名代码
| 2^3 = 0x8
| 0x59FC
| 验证码签名
| 2^4 = 0x10
| 0x5970
| Microsoft Store签名的PPL应用
| 2^5 = 0x20
| 0x5920
| Microsoft Store签名
| 2^6 = 0x40
| 0x5960
| 被使用AMPPL的AV产品签名
| 2^7 = 0x80
| 0x5080
| Microsoft签名
| 2^8 = 0x100
| 0x5900
| 未知
| 2^9 = 0x200
| 0x59F4
| 未知
| 2^10 = 0x400
| 0x0
| 仅用于.NET NGEN 编译器签名
| 2^11 = 0x800
| 0x800
| Windows签名
| 2^12 = 0x1000
| 0x5000
| 未知
| 2^13 = 0x2000
| 0x0
| Windows TCB签名
| 2^14 = 0x4000
| 0x4000
|
如我们在表中所见,索引0xC对应位掩码0x5000,意味着“Windows签名”和“Windows TCB签名”。此后的“仅用于.NET NGEN编译器签名”和“被使用AMPPL的AV产品签名”二级别也包含在信任映像列表中。从这里起函数将继续调用CipCheckSmartlockerEAandProcessToken来获取第一个验证分数。
我觉得这是个探讨命名的好机会,并希望微软的人能够找到我并澄清这些命名。一边是Smart App Control和Nights Watch,另一边是AppLocker和其内部命名SmartLocker。已经有四个不同的命名指代非常相似的东西了。这肯定会让人迷惑,尤其是对于逆向而言。
函数拥有如下原型:
- NTSTATUS
- CipCheckSmartlockerEAandProcessToken(
- PFILE_OBJECT FileObject,
- PTOKEN TokenObject,
- PCI_VALIDATION_CTX ValidationCtx,
- bool IsTrustedSigning,
- PVALIDATE_SCORE Score
- );
复制代码
有两条途径,其一总会执行,另一条则会基于布尔值IsTrustedSigning决定是否执行。若不信任,将为被验证的文件对象FileObject查询如下EA,同时尝试从当前进程文件对象FileObject获取同样的EA,但除了把这些存储在验证上下文以外我并未发现它们在其他地方使用过。
- $Kernel.Smartlocker.Hash:包含映像的哈希
- $Kernel.Purge.Smartlocker.Valid:用于决定是否有效的布尔值
- $Kernel.Smartlocker.OriginClaim:包含我命名为EA_ORIGIN_CLAIM的结构
- typedef struct _EA_ORIGIN_CLAIM {
- INT32 DataVersion;
- INT32 Origin;
- INT32 Type;
- INT32 Generation;
- INT64 SessionId;
- INT64 field_18;
- INT64 SubSessionId;
- INT64 field_28;
- INT32 SmartScreenClaim;
- INT32 RevocationId;
- UNICODE_STRING FileName;
- } EA_ORIGIN_CLAIM, *PEA_ORIGIN_CLAIM;
复制代码
若获取到了有效的EA,就会检查OriginClaim结构以确定此映像的评分。Origin值决定第一个分数,若Origin == 0,则Score |= 1;若Origin == 1,则Score |= 0x1002
我为我没能找到有关于此的更多信息而道歉。这大概与WDAC在策略中设置了Managed Installer选项时在AppLocker中使用的特殊规则集合有关。据我所知,appid.sys设置这些EA,另一条设置这些EA的途径是通过CI回调CiSetCachedOriginClaim。此函数在发出拥有标志0x2000的syscall NtSetCachedSigningLevel被内核调用。当然这比不上调用这个syscall设置EA OriginClaim简单,因为此syscall的previous mode为UserMode,而NtSetCachedSigningLevel2会确保请求源自一个受保护的进程。
无论我们是否检查了EA,下一步都是获取存储在令牌Token对象中的OriginClaim。在令牌Token对象中,OriginClaim存储在令牌的SecurityAttributes列表中,这些属性以Authz SecurityAttributes形式储存并可用函数SeQuerySecurityAttributesToken以名称查询/获取。在我的情形下会查询如下两个安全属性:
- SMARTLOCKER://ORIGINCLAIM
- SMARTLOCKER://SMARTSCREENORIGINCLAIMNOTINHERITED(22H2新增,先前为SMARTLOCKER://SMARTSCREENORIGINCLAIM)
首先会查询OriginClaim名称,若找到则分数会据此调整。再一次地,我没有找到太多这方面的信息,也没有此声明结构的的信息(appid.sys设置此值令牌)
此后,将查询SmartScreen OriginClaim未继承的属性,若找到且设置了CLAIM_DANGEROUS_EXT (0x80)标志(这不是官方名称,我基于我的检查命名了此标志),函数将继续检查映像文件ImageFile是否拥有被认为是危险扩展名DangerousExtension的扩展名。对于安装程序仅会检查.msi,其余如下:
- // DangerousExtensions
- ".appref-ms"
- ".appx"
- ".appxbundle"
- ".bat"
- ".chm"
- ".cmd"
- ".com"
- ".cpl"
- ".dll"
- ".drv"
- ".gadget"
- ".hta"
- ".iso"
- ".js"
- ".jse"
- ".lnk"
- ".msc"
- ".msp"
- ".ocx"
- ".pif"
- ".ppkg"
- ".printerexport"
- ".ps1"
- ".rdp"
- ".reg"
- ".scf"
- ".scr"
- ".settingcontent-ms"
- ".sys"
- ".url"
- ".vb"
- ".vbe"
- ".vbs"
- ".vhd"
- ".vhdx"
- ".vxd"
- ".website"
- ".wsf"
- ".wsh"
复制代码
若映像文件匹配这些值的任意之一,其分数将会被设为DangerousExtension (0x800),并通过调用CiCatDbSmartlockerDefenderCheck向Defender发起查询(我们会在后面更多的讨论此函数)
下面的伪代码大致说明了SmartLocker不继承属性是如何工作的:
- // (TOKEN_ORIGIN_CLAIM) 的大小== 0x20C
- typedef struct _TOKEN_ORIGIN_CLAIM {
- ULONG Flags;
- WCHAR ImageFileName[MAX_PATH];
- } TOKEN_ORIGIN_CLAIM;
- PCLAIM_SECURITY_ATTRIBUTES_INFORMATION Attr = NULL;
- RtlInitUnicodeString(&AttrName, L"SMARTLOCKER://SMARTSCREENORIGINCLAIMNOTINHERITED");
- Status = SeQuerySecurityAttributesToken( Token, &AttrName, 1, Attr, AttrSize, &RetLen );
- if ( NT_SUCCESS( Status ) ) { // 我们假设它直接工作,没有错误检查,也没有STATUS_BUFFER_TOO_SMALL检查
- if( Attr->AttributeCount ) {
- // 不完全是这个结构,内核使用了一个略微修改过的版本
- PCLAIM_SECURITY_ATTRIBUTE_V1 AttrV1 = Attr->Attribute.pAttributeV1;
- if ( AttrV1->ValueType == CLAIM_SECURITY_ATTRIBUTE_TYPE_OCTET_STRING &&
- Attr->Values.pOctetString->ValueLength == sizeof(TOKEN_ORIGIN_CLAIM) ) {
- PTOKEN_ORIGIN_CLAIM Claim = Attr->Values.pOctetString->Value;
- // 复制声明到ValidationCtx
- if ( Claim->Flags & CLAIM_DANGEROUS_EXT ) {
- auto isDangerExt = CipCheckForExtensionAgainstList( Claim->ImageFileName, DangerousExtensions );
- if ( isDangerExt )
- *Score |= DangerousExt;
- }
- auto isInstaller = CipCheckForExtensionAgainstList( Claim->ImageFileName, InstallerExtensions );
- if ( isDangerExt || isInstaller ) {
- RtlInitUnicodeString( &FileName, Claim->ImageFileName );
- CiCatDbSmartlockerDefenderCheck(
- &FileName,
- ValidationCtx->CurrentProcess,
- 0,
- NULL,
- &ReplySize,
- &ReplyData, ...);
- // 基于结果,或许会从分数中移除DangerousExt
- }
- }
- }
- }
复制代码
笔记:基于此函数的值稍后会用于填充TraceLogging字符串,已知Defender将整个评估过程称为IsDefenderShell
这基本上就是我们在文件资源管理器双击一个文件时会发生的事了,就在回调获取安全属性之后
额外补充:有关OriginClaim Token的来源
在回到CipExternalAuthorizationCallback之前,我很好奇OriginClaim是何时或怎么添加到令牌中的。于是我深入挖掘然后掉入了下面的这个兔子洞中,别有一番洞天。首先,我们搜索并发现了内核引用了SmartScreen OriginClaim字符串并在函数SepAddTokenOriginClaim中使用。在此函数中我们可以看到最后一个参数指示了添加的令牌是否为如下二情况之一:
- SMARTLOCKER://SMARTSCREENORIGINCLAIM → 最后一个参数设为FALSE
- SMARTLOCKER://SMARTSCREENORIGINCLAIMNOTINHERITED → 最后一个参数设置为TRUE
我们对后者感兴趣,因而发现是源自NtCreateUserProcess的SeDuplicateTokenAndAddOriginClaim调用了此函数将最后一个参数设置为TRUE的。我们可以看到存储在令牌中的TOKEN_ORIGIN_CLAIM结构已被NtCreateUserProcess传入SeDuplicateTokenAndAddOriginClaim。追查此参数的来源,我们可以看到它是作为“创建进程上下文”的一部分在PspBuildCreateProcessContext中分配的;并且,实际初始化过程中,此上下文是从拥有属性值PS_ATTRIBUTE_SAFE_OPEN_PROMPT_ORIGIN_CLAIM (0x20017)的PS_ATTRIBUTES_LIST取得的。令我意外的是,此值源自用户模式,而非内核创建。
了解此点后(我并非热爱逆向用户模式或C++),我们就需要检查此值是添加到PS_ATTRIBUTES_LIST何处的。我们进入KernelBase并检查CreateProcessInternalW,在此我们可以找到函数BasepConvertWin32AttributeList,它看起来像是将源自用户模式值的属性转换为内核模式的表达 - 参见下图,此属性的大小与OriginClaim结构的大小相匹配。
但我们仍然不知道TOKEN_ORIGIN_CLAIM源于何处。KernelBase看似直接就在StartupInfo从内存中获取了属性列表AttributesList——STARTUPINFOW的大小实际上为0x68,但在调用BasepConvertWin32AttributeList前代码检查了StartupInfo->cb是否等于0x70,若二者相等则意味着STARTUPINFOW大小之后的值包含了属性列表。
所以让我们找找是谁设立了此值,我在OriginClaim值转为内核类型的位置下了一个断点并检查此处的堆栈。我们发现是windows.storage.dll(Microsoft WinRT Storage DLL)调用了CreateProcessInternalW。为了简化这部分,简略来说就是在创建进程时引入了两个主要类别:CInvokeCreateProcessVerb和CBindAndInvokeStaticVerb
属性在实例化类别时由CBindAndInvokeStaticVerb复制入CInvokeCreateProcessVerb,稍后,CInvokeCreateProcessVerb将复制属性到STARTUPINFOW后并将StartupInfo->cb从0x68更新到0x70。(参见CInvokeCreateProcessVerb::CallCreateProcess)
接着寻找CBindAndInvokeStaticVerb是在何处获得OriginClaim的属性的,我们定位到类别函数CheckSmartScreen。此函数在内部调用了CheckSmartScreenWithAltFile——其会检查诸如是否应引入SmartScreen,文件是否为符号链接,使用等于SIGDN_FILESYSPATH的SIGDN获取DisplayName或检查“IsWindowsLockdownDangerousExtensionEnforcement”是否启用(调用外部动态链接库Wldp.dll)这样的事。随后调用ZoneCheckFile,其会进行更多检查,例如调用AssocIsDangerous并检查“WindowsLockdownDangerousExtensionValidation”是否开启。随后从DLL shdocvw.dll调用方法SafeOpenPromptForShellExec,这回决定为TOKEN_ORIGIN_CLAIM设置的标志。最终,CInvokeCreateProcessVerb类方法UpdateProcThreadAttribute调用将OriginClaim属性添加到PS_ATTRIBUTES_LIST。
切记当SAC启用时,部分基于信誉的保护的值(SmartScreen)默认启用且无法禁用。这就是补充信息的结束了!回到CipExternalAuthorizationCallback
这就是所有关于CipCheckSmartlockerEAandProcessToken需要知道的事了,现在我们回到已经拥有了从EA、Token或二者皆有获取的分数的CipExternalAuthorizationCallback。
从现在起,我们进入了被Intelligent Security Graph使用的代码,现在已经扩展了部分被SAC所使用的函数。首先,将再次检查策略选项Intelligent Security Graph Authorization(若未设置则函数会以从CipCheckSmartlockerEAandProcessToken获取的值退出)。若策略中的值为激活状态(SAC策略便是如此),函数将使用先前提到的IsTrustedSigning决定是否继续。若映像受信任,将进行如下检查
- 若ValidatedSigningLevel等于“被使用AMPPL的AV所签名(7)”且策略拥有VerifiedAndReputableAllowAntiMalware值,则分数将同值AllowAntiMalware (0x100000)进行OR运算,函数返回
若映像不受信任,函数将继续向Defender查询。如前所述,函数向Defender发起请求的函数是CiCatDbSmartlockerDefenderCheck。此函数接收2个MPFILE_TRUST_EXTRA_INFO结构,其一被请求数据填充,其二接收回复数据。代码同样从FileObject传递FileName。MPFILE_TRUST_EXTRA_INFO结构如下(省略了部分用户模式Defender的区域)
- typedef struct _MP_INFO_RESULT {
- INT32 Unknown;
- ULONG32 ClientStatusCode;
- ULONG32 CloudHTTPCode;
- GUID EngineReportGUID;
- } MP_INFO_RESULT, *PMP_INFO_RESULT;
- typedef struct _MP_INFO_HASH {
- INT32 Reserved; // 必须为 1
- INT32 Unknown; // 请求用1,回复用0
- struct HASH_DATA {
- ALG_ID AlgId;
- INT32 Size;
- BYTE Data[sizeof(Size)];
- };
- } MP_INFO_HASH, *PMP_INFO_HASH;
- enum MP_NW_CONTROL {
- SwitchNWOff = 0x2,
- SwitchNWToEnforcementMode = 0x4,
- IsUnfriendlyFile = 0x8 // 不是NW控制,但其在此 :D
- }
- union MP_EXTRA_INFO {
- MP_INFO_HASH HashData;
- MP_INFO_RESULT Result;
- MP_NW_CONTROL NightsWatchControl;
- PWCHAR PrivacyTag;
- };
- typedef struct _MPTRUST_INFO {
- INT32 Size;
- INT32 TrustLevel;
- INT64 Trust;
- } MPTRUST_INFO, *PMPTRUST_INFO;
- typedef struct _MPFILE_TRUST_EXTRA_INFO {
- INT32 RequestType;
- INT32 ReplyType;
- INT32 Size;
- PMP_EXTRA_INFO Information;
- } MPFILE_TRUST_EXTRA_INFO, *PMPFILE_TRUST_EXTRA_INFO;
复制代码
两部分间使用RPC通信。客户端使用CI.dll,服务端使用cryptcatsvc.dll(用于记录,RPC桩使用的IID为f50aac00-c7f3-428e-a022a6b71bfb9d43)。
cryptcatsvc在服务CryptSvc中运行。在RPC服务端的调用函数中,以下对我们的研究很有意义:
- s_SSCatDBSmartlockerDefenderCheck(已在22H1中出现)
- s_SSCatDBSmartlockerDefenderCheck2(在22H2中新增)
- s_SSCatDBSendSmartAppControlBlockToast
- s_SSCatDBSendSmartAppControlSwitchEnforceToast
v1 v2 SmartLockerDefenderCheck函数最大的不同在于v2接收请求和回复MPFILE_TRUST_EXTRA_INFO作为它的参数。两个函数最终都会调用辅助函数CatDBSmartlockerDefenderCheckHelper
这些函数后CI将调用首先会加载MpClient.dll的s_SSCatDBSmartlockerDefenderCheck2
笔记:首次执行SmartLocker将在Defender配置中打开。函数将调用MpClient的导出函数MpSmartLockerEnable。函数会注册Defender ELAM证书信息(打开WdBoot.sys的句柄并调用InstallELAMCertificateInfo)并使用RPC从MpSvc.dll调用方法ServerMpEnableSmartLocker。此方法会检查SmartLockerMode是否在Defender配置中设置,若无,则会写入。
一旦库的句柄打开,函数就会使用CI.dll提供的文件名打开文件的句柄,并传到MpClient导出函数MpQueryFileTrustByHandle2。此函数仅会从DefenderCheck2调用,若使用老版本的DefenderCheck,则会使用MpQueryFileTrustByHandle作为代替。
在MpQueryFileTrustByHandle2内,函数会使用文件的句柄创建文件映射,这些映射将在稍后被Defender用于内存扫描。下面的InSequence函数将通过从MpClient(客户端)向MpSvc(服务端)发起的RPC调用来执行 - 显然,我们刚刚看到的所有函数调用都接受CI.dll设置的MPFILE_TRUST_EXTRA_INFO作为其参数。
- ServerMpRpcMemoryScanStart:设立一个CMpMemScanContext和CMpMemScanEngineVfz(以GetAttributeTrustCheck为GetAttributes函数),并继续异步扫描
- ServerMpRpcMemoryScanQueryNotification:获取扫描信息
- ServerMpRpcMemoryScanClose:关闭并清理CMpMemScanContext
函数的内部机理不在此帖的探讨范围内,挖掘MpSvc和其扫描引擎需要更多的帖子。在此我们只需要大致明白SAC启用时Defender将会主动扫描文件并发起云查询
从扫描获取的信息有三种可能的信号:0x31001:获取MPTRUST_INFO(IGS);0x31002:获取MPFILE_TRUST_EXTRA_INFO(SAC);0x4005:RSIG_VIRINFO相关
此后结束同Defender的通信交互,下图展示了当代码抵达Defender时的客户端CI和服务端cryptcatsvc的堆栈:
需要在此提到的是,如果SAC为强制打开状态但没有Internet连接,默认操作为阻止进程,并发送并展示“智能应用控制无法验证此应用的安全性,请检查Internet连接并重试”的通知
回到外部授权回调,若RPC调用失败,策略VerifiedAndReputableAllowUnknown未设置且ValidateSigningLevel不为如下的任意之一:
- Microsoft Store 签名 PPL (Protected Process Light)应用
- Microsoft Store 签名
- Microsoft 签名
- Windows 签名
- 仅用于.NET NGEN编译器签名
- Windows Trusted Computing Base 签名
然后验证分数将同Unattainable (0x40000)进行OR运算,函数返回。若RPC调用成功,则继续调用CiHandleDefenderSignals函数。如其名所述此函数会处理Defender发回的消息。它将遍历返回的元素计数,每个元素都是MPFILE_TRUST_EXTRA_INFO类型。基于ReplyType区域它将执行不同的操作。比较有意思的情况有两种:首先是返回目标可信的结果,此时Information指向MP_INFO_RESULT,其中的值将被复制到Validation Context:
第二则是Information指向MP_NW_CONTROL数组。此时基于命令行SAC将禁用或切换到强制打开模式。这会更新VerifiedAndReputablePolicyState注册表键值和WorkItem的策略。
我们从学习模式更改至强制打开模式时将发起对函数s_SSCatDBSendSmartAppControlSwitchEnforceToast的RPC调用。此函数会加载wldp.dll并调用函数WldpSendSmartAppControlSwitchEnforceToast
从信号处理例程回来后,还有一些细微的差别。若NW控制命令行设置了IsUnfriendlyFile标志,则分数将会更新至值UnfriendlyFile (0x80000),函数返回。若标志未设置,则TrustInfo将与FileObject以标志0x82一同传入CipSetFileCache,意味着EA $Kernel.Purge.CIpCache将用于存储此信息。
最后,分数将基于Defender返回的信任信息Trust调整。有5个选项:
- Trust == 1:分数将同值0x202进行OR运算(我对此值了解不多)
- Trust == -1 (0xFFFFFFFF):若设置了策略配置VerifiedAndReputableAllowUnknown,分数将同值AllowUnknown (0x20000)进行OR运算
- Trust == -2 (0xFFFFFFFE):分数将同值Malicious (0x80)进行OR运算
- Trust == -3 (0xFFFFFFFD):分数将同值PUA (0x100)进行OR运算
- 任何其他情况,分数将同0x42进行OR运算
这便是外部授权回调的全部了,现在我们回到SIPolicyValidateImageInternal——外部授权回调被调用的地方!
SIPolicyValidateImageInternal
在讨论外部授权回调之前,我们讨论了SIPolicyObjectValidationEngine函数如何遍历策略并调用内部SIPolicyValidateImageInternal并在稍后继续调用外部验证回调。现在,在回调调用后回到SIPolicyValidateImageInternal,验证分数将从中返回。若SAC已启用,函数将继续评估分数,并将此分数传递给验证引擎分数并基于分数设置NTSTATUS。
可以在图中看到,大多数分支中它会将验证状态设为对应的NTSTATUS并跳转到我称之为ProcessDbgAndReprieve的地方。这不过是一个验证内核调试器是否附加并在控制台中记录策略违反情况的方法。类似于如下示例(不是上面的情况,是其他与此无关的断点):
- kd> g
- KDTARGET: Refreshing KD connection
- ************************************************************************************
- * \Device\HarddiskVolume3\Users\n4r1B\Desktop\usbview.exe violated CI base policy {0283ac0f-fff1-49ae-ada1-8a933130cad6}.CIP with error code 0xc0e90002 for scenario 1.
- * It is now allowed to load because debugger is attached.
- ************************************************************************************
复制代码
在未遵循上述任何一条分支,或分数为Unattainable但设置了AllowUnknown的情况下,函数将继续依据策略规则评估对象。首先在函数SIPolicyMatchFileRules中检查文件规则,函数会接收(除其他之外)如下参数:
- 评估的文件规则策略
- 评估的场景值
- 原始文件名
- 内部名称
- 文件描述
- 产品名称
与我们在第一部分中所见的策略安全设置类似,函数将设立一个结构,其拥有作为key传入函数bsearch的数据。关键结构有如下原型:
- typedef struct _POLICY_BINARY_DATA {
- INT32 Size;
- PVOID Data;
- } POLICY_BINARY_DATA, PPOLICY_BINARY_DATA;
- //
- // 抱歉,我没有每个字符串与哪个版本的策略相关以外的信息 :(
- //
- typedef struct _POLICY_STRING_DATA {
- INT32 StringType;
- UNICODE_STRING String;
- UNICODE_STRING StringPolicyV4;
- UNICODE_STRING String1PolicyV4;
- UNICODE_STRING String2PolicyV4;
- UNICODE_STRING StringPolicyV7;
- UNICODE_STRING StringPolicyV5;
- PVOID DataPolicyV5;
- PVOID V3DataStart;
- PVOID V3DataEnd;
- INT32 StringsCountPolicyV3;
- PUNICODE_STRING StringsPolicyV3;
- POLICY_BINARY_DATA BinaryData;
- } POLICY_STRING_DATA, *PPOLICY_STRING_DATA;
- typedef struct _SEARCH_FILE_RULES_KEY {
- INT32 PolicyStringsVersion;
- PUNICODE_STRING OriginalFileName;
- PUNICODE_STRING InternalName;
- PUNICODE_STRING FileDescription;
- PUNICODE_STRING ProductName;
- PUNICODE_STRING AppxPackageString;
- PPOLICY_STRING_DATA PolicyStringsData;
- } SEARCH_FILE_RULES_KEY, *PSEARCH_FILE_RULES_KEY;
复制代码
bsearch函数的base和num将从SI_POLICY结构获取。当策略解析到SI_POLICY结构时,将设立两个场景数组。每个都包含其特定的文件规则、允许的签名者、拒绝的签名者和例外规则。如上所述,调用SIPolicyMatchFileRules时,特定的评估场景数会传入此函数。此数将作为函数的索引指引函数选取场景数组中的特定元素。每条场景由如下结构表达:
- typedef struct _SI_RULES {
- PUINT32 IndexArray[];
- UITN64 field_8;
- UINT64 field_10;
- INT32 Count;
- } SI_RULES, *PSI_RULES;
- typedef struct _SI_POLICY_SIGNERS_RULES {
- SI_RULES Rules;
- UINT64 field_20;
- SI_RULES ExceptionRules;
- PVOID field_48;
- PVOID field_50;
- } SI_POLICY_SIGNERS_RULES, *PSI_POLICY_SIGNERS_RULES;
- typedef struct _SI_FILE_RULES {
- SI_RULES Rules;
- UINT64 field_20;
- } SI_FILE_RULES, *PSI_FILE_RULES;
- typedef struct _SI_POLICY_SCENARIO {
- UINT32 AlgId;
- SI_POLICY_SIGNERS_RULES AllowedSignersRules;
- SI_POLICY_SIGNERS_RULES DeniedSignersRules;
- SI_FILE_RULES FileRules;
- } SI_POLICY_SCENARIO, *PSI_POLICY_SCENARIO;
复制代码
若无文件名级别文件规则匹配,函数将继续评估哈希级别的文件规则
若文件名或哈希任意之一匹配,则SIPolicyMatchFileRules返回TRUE,随后验证状态将会设为STATUS_SYSTEM_INTEGRITY_POLICY_VIOLATION。
若无文件规则匹配,下一步,若映像被签名,是验证签名链信息是否匹配拒绝和允许签名者列表。首先检查拒绝签名者。若任一规则匹配此点,同之前一样,函数将设置验证状态到STATUS_SYSTEM_INTEGRITY_POLICY_VIOLATION。若无,则继续检查允许签名者规则。若匹配则任何先前的状态/分数都会被清空。验证映像签名和策略签名的过程主要在函数SIPolicyValidateChainAgainstSigner中完成。此函数将接收映像的SI_CHAIN_INFO作为第一参数,并在@r8接收POLICY_SIGNERS_DATA。
关于POLICY_SIGNERS_DATA结构,基本上SI_POLICY结构保存了POLICY_SIGNERS_DATA的一个数组。这在允许和拒绝签名的两个场景都有体现,这种情况代码知道每种场景对应应用的规则,意味着POLICY_SIGNERS_DATA数组索引的使用相当巧妙;同时也是我先前在文件规则中没有解释的地方,所以现在是个检查它的好方法。如果你回到SI_POLICY_SCENARIO结构并检查,你将看到每种规则类型结构(文件、允许、拒绝)都有一个包含了一个被我称为IndexArray的区域的SI_RULES结构。基本上这就是一个索引数组,指示了特定场景和规则下使用的数据数组中的索引。
- // 假设我们要检查场景1,允许签名使用`SIPolicyValidateChainAgainstSigner`
- PSI_POLICY Policy; // 假设在此处理策略
- PSI_POLICY_SCENARIO Scenario = Policy->ScenariosRules[1]; // 获取场景1
- PSI_POLICY_SIGNERS_RULES AllowSigRules = Scenario->AllowedSignersRules; // 获取场景1的允许签名
- while ( i < AllowSigRules->Rules.Count ) { // 遍历所有的允许签名规则
- INT32 Index = AllowSigRules->Rules.IndexArray[i]; // 从策略中获取用于PolicySignersData的
- // 索引
- PPOLICY_SIGNERS_DATA AllowSigData = Policy->PolicySignersData[Index]; // 获取PolicySignersData
- // AllowSigData拥有方案一的第一个允许签名的PolicySignerData
- // 同样的行为也适用于拒绝签名和文件规则 (获取POLICY_STRING_DATA的ptr)
- if ( SIPolicyValidateChainAgainstSigner( ChainInfo, AllowSigData ) ) {
- // 找的一个匹配
- break;
- }
- i++;
- }
复制代码
这未必100%准确,省略了部分中间的正确性检查,但希望观点表达出来了
为了更好的理解签名是如何验证的,请看POLICY_SIGNERS_DATA的原型——记住这同样适用于允许和拒绝签名者
- typedef struc _POLICY_SIGNERS_DATA {
- INT32 Type;
- UINT32 AlgId;
- POLICY_BINARY_DATA Value;
- INT32 EkusCount;
- INT32 EkuBufferLen;
- PVOID EkuBuffer;
- UNICODE_STRING IssuedBy;
- UNICODE_STRING IssuedTo;
- UNICODE_STRING field_48;
- PVOID SignersDataV3;
- ULONG FileRuleCount;
- PUINT32 FileRuleIndexArray[]; // 同 SI_RULES->IndexArray 一样
- } POLICY_SIGNERS_DATA, *PPOLICY_SIGNERS_DATA;
复制代码
通过查看SI_CHAIN_INFO和POLICY_SIGNERS_DATA你可以大致明白比较是如何在函数SIPolicyValidateChainAgainstSigner中完成的。最后是签名者规则验证的总结,下图是在验证ProcessHacker时的强制打开SAC策略时的SIPolicyValidateChainAgainstSigner入口点。
说实话,为了实现上图的效果,我不得不稍微修改了代码流程。因为在首个检查时Type将会匹配并退出循环。我想要达到此点因为POLICY_SIGNERS_DATA中包含了比第一个检查更多的信息。第一次检查时,唯一填充的值为设置为0x14的Type,而我对于这个值一无所知也一无所获。若有人知道更多的信息,请告诉我!
于是对每个活动策略和补充策略运行此过程后,我们回到了函数CipApplySiPolicyEx,为每个基础策略BasePolicy设置了CI_VALIDATION_RESULT。从补充策略返回的结果同样写入了与基础策略一样的CI_VALIDATION_RESULT。此时函数不会做比遍历存储在验证上下文中的验证结果更多的事情。同时SmartLocker日志将于函数CiLogSIPolicySmartlockerEvent中被记录输出。会记录四种类型的日志。
- SmartlockerOperationalAudit (日志Id: 3091)
- SmartlockerOperationalFailure (日志Id: 3092)
- SmartlockerVerbose (日志Id: 3088)
- SmartlockerOperationalSuccess (日志Id: 3090)
我们进入尾声,现在我们继续向上调用堆栈,将验证状态传递给上面的函数,最终回到CI入口点CiValidateImageHeader,此函数已经没有更多需要讨论的事了。对于SAC有趣的一点是,若SigningLevel符合如下条件:
- 签名级别尚未被检查
- 文件未签名
- 被WDAC(Windows Defender Application Control)策略信任
- 开发者签名代码
SAC结果是允许执行,操作将使用CipInstrumentNightsWatchAllow函数记录。此函数可为提供者CodeIntegrity.NWActivityVerbose和CodeIntegrity.NWActivity写入四种追踪记录基础的日志,它们拥有如下名称
- EventName
- ---------
- QuestionableAllow
- OriginClaimData
- Allow
- QuestionableAllowSignatureInfo
复制代码
此函数执行时将记录QuestionableAllow或Allow。若选择指定了记录QuestionableAllow的路径,则QuestionableAllowSignatureInfo和OriginClaimData也会被写入,若所需的数据可用的话。
由于这些是基于追踪记录的事件,我们需要一些魔法来捕获追踪。不过幸运的是,Matt已经替我们完成了这项艰巨的工作(https://posts.specterops.io/data ... ogging-e465f8b653f7),我们可以通过下面的powershell cmdlet启动ETW会话捕获NWActivity和NWActivityVerbose提供方:
- New-EtwTraceSession -Name NWTrace -LogFileMode 0x08000100 -FlushTimer 1
- # Add Microsoft.Windows.Security.CodeIntegrity.NWActivityVerbose
- Add-EtwTraceProvider -SessionName NWTrace -Guid ‘{3a82f218-fcc2-4183-afe9-a0febc4416ee}’ -MatchAnyKeyword 0xFFFFFFFFFFFF -Level 0xFF -Property 0x40
- # Add Microsoft.Windows.Security.CodeIntegrity.NWActivity
- Add-EtwTraceProvider -SessionName NWTrace -Guid ‘{28dcc28b-3e31-527b-efd6-b4cc4d73d158}’ -MatchAnyKeyword 0xFFFFFFFFFFFF -Level 0xFF -Property 0x40
- tracerpt -rt NWTrace -o NWTrace.evtx -of EVTX
复制代码
启动追踪并运行App/安装程序就可以找到如下的日志了!
本次就到这里了,还有一些未讨论的内容,可以查看CiGetCodeIntegrityOriginClaimForFileObject和CiDeleteCodeIntegrityOriginClaimMembers。
总结
感谢看到这里!!!抱歉写了这么多,但我确实想深入研究一下。肯定还是有很多错误或遗漏的地方,但还是希望能有助于了解SAC和相关的其他内容。
正如我在开头提到的,这并不是系统的分析,只是集中在了可能的重点功能上。期望后续他人的补充!我之所以说SAC的学习过程是重要的,是因为就我的体验而言,当强制打开SAC后此功能会非常受限。所以再次,学习过程可能会修改部分影响SAC的执行方式。
微软正在为操作系统安全采取措施,但一些措施(如签名验证)可能会限制部分开发者,而另一些措施(比如微软安全云)缺乏透明度和信息公开,会使人怀疑微软正在使用不公平的竞争手段。在此希望更多安全厂商的接入,但微软愿意采取一些措施还是非常好的。
不幸的是,大部分的设备上都未启用SAC,这可能会使SAC在操作系统中处于被动地位。而且不重新干净安装系统无法重新启用的设定会使其更难被接纳启用。若微软有心推动,这可能会使他们陷入两难,要么强制启用,要么提供排除选项(不太可能,这违背了此功能的初衷)。对于普通Windows用户,这可能是个杀手锏,不过我也想看看有多少人因为各种原因而选择禁用此功能却不清楚无法再次启用此功能。从企业IT管理员的角度,能拥有多少的控制权,能否指定一些用户/用户组启用而另一些不启用。不过总而言之,我期待SAC的未来。
若存在问题,欢迎指正!
此系列就此告一段落。我目前被迫需要投入更多的时间和精力到现实生活中去了,有缘再见
这一篇写了太久了,有些东西可能会错乱,敬请谅解
|