如果你能看懂上面的代码行是什么意思,那么你可能是一个.NET开发人员。同时你也可能知道结尾处的十六进制字符串表示的是一个公共密钥令牌,这是一个表示程序集具有强名称签名的标识。 但你知道如何计算这个令牌吗? 或者你知道强名称签名的结构吗? 在这篇文章中,我将详细介绍强名称的工作原理及其缺点。 我们还将看看基于证书的签名,最后,我们将检查程序集验证签名的过程。
强名称和公共密钥令牌
一个有效的强名称签名可以确保收件人收到的程序集没有被篡改。同时,它还可以作为给定的程序集的唯一地标识。虽然,它没有说明关于签名者身份的任何信息。有两部分程序集二进制代码在强名称签名验证过程中发挥着作用。
第一部分是公共密钥,它是程序集元数据中的#Blob流的一部分(下面的屏幕截图中显示了dnSpy窗口的一部分):
下图中列出了构建公钥块的元素:
用于唯一地引用程序集的公共密钥令牌是以公共密钥的SHA-1哈希的低8字节的十六进制并以反向字节顺序表示的。对于我本文中测试使用的程序集来说,公共密钥令牌等于:769a8f10a7f072b4(SHA-1(00 24 00 00 0c 80 … c8 8a c1 b1)= 9aa4de0a96ada8d83d6d7678b472f0a7108f9a76)。
第二部分是程序集内容哈希后的RSA签名。 在计算这个哈希值之前,我们需要用零填充文件的以下字节:认证签名条目(我们稍后会得到它),强名称块和PE头校验和。 之后,签名会存储在PE文件的text节中,其文件地址为保存在COR20头中的偏移量:
为了计算RSA签名,我们需要拥有与上一张图片中列出的公共密钥相对应的私钥。 如果你想看看实现验证的C#代码,你可以查看dnLib库中的StrongNameSigner.cs文件。
在研究了强名称签名结构之后,现在让我们关注我们可以用来创建强名称签名结构的工具。 我们从生成.snk文件开始,该文件将存储RSA密钥的详细信息(如果你对.snk文件格式的详细信息感兴趣,请查看我的010编辑器模板):
sn.exe -k 2048 TestLib.snk
我们应该保存好生成的.snk文件的密文。 接下来,根据我们的方案,我们可以使用C#编译器(csc.exe)或程序集链接器(al.exe)。 这两者都接受 /keyfile参数,我们为这个参数提供了我们刚才生成的.snk文件的路径,例如。
csc.exe /keyfile:TestLib.snk /t:library TestLib.cs
此命令将基于文件内容的SHA-1的哈希值生成签名。 现在,SHA-1被认为不足以进行安全哈希操作,强烈建议使用SHA-2摘要算法。 要使用SHA-2哈希来对我们的程序集签名,我们首先需要提取.snk文件的公钥部分:
sn.exe -p TestLib.snk TestLibPubKey.snk sha256
然后延迟私钥签名过程(强名称签名块将被填充为零,强名称签名标志不会被置位)。 csc.exe和al.exe都接受 /delaysign+参数:
csc.exe /keyfile:TestLibPubKey.snk /delaysign+ /t:library TestLib.cs
最后,我们需要使用私钥重新签名程序集:
sn -Ra TestLib.dll TestLib.snk
如果你有一个强名称的程序集,并希望迁移签名,请看看这篇文章。
验证码签名
验证码签名(Autheticode signature,),顾名思义,是用于验证程序集的所有者。它还用于保护程序集的完整性。签名的大小和位置存储在PE可选头中:
Force Integrity标志(我在图中已经标记了),用于强制加载器始终检查给定程序集的签名(针对加载受保护的进程的驱动程序和模块,Windows会跳过签名验证)。 对于本地代码,有一个特殊的链接器选项来启用此特性。 我没有在csc.exe或al.exe中找到这样的参数选项以及使用dnSpy来设置这个标志位(我需要首先延迟签名程序集的过程,设置此标志,并重新进行签名)。
要创建验证码,我们需要拥有包含私钥的证书(需要.pfx格式)。 为了测试目的,自签名证书也是有效的(除非你设置了强制完整性的标志),但对于发布部署的情况,你最好应该从受信任的提供者那里获取一个证书。 使用证书文件对程序集签名的命令示例如下所示:
signtool sign /v /ph /fd sha256 ` /f .fileSignature.pfx ` /p {certificate-password} ` /t http://timestamp.verisign.com/scripts/timstamp.dll .TestLib.dll
记住在创建验证码之前创建强名称签名。
签名验证
.NET从3.5版本开始,在程序集被加载到一个完全信任的应用程序域时不会执行强名称签名验证。这基本上意味着在完全信任的应用程序域中,我们可以用一个只包含公钥的程序集来替换一个具有强名称的程序集,并且没有人会注意到程序集被替换。我们可以通过在配置文件中启用运行时的bypassTrustedAppStrongNames属性,或者通过将HKLMSOFTWAREMicrosoft.NETFramework键中的AllowStrongNameBypass值设置为零(在64位系统上可以使用Wow6432作为32位应用程序)来更改此行为。
即使已经有以上这些设置选项保证程序集不被替换,但仍然有一种方法可以将部分签名的程序集加载到我们的应用程序域中。在延迟程序集签名过程时,可以使用此绕过机制。在开发期间,我们通常不想在每个构建的程序中对程序集进行完全签名(私钥放在在一个地方才能保持安全性),但同时,我们又希望程序集具有强名称的行为。这可以通过在HKLMSoftwareMicrosoftStrongNameVerification下的注册表中添加我们的程序集名称和公共密钥令牌来实现,例如:HKLMSoftwareMicrosoftStrongNameVerificationTestLib,8FCE6031CC56162D,或使用sn命令的-Vr选项。注:只有系统管理员可以修改验证列表。
当涉及Authenticode验证时,.NET仅在PE头中的强制完整性标志被设置时才会执行验证。
幸运的是,我们不需要依赖自动验证;我们可以自己执行验证。我们用来对程序集签名的相同工具,为我们提供了验证它们的方法。要检查强名称签名,我们可以使用sn -vf(-f选项强制 sn检查签名,即使它在注册表中已被禁用)。将密钥文件作为运行参数是可选的,但是建议在我们不使用Authenticode时使用此参数。用法示例:
sn -vf TestLib.dll TestLib.snk
对于Authenticode验证,我们可以使用signtool或sigcheck。 示例调用可能如下所示:
signtool verify TestLib.dll (add /pa if you are using a self-signed root certificate) sigcheck -i TestLib.dll (-i will show the certificate chain used to sign the assembly)
Sigcheck还可以递归地扫描目录并验证所有找到的二进制文件。我建议你看看它的帮助文档,可以找到更多有趣的运行选项(例如把二进制的哈希值提交到VirusTotal)。
简单的总结
由于很容易跳过签名验证,你可能会怀疑二进制文件签名的整个概念。考虑一下,加载时的验证不是那么重要。如果一个恶意的人获得了对二进制文件的写权限,即使对该文件进行签名也不能保护你不受TA的活动的影响。但是…签名程序集是用于证明程序集中的代码是合法的的唯一的方法,并且没有被篡改过(我鼓励你使用两个方法:强名称签名和Authenticode)。我们应该在客户端接收到二进制文件(这通常由安装程序完成)后执行第一次验证 ——以确保在传输过程中没有被篡改。接下来,在二进制文件所在的文件夹中设置有效的访问权限是非常重要的 —— 只有授权的人才能修改文件。最后,每当我们的应用程序出问题时,我们应该要求客户端在填写错误报告之前进行验证签名 ——只有这样我们才能确定该错误的确是由我们造成的。