深入理解.NET程序的原理 談一談破解.NET軟件的工具和方法

最近一段時間不忙,閒下來的空閒時間,重讀了一下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中,輸入的錢數,是如何提交到銀行,被銀行扣走的。但是大部分程序或是網站,沒有做到這麼高的安全級別,可以考慮嘗試。

 

 

轉載自:http://www.cnblogs.com/JamesLi2015/p/3160189.html

發佈了16 篇原創文章 · 獲贊 22 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章