内核中的代码完整性分析:深入了解CI.DLL
2020-04-19 11:20:00 Author: www.4hou.com(查看原文) 阅读量:302 收藏

ci.dll是什么文件?ci.dll是一款必要的系统文件,如果出现ci.dll丢失导致系统无法开机或软件不能正常运行的问题。系统文件ci.dll出错,极有可能是盗号木马、流氓软件等恶意程序所导致,其感染相关文件并加载起来,一旦杀毒软件删除被感染的文件,就会导致相关组件缺失,游戏等常用软件运行不起来。大多数的杀毒软件会把ci.dll认为是病毒软件,比如: 微软则将其标识为Trojan:Win32/Boaxxe.I,而卡巴斯基则将其标识为Rootkit.Win32.Podnuha.bpv 或 Rootkit.Win32.Podnuha.byb。 

总之,ci.dll 对于Windows来说不是必须的,而且容易引起麻烦。 Ci.dll 是存放在目录 C:\Windows\System32。

在某些情况下,你需要在允许流程采取某些操作之前可靠地标识它。验证它的Authenticode签名是一种可靠的方法,用户模式dll wintrust提供了专门用于此目的的API。wintrust.dll是 DLL文件信息,用于验证第三方应用程序的文件,目录,内存使用,数据签名等。

但是,如果需要在内核模式中进行可靠的身份验证,该怎么办呢?你可能需要这样做,原因如下:

1. 你的应用程序的用户模式部分不可用,可能是由于你尚处于流程的早期阶段,或是由于故障或配置问题;

2. 你希望获得对流程操作的内联访问权限,以便在未验证流程的情况下可以阻止它们;

3. 必须在内核模式下,Windows内核在加载驱动程序时才会验证驱动程序。

尽管在许多不同的论坛上都多次询问如何执行此操作,但我们无法在网上找到此共享的任何实现。

一些人建议你应该自己实现它,而另一些人则建议将OpenSSL源代码导入到你的项目中,其他人则将此任务委托给用户模式下的代码。但是所有这些替代方案都有重大漏洞:

1. 解析复杂的ASN1结构容易出错;

2. 将大量源代码导入驱动程序不是一个好主意,因为OpenSSL中的每个漏洞修复都会导致重新导入这些代码;

3. 进入用户模式可能无效,并且如上所述,用户模式并非始终可用。

Microsoft内核模式库ci.dll中存在对文件进行身份验证的功能,j00ru的研究表明,ntoskrnl通过CiInitialize()函数初始化CI模块,该函数返回一个带有回调列表的函数指针结构。如果我们可以使用这些函数或其他CI导出来验证正在运行的进程或文件的完整性和真实性,那么这将会改变内核驱动程序的运行规则。

除了ntoskernel.exe,我们发现了两个驱动程序,它们链接到ci.dll并使用其导出文件:

1.webp.jpg

链接到ci.dll的驱动程序

2.webp.jpg

链接到ci.dll的驱动程序

驱动程序可以链接到此模块,并调用有趣的函数,比如CiValidateFileObject(),它的名字表明它可以做我们正在寻找的事情!

在本文中,我们通过一个代码示例阐明了CI,以作为进一步研究的基础。

有关我们在安全行业中看到的趋势的更多信息,请观看有关2020年安全预测的网络研讨会。

背景资料

我们建议在深入了解ci.dll之前,你应该先熟悉以下内容:

1. PE安全目录,PE中包含Authenticode签名的部分;

2. WIN_CERTIFICATE结构,Authenticode签名之前的标头;

3. PKCS 7 SignedData结构,Authenticode所基于的结构;

4. X.509证书结构;

5. 证书的时间戳,这是一种通过过期或稍后被撤销的证书延长签名寿命的方法。

具体分析过程

在Windows 10上,CI导出以下函数:

3.webp.jpg

CI导出函数

前所述,调用CiInitialize()将返回一个名为g_CiCallbacks的结构,其中包含更多的函数。其中一个函数CiValidateImageHeader()是ntoskernel.exe在加载驱动程序时用来验证其签名的:

4.webp.jpg

加载过程中用于驱动程序签名验证的调用堆栈

我们的研究利用了导出的函数CiCheckSignedFile()和它所交互的数据结构,稍后我们将看到,这些数据结构出现在其他CI函数中,这使我们也可以将研究范围扩展到它们。

CICHECKSIGNEDFILE ()

CiCheckSignedFile()可以接收8个参数,但其名称尚无法告诉我们这些参数可能是什么。但是,我们可以通过检查内部函数来推断参数,例如,MinCryptGetHashAlgorithmFromWinCertificate():

5.webp.jpg

检查WIN_CERTIFICATE的结构成员

我们认识到常量0x200, 2是典型的WIN_CERTIFICATE结构,它为我们提供了第4和第5个参数。我们可以通过类似的方式找到其余的输入参数。输出参数是一个完全不同的过程,我们稍后将详细介绍。

经过一些逆向过程,我们发现了以下函数签名:

NTSTATUS CiCheckSignedFile(

    __In__ const PVOID digestBuffer,

    __In__ int digestSize,

    __In__ int digestIdentifier,

    __In__ const LPWIN_CERTIFICATE winCert,

    __In__ int sizeOfSecurityDirectory,

    __Out__ PolicyInfo* policyInfoForSigner,

    __Out__ LARGE_INTEGER* signingTime,

    __Out__ PolicyInfo* policyInfoForTimestampingAuthority

);

这个函数是如何工作的?

1. 调用者为函数提供文件摘要(缓冲区和算法类型)和指向Authenticode签名的指针;

2. 该函数通过以下方式验证签名和摘要;

3. 遍历文件签名并获取使用给定摘要算法的签名;

4. 验证签名和证书并提取其中出现的文件摘要;

5. 将提取的摘要与调用方提供的摘要进行比较;

6. 除了验证文件签名之外,该函数还向调用者提供有关已验证签名的各种详细信息。

函数如何工作的最后一部分非常有趣,因为仅知道文件已正确签名是不够的,我们还想知道是谁签的,我们将在下一部分中进行详细介绍。

POLICYINFO结构

到目前为止,我们已经获得了CiCheckSignedFile()的所有输入参数,并且能够调用它。但是除了它的大小(在Windows 10/x64上是0x30)之外,我们对PolicyInfo结构一无所知。

作为一个输出参数,我们希望这个结构能够以某种方式提供关于签名者身份的提示,从而避免我们自己提取它的麻烦。因此,我们调用该函数并检查内存以查看PolicyInfo填充了哪些数据,内存似乎包含一个地址和一些数字。

这个结构在内部函数MinCryptVerifyCertificateWithPolicy2()中被填充:

7.webp.jpg

填充PolicyInfo结构

此函数中的一些代码似乎会检查某个值是否超出某个范围,在证书验证的上下文中,我们怀疑这个范围是证书有效的时间段,事实证明这是正确的:

8.webp.jpg

检查证书有效期

这导致了以下结构:

typedef struct _PolicyInfo

{
 
int structSize;

    NTSTATUS verificationStatus;

    int flags;

    PVOID someBuffer; // later known as certChainInfo; 

    FILETIME revocationTime;

    FILETIME notBeforeTime;

    FILETIME notAfterTime;


} PolicyInfo, *pPolicyInfo;

虽然证书的有效期可能很有趣,但是它不能使我们对签名者有很强的识别能力。稍后我们将发现,大多数信息都位于成员certChainInfo中,我们将在后面讨论。

CERTCHAININFO缓冲区

在检查PolicyInfo的内存时,我们可以看到它指向结构之外的一个内存位置:一个动态分配的缓冲区。这个分配位于I_MinCryptAddChainInfo()中,这个函数的名称暗示了缓冲区的用途。

