防止对 Visual Basic .NET 或 C# 代码进行反相工程

Gabriel TorokBill Leach

本文假设您熟悉 .NET 与 C#

摘要

.NET 体系结构的优势之一在于,利用该体系结构构建的程序集包含很多有用的信息,使用中间语言反汇编程序 ILDASM 即可恢复这些信息。但是这样会带来另一个问题,就是可以访问您的二进制代码的人能够以非常近似的手段恢复原始源代码。作者将在文中介绍程序模糊处理,该处理可作为一种阻止反相工程的手段。此外,他们还将讨论可用的不同类型的模糊处理技术,并示范 Visual Studio .NET 2003 中包含的新模糊处理工具。

本页内容

反汇编
反编译
深入了解模糊处理
重命名元数据
删除非基本元数据
其他技术
使用 Dotfuscator Community Edition
检查映射文件
模糊处理程序的缺陷
小结

迄今为止,从减轻部署和版本控制的负担,到自描述二进制数据所实现的丰富 IDE 功能,您可能已经熟悉了这些元数据丰富的 Microsoft® .NET Framework 体系结构带来的所有好处。您可能不知道元数据的这种易用性带来的一个目前对于大多数开发人员来说还没有注意到的问题。为公共语言运行库 (CLR) 编写的程序更易于进行反相工程。不管怎么说,这并不是 .NET Framework 设计中的缺陷;它只是一种现代的、中间编译语言(Java 语言应用程序具有同样的特征)的现实状况。Java 和 .NET Framework 都使用内嵌在可执行代码中的丰富元数据:在 Java 中是字节码,在 .NET 中是 Microsoft 中间语言 (MSIL)。由于比二进制机器码要高级很多,可执行文件充满了可以轻松破解的信息。

借助于诸如 ILDASM 这样的工具(随同 .NET Framework SDK 一同发布的 MSIL 反汇编程序)或者类似于 AnakrinoReflector for .NET 这样的反汇编程序,任何人都可以轻松地查看您的程序集并将其反相工程为可读的源代码。黑客可以搜索安全缺陷,以探究、窃取独特的创意并破译程序。这足以使您犹豫不决。

尽管如此,请不必担心。有一种模糊处理解决方案可帮助您防止反相工程。模糊处理是为程序集中的符号提供无缝重命名的一项技术,它还提供了其他技巧以阻止反汇编程序。如果运用适当,模糊处理可以大幅度提升应用程序防止反相工程的能力,同时保持应用程序的完整无缺。模糊处理技术通常用于 Java 环境中,多年来已帮助了众多公司保护他们的基于 Java 技术产品的知识产权。

现在已有多家第三方通过创建 .NET代码的模糊处理程序来予以响应。通过与我们公司 PreEmptive Solutions 合作,Microsoft 在 Visual Studio?.NET 2003 中包含了 Dotfuscator Community Edition,而 PreEmptive Solutions 也推出了多种模糊处理程序软件包。

通过使用 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 的应用程序的结构。图 1 显示了经过反汇编的 undo 方法。

反编译

如果您认为只有少数真正了解 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);
    }
}

您可以看到,结果几乎与原始代码完全相同。稍后,我们将回到这里来查看经过模糊处理后的结果。

深入了解模糊处理

模糊处理是使用一组相关技术来实现的。模糊处理的目的是隐藏程序的意图而又不改变其运行时行为。它不是加密,但对于 .NET 代码而言,它比加密可能更胜一筹。您可以加密 .NET 程序集,使它们完全不可读。但是,该方法会造成一个典型的进退两难的局面,这是因为运行库必须执行未加密代码,解密密钥必须与已加密程序保存在一起。因此,可创建一个自动化的实用工具来恢复密钥、解密代码,然后以 IL 的原始形式将其写到磁盘上。一旦发生这种情况,程序就完全暴露在反编译之下了。

