摘要 .NET 体系结构的一个优势是:利用它构建的程序集包含了可以使用 ILDASM 进行恢复的很多有用的信息以及中间语言反汇编程序。尽管存在一个负面影响,即可以访问您的二进制文件的人可以恢复与原始源代码非常接近的代码。此处作者提供的程序模糊处理作为一种阻止反向工程的方法。此外,他们还讨论了可用的不同类型的模糊处理技术,并说明了包含在 Visual Studio .NET 2003 中的新的模糊处理工具。
到目前为止,您可能已经熟悉了元数据丰富的 Microsoft®.NET Framework 体系结构为表格带来的所有好处,从减轻部署和版本控制的负担到由自说明的二进制文件所启用的丰富的 IDE 功能。您可能不知道所有元数据的简单可用性已经引入了一个问题,直到目前为止尚未被大多数开发人员所关注。为通用语言运行时 (CLR) 编写应用程序对于反向工程来说越来越简单了。在 .NET Framework 中不允许出现任何错误,它仅仅是现成的中间编译语言(Java 语言应用程序表现出相同的特性)。Java 和 .NET Framework 都使用嵌入到可执行代码内部的丰富元数据:在 Java 中为字节代码,在 .NET 中为 Microsoft 中间语言 (MSIL)。比二进制机器码高级很多的可执行文件包含有可以被轻松破译的信息。
利用像 ILDASM(.NET Framework SDK 附带的 MSIL 反汇编程序)这样的工具或诸如 Anakrino 和 Reflector for .NET 这样的反编译程序,任何人都可以轻松地研究您的程序集并利用反向工程将它们转换为可读的源代码。黑客可以搜索可利用的安全漏洞、盗取独特的思想,甚至破解程序。这足以让您犹豫一阵。
但是,请不要担心。有一个解决方案,即模糊处理,它将帮助您防止反向工程。模糊处理是一种提供程序集中无缝重命名的符号以及阻止反编译程序的其他技巧的技术。正确应用该技术后,模糊处理可以极大地增加免遭反编译的保护,而使应用程序不受任何损害。模糊处理通常用于 Java 环境中,很多年来一直用于公司保护基于 Java 的产品的知识产权。
很多第三方根据需要创建了适用于 .NET 代码的模糊处理程序。Microsoft 在与我们公司 PreEmptive Solutions 的合作中将 Dotfuscator Community Edition 包括在 Visual Studio®.NET 2003 中,我们的公司提供了多种模糊处理程序包。
使用 Dotfuscator Community Edition,本文将教您有关模糊处理的所有知识(以及一些有关反编译的知识)、通常可用的模糊处理类型以及当使用模糊处理程序时您需要注意的一些问题。
要说明反编译和模糊处理,我们将使用一个经典 Vexed 游戏的开放源代码实现。Vexed.NET 是由 Roey Ben-amotz 编写的,位于 http://vexeddotnet.benamotz.com。这是一个智力游戏,您的目标是将相似的块移动到一起,之后它们就会消失。下面就是来自 Vexed.NET 源代码的简单方法:
public void undo() {
if (numOfMoves>0) {
numOfMoves--;
if (_UserMoves.Length>=2)
_UserMoves = _UserMoves.Substring(0, _UserMoves.Length02);
this.loadBoard(this.moveHistory[numOfMmoves -
(numOfMoves/50) * 50]);
this.drawBoard(this.gr);
}
}
反汇编
.NET Framework SDK 提供的名为 ILDASM 的反汇编实用工具,允许您将 .NET Framework 程序集反编译为 IL 程序集语言语句。为了启动 ILDASM,您必须确保已安装了 .NET Framework SDK,并在命令行上键入 ILDASM,后跟要进行反编译的程序名。在我们的例子中,将键入“ILDASM vexed.net.exe”。这将会启动 ILDASM UI,可以用来浏览任意基于 .NET Framework 的应用程序的结构。
反编译
如果您现在认为只有那些真正了解 IL 汇编语言的少数人会查看和可以理解您的源代码,那么请记住反编译并非止步于此。我们可以使用反编译程序重新创建实际的源代码。这些实用工具可以将 .NET 程序集直接反编译回高级语言,例如 C#、Visual Basic®.NET 或 C++。让我们看一下由 Anakrino 反编译程序生成的 undo 方法:
public void undo() {
if (this.numOfMoves > 0) {
this.numOfMoves =
this.numOfMoves - 1;
if (this._UserMoves.Length >= 2)
this._UserMoves =
this._UserMoves.Substring(0, this._UserMoves.Length - 2);
this.loadBoard(
this.moveHistory[this.numOfMoves -
this.numOfMoves / 50 * 50]);
this.drawBoard(this.gr);
}
}
正如您所看到的那样,结果几乎与原来的代码相同。稍后,我们将重新回到该示例以查看在使用模糊处理后的结果。
[url=http://www.microsoft.com/china/msdn/library/langtool/vbnet/NFissues0311netcodeobfuscation.mspx#top]
深入模糊处理
模糊处理是使用一套相关的技术完成的。它的目标就是隐藏程序的意图,而不更改其运行时行为。它并不是加密,但在 .NET 代码的上下文中,它可能会更好。您可以加密 .NET 程序集以使它们完全不可读。但是,这种方法会面临进退两难的局面 - 因为运行库必须执行未加密过的代码,而加密密钥必须保存在已加密的程序中。因此,可以创建一个自动的实用工具来恢复密钥、解密代码,然后将 IL 以其原始的格式写入磁盘。只要发生这种情况,程序就完全暴露于反编译。
作一个比喻,加密就像将六道菜锁入了一个带锁的盒子中。只有希望进餐的人(在这个例子中是 CLR)才有钥匙,我们并不想让其他任何人知道他或她想吃什么东西。遗憾的是,就餐时食物将会被所有旁观者一览无余。模糊处理工作就像是将六道菜放入了搅拌器,然后将其放入塑料袋送给进餐者。当然,每个人都可以看到传递中的食物,但是除了幸运的豌豆或者某些牛肉色的糊状物之外,他们并不知道原来的菜到底是什么。进餐者仍然获得了想要的菜肴,并且菜肴仍然提供了与以前相同的营养价值(幸好,CLR 并不过分挑剔味道如何)。模糊处理程序的诀窍就是使观察者糊涂,同时仍然为 CLR 提供相同的产品。
当然,模糊处理(或者加密)并不是百分之百的安全。即使编译的 C++ 也可以被反汇编。如果黑客足够有耐力,她可以重新生成您的代码。
模糊处理是应用到已编译的 .NET 程序集而不是应用到源代码的过程。模糊处理程序不会读取或更改您的源代码。图 2显示了模糊处理过程的流程。模糊处理程序的输出是另外一套程序集,在功能上与输入的程序集相同,只是改变了阻止反向工程的方式。现在,我们将考虑 Dotfuscator Community Edition 所使用的两个基本技术来达到该目标:重命名和删除不必要的元数据。
重命名元数据
模糊处理的第一道纺线就是使用无意义的名称重命名有意义的名称。正如您所知道的那样,经过慎重选择的名称有非常大的价值。它们有助于您的代码进行自我说明,并可以作为有价值的线索来揭示它们所表示项目的目的。CLR 并不介意名称的说明性如何,因此模糊处理程序可以毫无限制地对它们进行更该,通常将其更改为一个字符的名称,如“a”。
很明显,对于模糊处理程序可以在某个特殊应用程序上执行重命名的数量,还会有许多限制。一般而言有三种通用的重命名方案。
如果您的应用程序由一个或多个独立的程序集组成(即,不会有未模糊处理的代码依赖于任何程序集),那么模糊处理程序可以无限制地重命名程序集,无需顾及名称的视觉效果,只要它们的名称和引用在程序集集合中保持一致即可。Windows®Form 应用程序就是一个很好的示例。在相反的极端,如果您的应用程序设计为由未模糊处理的代码使用,那么模糊处理程序无法更改类型的名称或者对那些客户端可见的成员。该类型应用程序的示例是共享的类库、可重用的组件及其他。在某些位置中间是要插入到现有未模糊处理框架中的应用程序。在这个例子中,模糊处理程序可以重命名不被模糊处理特定环境(该模糊处理程序在其中运行)所访问的任意内容,无需考虑可视性。ASP.NET 应用程序是这种类型应用程序的典型示例。
Dotfuscator Community Edition 使用名为“重载归纳”的专利重命名技术,该技术向重命名中添加了转换。在彻底的作用域分析后,方法标识符会最大限度的过载。不再使用新的名称替换每个旧的名称,重载归纳技术使用尽可能多的方法重命名为相同的名称,进而迷惑试图要理解反编译代码的任何人。
此外,还有一个很好的附带作用,应用程序的大小会由于包含在程序集中字符串堆的减小而相应的减小。例如,如果您有一个长为 20 个字符的名称,将其重命名为“a”会节省 19 个字符。此外,连续地重用名称会由于保存字符串堆项而节省空间。将所有名称重命名为“a”意味着“a”仅存储一次,而且重命名为“a”的每个方法或字段都可以指向它。重载归纳增强了这个效果,因为最短的标识符可以连续地重复使用。通常情况下,一个重载归纳的项目可以将最多 35% 的方法重命名为“a”。
要查看重命名对反编译代码的影响,请在重命名过程后研究一下 undo 方法:
public void c() {
if (this.p > 0) {
this.p = this.p - 1;
if (this.r.Length >= 2)
this.r = this.r.Substring(0, this.r.Length - 2);
this.a(this.q[this.p - this.p / 50 * 50]);
this.a(this.e);
}
}
您可以发现如果没有任何其他种类的模糊处理,这种方法已经非常不容易理解。
[url=http://www.microsoft.com/china/msdn/library/langtool/vbnet/NFissues0311netcodeobfuscation.mspx#top]
删除不必要的元数据
并不是在已编译的基于 .NET 的应用程序中的所有元数据都由运行库使用。其中一些由诸如设计程序、IDE 和调试程序的其他工具使用。例如,如果您在 C# 中的某个类型上定义了名为“Size”的属性,则编译器会忽略属性名为“Size”的元数据,并且将该名称与实现 get 和 set 操作(分别为“get_Size”和“set_Size”)的方法相关联。当您编写设置 Size 属性的代码时,编译器将始终生成一个对方法“set_Size”本身的调用,并且不会通过其名称引用该属性。实际上,此处的属性名称用于 IDE 和那些使用您的代码的开发人员;它不会由 CLR 访问。
如果您的应用程序希望只由运行库使用而不被其他工具使用,则模糊处理程序删除这种类型的元数据是安全的。除了属性名外,事件名和方法参数名也属于这个类别。当 Dotfuscator Community Edition 认为将所有这些类型的元数据删除安全时,它就会这么做。
其他技术
Dotfuscator Community Edition 使用我们刚刚介绍的技术提供了很好的模糊处理,但是您应该注意到提供更强保护的其他模糊处理技术,可能会一起阻止反向工程。Dotfuscator Professional Edition 实现了很多其他的技术,包括控制流模糊处理、字符串加密、增量模糊处理和大小减小等。
控制流是一种强大的模糊处理技术,其目标就是隐藏指令结果的目的,而不更改逻辑。更重要的是,它用于删除反编译程序要查找以忠实地重新生成高级源代码语句(例如,if-then-else 语句和循环)的线索。实际上,这种技术趋向于终止反编译程序。
要查看该操作的效果,请在应用重命名和控制流模糊处理后,再次查看反编译的 undo 方法。您可以看到,取代了原来的嵌套 if 语句,反编译程序生成了一个 if 语句、两个嵌套 while 循环,以及将它们全部联系在一起的一些 goto。引用了标签 i1,但它不是由反编译程序生成的(我们认为这是一个反编译程序的错误)。
字符串加密是一项将简单的加密算法应用于嵌入到应用程序中的字符文字的技术。如前所述,在运行时执行的任何加密(或者尤其是解密)本质上就是不安全的。也就是说,聪明的黑客可以最终破解它,但对于应用程序代码中的字符串,这是值得的。让我们面对它,如果黑客要进入您的代码,他们不会盲目地开始搜索重命名的类型。他们可能确实要搜索“Invalid License Key”,这将他们正好引导到执行许可证处理所在的代码。在字符串中进行搜索出乎意料的简单;字符串加密建立起障碍,因为加密的版本只出现在编译后的代码中。
增量模糊处理帮助在模糊处理的局面下发布修补程序以修复客户的问题。修复代码中的错误通常会创建或删除类、方法或字段。更改代码(例如添加或删除方法)可能会引起随后的模糊处理运行重命名事务稍微有所不同。在前面称为“a”的事务现在可能称为“b”。遗憾的是,重命名不同的方式和不同的内容都是一个谜。
增量模糊处理可以解决这个问题。模糊处理程序创建一个映射文件来通知您它是如何执行重命名的。但是,这个相同的映射文件可以在随后的运行中用作模糊处理程序的输入,从而规定以前使用的重命名应该尽可能的再次使用。如果您发布您的产品,然后为一些类推出修补程序,则模糊处理程序可以以这种方式运行以模拟它以前的重命名架构。这样,您可以只为客户发行已修补的类。
大小减小并不严格阻止反向工程,但它值得讨论,因为模糊处理程序几乎始终必须在输入程序集集合上执行依赖性分析。这样该模糊处理程序就能够很好地进行模糊处理,并且一些更好的模糊处理程序将使用其对应用程序的了解来删除程序不使用的代码。将不使用的代码删除看起来有些多余,但它确实能够实现一定目的 - 不是有些人并不使用自己编写的代码吗?好了,这就是我们所有的回答。还有,我们所使用的库和类都是由其他人针对重复使用而编写的。
重复使用的代码表示可以处理许多情况的临时代码;但是,对于任意给定的应用程序,通常情况下您只使用这些情况中的一个或两个。高级的模糊处理程序可以确定这一点,并且可以剥离所有不使用的代码(同样,从编译后的程序集中,而不是从源代码中)。结果是输出精确地包含应用程序所需要的类型和方法 - 一点儿多余的都没有。较小应用程序的好处是节省计算资源和减少加载时间。对于在 .NET Compact Framework 或分布式应用程序上运行的应用程序来说,这可能尤其重要。
[url=http://www.microsoft.com/china/msdn/library/langtool/vbnet/NFissues0311netcodeobfuscation.mspx#top]
使用 Dotfuscator Community Edition
现在,让我们使用 Dotfuscator Community Edition 来模糊处理 Vexed 应用程序。Dotfuscator Community Edition 使用为特殊应用程序指定模糊处理设置的配置文件。它具有 GUI,可以帮助您简单地创建和维护配置文件以及运行模糊处理程序并检查输出。此外,Dotfuscator Community Edition 的命令行界面使您可以轻松地将模糊处理集成到自动化的构建过程中。您可以从 Visual Studio .NET 2003 的工具菜单中启动 GUI。
要为进行模糊处理配置 Vexed,您需要在 Dotfuscator Community Edition GUI 中指定三个项目:输入程序集、映射文件位置和输出目录。在“Trigger”选项卡上指定输入程序集(模糊处理程序将这些称为“触发程序集”)。您可以在此处添加任意多个,但是对于 Vexed 应用程序,您只需添加一个。
在“Rename | Options”选项卡上指定映射文件位置。映射文件是基本的信息块,包含原始和未模糊处理名称之间明确的名称映射。在模糊处理应用程序后保存该文件是非常重要的,如果没有它,您将无法简单地排解模糊处理应用程序的问题。由于其重要性,在默认情况下,模糊处理程序将不会覆盖现有的映射文件,除非您明确选中“Overwrite Map file”复选框。
最后,“Build”选项卡使您可以指定要放置模糊处理应用程序的目录。完成这个操作后,您就可以开始模糊处理应用程序了。您可以保存您的配置文件以便将来使用,然后按下“Build”选项卡上的“Build”按钮或使用工具栏上的“Play”按钮。在构建过程中,模糊处理程序会在 GUI 的输出窗格中显示进度信息。您可以通过在“Options”选项卡上选择“Quiet”或“Verbose”来控制在此处显示的信息量。
构建完成后,您可以在“Output”选项卡上浏览结果。正如您所看到的那样,模糊处理程序显示应用程序的图形视图,类似于对象浏览器。新名称立即出现在视图中原始名称的下面。在该图表中,您可以看到名为“board”的类重命名为“h”,并且带有不同签名(init 和 ToImage)的两个方法都重命名为“a”。
检查映射文件
Dotfuscator 所产生的映射文件是 XML 格式的文件,除了已经提到的名称映射外,它还包含有关重命名过程效率的一些统计。
映射文件也用于执行增量模糊处理。该过程使您可以从以前的运行中导入名称,这会通知模糊处理程序以它以前执行的相同方式进行重命名。如果您为已经模糊处理后的应用程序发布一个修补程序(或者新的插件),您可以使用与原始版本相同的名称集合来模糊处理更新。对于维护多个相互依赖应用程序的企业开发团队来说,这尤其有用。
[url=http://www.microsoft.com/china/msdn/library/langtool/vbnet/NFissues0311netcodeobfuscation.mspx#top]
模糊处理程序缺陷
在复杂的应用程序中,需要慎重对待模糊处理(尤其是重命名),它对正确的配置非常敏感。如果您不小心,模糊处理程序可能会破坏您的应用程序。在本部分中,我们将讨论当使用模糊处理程序时可能会出现的十分常见的问题。
首先,当应用程序包括强命名的程序集时,您需要做更多的工作。强命名的程序集是经过数字签名的,允许运行库确定在签名后程序集是否已经更改。该签名是一个 SHA1 哈希,该哈希签有 RSA 公钥/私钥对的私钥。签名和公钥都嵌入到程序集的元数据中。由于模糊处理程序修改了程序集,在模糊处理后进行签名很重要。在开发过程中和进行模糊处理之前,应该延迟签名该程序集,然后在模糊处理后完成签名过程。有关延迟签名程序集的更多详细信息,请参阅 .NET Framework 文档,并且记住在测试延迟签名的程序集时关闭强名称验证。
使用 Reflection API 和动态类加载也会使模糊处理过程复杂化。因为这些功能是动态的,所以它们趋向于取代由大多数模糊处理程序使用的静态分析技术。请考虑下面的 C# 代码片段,它按名称获得一个类型并动态对其进行实例化,将类型转换返回到接口:
public MyInterface GetNewType() {
Type type = Type.GetType( GetUserInputString(), true );
object newInstance = Activator.CreateInstance( type );
return newInstance as MyInterface;
}
类型的名称来自于另一个方法。GetUserInputString 可能会要求用户输入字符串,或者可能从数据库中检索字符串。任意一种方法中,类型名称都没有出现在用于恢复的静态分析的代码中,因此无法知道输入程序集中的哪个类型可能会以这种方式进行实例化。这个例子的解决方案就是防止实现 MyInterface 的所有潜在可加载的类型进行重命名(请注意,仍然可以执行方法和字段重命名)。这就是手动配置和了解进行模糊处理的应用程序扮演重要角色的原因所在。Dotfuscator Community Edition 为您提供了防止重命名选择类型、方法或字段的工具。您可以选择独立的名称;另外,您还可以使用正则表达式和其他标准(例如在作用域上的可视性)来编写排除规则。例如,您可以排除所有公共方法使其避免重命名。
使用模糊处理程序的另一个问题发生在部署已模糊处理的应用程序之后,并试图支持它时。假设您的应用程序引发一个异常(这甚至会发生到我们最优秀的人身上),客户向您发送了如下所示的堆栈转储:
System.Exception: A serious error has occurred
at cv.a()
at cv..ctor(Hashtable A_0)
at ar.a(di A_0)
at ae.a(String[] A_0)
很明显,这比从未模糊处理程序中进行堆栈转储所得到的信息要少得多。好消息就是您可以使用在模糊处理过程中生成的映射文件来将堆栈跟踪解码回原始符号。坏消息就是有时在堆栈跟踪中没有足够的信息来明确地从映射文件中检索原始符号。例如,转储中忽略方法返回类型的通知。在利用增强的重载归纳重命名算法模糊处理的应用程序中,只有由返回类型区别的方法可能被重命名为相同的名称。因此,堆栈跟踪是模糊的。在大多数情况下,您可以使可能性变得足够小,这样就会以很高的确定性发现原始名称。为此,Dotfuscator Professional 提供了一个工具来自动将堆栈跟踪转换回原始的有问题的方法。
小结
您不需要让黑客为了不可告人的目的在您的应用程序中使用手头的 ILDASM 实用工具。您可以使用很好的模糊处理程序来保护您的代码。模糊处理产生了反向工程的障碍。在 Visual Studio .NET 2003 框中,Dotfuscator Community Edition 可以只通过几次单击就能够完成很好的模糊处理。
相关文章,请参阅:
Inside Microsoft .NET IL Assembler,作者是 Serge Lidin (Microsoft Press, 2002)
[url=http://www.preemptive.com/dotfuscator/DotfuscatorFAQ.html]Dotfuscator FAQ
有关背景信息,请参阅: