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