【CLR】详解CLR中的程序集
目录结构:
1. 程序集的简介
程序集是一个.dll或者一个.exe文件,里面包含了很多类的定义和资源。比如我们使用的System.dll,System.Core.dll...文件。
CLR支持两种程序集:弱命名程序集和强命名程序集。弱命名和强命名程序集的结构完全相同。两者的区别在于:强命名程序集使用发布者的公钥/私钥进行了签名。这对秘钥允许对程序集进行唯一性的标识、保护和版本控制,并允许程序部署到用户机器的任何地方。
程序集可采用两种方式部署:私有或全局。私有部署的程序集是指部署到应用程序基目录或者某个子目录下面。全局部署是指部署到一些公认位置的程序集。弱命名程序集只能私有部署,强命名程序集既可以私有部署也可以全局部署。
如下:
全局部署 | 私有部署 | |
强命名程序集 | 是 | 是 |
弱命名程序集 | 否 | 是 |
2. 为程序集分配强名称
要由多个应用程序访问的程序集必须放到公认的目录。另外,检测到对程序集的引用时,CLR必须能自动检查该目录。
CLR支持对程序集进行唯一性标识的机制,这就是“强命名程序集”。强命名程序集共有4个重要特征,它们共同对程序集进行唯一性的标识:文件名(不计扩展名)、版本号、语言文化和公钥。由于公钥数字很大,所以经常使用从公钥派生的小哈希值,称为公钥标记(public key token)。例如下面4个程序集标识字符串显示了4个完全不同的程序集文件:
"MyTypes,Version=1.0.8123.0,Culture=neutral,PublicKeyToken=b77a5c561934e089"
"MyTypes,Version=1.0.8123.0,Culture="en-Us",PublicKeyToken=b77a5c561934e089"
"MyTypes,Version=2.0.8123.0,Culture=neutral,PublicKeyToken=b77a5c561934e089"
"MyTypes,Version=1.0.8123.0,Culture=neutral,PublicKeyToken=a0371q001382p23a"
2.1 如何指定程序集的版本资源信息
在PE(Portable Executable 可执行文件)文件中能嵌入标准的Win32版本资源。在生成程序集时,应该使用特性来设置各种版本资源字段,每种语言生成程序集版本资源的特性代码的都不一样,下面介绍使用VB语言生成PE文件时如何指定版本资源信息。
首先在Microsoft Visual Studio上新建一个VB的类库项目。在项目的本地文件夹下,可以找到AssemblyInfo.vb的文件
然后打开这个文件,可以看到其中的内容如下(笔者经过了修改,并且只贴出了部分代码):
<Assembly: AssemblyTitle("名称")> <Assembly: AssemblyDescription("描述")> <Assembly: AssemblyCompany("公司")> <Assembly: AssemblyProduct("产品")> <Assembly: AssemblyCopyright("Copyright © Microsoft 2018")> <Assembly: AssemblyTrademark("商标")> <Assembly: ComVisible(False)> <Assembly: AssemblyVersion("1.0.0.0")> <Assembly: AssemblyFileVersion("1.0.0.0")>
然后在Visual Studio中重新生成文件,找到生成的PE文件,右键“属性”->"详细信息",就可以看到如下图所示:
通过上面的方式指定程序集有点繁琐,好在Visual Studio为我们提供了便利,只需要在Visual Studio中右击项目,选择“属性”->"应用程序"->"程序集信息",就可以进行修改了。
2.2 如何对程序集签名
在上面我们已经知道了,弱命名程序集经过签名(使用公钥和私钥签名)就会成为强命名程序集,下面介绍如何签名程序集。
首先使用SN.exe来获得秘钥,SN.exe程序一般在C:\Program Files (x86)\Microsoft SDKs\Windows的子目录下面,下面使用SN.exe来创建一个包含公钥、私钥的文件。
sn -k MyCompany.snk
创建的MyCompany.snk是包含了一对公钥和秘钥的文件。公钥的数字很大,如果愿意的话,可以再次使用sn.exe来查看完整的公钥值和公钥标记值。只需要执行两次sn.exe。
第一次使用-p创建只含公钥的文件
sn -p MyCompany.snk MyCompany.PublicKey sha256
第二次使用-tp显示公钥标记和公钥本身。
sn -tp MyCompany.PublicKey
然后会得到类似如下的输出:
Microsoft(R) .NET Framework 强名称实用工具 版本 4.0.30319.17929
版权所有(C) Microsoft Corporation。保留所有权利。
公钥(哈希算法: sha256):
002400000c800000940000000602000000240000525341310004000001000100f70211026bf0c5
04ec93bd52e3c7c14373e18f65d385e7151fc2de3559b50668cc8f4d5eae739745ead0d0e16036
d2aa033b8ec9366e92cf2d90a5a0d02ae00ee7a915df4e1eeb01d74a473063b741c0473c345254
211060134f626c30e3bb1057e43fd56ee04810713ba05101a32d591278d6a0497d14db70e488a3
1a731cbe
公钥标记为 47ea4c468f699f0b
在创建好公钥/私钥标记对后,就可以利用具体语言的具体命令来创建强命名程序集了。
3. 全局程序集缓存(GAC)
我们已经知道了如何创建强命名程序集了,强命名程序集可以私有部署,也可以全局部署(部署到GAC中)。
这里先介绍一下全局程序集缓存(Global Assembly Cache,GAC)的概念:由多个应用程序访问的程序集必须放到公认的目录,而且CLR在检测到该程序集的引用时,必须知道检查该目录,这个公认的位置就是GAC。
GAC一般在如下的位置:
%SystemRoot%\Microsoft.NET\Assembly
GAC的目录是结构化的:其中包含许多子目录,子目录名称用算法生成。永远不要将程序集手动复制到GAC目录,相反应该用工具来完成。
开发和测试时在GAC中安装命名程序集最常用的工具是GACUtil.exe,该工具一般在 C:\Program Files (x86)\Microsoft SDKs\Windows 的子目录下面。
4. 如何查看程序集的信息
我们已经知道了一个程序集由四部分标识,分别为:文件名(不计扩展名)、版本号、语言文化和公钥标记(公钥很大,一般不使用)。弱命名程序集没有公钥(自然也没有公钥标记),若命名程序集经过公钥/私钥对签名后,就称为了强命名程序集。
当我们知道一个程序集后,如果获得这些标识呢?在程序集中,没有直接给我们相关的信息,这些信息都是分散的,需要我们去分别查找。
使用ILDASM反编译一个程序集,点击“视图”->“元信息”->“显示!”,然后查找“Assembly”的定义部分(Assembly一般位于文档最底部),如图:
其中Name代表文件名;Version代表版本号;Locale代表语言文化,如果空,则表明是中性语言(neutral);Public Key则表示是公钥。
现在我们知道了程序集全部信息,图片中的public Key是完整的公钥而不是公钥标记。可以按照下面的过程得到公钥标记,
利用sn.exe程序的 -T参数
比如:
sn -T System.Data.dll
然后就可以看到如下的输出:
Microsoft(R) .NET Framework 强名称实用工具 版本 4.0.30319.17929
版权所有(C) Microsoft Corporation。保留所有权利。
公钥标记为 b77a5c561934e089
除此之外,若强命名程序集安装在GAC中的话,直接通过文件夹名称的后缀就可以看出:
5. 强命名程序集防篡改
用私钥对程序集进行签名,并将公钥和签名嵌入程序集,CLR就可验证程序集未被修改或损坏。程序集安装到GAC时候,系统对包含清单的那个文件内容进行哈希处理,将哈希值与PE文件中嵌入的RSA数字签名进行比较(在用公钥解除了签名之后)。如果两个值完全一致,表明文件内容未被篡改。此外,系统还对程序集的其他文件的内容进行哈希处理,并将哈希值与清单文件的FileDef表中存储的哈希值进行比较。任何一个哈希值不匹配,表明至少有一个文件被篡改,程序集将无法安装到GAC。
应用程序需要绑定到程序集时,CLR根据被引用程序集的属性(名称、版本、语言文化和公钥)在GAC中定位查找该程序集。如果被引用的程序集不在GAC中,CLR会查找引用程序的基目录,然后查找应用程序配置文件中的任何私有路径,如果还找不到就会抛出System.IO.FileNotFoundException成一行。
如果强命名程序集文件从GAC之外的位置加载(通过应用程序的基目录,或者通过配置文件中的codeBase元素),CLR会在程序集加载后比较哈希值。也就是说,每次应用程序执行并加载程序集时,都会对文件进行哈希处理,以牺牲性能为代价,保证程序集文件中的内容没有被篡改。
下面这张图可以帮助理解该流程:
通过这张图片可以清楚的看出,MyLibrary.dll文件进行强命名签名时,会把MyLibrary.dll文件进行哈希值处理,并且将结果值用秘钥进行处理,再将结果值嵌入到CLR头部中。把公钥嵌入到元数据中。
当MyLibrary.dll被加载时,程序会用清单中的公钥对CLR头中的数字签名进行解密操作,和再次对MyLibrary.dll进行哈希处理,然后比较这两个结果值,如果这两个值不一致的话,说明有文件已经被篡改,将会阻止运行。