我们通过检查它的内存布局来逆转这个缓冲区:

1. 在前几个字节中,有指向缓冲区内不同位置的指针;

2. 在这些指定的位置中有重复的模式和指向缓冲区中更深处位置的指针;

3. 在最后这些指定的位置中,我们发现一些文本,看起来像是证书的摘录。

这个缓冲区包含关于整个证书链的数据,包括解析格式(即在子结构中组织)和原始数据格式(即ASN.1证书、密钥、EKUs块)的数据。

例如,这使调用者可以轻松地检查出证书的主题和颁发者是谁,证书链由什么组成以及用于创建每个证书的哈希算法。

为了更好地说明此缓冲区的格式以及我们从中得出的子结构,我们将向你展示其在32位计算机上的内存布局。使用32位计算机可减少混乱情况,因为为对齐要求添加了更少的填充字节。以下是由Microsoft签名的Notepad.exe的过程:

10.webp.jpg

CertChainInfo缓冲区的内存视图

我们能从中发现什么结果?

1. 缓冲区的顶部有两个4字节数字,一个是告诉我们在哪里可以找到一系列CertChainMember类型的结构的地址,一个是指示其中有多少个计数器。

2. 第一个CertChainMember位于地址0x89BF45C8中,用黑圈圈了起来。

3. 在CertChainMembers的末尾,地址0x89BF4688被蓝色包围,有一个纯文本的主题名。

4. 在CertChainMembers的末尾,地址0x89BF4688被蓝色包围,有一个纯文本的主题名。

5. 在红色箭头指向的地址0x89BF46BE处,ASN.1 Blob的开头包含实际证书。内存以little-endian的方式以4字节组的形式显示,因此证书的前两个字节实际上是0x3082,而不是图中显示的0x3131。

typedef struct _CertChainMember

{

    int digestIdetifier; // e.g. 0x800c for SHA256

    int digestSize; // e.g. 0x20 for SHA256

    BYTE digestBuffer[64]; // contains the digest itself

    CertificatePartyName subjectName; // pointer to the subject name

    CertificatePartyName issuerName; // pointer to the issuer name

    Asn1BlobPtr certificate; // pointer to actual certificate in ASN.1

} CertChainMember, * pCertChainMember;

这就是我们之前所说的解析数据,你无需自己解析证书即可获取主题或颁发者。

这个结构中的最后一个字节指向缓冲区内部的位置,下面的96个字节包含第二个CertChainMember,为了便于阅读,我们没有标记它,它包含有关链中下一个证书的信息。

类似的一系列指针和结构也存在于公钥和EKU(扩展密钥使用)中。换句话说,CI从证书中获取一些有趣的数据,并以子结构的形式使调用者随时可以使用这些数据。但它也包括原始的、未解析的数据,以备调用者调用其他数据。

注意:PolicyInfo和CertChainInfo结构都是从结构的大小开始的,由于这些结构体是由操作系统版本扩展的,所以在尝试访问其他结构体成员之前,必须检查这个大小。

CertChainInfo缓冲区的完整分解,以及各种子结构,可以在存储库中的ci.h文件中找到。

CIFREEPOLICYINFO()

这个函数释放PolicyInfo的certChainInfo缓冲区,该缓冲区由CiCheckSignedFile()和其他填充PolicyInfo结构的CI函数分配。该函数还重置其他结构成员。为了避免内存泄漏,必须调用它。

12.webp.jpg

CiFreePolicyInfo()的实现

由于该函数会在内部检查是否有可用的内存,因此即使未填充PolicyInfo,也可以安全地调用它。

CIVALIDATEFILEOBJECT()

如前所述,CiCheckSignedFile()使调用者可以工作很多,然后才能调用它。调用者必须计算文件哈希并解析PE,以便为函数提供签名的位置。

但是,函数CiValidateFileObject()可为调用者完成此工作。我们不必从头开始,因为它与CiCheckSignedFile()共享一些参数:

NTSTATUS CiValidateFileObject(

    __In__ struct _FILE_OBJECT* fileObject,

    __In__ int a2,

    __In__ int a3,

    __Out__ PolicyInfo* policyInfoForSigner,
   
    __Out__ PolicyInfo* policyInfoForTimestampingAuthority,

    __Out__ LARGE_INTEGER* signingTime,

    __Out__ BYTE* digestBuffer,

    __Out__ int* digestSize,

    __Out__int* digestIdentifier

);

此函数在内核空间中映射文件并提取其签名:

14.webp.jpg

通过CiValidateFileObject()在系统空间中映射文件

它还会计算文件摘要,如果你为其提供了足够长的非空缓冲区,它将使用此摘要来填充它。

注意:由于此函数仅在最新的Windows版本中添加,因此我们并未将研究重点放在此函数上。如果我们要继续研究,我们将专注于理解其验证政策。

请注意,它使用比CiCheckSignedFile()更严格的策略,这意味着它可能无法对CiCheckSignedFile()批准的PE进行验证。这可能会受到参数2和3的值的影响,我们没有逆转它们。

GITHUB REPO

为了演示如何使用ci.dll来验证PE签名,我们使用Github存储库对这个编写进行了补充。

该存储库包含一个简单的驱动程序,包含以下3部分:

1. 注册用于新流程通知的回调;

2. 尝试使用此处介绍的ci.dll函数来验证每个新进程的PE签名;

3. 如果成功验证了文件的签名,驱动程序将解析输出PolicyInfo结构,以提取签名证书及其详细信息。

我们鼓励你尝试使用此GITHUB REPO ,以初步了解CI,并扩大研究范围。

与CI.DLL连结

最后,我们要描述与此未记录的库进行链接的过程。尽管使用CI似乎是一个枯燥的技术性方面,但我们发现它并不简单,并且如果你使用更多功能扩展研究,则可能需要执行相同的步骤。

与特定的dll链接时,你通常使用供应商提供的导入库。在我们的案例中,Microsoft没有提供.lib文件,我们必须自己生成它。生成后,该文件应作为链接器输入添加到项目属性中。以下是生成.lib文件所需的步骤:

64位

使用dumpbin工具从dll中获取导出的函数:

dumpbin /EXPORTS c:\windows\system32\ci.dll

创建一个.def文件,它看起来像这样:

LIBRARY ci.dll

EXPORTS

CiCheckSignedFile

CiFreePolicyInfo

CiValidateFileObject

使用lib工具生成.lib文件:

lib /def:ci.def /计算机:x64 /out:ci.lib

32位

这是一种棘手的情况,因为在32位中,函数反映的是参数的和(以字节为单位),例如:

CiFreePolicyInfo@4

但是ci.dll导出的函数没有这个,所以我们需要创建一个.lib文件来进行转换。具体方法请参考这里(1 ,2 )。

1. 创建一个.def文件,如上面的64位部分的第1和第2阶段所述;

2. 创建一个c++文件,使用具有相同签名但没有实体的函数存根。你基本上可以模仿供应商在从其代码导出函数时所做的操作。例如:

extern "C" __declspec(dllexport) 
PVOID _stdcall CiFreePolicyInfo(PVOID policyInfoPtr)

{

    return nullptr;

}

3. 将其编译成OBJ文件;

4. 使用lib工具生成.lib文件,这次使用OBJ文件:

Lib /def:ci.def /machine:x86 /out:ci.lib < obj file >

另外,Github存储库包含存根的代码。

总结

本文介绍了如何使用CI API的一个子组,这使我们可以在内核模式中验证Authenticode签名,而无需自己实现它。

本文翻译自:https://www.cybereason.com/blog/code-integrity-in-the-kernel-a-look-into-cidll如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/v9E5
如有侵权请联系:admin#unsafe.sh