我们可以将加密比作是将包含六道菜的套餐锁到箱子里。只有预定的用餐者(在本例中为 CLR)才有钥匙,而且我们不想让任何其他人知道他或她将要吃什么。遗憾的是,到了用餐时间所有食物将完全处于众目睽睽之下。模糊处理工作更像将包含六道菜的套餐放到一个搅拌机中,然后将它放到一个塑料袋中送给用餐者。当然,每个人都可以看到正在递送的食物,但是除了碰巧可以看到一颗完整的豌豆或一些牛肉色的粘糊东西之外,他们并不知道原来的食物是什么。用餐者仍然获得了他想要的东西,而且这次用餐仍然提供了与以前一样的营养价值(幸运的是,CLR 对口味并不挑剔)。模糊处理程序的窍门就是把窥探者搞糊涂,同时该程序仍然能向 CLR 提交同样的产品。

当然,模糊处理(或加密)并不是百分之百地坚不可摧。即使编译后的 C++ 也可能被反汇编。如果黑客坚持不懈,他就可以重新产生您的代码。

Aa686048.misNETCodeObfuscationfig02(zh-cn,MSDN.10).gif

图 2 模糊处理过程

模糊处理是一个应用于编译后的 .NET 程序集而不是源代码的过程。模糊处理程序决不会读取或更改您的源代码。图 2 显示了模糊处理过程的流程。模糊处理程序的输出是另一组程序集,功能上与输入程序集等同,但是该程序以一种阻碍反相工程的方式进行了转换。现在我们将讨论 Dotfuscator Community Edition 用来实现该目标所使用的两项基本技术:重命名和删除非基本元数据。

重命名元数据

模糊处理的第一道防线就是将有意义的名称重命名为一个无意义的名称。如您所知,从精心选择的名称中可得出许多有价值的线索。它们有助于使您的代码自我文档化,并充当了揭示它们所表示的项目的有价值的线索。CLR 不关心一个名称的描述性如何,因此模糊处理程序可以自由地修改这些名称,通常是修改为类似于“a”这样的单字符名称。

显然,模糊处理程序对一个特定程序所能执行的重命名次数是有限制的。通常来说,有三种常见的重命名方案。

如果您的应用程序由一个或多个独立的程序集组成(即未模糊处理的代码不依赖于任何程序集),那么模糊处理程序可以自由地对程序集进行重命名,而无论名称的可见性如何,只要名称和对名称的引用在程序集组中是一致的。Windows 窗体应用程序就是一个很好的例子。作为一个反面的极端例子,如果您的应用程序设计目的就是供未经过模糊处理的代码使用,那么模糊处理程序将无法更改那些对客户可见的类型或成员的名称。这种类型的应用程序的例子,包括共享类库、可重用组件等诸如此类的东西。位于二者之间的是那些打算插到现有的未做模糊处理的框架中的应用程序。在这种情况下,模糊处理程序可以对它运行时所在的环境不访问的任何东西进行重命名,不管可见性如何。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);
    }
}

您可以看出来,在没有经过任何模糊处理,这个方法就已经比较难于理解了。

删除非基本元数据

在编译过的、基于 .NET 的应用程序中,并非所有的元数据在运行时都会得到使用。其中的一些数据将由其他工具使用(例如,设计器、IDE和调试器)。例如,如果您在 C# 中的类型上定义了一个名为“Size”的属性,则编译器将省略属性名“Size”的元数据,并将该名称与实现 get 和 set 操作的那些方法关联起来(它们被分别命名为“get_Size”和“set_Size”)。当您开始编写设置 Size 属性的代码时,编译器将始终生成一个对方法“size-Size”本身的调用,并且决不会通过其名称引用该属性。事实上,属性的名称供 IDE 和使用您的代码的开发人员使用;CLR 从不访问它。

如果只是由运行库而不是其他工具使用您的应用程序,那么模糊处理程序删除这种类型的元数据就是安全的。除了属性名,事件名和方法参数名也在这个范畴之列。Dotfuscator Community Edition 在它认为这样做安全时会删除所有这些类型的元数据。

其他技术

