網上很少看到有關.NET軟體保護與破解的文章,剛好分析了幾款有一定代表性的.NET軟體,於是便將他們的保護措施和如何破解方法記錄下來,以便和大家交流。在開始之前,首先申明:本文中反編譯和破解的軟體只是為學習和研究的目的,請勿非法使用。
.NET平台下的軟體(exe,dll檔案)叫做程式集。它使用一種擴充的PE格式檔案來儲存。.NET程式集與以往的應用程式不同,它儲存的是Microsoft中繼語言指令(MSIL)和中繼資料(Metadata),而不是機器指令和資料。.NET程式集在啟動並執行時候才會動態將Microsoft中繼語言編譯成機器指令執行。所以我們不能簡單的使用反組譯碼來解讀程式邏輯。初學者明白這點很重要。不過幸運的是,.NET程式集是一種自描述的組件,可以用它自描述的特性反編譯出進階程式碼(如c#,vb.net),這比彙編代碼更容易讀懂得多。即使不反編譯成進階程式碼,Microsoft中繼語言本身也是一種抽象、基於堆棧的物件導向偽組合語言。它本身也比彙編代碼更容易讀懂。在代碼易閱讀這點上,.NET程式更容易遭到破解。不過,有矛必有盾,現在有很多產品都可以混淆.NET代碼,使得反編譯出來後的結果也同樣沒有可讀性。
.NET程式集與以往的應用程式另一個不同點在於它可以使用強式名稱簽名來防止自身被篡改。使用強式名稱簽名的程式集包含公開金鑰和數位簽章資訊。.NET在執行具有強式名稱的程式集前會對它進行安全檢查,以防止它被非法篡改。這一點很厲害,它限制了我們通過修改程式碼(爆破)來破解程式的方法。
.NET平台還提供了很多安全相關的類庫,利用這些類庫可以寫成很強壯的註冊演算法(如使用RSA對註冊檔案簽名,只有使用私密金鑰才能產生正確的註冊碼)。這些演算法破解者很難破解。
另外,現在還出現了很多其他的保護措施,如流程混淆,中繼資料加密,加密殼,虛擬機器技術,編譯為本地代碼等保護手段。綜合使用這些保護手段,會使破解難道大幅度提高。
軟體破解一般有兩種方式。一種方式為不對軟體做出修改,只是分析軟體註冊演算法,根據註冊演算法寫出產生正確註冊碼的註冊機程式完成註冊過程。第二種方式需要對軟體邏輯作出修改,使軟體正常判斷註冊邏輯失效,總是認為軟體已經註冊成功。這種方式就是通常說的“爆破”。由於第二種方法需要修改軟體邏輯,不能保證軟體修改後的行為和原來一樣,故一般只有在第一種方式嘗試失敗後才使用第二種方式。
下面,我將通過對兩款軟體的分析來講解.NET平台下軟體的保護措施和破解方法。
準備的工具:
1,Reflector http://www.aisto.com/roeder/dotnet/
.NET平台下極好的反編譯工具。
2,ManagedSpy
可以查看.NET代碼寫的程式視窗。
執行個體一,GatherBird Copy Large Files 2.4
這是個拷貝大檔案的工具,它的註冊演算法不強,可以很容易寫出註冊機。我們可以通過破解這個軟體來瞭解破解.NET軟體的一般流程。
首先運行Copy Large Files 2.4 (Windows .Net 2.0 version) 。發現介面有個“Register”的按鈕。查看功能說明書,知道這是沒有註冊的標誌,註冊了介面上就不會多這個按鈕。點擊這個按鈕,就彈出註冊框,視窗標題為“Register”。然後運行ManagedSpy,可以看出DotNetCopyLargeFiles程式有兩個表單,一個“Form1_class”,另一個“RFLib_Forms_Registration_class”。很顯然,第二個就是我們彈出的註冊框。
運行Reflactor,將DotNetCopyLargeFiles.exe拖進Reflactor,尋找“RFLib_Forms_Registration_class”就是反編譯好的註冊框C#或VB原始碼。讀懂代碼。發現在點擊“Register”按鈕後會觸發“button3_Click”函數,在這個函數中將文字框中輸入的註冊碼儲存到了registerstring屬性中。使用Reflactor尋找哪些地方在調用registerstring屬性。發現在Form1_class中點擊“Register”按鈕後將這個registerstring屬性保持在Form1_class中的RegistryString欄位中。用Reflactor尋找誰在使用RegistryString,發現在Form1_class的OnTimerTick方法中將RegistryString傳給RSF1_class.SF40Helper方法檢查。
讀懂RSF1_class,這就註冊演算法所在的類。讀代碼的時候,可以藉助Reflactor反編譯成源檔案,然後使用VS2005或VS2008編譯後動態調試。這樣,可以分析SF4就是註冊碼產出的方法,根據這個方法,我們不難寫出註冊機演算法。下面就是我寫的一個註冊機演算法。當然,你也可以自己寫,方法中用到的其他方法和類都可以從Reflector反編譯的原始碼中得到。
GenerateKey
1public static string GenerateKey(byte[] exename)
2{
3 byte[] buffer = new byte[0x5f];
4 byte[] row = new byte[0x800];
5 DotNetRandom_class random = new DotNetRandom_class();
6 StringBuilder key = new StringBuilder();
7 if (!SF20())
8 {
9 random.RRandomSeed2(exename);
10
11 for (int i = 0; i < 0x5f; i++)
12 {
13 buffer[i] = (byte)(i + 0x20);
14 }
15
16 SF2(row, ref random);
17 Random r = new Random();
18 int numberIndex = GetNumberIndex(buffer,(byte)r.Next(50,57));
19
20 for (int i = 0; i < 6; i ++)
21 {
22 key.Append( Convert.ToChar(row[numberIndex * 2]));
23 key.Append(Convert.ToChar( row[numberIndex * 2+1]));
24 for (int j = 0; j < buffer[numberIndex]; j++)
25 {
26 random.RRandomUnsignedLong();
27 }
28 SF2b(row, ref random);
29
30 numberIndex = GetNumberIndex(buffer, (byte)r.Next(48, 57));
31 }
32 }
33
34 return key.ToString();
35}
36
37public static int GetNumberIndex(byte[] buffer, byte number)
38{
39 for (int i = 0; i < buffer.Length; i++)
40 {
41 if (buffer[i] == number)
42 {
43 return i;
44 }
45 }
46
47 return 20;
48}
執行個體二,ANTS Profiler
ANTS Profiler是一個檢測基於.Net Framework的任何語言開發出的應用程式的代碼效能的工具。它使用啟用碼和網路啟用的方式進行雙步驟驗證。代碼經過了混淆器混淆,程式集也經過強式名稱簽名,註冊演算法也用的是成熟的RSA演算法。所以整體安全性不錯,是.NET平台下比較成熟的做法,很有借鑒意義。
在安裝完ANTS Profiler後,運行ANTS Profiler ,會彈出Trial介面,提示還有14天試用期。另外,有一個啟用按鈕。點擊啟用按鈕,第一步會彈出提示輸入啟用碼的視窗。這裡需要先得到正確的啟用碼,才能進行下面的驗證步驟。那麼怎麼才能得到正確的啟用碼呢?像分析第一款軟體一樣,我們先開啟ManagedSpy。在ManagedSpy中我們發現Trial介面的表單類叫_53,輸入啟用碼的表單類叫_3。很顯然,都是經過代碼混淆的,這是ANTS Profiler安全保護的第一關-“代碼混淆關”。這一關只是增加過其他關的難度,不需要特別處理。
在找到註冊資訊相關的類名_53後,開啟Reflactor,搜尋_53類,順藤摸瓜,再找到_3類,這個類在RedGate.Licensing.Client.dll中(RedGate.Licensing.Client.dll是註冊相關的程式集,需要重點關注)。讀_3類的代碼,發現輸入的啟用碼被設定到_2類中SerialNumber屬性中,_2類中_2(string text1)方法就是校正啟用碼的方法。代碼如下:
Code
1private static bool _2(string text1)
2{
3 text1 = text1.ToUpper().Trim();
4 Regex regex = new Regex(@"[A-Z]{2}-[0-9A-Z]{1}-[0-9A-Z]{1}-\d{5}-[0-9A-F]{4}");
5 Regex regex2 = new Regex(@"\d{3}-\d{3}-\d{6}-[0-9A-F]{4}");
6 if (regex.IsMatch(text1))
7 {
8 string str = text1.Substring(0, 12);
9 string str2 = string.Format("{0:X4}", _7._1(str));
10 if (!text1.EndsWith(str2))
11 {
12 return false;
13 }
14 }
15 else if (regex2.IsMatch(text1))
16 {
17 string str3 = text1.Substring(0, 14);
18 string str4 = string.Format("{0:X4}", _7._1(str3));
19 if (!text1.EndsWith(str4))
20 {
21 return false;
22 }
23 }
24 else
25 {
26 return false;
27 }
28 return true;
29}
這段代碼錶明,啟用碼是符合regex和regex2這兩種Regex的形式。同時,滿足啟用碼使用_7._1(string text1)方法傳回值結尾。很顯然,前12為是啟用碼資訊,後四位是啟用碼校正位。_7._1(string text1)就是計算校正位的方法,代碼如下:
Code
1internal static uint _1(string text1)
2{
3 long num = 0L;
4 for (int i = 0; i < text1.Length; i++)
5 {
6 int num4 = text1[i];
7 for (int j = 7; j >= 0; j--)
8 {
9 bool flag = ((num & 0x8000L) == 0x8000L) ^ ((num4 & (((int) 1) << j)) != 0);
10 num = (num & 0x7fffL) << 1;
11 if (flag)
12 {
13 num ^= 0x1021L;
14 }
15 }
16 }
17 return (uint) num;
18}
19
知道了啟用碼的合法形式和校正位的計算方法,我們就可以構建一個合法的啟用碼。以regex表示的合法啟用碼形式為例,選擇前12位為最小值“AA-0-0-00000”的一個合法啟用碼,然後使用_7._1(string text1)方法計算校正碼為:“DE33”。那麼一個合法的啟用碼就這樣得到了“AA-0-0-00000-DE33”。複製啟用碼到啟用視窗,成功通過驗證。至此,ANTS Profiler安全保護的第二關-“啟用碼驗證關”通過了。在啟用碼驗證通過後,點擊下一步會進入到網路啟用或Email啟用介面。選擇Email啟用,我們可以看見ANTS Profiler收集了版本資訊,啟用碼,session,和機器硬體等資訊。這可以保障一個註冊碼只能用在一台電腦上,值得學習。啟用請求資訊如下:
activationrequest
1<activationrequest>
2<version>2</version>
3<machinehash>F6FB-285E-CD12-667D</machinehash>
4<productcode>5</productcode>
5<majorversion>3</majorversion>
6<minorversion>0</minorversion>
7<serialnumber>AA-0-0-00000-DE33</serialnumber>
8<session>689fae08-2fdb-485e-9df7-28e2888cfbff</session>
9<locale>zh-CN</locale>
10</activationrequest>
11
接下來,就是輸入啟用響應資訊,驗證啟用響應資訊的介面了。這是啟用介面的第四步,同樣代碼在_3類中。跟蹤代碼,發現驗證邏輯在RedGate.Licensing.Client.Licence類中的_2(XmlDocument document1, ref _2 _Ref1)方法中,代碼如下:
Code
1private bool _2(XmlDocument document1, ref _2 _Ref1)
2{
3 XmlNodeList elementsByTagName = document1.GetElementsByTagName("data");
4 XmlNodeList list2 = document1.GetElementsByTagName("signature");
5 if ((elementsByTagName.Count != 1) || (list2.Count != 1))
6 {
7 _Ref1._3 = _6._1(_6._16);
8 return false;
9 }
10 string outerXml = elementsByTagName[0].OuterXml;
11 string innerXml = list2[0].InnerXml;
12 if (innerXml.Length == 0)
13 {
14 _Ref1._3 = _6._1(_6._17);
15 return false;
16 }
17 RSACryptoServiceProvider provider = new RSACryptoServiceProvider();
18 string xmlString = "<RSAKeyValue><Modulus>zLizNmLUd4VlIWee1GXgn/KxEwcghPASQ+NUzZhbY2fTGzpW64T6yEOdHlIbhX1DX6yAz2gMZKfnpQL2aFqxh5ACFV9dONSTzuQzkqeXwFEARsMxGP3eTQSWMpwVhEcraSn1zOqMb3CRDeQpgasq0lv4HRFhbwalOifKarjEL/8=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
19 provider.FromXmlString(xmlString);
20 byte[] signature = Convert.FromBase64String(innerXml);
21 byte[] bytes = Encoding.UTF8.GetBytes(outerXml);
22 if (!provider.VerifyData(bytes, new SHA1Managed(), signature))
23 {
24 _Ref1._3 = _6._1(_6._18);
25 return false;
26 }
27 _Ref1._1 = false;
28 foreach (XmlNode node in elementsByTagName[0].ChildNodes)
29 {
30 string name = node.Name;
31 if (name == null)
32 {
33 continue;
34 }
35 name = string.IsInterned(name);
36 if (name != "productcodes")
37 {
38 if (name == "serialnumber")
39 {
40 goto Label_0294;
41 }
42 if (name == "productcode")
43 {
44 goto Label_02A7;
45 }
46 if (name == "majorversion")
47 {
48 goto Label_02BF;
49 }
50 if (name == "minorversion")
51 {
52 goto Label_02D7;
53 }
54 if (name == "edition")
55 {
56 goto Label_02EF;
57 }
58 if (name == "machinehash")
59 {
60 goto Label_02FF;
61 }
62 if (name == "extension")
63 {
64 goto Label_030F;
65 }
66 if (name == "session")
67 {
68 goto Label_0319;
69 }
70 continue;
71 }
72 foreach (XmlNode node2 in node.SelectNodes("product"))
73 {
74 _1 _ = new _1();
75 try
76 {
77 _._1 = node2.SelectSingleNode("productname").InnerText;
78 _._1 = Convert.ToInt32(node2.SelectSingleNode("productcode").InnerText);
79 _._2 = Convert.ToInt32(node2.SelectSingleNode("majorversion").InnerText);
80 _._3 = Convert.ToInt32(node2.SelectSingleNode("minorversion").InnerText);
81 _._2 = node2.SelectSingleNode("edition").InnerText;
82 _Ref1._1.Add(_);
83 continue;
84 }
85 catch (Exception)
86 {
87 continue;
88 }
89 }
90 continue;
91 Label_0294:
92 _Ref1._2 = node.InnerText;
93 continue;
94 Label_02A7:
95 try
96 {
97 _Ref1._1 = Convert.ToInt32(node.InnerText);
98 }
99 catch
100 {
101 }
102 continue;
103 Label_02BF:
104 try
105 {
106 _Ref1._2 = Convert.ToInt32(node.InnerText);
107 }
108 catch
109 {
110 }
111 continue;
112 Label_02D7:
113 try
114 {
115 _Ref1._3 = Convert.ToInt32(node.InnerText);
116 }
117 catch
118 {
119 }
120 continue;
121 Label_02EF:
122 _Ref1._5 = node.InnerText;
123 continue;
124 Label_02FF:
125 _Ref1._1 = node.InnerText;
126 continue;
127 Label_030F:
128 _Ref1._1 = true;
129 continue;
130 Label_0319:
131 _Ref1._4 = node.InnerText;
132 }
133 return true;
134}
135
細讀這段代碼可以發現,啟用響應資訊已經使用RSA演算法進行過數位簽章,這樣可以防止啟用響應資訊被篡改。要篡改或偽造啟用響應資訊,必需破解RSA私密金鑰公開金鑰對。公開金鑰已經在方法體中給出,所以需要破解私密金鑰。從公開金鑰可以看出,它使用的1024位密鑰長度,理論攻破時間1011MIPS年。雖然破解的可能性存在,但破解幾率很小。這就是ANTS Profiler安全保護的第三關-“RSA數位簽章關”。這使得我們不得不放棄偽造啟用響應資訊的辦法。通常碰到RSA等成熟演算法,我們只能選擇“爆破”這款軟體了。
通過前面的分析,我們發現程式中判斷軟體是否註冊的邏輯放在RedGate.Licensing.Client.dll程式集中的Licence類裡面。我們需要做的就是開啟Reflactor軟體,載入RedGate.Licensing.Client.dll檔案,將RedGate.Licensing.Client.dll檔案Export成C#工程源檔案。然後將源檔案中Licence類中所有公用方法都返回註冊成功的資訊,再重新編譯產生新的修改後的RedGate.Licensing.Client.dll檔案,用新產生的檔案替換原RedGate.Licensing.Client.dll檔案。這樣,註冊判斷邏輯就被非法篡改了,使軟體誤認為已經註冊。修改後的Licence類就像下面這樣:
Code
1namespace RedGate.Licensing.Client
2{
3 using System;
4
5 public class Licence
6 {
7 public bool DisplayUI()
8 {
9 return true;
10 }
11
12 public static Licence GetLicence(int productCode, string productName, int majorVersion, int minorVersion)
13 {
14 return new Licence();
15 }
16
17 public static Licence GetLicence(int productCode, string productName, int majorVersion, int minorVersion, string path)
18 {
19 return new Licence();
20 }
21
22 public static void InitializeAtInstall(string productName, int productCode, int majorVersion, int minorVersion, string guid)
23 {
24 }
25
26 public bool Activated
27 {
28 get
29 {
30 return true;
31 }
32 }
33
34 public int DaysLeftInTrial
35 {
36 get
37 {
38 return 0x7fffffff;
39 }
40 }
41
42 public string Edition
43 {
44 get
45 {
46 return "professional";
47 }
48 }
49
50 public string LicenceFilePath
51 {
52 get
53 {
54 return string.Empty;
55 }
56 }
57
58 public string SerialNumber
59 {
60 get
61 {
62 return "AA-0-0-00000-DE33";
63 }
64 set
65 {
66 }
67 }
68
69 public RedGate.Licensing.Client.TrialStatus TrialStatus
70 {
71 get
72 {
73 return RedGate.Licensing.Client.TrialStatus.InTrial;
74 }
75 }
76 }
77}
78
爆破雖然是一種好方法,不過.NET中有專門對付這種方法的殺手鐧-“強式名稱簽名”。不幸的是,ANTS Profiler使用了強式名稱簽名機制,是我們在爆破軟體這條路上受阻。這就是ANTS Profiler軟體保護的第四關-“強式名稱簽名”。強式名稱簽名會在軟體被加入GAC時驗證。如果軟體沒在GAC中,那麼運行軟體的時候也會驗證。如果軟體被篡改,在運行軟體的時候軟體會直接拋出異常,使得軟體無法正常運行。
那麼如何破解這種強式名稱簽名驗證機制呢?
有幾種辦法可以解除強式名稱的驗證機制。第一種方法適合單個檔案的軟體,對單個檔案的軟體我們可以移除強式名稱簽名資訊,使軟體變成弱名稱的程式,這樣在軟體啟動並執行時候就不會驗證是否被篡改了。對於參考關聯性複雜的多檔案軟體,我們可以採取第二種方法。這種方法需要理由.NET平台上設計的“後門”才能完成。這個“後門”是什麼呢?原來,為了使軟體開發後能夠使用混淆工具混淆,微軟允許延遲簽名程式集,被延遲簽名的程式集可以在混淆之後再重新簽名。而為了測試方便,只需要在註冊表中加入一條記錄就可以使混淆修改後的程式在沒有被重新簽名前也能運行,即不進行強式名稱驗證。
利用這個“後門”,我們只需要將修改後的修改完Licence類編譯成新的RedGate.Licensing.Client.dll檔案,編譯選項中使用任意一對公開金鑰私密金鑰對強式名稱簽名,並選擇延遲簽名,然後篡改編譯後的public key為原始RedGate.Licensing.Client.dll檔案的public key。再在註冊表中加入一條記錄就可以是強式名稱簽名驗證機制失效,達到破解的目的。具體做法如下:
1,我們用sn命令產生一個新的公開金鑰私密金鑰對,用於簽名
sn -k my.key
2,將反編譯的RedGate.Licensing.Client.dll工程檔案中Licence類修改成上面的代碼,並在編譯選項中使用第一步得到的my.key簽名程式集。注意,一定要選擇延遲簽名選項。編譯產生新的RedGate.Licensing.Client.dll檔案。
3,使用sn命令得到原始RedGate.Licensing.Client.dll檔案的public key和新RedGate.Licensing.Client.dll檔案的public key。
sn -Tp RedGate.Licensing.Client.dll
4,使用二進位編輯軟體,如WinHex開啟新的RedGate.Licensing.Client.dll檔案,替換掉它的public key為老檔案中的public key。
5,在註冊表“Software\Microsoft\StrongName\Verification\”下加一條子健“RedGate.Licensing.Client,7F465A1C156D4D57”即可完成破解。這步也可以使用sn命令代替。
sn -Vr RedGate.Licensing.Client.dll
當然,你可以將修改後的RedGate.Licensing.Client.dll檔案打包到一個Patch檔案中,並在Patch檔案中自動完成替換檔案和修改註冊表的操作。至此,ANTS Profiler就被破解了。
通過對以上兩款軟體的分析,我們可以看出,ANTS Profiler的保護手段還是比較成熟的,值得借鑒。它使用了名稱混淆,啟用碼+網路驗證,RSA數位簽章,強式名稱簽名等保護手段,整體安全係數較高。而GatherBird Copy Large Files則沒有充分運用上.NET平台提供的保護措施,還處於比較低的保護層級。如果我們綜合運用流程混淆,中繼資料加密,加密殼,虛擬機器技術,編譯為本地代碼等保護手段,軟體保護強度將更高。
最後提供一個高人寫的.NET版本的CrackMe。有興趣的朋友可以試一試。