最近一段時間不忙,閑下來的空閑時間,重讀了一下CLR的原理,回味一下有關程序集的的知識,順便練了一下手,學習致用,破解了若干個.NET平台的軟件。以此來反觀.NET程序開發中,需要注意的一些問題。
基本原理
.NET平台的編譯格式是依靠MSIL中間語言,運行時即時編譯(JIT)成CPU指令,對Win 32 的PE格式進行了擴展。程序集是自描述的,本身蘊藏了豐富的元數據信息。MSDN中有一段代碼例子,請參考下面的程序
using System; using System.Reflection; public class Example { public static void Main() { // Get method body information. MethodInfo mi = typeof(Example).GetMethod("MethodBodyExample"); MethodBody mb = mi.GetMethodBody(); Console.WriteLine("\r\nMethod: {0}", mi); // Display the general information included in the // MethodBody object. Console.WriteLine(" Local variables are initialized: {0}", mb.InitLocals); Console.WriteLine(" Maximum number of items on the operand stack: {0}", mb.MaxStackSize); // Display information about the local variables in the // method body. Console.WriteLine(); foreach (LocalVariableInfo lvi in mb.LocalVariables) { Console.WriteLine("Local variable: {0}", lvi); } // Display exception handling clauses. Console.WriteLine(); foreach (ExceptionHandlingClause ehc in mb.ExceptionHandlingClauses) { Console.WriteLine(ehc.Flags.ToString()); // The FilterOffset property is meaningful only for Filter // clauses. The CatchType property is not meaningful for // Filter or Finally clauses. switch (ehc.Flags) { case ExceptionHandlingClauseOptions.Filter: Console.WriteLine(" Filter Offset: {0}", ehc.FilterOffset); break; case ExceptionHandlingClauseOptions.Finally: break; default: Console.WriteLine(" Type of exception: {0}", ehc.CatchType); break; } Console.WriteLine(" Handler Length: {0}", ehc.HandlerLength); Console.WriteLine(" Handler Offset: {0}", ehc.HandlerOffset); Console.WriteLine(" Try Block Length: {0}", ehc.TryLength); Console.WriteLine(" Try Block Offset: {0}", ehc.TryOffset); } } // The Main method contains code to analyze this method, using // the properties and methods of the MethodBody class. public void MethodBodyExample(object arg) { // Define some local variables. In addition to these variables, // the local variable list includes the variables scoped to // the catch clauses. int var1 = 42; string var2 = "Forty-two"; try { // Depending on the input value, throw an ArgumentException or // an ArgumentNullException to test the Catch clauses. if (arg == null) { throw new ArgumentNullException("The argument cannot be null."); } if (arg.GetType() == typeof(string)) { throw new ArgumentException("The argument cannot be a string."); } } // There is no Filter clause in this code example. See the Visual // Basic code for an example of a Filter clause. // This catch clause handles the ArgumentException class, and // any other class derived from Exception. catch(Exception ex) { Console.WriteLine("Ordinary exception-handling clause caught: {0}", ex.GetType()); } finally { var1 = 3033; var2 = "Another string."; } } } // This code example produces output similar to the following: // //Method: Void MethodBodyExample(System.Object) // Local variables are initialized: True // Maximum number of items on the operand stack: 2 // //Local variable: System.Int32 (0) //Local variable: System.String (1) //Local variable: System.Exception (2) //Local variable: System.Boolean (3) // //Clause // Type of exception: System.Exception // Handler Length: 21 // Handler Offset: 70 // Try Block Length: 61 // Try Block Offset: 9 //Finally // Handler Length: 14 // Handler Offset: 94 // Try Block Length: 85 // Try Block Offset: 9
重頭戲在這一行: MethodBody mb = mi.GetMethodBody(); 它返回方法的元數據的字節流。說通俗一點,它返回的是方法的源代碼。把這個返回的字節流轉換成MSIL指令,不是一件難事。MSDN中有描述,CodeProject上面有一篇文章,講解如何寫一個解釋方法,把MethodBody傳化為方法的MSIL代碼,幾乎就是一個反編譯器的模型。
再去理解那句話:.NET程序集是自描述的,是不是理解更深刻了一些。
基本方法
熟悉MSIL語言。對照文檔手冊,邊讀邊看邊學。我的辦法是,想知道高級語言編譯時如何翻譯成MSIL的,找一本高級語言(C#,VB.NET)的入門手冊書,把代碼敲進去,編譯成程序集,再用反編譯器.NET Reflector一行一行對比看,進步神速。之所以要找入門書,是因為它會講解到高級語言的各個特性,反編譯時看到的MSIL代碼會更全面。
兩個軟件破解的實戰經驗
軟件A
軟件A采用的保護措施是根據用戶購買的許可數量,分成個人版,專業版,企業版。版本越高,能擁有的功能更高級。
比如個人版只能一天只能下載50個文檔,專業版一天可以下載1000份文檔,企業版則不受限制。
方法:跟蹤軟件啟動時,加載的程序集和執行的動作。.NET程序一般有兩個地方放程序文件,一是GAC,另一個是當前目錄,或是當前目錄的子目錄(需要在配置文件中指定)。找到GAC中的文件,先把它拷貝到普通文件夾。
再拿.NET Reflector打開看看,如果能打開,看是否有strong name,如打不開,則用IL DASM反編它,看生成的IL文件中,是否有strong name的值。再開一下軟件,看看哪些地方會顯示注冊/未注冊,試用期等信息。一般有幾個地方會暴露軟件的保護方法:
1 直接在主窗體的標題欄中顯示,“軟件已注冊”,”軟件未注冊,還有29天試用“
2 在關於對話框中顯示軟件是否注冊,剩余許可天數或次數。
再從IL代碼中追查,看它在哪些地方,保存當前的使用天數的數據。以我的追查經驗,多半是保存在注冊表中。於是開一個注冊表寫入監控程序,一下就知道它寫到什么去了,再來解碼就容易很多。
軟件B
我的軟件注冊方式也是這樣做的,所以我對這種方式非常熟悉。是運用Xml 簽名文件,生成一個只讀的許可文件,看起來是文本文件,但是你不能修改,一有任何的修改,重新計算Hash值會,會驗證失敗。這種方式,我的QQ群中的朋友都知道,用下面的代碼,重新生成一套密鑰匙對,替換程序集中的公匙,再用私匙生成一個注冊文件讓它驗證。
[TestMethod] public void SolutionValidationTest() { string publickey = RSACryptionHelper.GeneratePublickKey(false); string privateKey = RSACryptionHelper.GeneratePublickKey(true); }
基本思路與對策
1 替換策略 當程序集中有寫死一些基本信息,比如strong name的public token,xml signature的public key,這時,只有替換程序集中的這些元數據,才能破解成功。因為這些信息是全球唯一的,就像GUID字符串的值一樣,全世界再沒有任何一台電腦能生成和他一樣的數據(public token,public/private key),應用替換方法。
2 重簽名策略。strong name一般都會配合代碼進行檢查,讓它不會輕易被破解。遇到這種情況,可以考慮移除現在的簽名,用本機重新生成的key給它簽名。如果能保證程序集和它引用的程序都是一樣的簽名,則匹配成功。到目前為止,還沒有看到一套程序,會有幾個不同的strong name同時存在於不同的程序集中。
3 rouding-trip策略。根據程序,生成MSIL,修改MSIL,再生成程序集。這里涉及到修改MSIL代碼指令,可以讓程序直接繞過驗證,或是不驗證,這種方式威懾力最大。根本不用考慮驗證這一回事,直接跳過,把驗證方法方法體全部刪除,第一句IL代碼為nop,或是ret,直接返回。
4 代碼與反編譯結合策略。有時候面對程序集中五花八門的字符,完全不理解它的含義,無從下手。
遇到這種情況,它是應用了字符串加密技術。以其人之道,還其人之身,下面的幾行簡單的方法,破解文中的不可理解的字符:
Assembly assembly=Assembly.GetExecutingAssembly(); Type type=assembly.GetType("Class64"); MethodInfo mi=type.GetMethod("smethod_0"); mi.Invoke(null,new object [] { " ᓏᓒᓕᓎᒹᓊᓝᓑ "} );
再把這個方法做成一個GUI程序,依此對照文中亂碼字符,全部解碼。
5 直接編輯策略。程序集文件也是一個文件,編譯器以生成目標格式的方式生成這個文件,而我們用到的文本,則是手工敲入字符生成,你可以用十六進制文件去編輯它的值,依照規律即可。.NET 反編譯時,經常遇到的一個錯誤是的
This assembly does not contain a CLI header,This assembly is not a PE.NET format。借助於PE格式知識,把添加進去的錯誤的元數據刪除即可。這里有一個典型的例子,Visual Studio本身是不可以生成多Module的程序集,一個程序集只能有一個Module,但是加密程序通常會給它加上多余的Module,對照PE.NET格式標准,刪除多余的節即可。
6 利用Mono.Ceil.dll主動修改程序集策略。第四個策略中我提到字符串有加密,反其道行之,我把所有調用該方法的地方,再運算一次,重新生成一次,即可破解字符串,再把重新生成的代碼寫成一個程序集文件。
7 應用密碼學算法策略。如果應用對稱加密,試着給一些隨機的錯誤的序列號給它,看看它是如何驗證序列號的,再將此方向反向,導出如何生成它可以驗證的字符中。舉例說明,有一個軟件,它的序列號是這樣的
1234567890 =》 12 34 56 78 90 => 18 52 86 120 144
序列號1234567890,它把這個序列號以兩個為一組,前后相鄰的2個,轉化為10進制數,再運算一次DES對稱算法解密,看是是符合要求的密碼。破解它的方法,就是學會如何生成一個字符串,讓它通過驗證,也就是理解這個流程。
也有應用MD5加密算法的軟件。這樣,整個軟件只有一個序列號可用。把給你的序列號,驗證時生成MD5哈希值,與它保存在當前程序中的密碼匹配,驗證錯誤則失敗,否則通過。這種方式的好處是,你完全沒有辦法應用密碼學知識去破解它,要么用前面提到的,把驗證方法改成nop直接返回,別無它法。
8 跟蹤策略。.NET時代是開創綠色軟件時代,一個.NET Runtime,所有程序共用,再調用這個公共類庫。但是,我發現幾乎所有的加密方法中,都會涉及把注冊信息或是機密信息,寫到注冊表中去。寫到注冊表中去的鍵或值,肯定不會是明文,至少也要用個ToBase64把它變成一堆亂碼。鍵值不可讀,一般要還原到驗證,你要知道自己在哪個地方寫入了UserName,哪個地方寫入LicenseKey,一般會用可逆的算法,就像第六條中所說的。也有軟件應用可不可逆的算法,比如直接用MD5加密,這時,生成鍵值UserName或LicenseKey的變量,肯定是不變的,否則,無法再次生成鍵值,去對比驗證。找一個合適的注冊表跟蹤軟件,如Reg Monitor為你的破解之路添加一線希望。
與此相對應的,Process Explorer, Dependency Walker也都應當應用到實際中,以發現珠絲馬跡。
要做Web方面的破解,Findder,Http Watch可以很好的幫忙你分析服務器與IE客戶端之間,有哪些數據交互來往。
有的追蹤是死路(dead end),比如你想知道在網上買東西,網銀付款時,在自己的打開的IE中,輸入的錢數,是如何提交到銀行,被銀行扣走的。但是大部分程序或是網站,沒有做到這么高的安全級別,可以考慮嘗試。