前言

在某些场景中,如果我们希望在允许某个进程进行特定动作前,以一种可靠的方式确认该进程是否可信,那么验证该进程的Authenticode签名是一个不错的方式。用户模式下的DLL wintrust提供了专门用于此目的的API。但是,如果我们需要在内核模式下以一种可靠的方式来进行身份验证,这时应该如何进行呢?在以下的情况中,我们可能会遇到这样的场景:1、应用程序用户模式部分不可用,可能是由于正处于开发过程的早期阶段,也可能是由于运行失败或配置出现问题。2、我们希望获得对进程操作的内联访问权限,以便在进程未验证的情况下阻止它们。3、最典型的一种情况是Windows内核在加载驱动程序时对驱动程序进行验证,显然这一过程必须要在内核模式下完成。尽管在不少论坛上,都有人多次提问应该如何操作,但我们还没有在公开的地方找到解决该问题的任何实现。其中一些方案建议我们自行实现,一些方案则建议将OpenSSL源导入到我们的项目中。而另外一种方案则将这个任务委托给用户模式下的代码。但是,上述所有替代方案都有明显的缺点:1、在解析复杂的ASN1结构时容易出现错误;2、不适合将大量源代码导入驱动程序,因为OpenSSL中的每一个漏洞修复都会导致重新导入该代码。3、进入用户模式可能无效,并且用户模式并非始终都可用。实际上,Microsoft内核模式库ci.dll中,就包含对文件进行身份验证的功能。j00ru的研究表明,ntoskrnl通过CiInitialize()函数初始化CI模块,该函数以回调列表填充函数指针结构。如果我们可以使用这些函数或者其他CI导出来验证正在运行的进程或文件的完整性和真实性,这将会成为内核驱动程序的一个最佳方案。除了ntoskernel.exe之外,我们还发现了两个驱动程序,它们都链接到ci.dll,并使用其导出文件:

链接到ci.dll的驱动程序

链接到ci.dll的驱动程序驱动程序可以链接到这个模块,并且调用一些关键的函数,例如CiValidateFileObject()。从函数名称就可以看出,这样的方式完全可以满足我们的需求。在本文中,我们将通过一个代码示例来详细分析CI,可以以此作为进一步研究的基础。

背景信息

我们建议各位读者在详细分析ci.dll之前,首先熟悉以下相关主题:1、PE安全目录:PE中包含Authenticode签名的部分;2、WIN_CERTIFICATE结构:Authenticode签名之前的标头;3、PKCS 7 SignedData结构:Authenticode的基础结构;4、X.509证书结构;5、证书时间戳:通过过期或吊销证书来延长签名使用周期的方法。

研究过程

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

CI导出功能如前所述,调用CiInitialize()将会返回一个名为g_CiCallbacks的结构,其中包含更多函数(详情请参考[1][2][5])。而其中的一个函数,CiValidateImageHeader(),将会被ntoskernel.exe用于加载驱动程序以验证签名的过程:

调用堆栈以在加载过程中验证驱动程序签名在我们的研究中,利用了导出的函数CiCheckSignedFile()以及与之交互的数据结构。稍后我们将看到,这些数据结构也出现在其他CI函数中,我们也可以将研究范围扩展到这些其他的函数。

CiCheckSignedFile()

CiCheckSignedFile()可以接收8个参数,但目前我们还不清楚这些参数的名称是什么。但是,通过检查内部函数,我们可以推断出其参数。例如,我们可以检查MinCryptGetHashAlgorithmFromWinCertificate():

