.net预防反编译
Dotnet是一种建立在虚拟机上执行的语言,它直接生成 MSIL 的中间语言,再由DotNet编译器 JIT 解释映象为本机代码并交付CPU执行。它和Java是一种机制的语言。这种语言的优点就是您不需要去考虑您的程序在那里运行,您只需要把功能做出来,虚拟机会在任何地方实现您的功能。这是一个很好的趋势和想法,但虚拟机的中间语言由于带了大量的“元数据metadada”信息,所以也极容易被反编译。
Dotnet的保护分为三大类
由ms 提供的非第三方保护方案
a) 强名称
强名称是MS提供的保护机制。
它需要使用 sn 这个命令。
强名称是什么意思呢?在这里稍作解释。强名称的作用就是防止程序集被非法修改,当对程序集修改后,必须重新用您的私钥再对程序集加一次强名称,这也是如果含有强名称的程序集在混淆或加密后必须要重新加强名称的原因。
Sn / ? 可以看到它的使用方法,如果你安装的 Framework是中文的,那么参数的解释也是中文的,我就不多讲了。
那么强名称有用吗?网上轻松破解强名称的方法很多,Ildasm反编译加过强名称的程序集后,在IL文件中将强名称的相关信息去掉,再利用Ilasm编译,就可以解除强名称的限制了。强名称的PublcKey不管是加在程序集中,还是加在Class中,都可以被去掉,所以强名称不是一个完善的保护方式。
使用StrongNameIdentityPermissionAttribute类
这个类允许你将组件(或类、方法)与某一强名称(通常就是你发布程序时所用的强名称)绑定,这样,只有在客户端程序具有该强名称签名的情况下才能使用你的组件。也就是说,除了你自己编写的客户端代码因为拥有同样的签名而被允许使用组件以外,任何第三方代码都无法通过StrongNameIdentityPermissionAttribute的防护,因此也就无法恶意调用你的组件了。
为了简便起见,先创建一个很简单的Class Library项目,代码如下:
// SecureComp.dll
using System;
namespace musicland
{
public class SecureComp
{
public string Confidential()
{
return "This is confidential!";
}
}
}
首先引入System.Security.Permissions命名空间:
using System.Security.Permissions;
然后,在组件级加上StrongNameIdentityPermissionAttribute属性:
[assembly: StrongNameIdentityPermissionAttribute(SecurityAction.RequestMinimum,
PublicKey="0024000004800000940000000602000000240000525341310004000001000100c11c8497d”+“283259f23d645358d65812b69136846b03a7d15124545fc3ed27d89d1330cceda4232c7bc6e8a0e7ecd857f8”+“126d0859e2300237b3cab6f7737a92f585cbf2afb4b475c537703efb96e17e5921ff00c6e022b22f3d772f14”+“6a3a5c7f6ccad3131b8d0465e6709e5a28cc3ca1c8b610af4162c1a18c0feb8e6993ab1")]
namespace musicland
…
注意,这里使用了SecurityAction.RequestMinimum,这申明除非获得StrongNameIdentityPermissionAttribute所表明的资源访问权限(即对SecureComp.dll组件的访问权限,可以把SecureComp.dll看作一样资源),否则CLR不会准许调用方(即客户端代码)访问所请求的资源;此外,在PublicKey属性中加入了你所允许的公匙(Public Key)的十六进制表示(转化成字符串类型)。CRL在运行期间将依照这一段公匙来判断调用方是否合法,除非调用方拥有相应的私匙(Private Key),否则将无法访问。看来,平时一定要倍加保护你的密钥文件,因为密钥文件(特别是private key)的泄露将会成为你无尽恶梦的根源,而延迟签名(delay signing)在这里也就显得格外重要了
通过Sn.exe工具就可以把PublicKey给提取出来。打开命令行,定位到密钥文件所在目录并输入以下内容:
sn –p Key.snk PublicKey.snk
这样,提取出来的公匙信息就被存储在PublicKey.snk文件中。你现在只需使用Secutil.exe把公匙信息读取出来并转化成适当的格式就可以了。
b) 编译MSIL为本机代码(误区?)
关于这一点,我经常能在MS上的社区看到有MVP这样面对问题:
问:C#写的程序能编译成本机代码吗?
答:可以,使用 Ngen.exe 即可以 MSIL 代码编译为 本机代码。
MVP这样回答错了吗?其实,严格的说,MVP的回答是没错的,Ngen.exe的确是可以将 MSIL 编译为本机代码,并可以使JIT不需要进行再次编译MSIL。这样能加快程序的执行效率。
但用户这样的问题其实,并不是对执行效率不满意,而是对中间语言不满意,可惜 Ngen 并不能解决用户的问题。
让我们来浅浅的分析一下 Ngen的工作吧。
Ngen是MS提供的 本机映象生成器,它可以将中间语言程序集编译为本机代码存放在缓存中。这里请大家注意,是存放在缓存中,Dotnet在内存中建立了一个缓存,这个缓存中存放了许多常用的程序集编译后的本机代码,它们是常驻的,由此来加快Dotnet的执行速度。
所谓一个本机代码,因为本机映射时,会映射出一些 Framework 里需要的Method,编译为汇编就是 Call 0x0200000这样的样子,而这些东西必须是事件编译好的。那么理论上说 Ngen 必须要在当前执行的机器上运行,而直接编译成本机代码的程序copy到另一个地方不一定可以用。Ngen.exe 只是一个提速的工具,因为要执行编译为本机代码必须还是要原程序集,而原程序集中存在MSIL,所以让程序无法脱离被反编译的目地。
以上是ms提供的工具,下在讲讲,自己在编程的过程中,如何使用技巧来防止破解或反编译。
编程技巧保护方案
在这里,我会给大家介绍两种三种方式
1. 人为混淆
混淆顾名思意,就是混乱,不明确的意思。MetaData中都有一个Rid,程序集运行时就已经和名称没什么关系了,都使用Rid来调用的,所以可以将名称省去。
什么叫人为混淆呢,就是人为的制造混淆。
曾经看过一个程序集,手工的将一个Method折成几十个或上百个,从而达到让你看不懂的目的。不过可惜的说一句:现在的Dotnet程序集的分析工具都很强大,正引用,反调用都可以用程序来实现,所以即实这么做,了没多大用处。著名的Reflector就有这些功能。
2. 隐藏程序集
刚刚谈到了Reflector,它就是使用这种方式来隐藏自己的核心程序集的。相信我,Reflector并不是您看到的那一个可执行程序,它的可执行程序只是一个壳而以,里面是一个定义和接口,没有实例的方法。如果你想得到他是怎样反编译的核心,恐怕你会在它这个迷宫中迷失方向。
它的核心程序集事实上就是它的一个资源。而这个资源是一个加密的资源。他应该是在双击第一个需要反编译的Method的时候开始释放这个资源,并对资源解密然后动态的加载。这样做的优点核心程序集是不会在硬盘上留下任何痕迹的,它只解在内存中解密并被加载,你基本上无法得到这个程序集。而且Dotnet是不允许内存 Dump的。
大家是不是觉得这种保护方法不错呢?你可以把你的核心代码加密后做成资源包在程序里,在使用的时候再解密出来,这只需要你自己去实现就可以了。
不过我还得说句负责任的话,如果你有精力,并且很有耐心和技术,相信你还是可以在几天时间内找出它的核心程序集解密算法的位置。并成功的解出它的资源程序集。
如果是高手又非常有经验,这种方式的加密手段应该是秒杀。
3. 将程序集中的相关Method(方法)编译成Unmanaged(非托管代码)
下面介绍的内容无法得到核心代码
它可称之为终极的保护手段,因为它就是“非托管代码”。
什么是托管代码,什么是非托管代码。
托管代码就是基于.net元数据格式的代码,运行于.net平台之上,所有的与操作系统的交换有.net来完成,就像是把这些功能委托给.net,所以称之为托管代码。非托管代码则反之。
下面要介绍的方式就是如何在自己的程序集中即拥有托管代码,又拥有非托管代码。注意,非托管代码是无法被现在的反编译工具反编译的。
在Dotnet程序集中,允许托管代码和非托管代码共存,怎样实现呢?这并不是无偿的,这是需要条件的。它的条件就是必须使用VC++.NET非托管方式来写dll,再用VC++托管方式建立工程引入这个本机代码的dll。最终生成一个Dotnet程序集的dll。那么这个程序集里面即有托管代码,又有非托管代码。托管代码是可以反编译的,而非托管代码不可能被反编译。
有人可能要问了,这和自己用VC++写个dll有什么区别?区别就是这样的结合更紧密一些,而且也不能用常规的分析Asm的工具去分析这个dll。
写注册算法,并生成dll供给Dotnet程序集调用,防止破解。其实这句话只说对了一半,这只能增加破解注册机的难度,并防止不了破解。为什么呢?因为注册对不对还是要在Dotnet程序集中进行判断,所以,只要改掉这个判断,一样达到了破解效果。但是如果要分析注册算法,那可就是困难了一些了。
第三方保护工具
第三方保护工具较好的厂商有:
1. Aiasted.SOFT
a) 产品 :MaxtoCode ,种类 :加密、混淆
2. PerEmptive Solutions
a) 产品 :Dotfuscator Community ,种类 :混淆
3. Remotesoft
a) 产品 :Remotesoft Protect ,种类 :加密
b) 产品 :Remotesoft Dotfuscator ,种类 :混淆
4. XenoCode
a) 产品 :XenoCode ,种类:混淆
第三方工具的保护方式分类
1.混淆
这是目前最流行的方式吧。今天我们就来做个剖析。让大家去衡量一下混淆的强度如何。
混淆软件一般都有三个功能
1.字符串加密
字符串加密可以分为两类,第一类是混淆保护中的字符串加密技术。主要特征是修改代码执行路径。大部分混淆保护工具的字符串加密都是这一类。
加密前: MessageBox.Show("Hellow World!");
加密后: MessageBox.Show(Helper.Decode("A34579dfbbeyu346563345/=="));
第二类就是加密壳中的字符串加密技术。这种不用修改IL代码,直接对元数据中的字符串加密。这一类以remotesoft,maxtocode为代表。
实现就是直接对元数据中的String流进行加密,这类保护有一个缺陷,程序运行后 元数据中的String流会解密后在内存中完整还原。
2.名称混淆
最简单的混淆是名称混淆,即将命名空间名、类名、方法名、字段名等统统换成特殊符号或其它符号,目的就是让你不能与以前的名称建立关联。达到把你弄糊涂的目地。
比方如下代码所示:
public class Register // 一个注册类
{
public bool IsRegistered() // 判断是否已注册的方法
{
return true ;
}
}
这样的代码在程序编译后,名称完全被保留,但如果经过名称反混淆以后,它将变成这样:
public class a // 一个注册类
{ public bool a() // 判断是否已注册的方法
{ return true ; }
}
我们现在深入的谈谈它的优点:
名称混淆,如果使用短名称及不可见字符,将会缩小程序集的大小
名称混淆,因为名称混淆也只能骗骗门外汉和小孩
那么它有什么缺点呢?
名称混淆的缺点并不多,只有一个,而且非常致命,这就是有时候,当修改了类名之后不能执行的问题。
一般来说,这种情况在 DLL 身上发生的更多,但在 EXE 身上也经常发生。
因为 DLL 的某些 Public 方法是对外的结口,在程序开发和调试的时候使用的源名称,当混淆以后,天知道把这些方法改成了什么名称,所以调用肯定报错。处理办法:不混淆对外提供的 Public 方法
EXE 和 DLL 还有一个共同的容易出错的地方就是资源,混淆器也可以混淆资源名称,这样,就存在的同样的问题――“无法找到资源而报错(动态 Load 资源的时候)”,处理方法:不混淆程序内部调用的东西。
3.流程混淆
原理基本上是一样,即把方法中的代码分为几段,并把每一段都错开,然后利用“跳转”语句连接原来的流程逻辑,并达到执行正确的目地。原理图如下表所示:
块编号 块代码
1 第一个功能
2 第二个功能
3 第三个功能
4 第四个功能
块编号 块代码跳转
1 第一个功能 Jmp 2
4 第四个功能
3 第三个功能 Jmp 4
2 第二个功能 Jmp 3
基本流程混淆原理即是上表所示,总结就以下这么几个字:破坏原有程序结构,并利用Jmp语句接连原有流程。
如果你要手工混淆你的代码,你需要做以下几件事:
把源代码分成几块
把这么几块的顺序打乱
用 br.s 对这几块的顺序进行连接,并保护执行达到原来的逻辑
重新计算行号
这样,你就能拥有自己的流程混淆了。如果你加入真真假假的逻辑跳转来混淆,强度将会更大。
反混淆
名称混淆 - 反混淆
名称混淆反混淆,基本上是不太可能的事,因为以前的名称已经换掉了,也没有第二个名称备份表,所以根本无法还换。
不过,可以把不可见字符转换为可见字符,长字符串换成短字符串。
有两种方法可以做处理:
在 MetaData 中有一个区域叫做 _STRING 它存放了所有名称字符串,只要修改这里的内容,即可,此方法需要对元数据结构特别熟悉
如果你对元数据不了解,没关系,你可以用 ILDasm 把混淆后的程序集反编译,然后一个一个的对应改过来,再用 ilAsm 编译,一样可以达到反混淆的作用
流程混淆 - 反混淆
流程混淆,在上面已经给出例子。它才是有用的一种混淆方式。它改变流程的存放序顺,从而达到静态反编译的功能。(名称混淆还是可以反编译)
不过,不管怎样,他没有办法去阻止读取 IL ,这就是流程混淆的天生不足。反流程混淆也相当的容易,只要按照执行流程加入特定的条件即可以得到代码的序顺。