列举工作以来遇到的各种类型的软件所采用的代码保护技术,只讲原理不涉及技术细节实现,以避免产生法律问题。有些朋友说直接把代码放在Github开源下载,开源可以促进技术交流与进步,然而值钱的代码都积压在硬盘里面,即使很烂的代码都卖了很多钱,赢得了许多客户与市场。珍惜爱护自己写的代码,他们都是宝贵的财富。
以下保护技术主要测重于脱机验证与保护,不涉及联网(连接到许可证服务器)验证。
1 程序集混淆 Asembly obfuscate
CLR代码的运行是即时编译执行的,.NET编译器只是将源代码文件编译成中间语言,中间语言包含丰富的元数据。使用.NET Reflector/ILSpy/JustCode这类的反编译器几乎可以还原整个源代码,所以用.NET开发软件第一个要做的任务是混淆编译后的中间语言代码。这也是我见得最多的.NET代码保护技术。
2 写注册表 Registry
虽然.NET时代强调XCOPY部属,但很多很程序依然依赖于注册表来做保护技术的基础。可以把自己想要隐藏的信息放到很深的注册表键中。
一些程序会将注册表键做一个字符串处理,比如将字符串改成16进制值,或是做一个DES混淆处理,在不知道解密键(KEY)的情况下很难看出它的意思。
3 RSA 签名 RSA Signature
RSA签名技术可以生成二个密匙,私匙用于生成许可文件,公匙用于验证许可。
string publicKey = "
DH+PAHv9BAWiqa1cWeaVgCwC2LNMClaJu4jZg0aFDIzhZUNHV56KS5aaVD+MHH+iEGmPok9XRz7Jd8hyjD38jHl439Uaf6oltxMRdKS5KU=
string privateKey = "
AWiqa1cWeaVgCwC2LNMClaJu4jZg0aFDIzhZUNHV56KS5aaVD+MHH+iEGmPok9XRz7Jd8hyjD38jHl439Uaf6oltxMRdKS5KU= 82nuszkXKLx
+NDtQLiruBlDGozAUaJ50PqHKq7jglLV740jz521+v2O6EVA+59gpGSwFrX6E7HnUrkKBQGbY/w==
yiTO+1+xaNvmEcAyKdFboNRbP1/toUnakkREKyYCg/KLnjgpkb22gMtuu5ocbfY2vIdyDZbYYeXmLYSJ9Io+Ww==
1zbiy7i1Vi09XopKpNmdR2F7tCVJti50KKtKNeZHNBbolkGslqgaM5wPGy/3ZTadKHuV6gaio0E8/m15P6Q==
q28rFgXa//avpx05E3FIN0tfAOKJhy84VKElqPNyQyacNA==
1GdkqZp+gaqKbp73RP21Nmd+lZ/S1WvmUTe3Ge5I5FP/7zf9KnTXBm1yLel/9N5UL6o5EncPNjqUt2+oKQ+q7vJU=
因为私匙只存在于自己的电脑中,不会发布到程序中,只要私匙不泄露出去,这种加密方法是相当安全的。
破解RSA签名的唯一方法是替换公匙,用自己生成的私匙生成许可文件。
4 参数保护 Parameter Protection
基于第3条RSA签名技术,签名之后产生的字符串是只读的,在它的基础上我们可以增加许多控制点,比如可使用的用户数,过期时间,最大账套数等。
在我的Enterprise Solution开发框架中,提供基于第3条和第4条为原理的许可保护技术的例子,在一定程度上保护产品不被未授权用户使用。
5 机器硬件识别 Hardware Recognition
为了控制软件不会被客户随意拷贝分发,一方面通过法律合同条文明确禁止,但通常这没有什么卵用,另一方面则必须做硬件识别。在生成许可文件之前,要依赖于客户的机器配置,将客户的机器硬件信息完整的保存到许可文件中,运行时逐条检测,发现有不一致的地方立刻退出应用程序。
.NET 提供了WMI接口用于读取机器相关的信息,参数如下的代码例子:
private static string GetDiskDriveSize()
{
return WmiHelper.GetWmiPropertyValue("Win32_DiskDrive", "Size");
}
private static string GetDiskDriveTotalSectors()
{
return WmiHelper.GetWmiPropertyValue("Win32_DiskDrive", "TotalSectors");
}
private static string GetDiskDriveTotalTracks()
{
return WmiHelper.GetWmiPropertyValue("Win32_BaseBoard", "Manufacturer");
}
三个方法分别读取硬盘大小,硬盘分区,主板制造商,将这些信息组合起来放到一个字符串文件中,即是电脑的硬件信息,再对这个字符串做一个DES对称加密或是RSA签名处理,处理之后,客户的硬件信息看起来是这样的字符串。
lxocnnn7ofk2+Nheb7Qo+cBRDlCbVWHCfVSFG/XnyT3F7q/avyRL3Lj7gotPMtuo41A33AjSryt5a1ydvu7tTOnsOsizy8HJIrP13T5jHbgyHZgZaQk54eGLN3eft5zijKT3hyirZ5w2Mf6mWmv/9F
68jFxsnNh//OUUAVI3lx0=KMw2gRnZ8BqL4lNw4A5j+tuDlvbsj5gp3CGZNbAXlFlGW188uliAZexb6GhcvB0VSwTV37R6NUstT/VT5MJ7wyf/zyyHpa1uHGx4/UziLCAwawnap90BkQWlH1aVz
2lTX3I93iKKYOdAJF0y5TM9njlVvqToj2cBBWLUG+sYXxo=DUjIpSbrp5cl8L//WbIs7ErSX8RBTeVMFO7OCIYoTh7yogVzJNH3D3QZHYKnNfGmJDMSleJSgdLb0GdiWdEgiLTf0uv7lQsoA076
WM5VrtG7O/h0E3DvmisP7OkkuJIMEIpExaM+3GDlIHGvHYRK0nDfzBQM8AXM2k0ue8wj0xY=k5CRv+IkEdB6yZEy65iLXsU0lQT1FbHwSH7LawWBoOZHJt1TwIAajHuqH0AHAWZgRHYDbHCxnlJFv9t
OepsJdNGL6CvjUEnyuNA3RlhKNB6K51rOjIQ1ROKRiy9z+YsbHbJxdbsj3km/kITzfHvoDqQIg4zrTR0XfSn49j0rMkI=C4oBw14/k1OWPzMC1SUe+YxrJtyUqzL2k/cKz/feAhZ0DELbUiCUcbjhHg
+ryg+E7P5Bfy53n3kH5SBud4rVBpRUZGx4xZFPB8eXQ40SUCMxs74cbQRUPh7+zQo6qF/AtrQkXKbzoiZKHCz50n6h/afaa+jwuOplYUx191269w4=RGDYBw6j5VXJO7phILX1zLRcbO3+6SrBuCLEFwk
sitOk/z+OGtFEoIfHD5DZRp9t6GVYtpdI7jPVz7lTV4DL5xfCQ8woJjpqUdB++pYaIw3gx9EL6PD+ucRfLKtiNng6ujKe0r6A51y9eGJRic9p8uAPGSEj8OtNLmO0mVWcwhE=GJg9mdr/YdDMZCVe0WPb3
S8YGKfzdPYj3xJTQtRYdFLV9E6GHzQj8FFgIrxjdoD3n9FGthhF82MTpjS3kOdIyQEc7moienSbzqevcUPs4ZmDwmveH/F4DSynS7JAiPqCcnAj7N4EI/PkXZWAtdHSra2yWBkEBw5PlVhLTLbcLGE=FL
+JSw0yGUIpIZUgq8VRt27e5J+D0vRoEw9t5NKPROysnGw=YN1yhy2reEhQPs30aCnOpNDz2
He2E9re26QBwipNgivHM8Z/FcLk0hjALeiC8zY4Dcn+0PKag6DH7tmIBwTpBus2+ZqjLlgIx7uKMPB49+yPC5qOV8vsJGV/s81xVEA+prgNbh9Sa3TSsBV3qMSyZDEyvkHc+1QbBoLd/1h2WUI=p0/G
lIKMQ7XoRom9njDq5XPn3sj4V8P137v6eDDq7uTpRcTXD/qOWi/IZMcNX56LqVb1PJko0BCUYXa8h0zzEvsmTJNXUzggZlpkzk4EOzq4GekTwM6hXcn0Es/6iZ2J5Et1h5kSOOk1MLZS3cmUHeSRn0js
Ed0O/+L8+qBNpYapf6GL2iYf5j/Qfi+2djOwqCspHawZcBZ/e/WtK0b/0F4=lmHD5BVilXA7KIQk8yma7d4dP5mawdNhMEuYG68sN15SJOUMZlGiFT17rh0Y7Z
54itnjVmhaRVXBsc/LDKYPIwsD+TZBTGDbSOebmxvDe+Jdh6ZLsmGUIdREH4/L19y1713PyJ2r6HYi9v/8GobkWWg4chIKggBhSYUJFP8jj4Q=JHNsEDYacXEIwWhR
nDfiQoWZbrZjmCgF7lo9g4BCTYD0KbLLRKdrDERNPTQs+e3kLf0/fhc0x0BsE/yCuYcnj8rh9/iA08ZUxO+TZkz/p5rseWklWboc45o90q/ZXfuu5f+TCVPnfTXCxMjwdH3
F4I7EotPzweUS/P++YDvHHWA=d8l5s9fNa0fUX4eo9LoHVT4Wb8YbHKDvDW7o1P+wTDykIrOCLoCKklrSGdHjOi8EWw=LDrOGw3pq7rv4L7YoGLaZAmXfRdcw1wp2Zi/Hn2
这样既避免了客户隐私信息泄露,也可以解决当前软件氛围环境下软件分发的困扰。
6 序列号/注册码 Serial Key
相当多的软件还是采取这一传统的序列号保护方法,比如Windows Server 2012的注册码:
NB4WH-BBBYV-3MPPC-9RCMV-46XCB
一组看似无序的数字与字母的组合,对软件保护起了很重要的作用。如果是在软件安装时验证序列号,则破解难度会更大。
序列上面的字母数字组合,可以包含很多信息,比如客户名称,过期时间,最大用户数。将这样的序列号分享给同行的其它客户,一旦软件公司追查起来很容易就能识别这个序列号是否合法。
在我的博客下载工具中用到的一个组件aspNetMHT,它就是采用这个方法来分发序列号的,序列号中包含过期时间。aspNetMHT的序列号看起来是这样的:
SM48Z-FMXGZ-25P67-4ZJKF-GS211-AQYA4-7VHUX-KCF1C-RD4RC-RU7XS-XK8JY-6JT54-M9CX
7 强名称 Strong Name
强名称不是一种代码保护技术,而是程序集标识技术。每个经过strong name签名后的程序集都是唯一的,在运行时可以验证这个文件的签名是否被改动过。
验证强名称的关键地方在于调用CLR中的基础方法:
[DllImport("mscoree.dll", CharSet = CharSet.Unicode)]
public static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification, ref bool pfWasVerified);
所以开发.NET程序,不仅仅要给程序签名,还需要在运行时验证所给的签名是否被改动过。
.NET CLR支持对程序集跳过强名称验证,不过这样做的风险太大,直接修改了CLR的行为,未见有很多实例。
8 内存保护 Memory Protection
Delphi XE采取了这种方法,运行时会生成一段验证代码,验证结束后代码销毁,因为验证在内存中进行,不会在硬盘上留下痕迹,破解的难度相对困难了很多。
Win32支持动态加载和卸载程序集,所以Win32 API中有接口LoadLibrary和UnloadLibrary供调用,可实现内存保护。
.NET也支持动态加载程序集,但不支持卸载(unload)程序集,程序集的卸载由CLR控制,这一点给破解内存保护技术提供了方便。
在我的Enterprise Solution开发框架中,将第7条和第8条组合一下,简单的解释如下:创建一个类型定义文件,然后将这个文件用强名称签名,之后将这个签名的程序集经过混淆,字符串加密等处理,附加到启动程序的资源文件中去,运行时,我从资源文件中将此程序集解析释放出来,同时强制验证程序集的强名称,运行类型定义文件中指定的验证方法,实现简单的内存保护技术。
破解这种保护需要知道签名程序集的强名称,字符串加密处理的全流程和相应的键,给破解增加了不少难度。要验证什么和怎么验证都是在我设计的类型定义文件中指定的,启动程序只是一个壳,运用反射调用要验证方法。而且这个经过签名后的程序集是附加到主程序中去的,要替换这个资源文件,也需要一些方法和技巧。
9 源代码混淆 Source code obfuscate
经过编译混淆后的.NET程序集,在运行时依旧可以被调试器附加调试。如果我能将源代码中的敏感字符串替换成乱七八糟的字符串,减少明文的出现,虽然降低了性能,但这样可以增加安全性。比如,我运行时检测找不到许可文件时,抛出以下异常:
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"license.lic");
if (!File.Exists(path))
throw new LicenseException("License file not found");
经过源代码敏感字符串替换后的程序如下:
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"license.lic");
if (!File.Exists(path))
throw new LicenseException(B("/jNloqd4U59LBT64eX1hb9eO8kYDD7tD"));
经过一个B方法的调用,无法看到异常的明文,增加了破解难度。如果代码中这样的B方法有很多,而且B方法的字符串处理方式各不相同,无疑提高了系统的安全性。
现在市场上有很多JavaScript源代码混淆工具,就是把JavaScript代码的格式打乱,让它不容易读取。
C#源代码也可以采用此方法,不过,读强大的Visual Studio有文档格式化功能(Edit-Advanced- Format Document ,Ctrl + E,D) ,瞬间变为可读的格式。
10 本机代码 Native Code
即将发布的Visual Studio 2015提供.NET Native功能,可将.NET代码编译为本机代码。然而经过近一年的等待,.NET Native目前也是仅仅限定于Windows应用程序商店项目,真正的Windows Forms/WPF/ASP.NET要实现编译为本机代码可能不现实。
这种保护方法的核心是许可验证逻辑放在本机代码中,直接编译成机器码。一般采用Visual C++/Delphi/VB6之类的工具,将重要的算法与验证编译成机器码,这样会增加破解难度。本机代码难于处理的一个方面是.NET Any CPU的问题。当我们设定.NET编译为Any CPU时,CLLRU会依据程序运行的平台(x86,x64)编译为本机代码。
在x64的系统中加载32位的本机代码,常常会抛出无效的程序集异常。破解这种保护的方法可直接修改.NET程序中的调用方法,可以忽略本机代码的逻辑。
相对于前面的几种技术,这种保护技术破解难度更大,要懂的知识点也非常多。