Windows在vista版本时就已经提供了二进制代码签名机制,到win10版本时代码签名机制进化的更加全面,从当初的内核校验深入到硬件虚拟化层校验(vbs),管理员可以编写丰富的policy支持不同粒度的校验等级,可以说windows的代码签名要比ios更加细致与完善。
Ios及android的某些版本是通过比较二进制程序的hash值来做校验的,而windows提供了更加丰富的校验等级,在微软的WDAC文档中,提供了如下的等级分类:
可以看到除了使用hash值,还可以使用文件名、文件路径、证书的发布者等等种类进行校验。
Windows的签名信息可以存储在两个地方:PE文件和catalog文件。
在PE文件的Optional header里加入一个Security Data Directory:
它指向Section末尾的Attribute Certificate Table存储签名信息,它的结构体在wintrust.h中定义:
typedef struct _WIN_CERTIFICATE {
签名结构架构如下:
WIN_CERTIFICATE结构体的bCertificate指向具体的签名信息,它是一个pkcs#7结构。ContantInfo里保存了当前文件的hash值, windows支持两种格式的hash:一个是文件的整个hash值,另一种是已PAGE为单位的hash值。IOS的代码签名机制只允许以页为单位的hash值校验,那么当一个文件非常大时,签名信息里就要携带非常大的hash值,会使文件体积变得更大。在笔者的win10系统中,几乎所有的pe文件都只携带了文件的hash值。
已smss.exe为例,右键点击属性,如果文件有签名信息,可以看到有数字签名一栏,点击查看证书信息:
序列号为文件的hash值,采用sha256算法。
在笔者的win10系统中,并不是所有的二进制都做了签名信息,微软公司只对重要的系统文件加了签名信息。
文件hash值的计算并不是计算所有的文件内容,需要去掉pe文件中与签名有关的数据,以下算法来自微软的官方文档:
同时windows api还提供了一系列关于签名的辅助函数:
The image integrity functions manage the set of certificates in an image file.
DigestFunction
ImageAddCertificate
ImageEnumerateCertificates
ImageGetCertificateData
ImageGetCertificateHeader
ImageGetDigestStream
ImageRemoveCertificate
将文件签名信息保存在pe文件中,就会变得不是很灵活,为此微软公司提供了另一种签名信息的存储格式catalog文件类型,并提供了一些内核api可以动态增加或删除一个文件的签名信息, 结合前面提到的WDAC policy就会变得更加灵活,在稍后的章节中会详细介绍。
由于win10启动了WDAC policy策略SIPolicy.p7b,它是一个pkcs#7格式的文件,它由windows loader加载并解析,然后在传给nt内核。德国bsi的安全研究人员已经对其启动过程做了详细的分析,强烈推荐大家仔细阅读,windows loader除了加载wdac policy外,还要加载ci.dll,因为Windows的签名代码逻辑基本都在ci.dll中实现。
可以看到它导出了以下函数:
在笔者的win10系统中,没有发现内核有调用这两个接口,但是内核在初始化中调用了CiInitialize函数进行了代码签名的初始化工作:
InitBootProcessor->SeInitSystem->SepInitializationPhase1->SepInitializeCodeIntegrity:
__int64 SepInitializeCodeIntegrity()
Nt内核向ci.dll传递了SeCiCallbacks数组,写入了一系列回调函数:
__int64 __fastcall CipInitialize(__int64 a1, const UNICODE_STRING **a2, __int64 a3, __int64 a4)
可以看到CiCheckSignedFile和CiValidateFileObject这两个函数并没有写入回调数组,说明nt在代码签名校验时是没有使用这两个函数的。相反,nt内核使用CiValidateImageHeader和CiValidateImageData两个函数做签名校验,一个用来做文件hash的完整性校验,一个用来做page的完整性校验。
在CiValidateImageHeader处下个断点:
3: kd> k
MiCreateSection建立section的路径中会调用CiValidateImageHeader做文件hash校验,它的大致流程如下:
CiValidateImageHeader()
首先调用CipIsFileInUMCIExclusionPaths,这个函数是win11新增的用于开发者模式的白名单功能,它维护了一个列表,在列表中的path可以不用做校验。
char __fastcall CipIsFileInUMCIExclusionPaths(__int64 a1)
在笔者的win11版本中,这个列表为空:
4: kd> p
然后调用CiGetActionsForImage得到要执行的动作,ActionsForImage & 1表示在签名的cache中校验,否则ActionsForImage & 0x40表示从文件中提取签名信息。进一步ActionsForImage & 0x100表示对文件hash做校验,ActionsForImage & 4表示对文件的page做hash校验。
同ios一样,在每次做完签名校验后,会把文件的签名信息加入到一个cache中,如前所述,以后每次优先从cache中获取签名信息,避免了解析文件获取签名信息的性能开销。
除了在内存维护一个cache,还要给文件打个标签,利用了ntfs的文件扩展属性,利用FsRtlSetKernelEaFile函数将文件扩展属性设置为“$Kernel.Purge.CIpCache”。
CiBuildEaCacheContents函数负责建立cache结构体,对其参数进行跟踪分析:
CI!CiBuildEaCacheContents:
1: kd> dq r9 L8
所以进一步猜测黄色部分对应的地址应该为保存hash的地址,蓝色部分代表hash值的大小,sha256为0x20字节,sha1为0x14字节,确实没错。由此笔者推导出的cache结构体为:
struct FileEaCache {
或者更合理的定义为:
struct unkown_struct1 {
CiValidateImageHeader函数中还有一个重要的函数CipAllocateValidationContext,用于构造签名校验信息时统一用到的数据结构,这个结构体非常庞大,接近1k字节。笔者只推导出了其中几个关键成员结构:
Struct ValidateContext:
至于CipValidateFileHash函数,太复杂了,大致操作就是验证签名信息本身是否正确,然后做hash的对比。
前面提到文件签名信息除了可以保存在pe文件内,还可以保存在独立的catalog文件内,win11版本路径为:C:\Windows\SystemApps\Microsoft.UI.Xaml.CBS_8wekyb3d8bbwe\AppxMetadata\CodeIntegrity.cat
通过CiValidateImageHeader->CipFindFileHash->CI!CiVerifyFileHashInCatalogs路径进行调用。
微软还提供了动态添加和删除catlog文件的接口:CiAddDynamicCatalog和CiRemoveDynamicCatalogs。
__int64 __fastcall CiAddDynamicCatalog(WCHAR *a1, unsigned int a2)
可以看到catalog文件名保存在一个链表中。
同ios一样,除了进程在建立时校验一次hash外,在程序运行中,如果触发了页异常错误,也要对造成异常的页面做一次hash值校验,包括当进程一个页面被换出到磁盘上,某个时刻又换回内存后,势必要进行一次hash校验。当前win11版本的页异常处理hash值校验路径为:
MmAccessFault->MiIssueHardFault->MiWaitForInPageComplete->MiValidateInPage->SeValidateImageData->CiValidateImageHeader
在页异常中处理hash校验会非常消耗性能,微软的代码中虽然有了这些代码逻辑,但是如前面的章节所述,文件的签名信息内只包含了文件的hash值,并没有包含文件的page hash值。
除了页异常,我们看到ios在两个内存物理页进行拷贝的函数中也加入了hash值校验,但是微软的工程师似乎忘记了这个路径。
IOS使用了Entitlement机制,可以将一个app标记为可以使用动态代码内存,比如浏览器进程需要使用jit动态映射代码。Windows的做法是内核提供接口,可以设置文件的某个扩展属性,把它标记为Trust进程,可以使用动态代码,当签名校验时会判断是否为trust进程,就可以绕过签名校验。
__int64 __fastcall CiSetDynamicCodeTrustClaim(__int64 a1, __int64 a2)