Dotfuscator Community Edition 使用我们刚刚介绍的技术来提供良好的模糊处理,但是您应该知道模糊处理技术在提供更为强大的保护的同时,可能还会阻止反相工程的进行。Dotfuscator Professional Edition 实现了很多其他技术,其中包括控制流模糊处理、字符串加密、增量模糊处理和规模降低。

控制流是一种强大的模糊处理技术,它的目的是隐藏一系列指令的意图而又不会更改逻辑。更为重要的是,它可用来删除反编译程序为了忠实重现高级源代码语句(比如 if-then-else 语句和循环)而寻找的那些线索。事实上,这项技术试图破坏反汇编程序的工作。

要查看运行效果,在运用重命名和控制流模糊处理后,再次研究反编译后的undo方法(请参见图 3)。您可以看到反汇编程序并没有生成原始的嵌套 if 语句,而是生成了一个 if 语句、两个嵌套 while 循环和一些将其捆绑在一起的 goto。标签 i1 被引用了,但它不是由反编译程序生成的(我们假定它是一个反编译程序错误)。

字符串加密是一种将简单加密算法应用到嵌入您的应用程序中的字符串的技术。如上所述,在运行时执行的任何加密(或特殊情况下的解密)从根本上讲都是不安全的。也就是说,技术高超的黑客事实上是可以破解它的,但对于应用程序代码中的字符串而言,这样做是值得的。我们所面对的事实是,如果黑客希望进入您的代码,那么他们不会盲目地开始搜索已重命名的类型。他们可能确实会搜索“无效许可证密钥”,这会将他们直接引导到执行许可证处理的代码。对字符串进行搜索非常简单;字符串加密设置有保护,这是因为在编译的密码中只存在加密的版本。

增量模糊处理有助于发布修补程序来解决客户在面对模糊处理时碰到的问题。修复代码中的错误时经常会创建或删除类、方法或字段。更改代码(例如,添加或删除某个方法)可能会导致随后的模糊处理运行,从而使事物的重命名稍有不同。先前称为“a”的名称现在可能称为“b”。遗憾的是,如何不同地进行重命名和不同地重命名哪些内容却不容易弄清楚。

增量模糊处理可以解决这一问题。Dotfuscator 将创建一个映射文件以告知您它是如何执行重命名的。但是,这个映射文件在随后的运行中同样可用作对 Dotfuscator 的输入,以指示先前使用的重命名应在任何可能的地方再次使用。如果发布您的产品,然后修补一些类,Dotfuscator 就会以一种模仿其先前重命名方案的方式运行。这样,您就可以只将修补过的类发布给您的客户。

减小规模不会严格地阻止反相工程,但这里仍值得提一提,因为模糊处理程序几乎始终必须要在输入程序集上执行依赖性分析。因此,模糊处理程序不仅可以很好地进行模糊处理,更好的是,它还可以利用对您的应用程序的了解来删除您的程序没有使用的代码。看起来有点奇怪,实际上,删除未使用的代码非常容易,那么,是谁编写了这些不使用的代码呢?答案是,我们所有的人。此外,我们都使用其他人编写的、可重用的库和类型。

可重用代码意味着存在一些可处理许多用例的随附代码;但在任何给定的应用程序中,您通常只使用这些众多用例中的一种或两种。高级模糊处理程序可以确定这一点并删除所有未使用的代码(而且,是从已编译的程序集而非源文件中删除)。结果是,输出中所包含的正是您的应用程序不再需要的类型和方法。较小的应用程序具有节省计算资源和缩短加载时间等好处。这些好处对于在 .NET Compact Framework 上运行的应用程序或分布式应用程序尤为重要。

使用 Dotfuscator Community Edition

现在让我们使用 Dotfuscator Community Edition 来模糊处理 Vexed 应用程序。Dotfuscator Community Edition 使用一个配置文件来指定特定应用程序的模糊处理设置。它让一个 GUI 来帮助您轻松创建和维护配置文件,以及运行模糊处理程序并检查输出。此外,Dotfuscator Community Edition 的命令行界面允许您将模糊处理轻松集成到您自动生成过程中。您可以从 Visual Studio .NET 2003 的工具菜单直接启动 GUI。

