长亭百川云 - 文章详情

Windows代码签名机制分析

kernsec

100

2024-07-13

1 简介

    Windows在vista版本时就已经提供了二进制代码签名机制,到win10版本时代码签名机制进化的更加全面,从当初的内核校验深入到硬件虚拟化层校验(vbs),管理员可以编写丰富的policy支持不同粒度的校验等级,可以说windows的代码签名要比ios更加细致与完善。

2 代码签名原理

2.1 签名信息的等级

    Ios及android的某些版本是通过比较二进制程序的hash值来做校验的,而windows提供了更加丰富的校验等级,在微软的WDAC文档中,提供了如下的等级分类:

  可以看到除了使用hash值,还可以使用文件名、文件路径、证书的发布者等等种类进行校验。

2.2 签名的存储结构

     Windows的签名信息可以存储在两个地方:PE文件和catalog文件。

2.2.1 PE文件签名信息

  在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系统中,并不是所有的二进制都做了签名信息,微软公司只对重要的系统文件加了签名信息。

2.2.2 文件hash值的计算方法

  文件hash值的计算并不是计算所有的文件内容,需要去掉pe文件中与签名有关的数据,以下算法来自微软的官方文档:

  同时windows api还提供了一系列关于签名的辅助函数:

Image Integrity

The image integrity functions manage the set of certificates in an image file.

DigestFunction
ImageAddCertificate
ImageEnumerateCertificates
ImageGetCertificateData
ImageGetCertificateHeader
ImageGetDigestStream
ImageRemoveCertificate

2.2.3 Catalog文件

  将文件签名信息保存在pe文件中,就会变得不是很灵活,为此微软公司提供了另一种签名信息的存储格式catalog文件类型,并提供了一些内核api可以动态增加或删除一个文件的签名信息, 结合前面提到的WDAC policy就会变得更加灵活,在稍后的章节中会详细介绍。

2.3 签名的校验逻辑

2.3.1 代码签名的初始化

  由于win10启动了WDAC policy策略SIPolicy.p7b,它是一个pkcs#7格式的文件,它由windows loader加载并解析,然后在传给nt内核。德国bsi的安全研究人员已经对其启动过程做了详细的分析,强烈推荐大家仔细阅读,windows loader除了加载wdac policy外,还要加载ci.dll,因为Windows的签名代码逻辑基本都在ci.dll中实现。

可以看到它导出了以下函数:

cybereason.com的安全研究人员给出了一篇非常棒的paper解析了CiCheckSignedFile和CiValidateFileObject两个函数的用法,还原了它们的参数和部分数据结构体,同时在github提供了一个demo使得一个驱动程序链接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的完整性校验。

2.3.2 文件hash校验逻辑

2.3.2.1 CiValidateImageHeader****逻辑

  在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的对比。

2.3.2.2 Catalog接口

  前面提到文件签名信息除了可以保存在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文件名保存在一个链表中。

2.3.2.3 Pagefault处理

  同ios一样,除了进程在建立时校验一次hash外,在程序运行中,如果触发了页异常错误,也要对造成异常的页面做一次hash值校验,包括当进程一个页面被换出到磁盘上,某个时刻又换回内存后,势必要进行一次hash校验。当前win11版本的页异常处理hash值校验路径为:

MmAccessFault->MiIssueHardFault->MiWaitForInPageComplete->MiValidateInPage->SeValidateImageData->CiValidateImageHeader

  在页异常中处理hash校验会非常消耗性能,微软的代码中虽然有了这些代码逻辑,但是如前面的章节所述,文件的签名信息内只包含了文件的hash值,并没有包含文件的page hash值。

  除了页异常,我们看到ios在两个内存物理页进行拷贝的函数中也加入了hash值校验,但是微软的工程师似乎忘记了这个路径。

2.3.2.4 动态代码签名

   IOS使用了Entitlement机制,可以将一个app标记为可以使用动态代码内存,比如浏览器进程需要使用jit动态映射代码。Windows的做法是内核提供接口,可以设置文件的某个扩展属性,把它标记为Trust进程,可以使用动态代码,当签名校验时会判断是否为trust进程,就可以绕过签名校验。

__int64 __fastcall CiSetDynamicCodeTrustClaim(__int64 a1, __int64 a2)
相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2