适用于 .NET 的代码保护框架






3.96/5 (9投票s)
一篇关于代码保护以及为 .NET 语言提供灵活支持框架的文章
引言
软件开发人员为了谋生(物质和精神上的),可能会发现有必要直接或间接地将其宝贵的脑力劳动和劳动成果“出售”给某些人。这里的“某人”是指他的老板、某个组织/事业,或是中间/最终客户。“出售”一词在此处并非一定指代现金回报,而是更广泛的意义。例如,在开源社区中,开发人员“出售”他的贡献,并从中获得成就感、归属感,以及分享和帮助他人的精神,获得改进或使用他人贡献的权利,以及这种开发模式带来的其他技术优势。本文并非有意或无意地反对这一点,而是从一个正交的角度来看待它。CryptoGateway 开发的技术能够使“出售”(如上所述)给“某人”的过程,对于权利所有人(例如,拥有公共许可的开源社区的服务提供者)和“某人”来说,更加可控。
本文介绍一种用于 .NET(可能也包括 Java)程序集(assemblies)的访问控制技术及其支持框架。这是一篇架构概述,因此不包含可以直接使用的完整程序。本文提出的框架是通用的,不依赖于需要它的访问控制技术。由于作者不熟悉 Java 编程,因此本文其余部分将使用 .NET 术语来指代特定的概念。
.NET 等框架中的程序集包含关于程序集中类型层次结构的广泛的自反元信息,这些信息可以使用 .NET 框架本身进行查询。这些信息是设计、开发、操作(例如,将操作转换为元系统的数据)和维护代码的出色语言特性。然而,当软件系统中至少某些程序集的内部细节不打算暴露或公开时,这就不再是一个受欢迎的特性了。代码的公开或授权共享通常发生在源代码层面,程序集通常不用于此目的。这是因为存在(或可以低成本地开发)可以用来逆向工程已给出完整信息的程序集的工具。不可否认,当前软件行业的趋势正转向面向服务的模式,服务提供商将其大部分重要程序集托管在自己的服务器上。但仍然需要将一些程序集分发给最终用户,这些程序集很可能包含
- 作者不希望其被探测和/或操纵,例如,由于法律责任的原因。
- 客户需要被保证这些程序集不包含未声明的功能,作者应对不需要的功能(如后门、木马等)负责。
一种尝试实现程序集保护的方法是使用 代码混淆。另一种方法(或其他方法之一)是程序集加密(可选择附加数字签名)。这里不讨论这些方法的优缺点。如果需要,这两种技术可以结合使用,特别是当采用非侵入式加密方法时,例如我们的 P2P 数据访问控制技术(参见,例如,1、2、3,以及 本文简要讨论的媒体播放器)来实现分层防御。
框架
A. 主机
一个被认为是通用且足够灵活以支持代码保护技术和由此产生的开发文化的框架,应该具有或能够发展成具有以下特性的框架:
- 存在一个主机程序集,负责加载、管理和提供任意一对子组件之间的消息传递服务。主机原则上可以是一个父主机的组件。它们共同构成一个松耦合的、动态的对象图。消息服务的設計不應導致本應鬆耦合的組件之間產生隱含的緊密耦合,例如,直接使用委托/事件無法防止隱含的緊密耦合以及由於不良設計或缺乏其他組件實現細節而產生的潛在死鎖或無限循環。需要的是一種異步消息傳遞機制,其中主機充當調解者。
- 受保护的程序集从通用流(最终成为字节数组)加载。出于安全考虑,从字节数组加载的程序集在 .NET Framework 中与从本地磁盘文件隐式或显式加载的程序集处理方式不同,在本框架中也应如此。发现插件框架符合需求。
- 插件框架必须在以下方面具有灵活性:
- 包含子组件的受保护程序集通过符合套接字(不是网络套接字 :-)) 的可编程接口与主机松散耦合,该接口可以在运行时按需加载和卸载。如果需要,它们也可以被管道化。
- 只要某些接口(契约)未发生变化,主机就不应与特定版本的子组件集合紧密绑定。
主机可以作为 .NET 语言的脚本引擎。卸载能力提供了必要条件。这对于服务器应用程序和实时调试场景很重要,在这些场景中,主机在其生命周期内支持许多子组件,而这些子组件要么无法停止,要么停止成本太高,但其中一些子组件在此期间需要进行更改(在源代码层面!)。
灵活性也是必需的,原因如下。首先,许多开发人员认为它们是开发松耦合的分布式服务/智能客户端应用程序的最佳方法。CryptoGateway 的技术进步使此类系统的实际部署离现实更近了一步。
- 允许管道分解,其中一个套接字可以与实现特定接口的特定序列中的一个或多个处理程序关联(参见图 1)。
- 等等。
有许多属于各种设计模式的 .NET 插件框架,本文由于篇幅限制将不讨论或比较任何一个。主机如何在子组件之间实现异步消息传递在此也不涵盖。图 1 展示了一个示意性的 UML 图,说明子组件如何连接到主机以及彼此连接,以满足上述要求。接口和套接字与组件之间存在一对多的关系。组件是套接字的一部分。每个套接字都可以允许一个由一个或多个组件组成的有序处理管道插入。下文将对此进行更详细的说明。
B. 连接
本框架基于接口。顶层开发“流程图”如下图所示。
步骤 1:接口规范
将组件连接到主机(主机先前不知道该组件)的尝试的第一步是提供一个接口,该接口声明对主机开放的属性和方法,以及一组套接字(见下文)。
接口至少通过两种方式引入:
- 在自顶向下开发导向的过程中,首先设计接口,然后实现。当实现过程中产生需求时,过程将进行迭代。此过程要求设计者对组件具有足够的前期知识,这些知识来自先前的经验或原型构建。
- 在自底向上开发导向的过程中,首先开发组件或组件已预先存在。需要将其插入主机。在这种情况下,应首先识别导出的属性和方法,然后将相应的接口作为泛化产生。该过程也期望进行迭代,直到达到设计的总体目标。
步骤 2:组件套接字

