MaxtoCode對.Net程序加密的原理及解密探討

這裏研究的對象是 MaxtoCode 3.1試用版.這裏只探討程序代碼的加密.

 

.Net程序代碼的加密過程如下:

1. 運行 ildasm 將程序集反編譯成 il代碼文件.

2. IL代碼文件進行處理.(*)

3. 運行 ilasm IL代碼文件編譯成程序文件.

4. 直接對程序文件中的il字節碼加密.(**)

 

粗體表示的 2 , 4 是關鍵步驟.

我們先來看看第四步.這一步就是加密的關鍵步驟,這裏就是使用MaxtoCode的加密算法對程序代碼進行加密。

顯然,對於破解來說最直接直觀的方法就是對其第四步的逆向解密。

如果從這個方向去破解解密加密過的程序,那就像MaxtoCode號稱的那樣MAXTOCODE的強度建立在加密算法之上。

理論上方法是可行的,但是工作量是非常大的。

 

那麼我們還有其它的路可行呢?

現在來看看第二步MaxtoCode都做了什麼。

vs2003建一個最簡單的winform程序,然後用MaxtoCode加密試試。我們將第三步之後,第四步之前的exe文件拿來研究。這個時候的exe程序代碼是還沒有被加密的。可以reflector

看看 這個exe和我們直接的exe有什麼區別:

1. 增加了一個類InFaceMaxtoCode .

2. 類都被增加了一個靜態構造函數,在這個函數裏面調用了InFaceMaxtoCode的一個靜態函數Startup

3. 類的原有構造函數裏面也增加了調用InFaceMaxtoCode.Startup的語句。

從這些來看,MaxtoCode的目的是要確保InFaceMaxtoCode.Startup 在程序中能夠最早的運行。

這個行爲和win32程序加殼很像,一般殼都是加密程序代碼,然後修改程序的啓動入口,首先執行殼的代碼,完成程序的解密,然後再執行程序。一般殼有一個特點:加密是對整個程序,啓動時也是整個程序完全解密,然後再執行。(我也見到過一個很特別的殼,程序是部分解密的,軟件註冊算法的那一塊, 是執行一部分解密一部分,然後之前解密的又被垃圾信息填充了。)

對於殼只要我們找對了時間和地點,就能從內存中得到我們需要的東西。

那麼 MaxtoCode加密後的。Net程序呢?

先來看看 MaxtoCode的加密方式。用ildasm反編譯 加密後的程序,會報很多錯誤,這是正常的,從生產的IL文件看,各個類,函數都還在,只是函數體裏面是隻有ildasm的錯誤信息。顯然是加密後的代碼無法反編譯。MaxtoCode對。Net程序的加密不是對程序整體的,而只是對函數體加密,程序類結構不變。有一點我們是很清楚的,加密後的程序要能夠正常運行,在運行時肯定是需要解密的。而解密的關鍵就在InFaceMaxtoCode.Startup 裏面。

現在我們來看看InFaceMaxtoCode.Startup 裏面究竟做了什麼。InFaceMaxtoCode 類的代碼如下:

 

 

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;