检查WIN_CERTIFICATE的结构成员我们发现,对于WIN_CERTIFICATE结构来说,常量0x200和2是比较常见的值,该结构为我们提供了第四个和第五个参数。我们可以通过类似的方式找到其余的输入参数。而对于输出参数来说,则方法完全不同,我们将在后文中详细描述。进行一些逆向之后,我们得到了函数签名:

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、该函数通过以下方式验证签名和摘要:(1)遍历文件签名,并获取使用特定摘要算法的签名;(2)验证签名(和证书),并提取其中显示的文件摘要;(3)将提取的摘要与调用方提供的摘要进行比较。3、除了验证文件签名之外,该函数还为调用方提供有关已验证签名的各种详细信息。该函数后面一部分的工作原理非常值得关注,因为仅仅知道文件已经经过正确签名是不够的,我们还需要知道是由谁进行签名的。在下一节中,我们将解决这一问题。

PolicyInfo结构

到目前为止,我们已经将所有输入参数输入到CiCheckSignedFile()并且能够进行调用。但是,我们除了其大小(在Windows 10 x64上为0x30)之外,对于PolicyInfo结构几乎一无所知。作为输出参数,我们希望该结构能以某种方式提供有关签名者身份的提示。因此,我们调用该函数,并对内存进行检查,以确认哪些数据填充到PolicyInfo之中。在内存中,似乎包含一个地址和一些较大的数字。该结构正在内部函数MinCryptVerifyCertificateWithPolicy2()中填充:

填充PolicyInfo结构该函数中的某些代码似乎正在检查该值是否在特定范围之内。对于证书验证的过程来说,我们推测这个范围是证书有效的时间范围,事实上证明这是正确的:

检查证书有效期这将引向以下结构:

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、在最后指向的这些位置中,我们找到了一些文本,看起来像是证书的摘要。该缓冲区中包含有关整个证书链的数据,既有解析格式(位于子结构中),也有原始数据格式(包含证书、密钥、EKU的ASN.1证书)。这一部分使调用方可以轻松地查看证书的主题、颁发者、证书链的组成,以及用于创建每个证书的哈希算法。为了更好地解释这个缓冲区的格式,以及我们从中得到的子结构,我们将分析其在32位计算机上的内存布局。如果使用32位计算机,可以减少混乱的情况,这里可以利用更少的填充字节来满足对齐要求。下面是由Microsoft签名的Notepad.exe的示例:

CertChainInfo缓冲区的内存视图我们在这里可以发现:1、缓冲区的顶部有两个4字节的数字。其中的一个表明在哪里可以找到一系列CertChainMember类型结构的地址,另一个是可以指示其中有多少个结构的计数器。2、第一个CertChainMember位于地址0x89BF45C8中(以黑色标出),我们将其格式化如下:(1)在CertChainMembers的末尾,以蓝色标出的地址0x89BF4688处,有纯文本格式的主题名称。(2)在橙色标出的地址0x89BF4699处,有纯文本格式的发行者名称。(3)在红色箭头指出的地址0x89BF46BE处,包含实际证书的ASN.1 blob的开头。内存以小端对齐的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结构都以结构的大小开始。由于这些结构是可以在OS版本之间实现扩展的,因此在尝试访问其他结构成员之前,必须要检查这里的大小。在存储库中的文件ci.h中,可以找到CertChainInfo缓冲区的完整分类和各种子结构。

CiFreePolicyInfo()

该函数将释放PolicyInfo的certChainInfo缓冲区,该缓冲区由CiCheckSignedFile()和其他填充PolicyInfo结构的CI函数分配。该函数还会重置其他结构成员。在这里,必须要对其进行调用,以避免内存泄漏。

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);

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

通过CiValidateFileObject()在系统空间中映射文件。该函数还会计算文件摘要,如果为其提供了足够长的非空缓冲区,将会使用摘要来进行填充。注意:由于该函数仅在最新的Windows版本上添加,因此我们并未将研究的重点放在这个函数上。如果我们要继续研究,我们会专注于分析其验证的策略。在这里,使用了比CiCheckSignedFile()更为严格的策略,这意味着它有可能无法验证通过此前经过CiCheckSignedFile()验证的PE。这里可能会受到第2个和第3个参数值的影响,但我们还没有对其进行逆向。

GitHub Repo

为了演示如何利用ci.dll来验证PE签名,我们使用了GitHub存储库来对本文进行了补充。该存储库中,包含一个简单的驱动程序,可以用于测试我们上述的研究成果:1、注册用于新进程通知的回调;2、尝试使用ci.dll函数来验证每个新进程的PE签名;3、如果成功验证了文件的签名,驱动程序将解析输出PolicyInfo结构,以提取签名证书及其详细信息。我们鼓励大家尝试使用这个repo,以初步了解CI,并扩大研究的范围。

与CI链接

最后,我们要分析如何与这个未记录的库相链接的过程。尽管使用CI的过程看起来非常枯燥,但我们发现它并不简单,如果大家对其中的更多函数进行扩展研究,可能需要执行与本文相同的步骤。在与特定的dll链接时,通常使用厂商提供的导入库。在我们的案例中,Microsoft并没有提供.lib文件,我们必须自己生成该文件。在生成之后,该文件应该作为链接器输入添加到项目属性中。下面是生成.lib文件所需的步骤。

64位

1、使用dumpbin实用程序从dll中获取导出的函数:

dumpbin /EXPORTS c:windowssystem32ci.dll

2、创建一个.def文件,如下所示:LIBRARY ci.dllEXPORTSCiCheckSignedFileCiFreePolicyInfoCiValidateFileObject3、使用lib实用程序生成.lib文件:

lib /def:ci.def /machine:x64 /out:ci.lib

32位

这里的情况比较棘手,因为在32位系统中,函数反射参数的总和(以字节为单位),例如:

CiFreePolicyInfo@4

但是ci.dll会导出没有这部分的函数,因此我们需要创建一个.lib文件以进行这样的转换,所以我们使用了[3]和[4]文章中所描述的方法。1、如同64位中的第1步和第2步所述,创建一个.def文件。2、使用具有相同签名的伪装实体的函数stub创建一个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 

在GitHub存储库中,包含stub的代码。

总结

本文演示了如何使用CI API中的一部分。我们通过这种方式,成功在内核模式下验证了Authenticode签名,而无需再自行实现。我们希望本文能为大家对这个dll的后续研究铺平道路。在这里,我想向对本篇文章提供帮助的几位研究人员表示感谢,他们分别是Yuval Kovacs、Allie Mellen、Philip Tsukerman和Michael Maltsev。

参考文章

