在ASP.NET中,MachineKey被廣泛應用於ViewState加密、Forms Authentication及Membership Cookie加密、Out-of-Process Session資料加密、Membership密碼雜湊(或加密)... 等運算。由於預設為自動產生,大部分的人(甚至開發者)不曾感受過它的存在,但它卻一直默默扮演捍衛ASP.NET安全的重要角色。比較有機會察覺到MachineKey的場合,多半出現在多台伺服器組成Web Farm的架構中,此時需手動將各伺服器的MachineKey設成一致,以避免ViewState MAC驗證失敗問題。(延伸閱讀: 保哥的文章)

以上的概念我在剛學ASP.NET時就確立了,最近在處理主機ViewState MAC驗證相關問題時,倒產生了一些新的疑問:

  1. 當MachineKey設為AutoGenerate時,金鑰會如何產生?
  2. MachineKey在什麼情況下會重新產生? (IIS Reset時會嗎?)
  3. 指定IsolateApps時,會如何影響所產生的MachineKey?

秉持追根究底的精神,研讀過相關文件,甚至追蹤了部分ASP.NET核心程式,理出一點頭緒:

  1. 當指定AutoGenerate時,MachineKey的金鑰資料來自Registry(IIS5則是存在Local Security Authority, LSA): HKEY_LOCAL_MACHINE\Software\Microsoft\ASP.NET 及HKEY_CURRENT_USER\Software\Microsoft\ASP.NET中 ,故會依ASP.NET AppPool執行身分而不同。當ASP.NET程式建立時,會先尋找HKEY_CURRENT_USER(HKCU),找不到時再找HKEY_LOCAL_MACHINE(HKLM),若HKLM也沒有,就會在HKLM下建立新的金鑰資料,若以上動作都失敗,則當場產生新的金鑰組。(參考文件: Intermittent Invalid Viewstate Error in ASP.NET Web pages)
  2. HttpRuntime的初始化過程會呼叫private static SetAutogenKeys(),透過webengine.dll(Unmanaged程式庫)進行上述程序取得金鑰,並將其存入private byte[] s_autogenKeys欄位。
    以下是用ProcMon觀察到的證據(點選後可檢視原尺寸圖檔):
     
  3. MachineKeySection透過private void RuntimeDataInitialize(),依machine.config或web.config的machineKey的validationKey及decryptionKey Attribute決定使用自動產生金鑰或是指定金鑰。當為AutoGenerate時,會由HttpRuntime.s_autogenKeys取得金鑰值;而指定IsolateApps時,金鑰的前4個byte會由ASP.NET Web Application路徑字串的Hash值決定,亦即當多個Web Application共用AppPool時,指定IsolateApps會讓各Web Application的金鑰前四碼不同。
  4. MachineKeySection有個internal byte[] DecryptionKeyInternal,有些文章提及可透過它取得自動產生的解密金鑰,發現在呼叫MachineKeySection某些功能(尤其需動用金鑰的功能)後,MachineKeySection會呼叫internal void DestroyKeys()清除ValidationKey及DecryptionKey的內容(猜想是為了降低被竊取盜用的風險)。因此即便透過Reflection可讀出DecryptionKeyInternal私有屬性,在呼叫FormsAuthentication.Encrypt()或Postback(可能取了金鑰以加密ViewState)後,DecryptionKeyInternal的byte[]值就會全部變成0。(針對此一狀況,我找到改讀取private SymmetricAlgorithm s_oSymAlgoDecryption,再由其Key值取得解密金鑰的變通做法。)

既然有了理論,就該用程式碼實測驗證一番,心裡才會踏實。

以下是我設計的測試程式:

<%@ Page Language="C#" EnableViewStateMac="true" %>
<%@ Import Namespace="System.Text" %>
<%@ Import Namespace="System.Web.Configuration" %>
<%@ Import Namespace="System.Reflection" %>
<%@ Import Namespace="System.Security.Principal" %>
<%@ Import Namespace="System.Security.Cryptography" %>
<%@ Import Namespace="Microsoft.Win32" %>
 