要配置 Vexed 以进行模糊处理,您需要在 Dotfuscator Community Edition GUI 中指定 3 项:输入程序集、映射文件位置和输出目录。输入程序集(Dotfuscator 称之为“触发器程序集”)在 Trigger 选项卡上指定。您可以在这里根据所需添加任意多的程序集,但对 Vexed 应用程序来说只需要一个。

在“Rename | Options”选项卡上指定映射文件的位置(请参见图 4)。映射文件中含有原始名称和被模糊处理名称之间的明确名称映射,这些信息至关重要。重要的一点是,对应用程序进行模糊处理后,要保存该文件;如果没有它,您就不能轻松地对模糊处理过的应用程序进行查错。由于其重要性,Dotfuscator 在默认情况下不会改写现有映射文件,除非您显式地选中 "Overwrite Map file" 框。

最后,“Build”选项卡允许您指定放置经过模糊处理的应用程序的目录。完成上述工作后,就可以对应用程序进行模糊处理了。您可以保存配置文件以备后用,然后可以在“Build”选项卡上按“Build”按钮,或在工具栏上使用“Play”按钮。在构建时,Dotfuscator 会在 GUI 的输出窗格中显示进度信息。在“Options”选项卡上选择 Quiet 或者 Verbose,可以控制在这里显示的信息量。

一旦完成生成,您就可以在 Output 选项卡上浏览结果,如图 5 所示。如您所见,Dotfuscator 显示了一个与对象浏览器类似的应用程序图形视图。新名称位于视图中原始名称的正下方。在此图中,您会看到名为“board”的类被重命名为“h”,具有不同签名的两个方法(init 和 ToImage)都被重命名为“a”。

检查映射文件

Dotfuscator 生成的映射文件是一种 XML 格式的文件,这种文件除包含上述名称映射外,还包含一些统计数据,这些数据指出重命名过程的有效性。图 6 汇总了对 Vexed 应用程序进行模糊处理后类型和方法的统计。

映射文件还被用于执行增量模糊处理。此过程允许您从以前的运行中导入名称,这样会通知模糊处理程序采用与以前同样的方式来执行重命名。如果为一个经模糊处理的应用程序发布修补程序(或新插件),您可以使用与原始版本相同的名称集对更新程序进行模糊处理。这对维护多个相互依赖的应用程序的企业开发小组特别有用。

模糊处理程序的缺陷

有关复杂应用程序的模糊处理(特别是重命名)比较棘手,它对正确配置高度敏感。如果不慎,模糊处理程序就会中断您的应用程序。在本部分中,我们将讨论一些使用模糊处理程序时可能出现的常见问题。

首先,如果您的应用程序包含强名称程序集,则您需要多做一些工作。强名称程序集是经过数字签名的,允许运行库确定是否已在签名后改变了程序集。签名是一个利用 RSA 公钥/私钥对的私钥签名的 SHA1 哈希值。此签名和公钥都被嵌入到程序集的元数据中。因为模糊处理程序将修改程序,所以在模糊处理后进行签名非常重要。在开发过程中和进行模糊处理之前,您应该对程序集延迟签名,然后完成签名过程。有关延迟签名程序集的详细信息,请参阅 .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 by Serge Lidin (Microsoft Press, 2002)
Dotfuscator FAQ
有关背景信息,请参阅
Ildasm.exe Tutorial
Anakrino
http://vexeddotnet.benamotz.com
http://www.preemptive.com

Gabriel Torok 是 PreEmptive Solutions 的总裁。他与别人合著有 JavaScript Primer PlusJava Primer Plus,这两本书均由 Macmillan 出版。Gabriel 在世界各地的开发会议上发表演讲并讲授课程。

Bill Leach 是 PreEmptive Solutions 的首席技术官。他是 Dotfuscator 产品线的架构师和技术主管。Bill 还担任了软件开发书籍和文章的技术评论。

转到原英文页面