[1] Microsoft Windows FIPS 140 验证安全策略文档(https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp3093.pdf)[2] Windows驱动签名绕过(作者:derusbi)(https://www.sekoia.fr/blog/windows-driver-signing-bypass-by-derusbi/)[3] 如何创建32位导入库(https://qualapps.blogspot.com/2007/08/how-to-create-32-bit-import-libraries.html)[4] Q131313: 如何创建没有.OBJ或源代码的32位导入库(https://jeffpar.github.io/kbarchive/kb/131/Q131313/)[5] j00ru关于CI的博客文章(https://j00ru.vexillium.org/2010/06/insight-into-the-driver-signature-enforcement/)

原文链接:https://www.anquanke.com/post/id/200478

dll 导出函数 下划线_内核中的代码完整性:深入分析ci.dll相关推荐

  1. dll oem证书导入工具_技术干货 | 恶意代码分析之反射型DLL注入

    欢迎各位添加微信号:qinchang_198231   加入安全+ 交流群 和大佬们一起交流安全技术 01 技术概要 这是一种允许攻击者从内存而非磁盘向指定进程注入DLL的技术,该技术比常规的DLL注 ...

  2. Python中带下划线_的变量和函数命名的用法

    Python 的代码风格由 PEP 8 描述.这个文档描述了 Python 编程风格的方方面面.在遵守这个文档的条件下,不同程序员编写的 Python 代码可以保持最大程度的相似风格.这样就易于阅读, ...

  3. Python中单个下划线“ _”变量的用途是什么?

    这段代码中_ after for的含义是什么? if tbh.bag:n = 0for _ in tbh.bag.atom_set():n += 1 #1楼 下划线_在Python中被视为" ...

  4. scala中何时使用下划线_在Scala中使用下划线

    scala中何时使用下划线 Underscore (_) character is reserved in Scala and has multiple usages in the programmi ...

  5. python中init方法的两个下划线_为什么Python中有各种各样的“_”下划线?分别有什么用?...

    刚开始学Python的你一定很疑惑,为什么Python里会出现各种各样的下划线 "_",而且位置都不相同,有时候在名称后面,有时候在前面,有时候还会在数字中间......这些下划线 ...

  6. python中的下划线_讲解

    python中,下划线 "_""_"" \_" 不管是单独作为变量名或者作为变量名的前缀或者后缀,是有特殊含义的,下面简要的来总结一下. 1 ...

  7. Swift实战问题之Swift 中的下划线_是什么意思?

    实战问题 Swift 中的下划线_是什么意思?例如下面的函数参数 for _ in 1-5 { print("hello") } if let _ = optionalValue ...

  8. python中的符号下划线_详解Python中下划线的使用方法

    编程派微信号:codingpy 这篇文章讨论Python中下划线_的使用.跟Python中很多用法类似,下划线 _ 的不同用法绝大部分(不全是)都是一种惯例约定. 单个下划线(_) 主要有三种情况: ...

  9. python中五种下划线 _

    python中五种下划线 "_" 单前导下划线:_var 单末尾下划线:var_ 双前导下划线:__var 双前导和末尾下划线:var 单下划线:_ 在文章结尾处,你可以找到一个简 ...

最新文章

  1. 提高网站页面收录的几个方法 返回列表 发新帖回复
  2. Python数据分析Numpy库方法简介(三)
  3. 360下载器怎么打开 360下载器使用方法
  4. bash 函数内部变量_使用源命令将函数和变量导入Bash
  5. 使用dex2jar和luyten查看apk中的class代码
  6. 济南2021高考成绩查询,@全体济南人:2021夏季高考时间公布!
  7. linux终端 美化 git,linux终端美化oh-my-zsh
  8. Don't Panic! KRACK 没你想象的那么糟
  9. css3中旋转坐标轴的问题
  10. delphi10.2.1下载地址
  11. Python:批量爬取下载中国知网(CNKI)PDF论文
  12. 01-C语言之父:丹尼斯·里奇
  13. 杭州电子科技大学计算机网络考研,2017杭州电子科技大学计算机网络考研大纲...
  14. 局域网共享文件的方法
  15. 如何实现文字逐个出现的打字机效果
  16. matlab中ln4怎么表示,matlab里ln怎么表示
  17. 基于PLC音乐喷泉控制系统设计音乐喷泉组态设计音乐喷泉
  18. HTML/CSS/Js/Jquery/PHP网站0基础开发到大神系列【飞鸽学院】
  19. 官宣|深圳新增6条新建地铁线路和100个站点
  20. c语言小红今年12岁小明13岁,[转载]三年级下“创新思维数学讲义”——年龄问题...

热门文章

  1. wpf 如何设置弹出窗口必须关闭才能打开其他软件_Mac忘记登录密码?以防万一,必须收藏。...
  2. PHP面向对象基础总结
  3. HTTP协议中几个状态码
  4. python写数据库校验_python 验证 sqlite数据库隔离级别
  5. php excel中解析显示html代码_骑士cms从任意文件包含到远程代码执行漏洞分析
  6. 十二月份找工作好找吗_淘宝美工前景怎么样?好找工作吗?
  7. eclipse android开发环境搭建_聊聊Spring boot2.X开发环境搭建和基本开发
  8. c 获取char*的长度_C/C++编程笔记:C语言字符串比较函数,超详细,值得收藏
  9. java 微信jssdk签名_JAVA生成微信JSSDK接口签名
  10. 宝塔安装php遇错libicui18n.so.42: cannot open shared object file: No such file or directory