<!DOCTYPE html>
 
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("<dl>");
        sb.AppendFormat("<dt>Request Url</dt><dd>{0}</dd>",
            Request.Url);
        sb.AppendFormat("<dt>DomainAppVirtualPath</dt><dd>{0}</dd>",
            HttpRuntime.AppDomainAppVirtualPath);
        //Display ASP.NET process identity information
        WindowsIdentity wid = WindowsIdentity.GetCurrent();
        sb.AppendFormat("<dt>AppPool Identity</dt><dd>{0}({1})</dd>", 
            wid.Name, wid.User.Value);
        //Get current ASP.NET version
        Version version = System.Environment.Version;
        string aspNetVer = string.Format("{0}.{1}.{2}.0",
            version.Major, version.Minor, version.Build);
        //Check HKCU
        string regPath = string.Format(@"Software\Microsoft\ASP.NET\{0}", 
                                        aspNetVer);
        RegistryKey key = Registry.CurrentUser.OpenSubKey(regPath);
        sb.AppendFormat("<dt>Registry</dt><dd>[HKEY_CURRENT_USER\\{0}]</dd>", 
                         regPath);
        //Check HKLM
        regPath = string.Format(@"Software\Microsoft\ASP.NET\{0}\AutoGenKeys\{1}",
            aspNetVer, wid.User.Value);
        key = Registry.LocalMachine.OpenSubKey(regPath);
        sb.AppendFormat("<dt>Registry</dt><dd>[HKEY_LOCAL_MACHINE\\{0}]</dd>",
            regPath);        
        //Get machineKey settings
        MachineKeySection mks = (MachineKeySection)
            ConfigurationManager.GetSection("system.web/machineKey");
        sb.AppendFormat("<dt>DecryptionKey Setting</dt><dd>{0}</dd>",
            mks.DecryptionKey);        
        Type mksType = typeof(MachineKeySection);
        //After using machine key to encrypt, DecryptionKeyInternal will be cleared
        //Uncomment below line, you will always get 00-00-00-00
        //FormsAuthentication.Encrypt(new FormsAuthenticationTicket("Jeffre", false, 60));
        sb.AppendFormat("<dt>DecryptionKeyInternal</dt><dd>{0}</dd>",
            BitConverter.ToString(
            (byte[]) mksType.GetProperty("DecryptionKeyInternal", 
                BindingFlags.Instance | BindingFlags.NonPublic).GetValue(mks, null)));
        SymmetricAlgorithm sa = 
            (SymmetricAlgorithm)mksType.GetField("s_oSymAlgoDecryption", 
            BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
        string result = "null";
        if (sa != null) result = BitConverter.ToString(sa.Key);
        sb.AppendFormat("<dt>s_oSymAlgoDecryption.Key</dt><dd>{0}</dd>",
            result);
        //HttpRuntime.s_autogenKeys
        byte[] rtAutoGenKeys = (byte[])typeof(HttpRuntime)
            .GetField("s_autogenKeys", BindingFlags.NonPublic | BindingFlags.Static)
            .GetValue(null);
        sb.AppendFormat("<dt>HttpRuntime.s_autogenKeys</dt><dd>{0}</dd>",
            BitConverter.ToString(rtAutoGenKeys));
        sb.Append("</dl>");
        disp.InnerHtml = sb.ToString();
    }
</script>
 
<html>
<head runat="server">
    <title>AutoGen MachineKey Test</title>
</head>
<body>
    <form id="form1" runat="server">
    <div id="disp" runat="server" enableviewstate="false"></div>
    <asp:Button ID="bPostBack" runat="server" Text="Postback"/>
    </form>
</body>
</html>

在以上程式中, 會取得ASP.NET執行身分,計算其對應金鑰的Registry路徑,並透過Reflection技巧取得DecryptionKeyInternal及oSymAlgoDecryption.Key以觀察金鑰。

【實驗1】在Windows 2003 R2執行

 
圖1 在Windows 2003 R2下,執行身分為NT AUTHORITY\NETWORK SERVICE,第一次執行時,DecryptionKeyInternal有值,此時s_oSymAlgoDecryption還是null

 
圖2 Postback後,DecryptionKeyInternal陣列值被清成0,但由s_oSymAlgoDecryption.Key可取得一模一樣的金鑰內容

【實驗2】在Windows 7 IIS7執行

 
圖3 在Win7下的AppPool身分為IIS APPPOOL\AspNet35(紅框中為其SID,可對應先前ProcMon所觀察到的Registry讀取路徑)

 
圖4 切換成另一個AppPool後,執行身分不同,自動產生的金鑰也不同

【實驗3】啟用IsolateApps

  圖5 啟用IsolateApps後,金鑰的前4個byte會被修改(亦可與圖4對照)

【實驗4】驗證IsolateApps時,前4個byte內容與VirtualPath有關

  1. AppPool: x86, VirtualPath: /ASPNET35 –> 1F-35-E5-A1-82-26-D8-25…
  2. AppPool: AspNet35, VirtualPath: /ASPNET35 –> 1F-35-E5-A1-DC-4A-61-ED…
    (與1.相比,前4 byte不變)
  3. AppPool: AspNet35, VirtualPath: /ASPNET-35 –> 7C-7B-C6-9C-DC-4A-61-ED…
    (與2.相比,只有前4 byte不同)

【實驗5】IISRESET後,解密金鑰並未改變

網路上有些說法是一旦AppPool被Recycle後,加密金鑰就會重新產生(參考: The encryption key is generated and used once per app pool cycle.)。但在我的實測中,不管是重啟AppPool或IISRESET,自動產生的解密金鑰內容都未發生改變。

【實驗6】<machineKey>指定自訂decryptionKey

 
圖6 IIS7管理介面就已內建方便的金鑰產生器

 
圖7 指定後,解密金鑰將等於web.config中的設定內容

【重要資安提醒】在實際環境中,ASP.NET金鑰應嚴格保密,不可外洩給任何人,否則會讓惡意人士得以偽造登入身分或擷取隱密資訊,嚴重危害網站安全。


Comments

Be the first to post a comment

Post a comment