在某个迭代中完成步骤一,或者如果组件实现了现有接口,并且这是第一次尝试将其插入主机,或者需要将其插入一种新型套接字,那么就必须设计和实现套接字。
套接字充当从主机到组件(序列)的调用请求的调度程序,该序列源自同一接口。它还负责加载和卸载包含实现该接口的程序集。它从 XML 配置“脚本”初始化,该脚本在处理它所处理的组件之前或之前到达的执行分支中。套接字应在单独的 AppDomain
中创建,以便主机能够卸载属于它的程序集集合。它还应派生自 MarchalByRefObject
(或其等价物),以便主机可以通过对象(远程处理)代理与其通信。在此阶段初始化的信息属于套接字类,该类对于它的所有实例都是通用的。它的一个实例在其自己的 AppDomain
中创建,以促进初始化(static
成员变量),然后被丢弃而不创建任何组件。图 2 右侧顺序图中的 6-7 代表初始化过程。
当需要使用其中一个组件时,首先创建一个持有该组件的套接字实例,然后由它加载相应的程序集集合。加载过程对主机是隐藏的,由图 2 中的调用堆栈 9->10->11->12 表示,稍后将对此进行讨论。安全程序集加载器本身就是一个套接字,持有 CryptoGateway 服务器内部的进程服务器的客户端,身份验证和解密在此执行。在加载相应的程序集后,将创建配置为附加到套接字的那组类型的实例,这由图 2 中的调用堆栈 13->14 表示。创建套接字后,所有配置为附加到它的对象也会被创建。这些对象及其类型用于通过反射访问它们的成员。下文将提供更多详细信息。
方法调用和属性访问现在可以由套接字委托给组件对象(如果配置为这样,则为对象管道),套接字还可以配置为执行日志记录、异常捕获和处理等。接口还可以定义一组事件供组件回调主机,这些事件可以
- 处理它
- 将调用委托给其他子组件
- 将其向上级主机层次结构传播
图 2 中的同步版本由 20->...->27 说明。对于上面建议的异步版本,调用 20 在 21 之后立即返回(26->27),主机负责处理消息。典型的套接字调用处理程序如下所示:
namespace Media
{
public class Mp3PlayerSocket : IStreamClient
{
.....................
public void LoadPlayList(System.Collections.ArrayList list)
{
try
{
if (!pipeline)
{
if (miLoadPlayList==null)
miLoadPlayList = otype.GetMethod("LoadPlayList",
new Type [] {typeof(System.Collections.ArrayList)});
miLoadPlayList.Invoke(o,new object [] {list});
}
else
{
foreach (string key in ttable)
{
otype = atable[key] as Type;
ArrayList item_list = new ArrayList();
miLoadPlayList = otype.GetMethod("LoadPlayList",
new Type [] {typeof(System.Collections.ArrayList)});
o = ol[key];
miLoadPlayList.Invoke(o,new object [] {item_list});
list.AddRange(item_list);
}
}
}
catch (Exception ex)
{
LogError(ex);
}
finally
{
................
}
}
private static MethodInfo miLoadPlayList = null;
..........
}
}
其中 IStreamClient
是接口。典型的套接字配置文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<config>
<sactserver ip="127.0.0.1" port="1221" path="/"
channelexpires="120" channelpersists="false" />
<holder>
<!--Auto generated nodes, edit with caution!-->
<assembly secured="true"
default="true" feature-index="0"
atoken="/MediaPlayer/MediaClients_0.ctk"
id="e6f511d2-f9f9-4ad6-b700-2505b92c64dc"
depend_id=""
name="MediaClients, Version=0.2.2059.28511,
Culture=neutral, PublicKeyToken=null"
display="Media Clients">
<description />
<server ip="127.0.0.1" port="1221" />
<gui type="Media.Mp3Player,Mp3Player" show-background="images\Img12345.jpg" />
<interface type="Media.IStreamClient" wrapper="Media.Mp3ClientSocket" >
<type name="Media.Mp3Client" activate="true" ext=".mp3" descr="MP3 Audio"/>
<type name="Media.Equlizer" activate="false" ext=".mp3" descr="MP3 Filter"/>
</interface>
</assembly>
</holder>
</config>
步骤 3:实现
然后进行实现。在此过程中,可能会出现需要转到上一步的需求。
步骤 4:测试过程
然后进行测试。在此过程中,可能会出现需要转到上一步的需求。
步骤 5:完成?
如果未完成,请转到上一步,否则退出流程。
C. 实现
上述设计目标中的大多数已在 CryptoGateway 的各种 .NET 程序中实现并经过测试,这些程序正在内部使用、评估或公开下载。虽然没有一个程序包含了所有这些目标的实现,但它们的组合并没有预见的障碍。
程序集保护和分发

简介
CryptoGateway 的数据访问控制技术基于声明式身份管理和访问权限分发系统,使用加密(RSA 和 AES)手段。它可以应用于任何数字数据。通过电子邮件等常规通信渠道,使用包含生产者和用户身份验证信息的软令牌分发访问权限。软访问令牌使用相同的访问控制方法进行保护。生产和分发过程如图 3 所示。感兴趣的读者应阅读上面提供的链接文章以获取更多详细信息。
.NET 程序集
在加密镜像生成、访问令牌分发和用户访问方面,它们没有特殊之处。有关如何实现这一点的更多详细信息,请访问 此处。特殊之处在于客户端,在图 1 中称为“安全程序集加载器”,它由 CryptoGateway 服务器托管的进程服务器提供。它专门设计用于与上述服务器通信以加载 .NET 程序集、连接到身份验证用户界面等。用于 媒体播放器 的客户端是用 C# 编写的。进程服务器负责实际的身份验证用户工作。它还在后台确保数据确实由生产者签名。仅在用户通过身份验证且生产者数字签名得到验证后,才会加载有效数据。
在上述加载器加载数据后,它将尝试通过调用来生成程序集:
Assemb assembly = Assembly.Load(data);
Type type = assembly.GetType("...type name ...");
ConstructorInfo cinfo = type.GetConstructor(....);
object obj = cinfo.Invoke(args);
....
MethodInfo minfo = type.GetMethod(...);
minfo.Invoke(...);
....
PropertyInfo pinfo = type.GetProperty(...);
pinffo.GetValue(obj,...);
pinfo.SetValue(obj,...);
......
FieldInfo finfo = type.GetField(...);
finfo.GetValue(obj);
finfo.SetValue(obj,...);
......
其中“data
”是包含由进程服务器恢复的序列化程序集数据的字节数组。在成功构造程序集后,可以使用反射来创建实例和访问其成员。在所有组件线程停止后,可以通过主机调用 Assembly.Unload()
来卸载此程序集。
在设计阶段,应将系统中使用的各种功能单元划分为一组程序集,同时考虑到 .NET 程序集不是 MarshalByRefObject
,因此每个 AppDomain
必须有自己的重叠程序集版本。为了减少冗余和潜在的不一致性,最好努力减少或消除重叠。
分解还应着眼于逻辑分离职责。主机作为上层,应以尽可能少的方式处理各种组件,这可以通过使组件尽可能自给自足和自描述来实现。这在本应文章中称为“最大对称性”原则。读者可能会在信息/统计学相关科目的研究中遇到类似的原则,称为“最大熵”,其根源在于统计物理学(可以尝试搜索本网站,读者会发现一些)。“对称性”,与某种不变性有关,被使用是因为有些东西知道将被放置在适当的顺序中。对于工程来说,这是一个更好的术语。“熵”另一方面,与未知的事物有关,这些事物将要被知道或学习。事实上,更好的学习策略也包括将接下来需要知道的东西以正确的顺序排列,更好的工程也应该承认存在未知。在艺术方面,该原则可能导致代码的更大简化。在实际方面,它提高了代码的可重用性(不变性!),降低了维护成本,并为未来的扩展打开了可能性。遵循这一原则的实践也预期会提高代码性能,因为通过套接字->主机->套接字层和 AppDomain
边界传递消息(调用请求)预期效率不高。
诚然,上述形式化过程从执行实际工作的类生成大量高级程序集和类(接口、套接字、配置文件等)。在迭代代码开发过程中,这些类之间的同步非常繁琐,并且可能容易出错。实际上,可以通过利用 .NET 和其他框架的反射能力来自动化这个预处理工作。使用软件工具,例如 x-script 生成器,可以大大简化任务并减少代码不一致的可能性。后处理,包括编译、数字签名、加密、初始访问令牌分发、注册、打包、上传等,也可以通过这些工具进行简化。
关注点
可以预期,将相同的思想应用于 Java 程序集(字节码)是直接的,因为这两个框架在与代码保护和此处引入的插件(-out)框架相关的方面具有很多相似之处。
正如读者所知,本文尽可能以通用形式撰写,它提供了一个理想或背景参考,希望能促成允许稍后以更少的精力向更灵活的系统过渡的设计和结构。对于简单的应用程序,这有点大材小用。在实际实现中,根据规模、灵活性和未来扩展需求,许多套接字层、接口定义和代码保护步骤可以被跳过或短路,至少在系统的早期阶段/版本中,仅在因子化过程中考虑到扩展的可能性。
待办事项
继续扩展“等等”,等等。
历史
- 2006 年 5 月 25 日:首次发布