public class InFaceMaxtoCode
{
static InFaceMaxtoCode()
{
InFaceMaxtoCode.started
= false;
}

[DllImport(
"MRuntime3.dll", EntryPoint="CheckRuntime", CharSet=CharSet.Unicode, SetLastError=true, ExactSpelling=true)]
private static extern int A______();
[DllImport(
"KERNEL32.DLL", EntryPoint="GetModuleHandleA", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern int B______(string x13d52f7d8e232e61);
private static string ByteToString(byte[] x5fc6100148519126)
{
return Encoding.ASCII.GetString(x5fc6100148519126);
}

[DllImport(
"MRuntime3.dll", EntryPoint="MainDLL", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern bool C______(int x19218ffab70283ef, int xe7ebe10fa44d8d49);
[DllImport(
"KERNEL32.DLL", EntryPoint="SetEnvironmentVariableA", CharSet=CharSet.Ansi, SetLastError=true, ExactSpelling=true)]
private static extern bool D______(string x427bb0e14ed9e9b1, string x84ee6c5b88919f4c);
public static void Startup()
{
if (!InFaceMaxtoCode.started)
{
string text1 = "";
string text2 = "MRuntime3.dll";
if (AppDomain.CurrentDomain.RelativeSearchPath != null)
{
if (AppDomain.CurrentDomain.RelativeSearchPath.IndexOf(@":/") != -1)
{
text1
= AppDomain.CurrentDomain.RelativeSearchPath;
}
else
{
text1
= AppDomain.CurrentDomain.BaseDirectory + AppDomain.CurrentDomain.RelativeSearchPath;
}
}
else
{
text1
= AppDomain.CurrentDomain.BaseDirectory;
}
string text3 = Environment.GetEnvironmentVariable("path");
if (text3.IndexOf(text1) == -1)
{
InFaceMaxtoCode.D______(
"path", text3 + ";" + text1.Replace("/", @"/"));
}
if (text1.Substring(text1.Length - 1, 1) == @"/")
{
text1
= text1;
}
else
{
text1
= text1 + @"/";
}
if (File.Exists(text1 + text2) && !File.Exists(Path.GetTempPath() + text2))
{
File.Copy(text1
+ text2, Path.GetTempPath() + text2);
}
if (text3.IndexOf(Path.GetTempPath()) == -1)
{
InFaceMaxtoCode.D______(
"path", text3 + ";" + Path.GetTempPath().Replace("/", @"/"));
}
int num1 = 5;
num1
= InFaceMaxtoCode.A______();
if (num1 == 0)
{
int num2 = InFaceMaxtoCode.B______(text2);
int num3 = InFaceMaxtoCode.B______(Assembly.GetExecutingAssembly().Location);
InFaceMaxtoCode.started
= InFaceMaxtoCode.C______(num2, num3);
}
else
{
//一堆垃圾代碼,報告啓動錯誤信息的。
}
}
}


private static bool started;
}


Startup精簡後的代碼如下:

public static void Startup()
{
if (!InFaceMaxtoCode.started)
{
//準備運行庫;
int num1 = 5;
num1
= InFaceMaxtoCode.A______();
if (num1 == 0)
{
int num2 = InFaceMaxtoCode.B______(text2);
int num3 = InFaceMaxtoCode.B______(Assembly.GetExecutingAssembly().Location);
InFaceMaxtoCode.started
= InFaceMaxtoCode.C______(num2, num3);
}
else
{
//一堆垃圾代碼,報告啓動錯誤信息的。
}
}

 

從代碼裏面我們看得到InFaceMaxtoCode.Startup 正常啓動後,在整個程序集中只會運行一次。

關鍵函數是 運行庫的MainDLL,這個函數有兩個參數,一個是運行庫的句柄,一個是程序集的句柄。這個句柄實際上就是程序在內存中加載的位置。MaxtoCode加密後的程序都是對齊到0x1000的。

 

自上次寫第一篇文章到現在不知不覺兩個月過去了,這篇文章我們將介紹怎麼獲取解密後的IL字節代碼。
我們先回顧一下前文,在上一回我們提到“InFaceMaxtoCode.Startup 正常啓動後,在整個程序集中只會運行一次。”。
當時這種說法是很武斷的,如果 “InFaceMaxtoCode.C______(num2, num3)” 的返回值總是 false的話,該函數就會被執行多次,
不過根據後來動態調試的結果,我們證實了“InFaceMaxtoCode.C______(num2, num3)” 的返回值爲 true,因此上次的說法是正確的。

現在言歸正傳,怎麼取得解密後的代碼呢?大概兩個方向,
1.正面交鋒,直接攻破maxtocode的運行庫。
這就將問題直接回到了傳統的win32層面,不過這個東西是業內人士寫的在這方面的保護工作做得很好,像我這樣的菜鳥就很難直接攻破了。
我曾有一個設想,就是通過分析運行庫找到解密函數的入口,然後弄一個stub dll,hook這個地方,把解密後的il代碼dump出來。
實際跟蹤幾次後我就放棄了。從跟蹤到的信息來看,我猜測,運行庫是通過 mscorwks.dll 掛接到 jit,在jit的前面實時解密代碼。
理論上我們也可以掛一個到jit前面,在那裏dump解密的il代碼,不過這個實現的方式,還不清楚,如果弄明白了,也就能寫一個同樣原理的加密軟件了。
這個難度比較大,所以我最終放棄了這個方案。

2.避開運行庫,我們直接利用dotNet 2.0的特性獲取IL代碼。
如是我就試着用2.0寫了一個winform程序,加密,運行,發現報錯。
maxtocode3.1不支持2.0的winform程序,這就使我的這個方案實驗夭折了。
兩個月過去了,發現maxtocode升級到3.11了修正了這個bug,今天終於可以繼續實驗了。

我們來建一個簡單的winform程序。一個窗體,然後一個按鈕。
代碼如下:


 1
 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel;
 4 using System.Data;
 5 using System.Drawing;
 6 using System.Text;
 7 using System.Windows.Forms;
 8 using System.Reflection;
 9 using Spaces;
10 namespace Test5
11 {
12     public partial class Form1 : Form
13     {
14         public Form1()
15         {
16             InitializeComponent();
17         }
18    
19         private void TestMethod()
20         {
21             //  [7/17/2006]
22             int i = 0;
23             i = 1;
24             i++;
25             if(i>0)
26             {
27                 MessageBox.Show("OK");
28             }
29         }
30 
31         private void button1_Click(object sender, EventArgs e)
32         {
33             Type tp = this.GetType();
34 
35             MethodInfo mi = tp.GetMethod("TestMethod",
36                 BindingFlags.NonPublic|BindingFlags.DeclaredOnly|
37                 BindingFlags.Public|BindingFlags.Static
38                 |BindingFlags.Instance);
39             if(mi == null)
40             {
41                 MessageBox.Show("err");
42                 return;
43             }
44             MethodBody mb = mi.GetMethodBody();
45             byte[] bt= mb.GetILAsByteArray();
46             StringBuilder sb = new StringBuilder();
47             for (int i = 0; i < bt.Length; i++)
48             {
49                 sb.Append(bt[i].ToString("X2"));
50                 sb.Append(" ");
51             }
52             string stxt = sb.ToString();
53             MessageBox.Show(stxt);
54                        
55         } 
56      
57     }
58 }


編譯運行,我們點擊按鈕就能看到 TestMethod 的IL字節碼。
然後用maxtocode加密在運行,同樣能看到 TestMethod 的IL字節碼。
兩次看到的結果一樣的,這個是當然了,如果不一樣,maxtocode就破壞了程序的正確性了。

好了,我們的實驗成功了。

看到這裏大家應該知道怎麼獲取解密後的IL代碼了吧。

這種方式比在內存裏面找代碼或者hook到maxtocode解密後dump代碼的方式要優越很多,
因爲內存dump的方式你還要擔心運行時的函數覆蓋率,沒有運行到的就dump不到。

這種方式我們利用 DotNet的反射機制,可以枚舉出程序集中的所有類型,以及每個類型的所有方法,成員,字段,構造函數等。


初步實驗了一下,對於加了密的dll文件還是比較好弄的,2.0的、1.1的都能弄出IL代碼來。
對於exe文件還有一關需要解決,那就是如何將我們的DotNet dll程序集插入到exe的運行空間中去。


今回就先到這裏了,下回再實際寫程序演練獲取解密後的IL字節代碼。

 

上一回我們試驗了通過反射的方式獲取method的源代碼。
這次我們就用一個實例來演示dump一個程序集中的所有類型和方法的IL源代碼。

首先打開VS2005 新建一個C#的windows程序:
在窗體添加添加一個2個 button,2個label,一個textbox,一個 checkbox,一個savefiledialog。
界面如下:


事件代碼如下:

  1  public class Form1 : Form
  2       {
  3             // Methods
  4             //選擇IL字節碼保存文件
  5             private void button1_Click(object sender, EventArgs e)
  6 {
  7       if (this.saveFileDialog1.ShowDialog() == DialogResult.OK)
  8       {
  9             this.textBox1.Text = this.saveFileDialog1.FileName;
 10       }
 11 }
 12              //點擊開始dump。
 13             private void button3_Click(object sender, EventArgs e)
 14 {
 15       this.button3.Enabled = false;
 16       this.DumpAssembly(Assembly.GetExecutingAssembly(), this.textBox1.Text);
 17       MessageBox.Show("dump ok");
 18       this.button3.Enabled = true;
 19 }
 20             //這個函數將一個Assembly全部dump到path中。
 21             private void DumpAssembly(Assembly ass, string path)
 22 {
 23       StreamWriter writer1 = new StreamWriter(path, false);
 24       Type[] typeArray1 = ass.GetTypes();
 25       for (int num1 = 0; num1 < typeArray1.Length; num1++)
 26       {
 27             this.DumpType(typeArray1[num1], writer1);
 28       }
 29       writer1.Flush();
 30       writer1.Close();
 31 }
 32 
 33             //dump單個類型,由dumpassembly調用
 34              private void DumpType(Type tp, StreamWriter sw)
 35 {
 36       BindingFlags flags1 = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly;
 37       string text1 = tp.ToString();
 38       sw.Write("TYPE: " + text1 + "/r/n");
 39       if (tp.IsEnum)
 40       {
 41             sw.Write("IsEnum ");
 42       }
 43       if (tp.IsImport)
 44       {
 45             sw.Write("IsImport ");
 46       }
 47       if (tp.IsNested)
 48       {
 49             sw.Write("IsNested ");
 50       }
 51       if (tp.IsClass)
 52       {
 53             sw.Write("IsClass");
 54       }
 55       sw.Write("/r/n");
 56       if ((text1 != "InFaceMaxtoCode"|| !this.checkBox1.Checked)
 57       {
 58             sw.Write("**********Begin MemberInfo**********/r/n");
 59             MemberInfo[] infoArray1 = tp.GetMembers(flags1);
 60             for (int num1 = 0; num1 < infoArray1.Length; num1++)
 61             {
 62                   MemberInfo info1 = infoArray1[num1];
 63                   sw.Write(info1.MemberType.ToString() + "/t" + infoArray1[num1].ToString() + "/r/n");
 64                   if ((info1.MemberType == MemberTypes.Method) || (info1.MemberType == MemberTypes.Constructor))
 65                   {
 66                         this.DumpMethod((MethodBase) info1, sw);
 67                   }
 68             }
 69             sw.Write("**********  End MemberInfo**********/r/n");
 70             sw.Write("/r/n/r/n");
 71       }
 72 }
 73 
 74  
 75 
 76         //dump單個方法,由dumptype調用
 77          private void DumpMethod(MethodBase mb, StreamWriter sw)
 78 {
 79       MethodBody body1 = mb.GetMethodBody();
 80       if (body1 != null)
 81       {
 82             byte[] buffer1 = body1.GetILAsByteArray();
 83             try
 84             {
 85                   sw.Write("/tMaxStackSize: " + body1.MaxStackSize.ToString());
 86                   sw.Write("/tCodeSize: " + buffer1.Length.ToString());
 87                   sw.Write("/r/n");
 88             }
 89             catch (Exception exception1)
 90             {
 91                   MessageBox.Show("1:" + mb.ToString() + "/r/n" + exception1.ToString());
 92             }
 93             foreach (LocalVariableInfo info1 in body1.LocalVariables)
 94             {
 95                   sw.Write("LocalVar: " + info1.ToString());
 96                   sw.Write("/r/n");
 97             }
 98             sw.Write("/r/n/r/n");
 99             StringBuilder builder1 = new StringBuilder();
100             foreach (byte num1 in buffer1)
101             {
102                   builder1.Append(num1.ToString("X2"));
103             }
104             sw.Write(builder1.ToString());
105             sw.Write("/r/n/r/n");
106             foreach (ExceptionHandlingClause clause1 in body1.ExceptionHandlingClauses)
107             {
108                   sw.Write(clause1.ToString());
109                   sw.Write("/r/n");
110             }
111             sw.Write("/r/n");
112       }
113 }
114 
115  
116 
117             
118      
119       }
120 
121 

 

編譯這個程序,運行,dump出il字節碼,
然後拿 maxtocode加密。再運行,dump出il字節碼,然後找一個method 如 button1_click,比較一下他們的IL字節碼是否一樣。
當然結果應該是一樣的。

這裏主要有三個關鍵函數
            private void DumpAssembly(Assembly ass, string path);
            private void DumpMethod(MethodBase mb, StreamWriter sw);
            private void DumpType(Type tp, StreamWriter sw);
這三個就是一個例子演示如何dump整個程序集。

如要dump 一個加密的dll,我們就可以直接用這個程序來改,
首先添加引用,引用那個dll,然後隨便實例話一個該dll中的type。
然後獲取該dll的 Assembly 對象,再調用DumpAssembly函數即可。

好了,今回就到這裏,下回再講解怎麼理解、查看IL字節碼。

在前面幾章我們已經能夠去掉被加密程序原始的IL字節碼了。這些字節碼是十六進制的,我人腦直接來閱讀是非常困難的。這一章主要介紹將字節碼翻譯成 可閱讀的 MSIL 彙編代碼,以及前幾章的遺留問題解決。
 

這裏我們將用到上面這個工具軟件 IlByteDecoder.

軟件下載地址:http://www.bbsftp.com/temp/ILByteDecode.rar
使用比較簡單,注意中間那個 文件名 一項,這個可以填也可以不填,如果沒有填的話,
解碼出來的 msil 代碼中將無法顯示字符串值和方法名稱。

在前面提到的 對Exe程序的注入問題,現在已經找到的解決方案:
1。傳統win32注入方式,採用C++/CLI 編寫dll 注入。
2。profile 方式 modify IL on the fly ,直接注入dot net dll。

注入之後,就可以直接在內存裏面操作,

 tankaiha:  "實踐證明被MaxtoCode加密的軟件(這裏用的CodeLib)原代碼可以在內存中還原。"

前一回講了 IL字節碼的解碼問題,並提供了一個小工具,但解碼的效果和 ildasm還是差很多,給閱讀也帶來了一些困難。還有就是有些文件選擇文件後解碼會出錯,這是因爲maxtocode對文件裏面的元數據進行了隨機加密。這一回主要解決元數據的還原以及對解碼進行改進。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章