NET软件保护与破解浅析 (转)
网上很少看到有关.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这两种正则表达式的形式。同时,满足激活码使用_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。有兴趣的朋友可以试一试。