关键词 加壳;脱壳;DLL;DES;NET;MSIL
为了保护自己的软件的技术内核不被他人轻易盗用,软件开发人员使用了各种加密技术来保障软件的版权不被侵犯,壳便是我们常用的一种软件保护手段。对于Win32 中软件加壳技术已经有非常成熟的商业产品,然而,对于.NET环境下软件,由于.NET程序的编译结果不是机器语言代码,而是一种MSIL中间代码,因此不能使用传统的加壳技术。目前,专门对.NET软件实施加壳的商业软件主要有MaxtoCode,另外,SafeNet公司也推出了其软件保护产品圣天狗最新的外壳工具,满足了软件开发商的一大愿望。圣天狗外壳加密工具可以自动完成对可执行文件的加密过程,从而让开发商快速方便地完成软件加密和授权管理的工作。
本文介绍了.NET环境下一种简单的软件加壳技术,该技术使用了数字签名、MSIL代码混淆、加密等技术,可达到高强度的软件保护。与MaxtoCode相比,这种技术的优点是:可以防止软件非法拷贝,针对计算机硬件“指纹”授权(指纹:即硬件信息中终身不变的识别号,如CPU和硬盘的序列号等,下同),对于网络版,可以防止非本服务器的客户端访问服务。
壳是对加密软件的一个形象的比喻,顾名思义,壳是软件外部的一件“外衣”,是软件的保护屏障。有了它,恶意攻击者就无法在对软件反汇编后,直接找到软件的核心代码。
壳是一段程序,它先于程序运行,壳在运行后就获得了该软件控制权,利用其保护功能对软件进行安全保护。
壳的工作原理大致是:先运行壳程序,壳将加密的主程序(主程序即原来的待加壳的程序,下同)代码解密到内存中,运行其中相应功能,并将程序的控制权交还给主程序。
本文中研究的加壳技术正是基于这种原理,只是壳也是用.NET编写的,加密与解密密钥与用户的机器硬件指纹有关,而不是固定的密钥,所以可以防止软件非法拷贝,达到更高的保护强度。
先将原来的主程序编译成DLL,再编写一个壳程序,编译成EXE,并将程序的运行入口(即main函数)移到壳中来,由壳开始执行整个程序。首先,可以用自编的加密工具,在软件发布前将所有DLL文件分别进行加密(输入特定的密钥),在壳加载DLL时临时将其解密到内存中,并加载运行。由于解密DLL的结果只存在于内存,所以攻击者无法获得解密后的DLL,除非他能找到DLL的解密密钥。同时,为了避免攻击者分析外壳程序的逻辑,从中寻找解密密钥,还可以将壳编译成EXE文件后,再用第三方软件进行混淆(如:XenoCode)或加密(如:MaxtoCode),这样攻击者将无法了解DLL加密的算法及处理逻辑。从而,更有效地保护主程序DLL。加壳与脱壳的原理如图1所示。
图 1 NET软件加密型加壳的原理
为了推广软件,开发商一般会将软件的试用版放在网上,让用户自由下载试用,用户基本满意后再注册正式版。
软件发布与用户注册的流程如图2所示。
为了达到软件防拷贝的功能,开发商必须为不同的用户制作不同的安装文件,一套程序只能在一台机器上运行。为此,开发商在制作安装文件时,必须取得软件将要运行的目标机器的指纹和用户的单位名称,对于有应用程序服务器的网络版软件,只需要取得应用服务器的指纹。指纹数据可以由用户使用特定的程序取得,并通过短信或邮件的方式告知开发商。
指纹的提取有两种方法:其一,通过软件的试用版。试用版中设计申请注册的模块,用户通过此模块提取本机的指纹数据。软件的试用版文件的制作不是针对目标机器的指纹制作的,可以在任何机器上运行。为了防止Cracker找到DLL的解密密钥后,将试用版破解。通常可以对试用版软件设置功能限制(如:去掉部分关键代码),这样即使试用版被破解,也无法投入正式的应用。
方法二:使用专用于注册申请的程序。对于网络版的应用服务程序,如果没有用户界面,或无须试用的用户,只能使用由开发商提供的专用注册程序来提取机器指纹。
这种加壳技术本来可以省去输入注册码手工注册这个过程,因为每一个发布的版本只能在指定的机器上运行,但为了定制用户单位信息以及对用户数进行限制,还是要有注册过程的。注册码是由用户单位、机器指纹及用户数限制等信息经过加密处理后得到的,处理可以用自制的注册码计算工具来实现。
本加壳技术的核心之一是加密DLL文件,加密过程可以用自制的加密工具完成。加密算法可以选择.NET框架中提供任何加密算法或者自行设计加密算法。算法可以不用公开的算法,因为加密解密都是在自己的程序中进行。因此,此方案的安全性完全可以由开发商自己保证,而不依赖于第三方。
制作之前,只需用专为此用户加密的DLL文件及授权文件等替换安装工程中相应的文件,再生成安装盘。安装盘中可以单独存放一份加密DLL文件和授权文件,以备客户升级正式版时用户直接拷贝。
一般而言,安装文件中不能直接包含注册码或授权文件的,但在这种技术下,可以将授权文件打包到安装盘中,因为,即使安装文件被复制,也无法在非授权的机器上运行。
对于没有安装过试用版的机器,可以直接使用安装盘安装正式版。对安装过试用版的机器,可以用安装盘中的正式版文件替换相应文件即可变成正式版。
调用正式版中“帮助”à“关于”à“注册”功能,输入注册码或选择授权文件进行注册。由于安装文件中包含授权文件,也可以在正式版首次运行时,通过授权文件自动注册,免去了手工注册的过程。
脱壳实际上是将加密的程序代码解密并加载到内存程序区,脱壳需要特定的解密密钥或特定的解密算法。对于较简单的程序,如果只有一两个DLL,可以由壳程序进行一次性脱壳,全部放在内存中,这没有什么技术上的难度,只是内存消耗较多。对于有多个DLL的程序,DLL不一定都要使用,有时可能只用其中部分,所以没有必要一次脱壳,全部占据在内存中,可以根据需要来脱壳。脱壳涉及到DLL解密、DLL调用请求的捕获等技术。
一般的加壳技术使用与用户无关的密钥,密钥是固定中壳代码中的,所以脱壳可以在任何一台机器上实现,无法实现软件防拷贝。而在本方案中,DLL加密密钥与用户计算机硬件指纹相关,当然解密密钥也不是壳代码中固定的,而需要临时从目标计算机上提取指纹生成密钥,才能解密,所以脱壳只能在授权的计算机上进行,从而可以很好地防止软件的非法拷贝。
解密密钥的是由硬件指纹生成的,指纹的提取与申请注册时提取指纹的算法相同,并且,指纹通过相同的保密算法转换后生成加密解密密钥。因此,指纹的提取算法存在于壳和专用的注册申请程序以软件的试用版中,为了防止这些算法的破解,需要使用第三方工具(如:XenoCode,MaxtoCode)对这些程序的MSIL代码进行混淆或加密。对于网络版软件,解密密钥最好是由壳临时从应用程序服务器获取;但为了简单起见,也可以将解密密钥存入授权文件中,授权文件经加密后随客户端软件一起安装,脱壳时从授权文件中提取解密密钥。
DLL的解密只能在内存中进行,不能生成临时文件,避免Cracker截获解密后的DLL。这就要用到文件流与加密流的技术,脱壳的部分代码如下:
public static Assembly asmload(string asmName)
{ Assembly asmsvr = null;
FileStream fsr = null;
byte[] byVec=new byte[16], byKey=new byte[32];
//asmName待加载的程序集名,由参数带入
string toLoad=AppDomain.CurrentDomain.BaseDirectory+asmName +"e.dll";
if ( ! File.Exists( toLoad ) ) return null;
fsr = new FileStream( toLoad, FileMode.Open, FileAccess.Read );
byte[] rawAssembly = new byte[ fsr.Length ];
//提取机器指纹并生成DES加密密钥与初向量
createDesKeyVec( ref byVec,ref byKey );
SymmetricAlgorithm des=SymmetricAlgorithm.Create();
CryptoStream encStream=new CryptoStream(fsr, des.Create Decryptor ( byKey, byVec), CryptoStream Mode.Read );
//读取并解密到到缓冲区
encStream.Read( rawAssembly, 0, (int)fsr.Length );
encStream.Close();
fsr.Close();
asmsvr = AppDomain.CurrentDomain.Load(rawAssembly );
}
根据需要来脱壳也就是当程序集被调用时,临时脱壳并加载,程序集一旦加载,以后需要调用其中的功能时就可以直接从内存中运行,这就既避免了内存的浪费又不会影响程序运行速度。关键是程序集的调用不一定从壳中调用,可以从任何一个已经运行的程序集中调用,怎么才能截获程序集的调用请求呢?
首先要了解应用程序域,它由AppDomain对象来表示,为执行托管代码提供隔离、卸载和安全边界。多个应用程序域可以在一个进程中运行;但是,在应用程序域和线程之间没有一对一的关联。多个线程可以属于一个应用程序域,尽管给定的线程并不局限于一个应用程序域,但在任何给定时间,线程都在一个应用程序域中执行。每当程序运行时,便会自动创建应用程序域。AppDomain实例用于加载和执行程序集(Assembly),AppDomain 类实现一组事件,这些事件使应用程序可以在加载程序集、卸载应用程序域或引发未处理的异常时进行响应。本方案中就是通过事件AssemblyResolve来捕获程序集调用请求的。
实现方法是:首先,在壳的main()函数中注册事件AssemblyResolve的响应代码,形如:
AppDomain.CurrentDomain.AssemblyResolve += new
ResolveEventHandler( CurrentDomain_AssemblyResolve );
然后,再编写一段事件响应代码,来实现程序集脱壳与加载。这样,在调用任何程序集时,就可以直接调用了,因为程序集的脱壳会自动进行。下面是事件响应的部分代码:
/// <returns>返回找到的或临时加载的程序集</returns>
private static Assembly CurrentDomain_AssemblyResolve (object sender, ResolveEventArgs args)
{
Assembly ret = null;
try{
AppDomain dm=(AppDomain)sender;
string dllName= args.Name.Split(',')[0];
//用xx开头的文件表示加密过和DLL,区别于其它的DLL
if (dllName.StartsWith("xx")&&!dllName.EndsWith ("resources") )
ret = asmload( dllName );
}
catch (Exception ae )
{ MessageBox.Show("加载数据集" + args.Name + "时出错" ); }
return ret;
}
以上软件保护方案的安全性能由DES算法(若采用)和机器指纹的安全性决定。DES算法的安全主要决定于对密钥保护。另外,由于密钥来自于机器指纹,而指纹数据来自于机器硬件信息,虽然,取指纹的算法要随软件外壳程序一起发布,但从指纹到密钥要经过自己设计的保密算法来转换,且保密算法不公开,所以算法本身是安全的。因此,密钥的安全还取决于指纹提取算法的安全性和指纹转密钥的算法安全性,而它们的安全性又取决于第三方加密或混淆的强度了,这是本方案所无法控制的,这也许就是此方案安全性最薄弱的环节。
对于用商业加壳软件的攻击较多,所以一旦商业加壳技术被破解,用它加密过的软件便没有安全了。但对于自编的加壳技术,加密技术可以自行设计(保密),并且可以随时调整,而且,用户数不多,因此,攻击者比较少,用这种技术加密的软件安全性也就相对更好。使用本文中的加壳技术,可以更有效地抵抗拷贝、反编译、分发序列号、注册机各种常用的软件破解方法。然而,这种自编加壳技术虽然安全,但软件分发起来却十分复杂,这也是此方案的美中不足之处。
[1] John Robbins. Microsoft .NET & Windows Application Debug[M]. Beijing: Tsinghua University Press, 2004.6:50
[2] 彭明柳.Visual Basic.NET彻底研究[M].北京:中国铁道出版社,2003.2:13
[3] Ethan Milleretal. Strong Security for Disbributed File Systems[C]. In Proc of 2001 IEEE international Conference on Performace, Computing and communications, 2001:34~40
[4] 刘晓华.精通.NET核心技术——原理与构架[M].北京:电子工业出版社,2002.8:183~231
[5] Richter J. Programming Applications For Microsoft Windows[M]. Microsoft Press, 2000.