注:(本文是從我的某文檔中抽取出來的,所以每小節以9.2.x開頭,讀者勿怪)
當我們從互聯網上下載一個程序集供本地調用的時候,如何保證這個程序集是未經第三方惡意篡改過的呢?如果兩個程序集的名稱、大小、版本號都相同是不是就意味著這兩個程序集文件就相同了呢?
在.NET平台下區分程序集采用的方法和Win32一樣,使用名稱,但是名稱有強弱之分。弱名稱包含版本號、程序集名稱和文化。強名稱在弱名稱的基礎上添加了數字簽名。強名稱的作用主要有三個:一是區分不同的程序集;二是確保代碼沒有被篡改過;三是在.NET中,只有強名稱簽名的程序集才能放到全局程序集緩存中。
使用強名稱來包含程序集我們首先要生成用於非對稱加密的密鑰對,這對密鑰將用於程序集的簽署和驗證。簽署和驗證的流程如圖9-3所示。
圖9-3 簽署(上)與驗證(下)強名稱流程
如圖9-3所示,在進行強名稱簽名的時候我們首先對程序集(不包括DOS頭和PE頭)進行Hash運算,得到文件的散列值,然後使用私鑰對散列值進行加密,得到密文。然後將公鑰、公鑰標識(對公鑰進行SHA-1散列運算後得到的密文的最後8個字節)和密文三個信息保存在程序集中。在加載該程序集時,首先對該程序集進行Hash運算得到一個Hash值(我們稱之為“新Hash值”),然後從程序集中提取公鑰對密文解密得到原始的Hash值,如果兩個Hash值相同,即通過驗證。
對程序集簽名有正常簽名和延遲簽名兩種方式。
延遲簽名是在我們開發人員只具有對公鑰的訪問權限而沒有對私鑰的訪問權限時使用的。這是我們可以先將程序集編譯並預留簽名空間。此時的程序集無法正常運行和調試。
9.2.2.1 在SDK中創建強名稱簽名的程序集
對程序集進行強名稱簽名,我們要首先准備好密鑰。在本書的第6章中,介紹過使用Visual Studio SDK中提供的強名稱工具(Sn.exe)可以生成密鑰對。我們使用如圖9-4的命令生成一個新的密鑰對並保存到本地文件test.snk中。
圖9-4 生成密鑰文件
接下來我們新建一個控制台測試項目StrongName,主要代碼如代碼清單9-3所示。
代碼清單9-3 StrongName項目主要代碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
namespace StrongName
{
class Program
{
static void Main(string[] args)
{
string aa = "";
Assembly ass = Assembly.GetExecutingAssembly();
Console.WriteLine(ass.FullName.ToString());
byte[] pKey = ass.GetName().GetPublicKey();
byte[] pKeyToken = ass.GetName().GetPublicKeyToken();
string pKeyString = GetString(pKey);
string pKeyTokenString = GetString(pKeyToken);
Console.WriteLine("公鑰是:{0}",pKeyString);
Console.WriteLine("公鑰標識是{0}", pKeyTokenString);
Console.Read();
}
private static string GetString(byte[] bytes)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in bytes)
{
sb.Append(string.Format("{0:x}",b));
}
return sb.ToString();
}
}
}
代碼清單9-3的代碼很簡單,使用反射來列舉當前程序集的名稱和使用的公鑰及公鑰標識。在沒有對程序集進行強名稱簽名時我們運行程序得到如圖9-5的結果。
圖9-5 沒有對程序集進行強名稱簽名時代碼清單9-3的運行結果
下面我們在命令行對C#編譯器(csc.exe)執行如圖9-4所示的命令。
圖9-4 對程序集應用強名稱
現在我們再來執行剛才生成的Program.exe,看看結果如何,如圖9-5所示。
圖 9-5 對程序集進行強名稱簽名之後代碼清單9-3的運行結果
從圖9-5我們可已看出,執行強名稱簽名之後我們成功的輸出公鑰和公鑰標識。
為了使編譯器能自動為代碼進行強名稱簽名我們可以為代碼添加特性指明強名稱簽名需要的密鑰文件。添加特性之後的代碼示例如代碼清單9-4所示。
代碼清單9-4 使用特性進行強名稱簽名
using System;
using System.Reflection;
[assembly:AssemblyKeyFileAttribute("TestKey.snk")]
代碼清單9-4使用 AssemblyKeyFileAttribute 指定包含密鑰對的文件的名稱。
當我們需要對程序集進行延遲簽名的時候,我們需要對 AssemblyDelaySignAttribute特性和AssemblyKeyFileAttribute 聯合使用,形式如代碼清單9-5所示。
using System;
using System.Reflection;
[assembly:AssemblyKeyFileAttribute("myKey.snk")]
[assembly:AssemblyDelaySignAttribute(true)]
如代碼清單9-5,當我們需要對程序集延遲簽名的時候,我們要指定包含公鑰的文件並設定AssemblyDelaySignAttribute特性值為true。
9.2.2.2 在VS中創建強名稱簽名的程序集
在SDK中進行強名稱簽名未免麻煩了一些,下面我們以VS2010為例,講解如何在Visual Studio中進行強名稱簽名的操作。
我們打開項目的屬性,切換到簽名頁,如圖9-6所示。
圖9-6 項目的簽名頁
從圖9-6中我可以看出,項目簽名屬性頁中包含了三個大的配置項,第一個是為ClickOnce清單簽名,第二個是為程序集簽名,第三個是延遲簽名。
為了使用ClickOnce部署發布應用程序,應用程序和部署清單必須使用公鑰/私鑰對進行強命名並使用Authenticod 技術進行簽名。可以使用Windows證書存儲區的證書或密鑰文件為清單簽名。也可以創建新的測試證書。
為程序集簽名的選項中,我們可以選擇密鑰文件或者生成新的密鑰文件來對程序集進行簽名。
如果勾選了僅延遲簽名,那麼將對程序集進行延遲簽名。
如圖9-7,在創建了ClickOnce簽名和程序集簽名之後,項目自動添加了兩個密鑰文件StrongName_TemporaryKey.pfx和test.pfx。
圖9-7 創建ClickOnce簽名和程序集簽名
強名稱簽名的程序集如果被篡改,那麼CLR在加載該程序集進行完整性驗證的時候就會失敗。我們現在使用文本編輯工具打開StrongName.exe,在保證不破壞PE文件格式的前提下對其進行簡單的修改,這裡我只把程序中定義的變量aa替換成bb,如圖9-8所示。
圖9-8 修改強名稱簽名的程序集
修改之後,我們再次運行StrongName.exe,看到程序報出的異常為強名稱驗證失敗,入圖9-9所示。
圖9-9 強名稱驗證失敗
注意:
1. 強名稱簽名的程序集不能引用未被簽名的程序集。
2. 從 .NET Framework 3.5 版Service Pack 1開始,當程序集載入完全信任應用程序域(例如 MyComputer 區域的默認應用程序域)中時,將不會驗證強名稱簽名。 這稱為強名稱跳過功能。 在完全信任環境中,對於已簽名的完全信任程序集,無論這些程序集的簽名是什麼,對 StrongNameIdentityPermission 的要求將總是成功。 強名稱跳過功能避免了在此情況下驗證完全信任程序集的強名稱簽名所帶來的不必要開銷,從而可使程序集更快加載。
該跳過功能適用於用強名稱簽名並具有以下特征的任何程序集:
1) 完全受信任而沒有 StrongName 證據(例如,具有 MyComputer 區域證據)。
2) 加載到完全受信任的 AppDomain 中。
3) 從該 AppDomain 的 ApplicationBase 屬性下的位置加載。
4) 未經延遲簽名。
可以對單獨的應用程序或整個計算機禁用此功能。
1) 對所有應用程序禁用強名稱跳過功能
在 32 位計算機上,在系統注冊表中的 HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/.NETFramework 項下創建一個子項。 使用 DWORD 值為 0 的項名稱 AllowStrongNameBypass。
在 64 位計算機上,在系統注冊表中的 HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/.NETFramework 項下創建一個子項。 使用 DWORD 值為 0 的項名稱 AllowStrongNameBypass。 在 HKEY_LOCAL_MACHINE/SOFTWARE/Wow6432Node/Microsoft/.NETFramework 項下創建相同的子項。
2) 對單個應用程序禁用強名稱跳過功能
打開或創建應用程序配置文件。添加下面的項:
<configuration>
<runtime>
< bypassTrustedAppStrongNames enabled="false" />
</runtime>
</configuration>
可通過移除該配置文件設置或將該特性設置為“true”為該應用程序恢復跳過功能。
引用強名稱程序集的過程對我們來說都是透明,我們無需做額外的工作。但是我們可以通過這種方式來檢驗強名稱程序集的作用。
如圖9-10我們首先創建一個類庫項目StrongNameReferenceLib,對其進行強名稱簽名。
圖9-10 引用強名稱程序集
接下來我們修改之前創建的StrongName項目,讓它來引用StrongNameReferenceLib項目,調用其GetHello方法。
StrongNameReferenceLib項目的主要代碼如代碼清單9-6所示。
代碼清單9-6 StrongNameReferenceLib項目主要代碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace StrongNameReferenceLib
{
public class Class1
{
public static string GetHello()
{
return "Hello";
}
}
}
修改後的StrongName項目代碼如代碼清單9-7所示。
代碼清單9-7 StrongName項目代碼
using System.Text;
using System.Reflection;
using StrongNameReferenceLib;
namespace StrongName
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine( Class1.GetHello());
Console.Read();
}
}
}
重新編譯StrongName項目,我們現在得到新的StrongName.exe文件。使用ILDasm打開StrongName.exe文件,查看它的程序集清單,如圖9-11所示。
圖9-11 StrongName.exe程序集清單
在圖9-11中的這份程序集清單,我們可以看到它引用了兩個具有強名稱簽名的程序集,mscorlib和我們新創建的StrongNameReferenceLib,對兩個程序集分別添加了版本和publickeytoken標識。
下面我們去除StrongNameReferenceLib的強名稱簽名,重新編譯該項目,但我們並不重新編譯StrongName項目,而用新生成的StrongNameReferenceLib.dll替換StrongName.exe之前引用的StrongNameReferenceLib.dll,看看會發生事情。結果如圖9-12所示。
圖9-12 StrongName項目替換dll之後結果
我們從圖9-10的異常信息可以看到,StrongName項目找不到匹配的程序集。原因在於在StrongName的程序集清單中存儲著PublicKeyToken值,而沒有強名稱簽名的項目是沒有該屬性的。
9.2.3 強名稱的脆弱性
上面的幾個小節我們共同體驗了強名稱對程序集的保護方式和原理,但是這種保護的強度到底有多大呢?對惡意篡改者來說能得到有效的防御嗎?我們先看下面的例子。
現在我們回到代碼清單9-7,重新對StrongNameReferenceLib項目進行強名稱簽名,然後編譯StrongName項目。現在在StrongName項目的bin目錄裡有StrongNam.exe和StrongNameReferenceLib.dll兩個文件。然後使用ILDasm打開StrongNameReferenceLib.dll文件,轉儲為il文件,這裡我使用記事本打開il文件,如圖9-13所示。
圖9-13 StrongNameReferenceLib.dll的IL源碼
我們在.il文件中找到三處代碼:publickkeytoken、publickey和hash,把對應的內容都刪除,再重新使用ILAsm編譯,這時該程序集的強名稱就被成功的去除。
替換程序集的強名稱方法基本相同。目前網絡上有很多去除和替換強名稱的工具,這裡就不掩飾了。
那麼我們應該通過什麼樣的方式來保護強名稱不被去除或者篡改呢?這個問題我會在專門的文章裡討論。
代碼清單9-5 對程序集延遲簽名的特性聲明