摘要:混淆器與序列化
費盡心力、絞盡腦汁才寫出來的程式,相信每個人都不希望當中的智慧結晶被別人任意取用,可偏偏 .NET 程式是編譯成 IL code,只要 Reflector 利劍在手,便可輕易將程式碼層層剖開,任憑享用。正因為如此,我們需要混淆器(obfuscator)來保護自己的心血結晶。這篇文章並不是要講如何使用混淆器,而是我在使用混淆器時碰到的一個問題。
舉例來說,假設你寫了一套特殊的文書編輯軟體,你將一份文件封裝程 Document 物件,而 Document 裡面又包含許多 Line 物件,每個 Line 物件又包含數量不等的 Word 物件,Word 裡面又包含其他小物件。這種情況就好像一個大箱子裡面層層包著許多小箱子一樣。
當你要把一份文件(Document 物件)儲存到磁碟,你會怎麼做?(當然這裡舉的例子並不是純文字文件,否則用 StreamWriter 就好了。)一種作法是寫程式將整個 Document 物件的內容輸出至檔案,你可能會先把一些文件資訊輸出成檔頭,然後用好幾個巢狀迴圈分別輸出 Line 物件、每個 Line 裡面的 Word、以及每個 Word 裡面的資料。光想就覺得累了,更何況在載入檔案時,還得反過來,將讀取的資料重建為層層包裹的物件。
如果你知道序列化(serialization),你可能會喜歡用序列化的方法。實作起來非常簡單,首先,將那些需要儲存至檔案的 Document、Line、Word 類別都標上 Serializable Attribute。類似這樣:
1 using System.Runtime.Serialization;
2 ......
3 [Serializable]
4 public class Document
5 {
6 private List<Line> m_Lines;
7
8 [NonSerialized]
9 private string m_FileName; // 不需要儲存至檔案的,就標上 [NonSerialized]
10 }
11
12 [Serializable]
13 public class Line
14 {
15 private List<Word> m_Words;
16 }
加上可序列化 attributes 之後,剩下的儲存動作就簡單多了:
1 using (FileStream fs = new FileStream(filename, FileMode.Create))
2 {
3 BinaryWriter bw = new BinaryWriter(fs);
4 IFormatter fmter = new BinaryFormatter();
5 fmter.Serialize(fs, doc); // 這裡的 doc 就是Document 物件
6
7 bw.Flush();
8 bw.Close();
9 }
你可以看到,簡單幾行就完成了。而且,注意第 5 行,我們只要傳入 Document 物件而已。這是因為序列化的行為是會「傳染」的,Document 物件裡面包著需要序列化的 Line 物件,Line 又包含需要序列化的 Word 物件,因此一整串的巢狀物件都會輸出至檔案。不管多複雜的巢狀物件,程式的寫法都一樣這麼簡單(如果不使用序列化,而由自己寫程式處理,可就麻煩多了)。 反序列化的動作也一樣簡單。
好,目前為止都很完美。開發完成後,你希望不要被別人輕易看見程式內部的寫法,於是你用 Visual Studio 2005 附的 Dotfuscator Communication Edition 將編譯過的 .NET 組件「翻攪」過一遍。雖然 Dotfuscator 已經拿掉很多專業版的功能,但這些被翻攪過的組件還是被處理得挺徹底,裡面的所有類別、變數、方法名稱全都變成了 a、b、aa、ad 這類毫無意義的名稱。看到 Reflector 反組譯之後結果,你相當滿意:
可是,當你將程式執行起來,並且載入之前儲存的文件檔案時(這裡指的是程式用混淆器翻攪之前所儲存的檔 案),程式就會發生錯誤而異常中止。因為程式用混淆器翻攪之前所儲存的檔案,裡面含有序列化資訊,包括變數值、變數名稱、型別名稱等等。當程式用混淆器翻 攪之後,裡面的類別、變數名稱全都變了,反序列化時找不到原先序列化時的型別、欄位,當然就會發生錯誤。
這是使用混淆器時可能會碰到的問題之一。所幸 Dotfuscator 提供了彈性的設定,可以指定哪些類別、變數、方法名稱維持原狀。只要在 Dotfuscator 視窗的 Rename 頁籤中,把那些需要序列化的類別及其成員變數勾選起來,就能解決上述問題。圖就不剪了,若有興趣,你可以在 VS2005 中點選 Tools > Dotfuscator Community Edition,自己動手試試看,然後比較一下混淆前、混淆後的組件用 Reflector 反組譯的結果有何差異。
在 Dotfuscator 的一份 FAQ 文件中也有提到這個問題,順便附上連結: