上一篇提到使用混淆器(Dotfuscator)來保護軟體時,如果應用程式有使用 .NET 序列化機制,會出現什麼問題,以及如何解決。這次要討論的,則是使用混淆器時可能碰到的另一個問題:要混淆的組件是強式名稱組件。

上次曾提到,使用混淆器的目的,主要是避免自己的智慧結晶(原始碼)遭人窺視、竊取,這是屬於原始碼層級的保護機制。不過,單靠這種方法,並不能防 止軟體被別人任意複製、使用,以及破解。舉例來說,假設你已經使用混淆器將編譯過的組件加工處理,現在你想要在程式中加個試用版的使用期限,當使用者安裝 你的應用程式之後,超過 30 天就必須註冊,否則應用程式會無法提供原有的功能。你也許會在主視窗的 Load 事件中檢查是否已超過試用期限,若超過期限,就顯示要求註冊的訊息,並將應用程式強制關閉。

雖然你的應用程式已經使用混淆器加工過,但只是無法直接從類別、方法、變數名稱直接看出其用途罷了,組件還是可以反組譯、單步執行,而且只要找到程式中檢查試用期限的關鍵點,就能利用工具直接修改組件的 IL code(例如: Dotnet IL Editor), 輕易破解試用期限的保護。因此,我們希望能夠避免組件的 IL code 被竄改,而 .NET 的強式名稱組件(strong-named assembly)便可以提供一道簡易的防護機制。不過,首先要聲明的是,強式名稱組件只是稍微增加破解的難度而已(就跟機車鎖上大鎖一樣),並不能完全 保證組件不被竄改。那麼,為什麼強式名稱組件能夠減少組件被竄改的機會?簡單地說,因為強式名稱組件會內嵌一個簽章,這個簽章其實就像 check sum 的作用;當應用程式執行起來時,也就是 .NET CLR 載入組件時,CLR 會重新將組件的二進位檔案內容重新運算產生一組 check sum,並且跟原先嵌入於組件中的 check sum 比對,如果兩個值不同,就表示組件內容被竄改過了,CLR 就會顯示錯誤訊息,並中止組件的載入程序。至於如何確保這組 check sum 不被破解,這就用到了現代密碼學的非對稱式加解密技術,若有興趣進一步研究,可參考蔡學鏞的文章:〈密碼學在 .NET 組件上的應用〉。當然,前面說過,這種方法只是增加破解的難度而已,如果你知道方法,其實很容易就能把強式名稱組件裡面的簽章拿掉。

雖然這個方法不怎麼好,我還是決定將應用程式的所有組件都變成強式名稱組件,以提供一點基本的防護(當然還有其他好處,例如:識別組件發行者)。 OK,於是我用 VS2005 開啟我的應用程式專案,進入專案的屬性設定視窗,切到 Signing 頁籤,產生一個金鑰檔並以該金鑰簽署這個組件。接著再用 Dotfuscator 將編譯過的組件加工處理。簡單地說,應用程式建置的步驟就是:

  1. 將組件編譯成強式名稱組件。
  2. 使用混淆器將組件加工。

可是問題來了,既然第一個步驟已經產生強式名稱組件,那麼第二個步驟不就是在竄改組件的 IL code 嗎?沒錯!嘗試執行依上述步驟所建立的應用程式時,.NET CLR 會發現組件在編譯後遭到竄改,因此拒絕載入應用程式。

解決的方法是使用延遲簽署,也就是在第一個步驟時,告訴 .NET 編譯器暫且不要將簽章嵌入組件,只為該簽章預留空間就好。等到混淆器將組件加工完畢,再補上組件簽章。因此,前面的建置步驟要稍微修改一下:

  1. 將組件編譯成強式名稱組件,但使用延遲簽章。
  2. 使用混淆器將組件加工。
  3. 簽署強式名稱組件(使用 .NET Framework 提供的 SN.exe 工具)。

如此一來,組件就同時有了兩層保護機制:原始碼層級的保護,以及陽春的二進位層級(組件)保護。.

關於延遲簽章的實務細節,包括如何設定延遲簽章,以及事後補簽署,可參考舊文:〈NET 2.0 筆記 - Delay-signed Assembly〉。 這裡要提醒的一點是:強式名稱組件只能呼叫強式名稱組件。因此如果你的應用程式(.exe)是強式名稱組件,那麼它呼叫的所有組件也都必須是強式名稱組件 (為何要有這條規則,簡單地說,是為了確保 .NET 組件安全性的完整,試想:一般的非強式名稱組件是有可能遭到竄改的,如果強式名稱組件可以呼叫一般的組件,那麼整個程式執行的過程就不能說是絕對安全 了)。