65.9K
CodeProject 正在变化。 阅读更多。
Home

Anti-Reflector .NET 代码保护

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (121投票s)

2013年10月26日

CPOL

10分钟阅读

viewsIcon

382857

downloadIcon

17102

本文介绍了一种技术,允许开发人员通过反射可执行文件来保护其.NET代码免受逆向工程的侵害

引言

从.NET早期开始,受管二进制文件通过反射进行逆向工程已成为常态。Lutz Roeder 的 Reflector 和许多随后的反射器确实能够破解 .NET 可执行文件,并礼貌地询问用户希望以哪种语言获取原始代码。事实上,大多数开发人员已经接受了通过二进制文件轻松揭示原始代码的情况,将其视为使用受管代码的必然代价。一些人认为,大多数受管可执行文件可能放置在服务器端,客户无法访问。然而,如今放置在公共云上的代码也可能需要保护。另一些人则尝试使用一些缓解措施进行代码保护,例如混淆器。但混淆是一个问题重重的解决方案,因为在这种情况下,代码通常可以通过反射器读取,应用程序逻辑保持完整,更重要的是,可能会干扰应用程序自身的反射使用。

本文介绍了一种方法,并附有代码示例,允许开发人员阻止通过反射其可执行文件直接揭示其受管代码的尝试。

这项工作是基于其他作者以前的文章和代码。其中一些在文章末尾引用。但这份列表绝不是详尽无遗的——还使用了更多的文章和帖子(尤其是在 Stack Overflow 网站上发布的)。

背景

我早在2002年读过的第一本.NET书籍(Jeffry Richter的《Applied Microsoft .NET Framework Programming》[1]——一本优秀之作!)已经为更好地保护托管代码提供了很好的线索。在题为“CLR Hosting、AppDomains和Reflection”的第20章中,作者解释说,.NET公共语言运行时(CLR)构成了一个COM对象,因此可以由任何Windows非托管应用程序托管。这一事实提出了一种保护托管应用程序免受反射器攻击的方法。这种方法允许.NET开发人员达到与其非托管应用程序相当的代码保护级别。

这项工作的主要思想是:为了欺骗反射器,托管代码通过嵌入到非托管应用程序中的 CLR 对象启动。实际上,整个托管代码在非托管主机进程中运行,其二进制文件无法通过反射器读取。在本文中,我将介绍整个反反射代码保护算法及其可能的实现,重点关注 WPF 应用程序的保护。

值得注意的是,保护处理完全不会影响有用的 .NET 应用程序。但是,从托管代码实现应用程序的正确启动(特别是对于 WPF 应用程序)、加载和解密引用的 DLL 等功能要容易得多。因此,托管的 `LauncherLib` DLL 被用作有用应用程序程序集的包装器和启动器。

设计

处理流程如下图所示

开发完有用的 .NET 应用程序及其 DLL 后,应执行以下步骤以保护其代码免受反射器直接逆向工程的影响。

  1. 工具 `File2Chars.exe` 已构建。此工具将文件(在本例中为托管可执行文件)转换为字节数组,如果需要则加密,并创建包含字节数组作为只读变量的代码文件。该工具可通过命令行参数进行配置。`File2Chars` 工具在流程图中显示为蓝色的“Utility”矩形。
  2. 托管的 `LauncherLib.dll` 已构建。在此构建过程中,在预构建事件中,通过 `File2Chars` 工具将初始 EXE 和本地复制的有用托管应用程序的引用 DLL 文件处理为字节数组。EXE 文件的字节被放入只读变量中,生成到一个新创建的 `_UsefulAppBytes.cs` 文件中,该文件作为 `LauncherLib.dll` 的一部分进行编译。如果客户希望加密引用的本地复制 DLL(通常与有用的 EXE 文件一起开发),则 `File2Chars` 工具会进行相应配置。在我们的示例中,加密 DLL 文件的扩展名更改为 DLLX。DLLX 文件被复制到 `ReflectProtected` 目录,该目录是受保护应用程序的工作目录。但是,如果客户希望使用未加密的 DLL(标准 DLL 的常见情况),则 DLL 将保持不变地复制到 `ReflectProtected` 目录。配置文件 `[UsefulApplicationName].exe.config` 也被复制到 `ReflectProtected` 目录,并重命名为 `[ProtectedApplicationName].exe.config`。

    在构建后事件中,`File2Chars.exe` 工具处理新构建的 `LauncherLib.dll`,并将其作为加密的字节数组放入只读变量到 `_ArrayContainer` 文件中。因此,该变量包含加密的 `LauncherLib.dll`,而 `LauncherLib.dll` 又包含有用应用程序的 EXE 文件作为字节数组。

  3. 非托管的 ProcessingLib.lib 被构建为一个静态库。它包含负责创建 CLR COM 对象和执行托管 LauncherLib.dll 方法的代码。
  4. 最后,将与 `ProcessingLib.lib` 静态链接的非托管 `ReflectProtectedApp.exe` 构建并放置到 `ReflectProtected` 目录中。包含字节数组数据的文件 `_ArrayContainer` 被包含在构建中。

因此,有用的应用程序 EXE 文件经历了以下转换。首先,它作为字节数组放入托管启动器库 `LauncherLib` 中,然后启动器库构建成 `LauncherLib.dll`。这个二进制文件被转换为字节数组,加密并放入非托管代码中,最终构建成最终的受保护可执行文件。至于引用的 DLL 文件,如果决定保护它们,则它们会被加密并复制到受保护的工作目录。如果 DLL 文件未受保护(标准 DLL),则它们会原样复制到受保护的工作目录。有用应用程序的托管包装器 `LauncherLib.dll` 负责加载引用的 DLL。对于当前应用程序域,它实现了 `AppDomain.CurrentDomain.AssemblyResolve` 事件的处理程序。该处理程序查找、解密(如果需要)并加载适当的 DLL。

代码示例

在我们的示例中,上述步骤通过构建 `Protection.sln` 解决方案来完成。让我们详细检查代码示例的整个流程。在我们的示例中,有用的应用程序是 `WpfDirectoryTreeApp` WPF 应用程序。该应用程序及其引用的 DLL 放置在 `.\bin` 目录中。`System.Windows.Interactivity.dll` 是一个标准的本地复制引用 DLL。`WpfDirectoryTreeApp` 显示带 Windows 安装的磁盘的目录树。它执行简单的操作。单击树面板中的目录或文件项会切换主面板中的控件,双击会在树中展开特定文件夹,并在日志面板中写入相应的消息。该应用程序还具有一个简单的菜单和工具栏,允许用户交换树和主面板(定义树面板位置的变量的初始值存储在应用程序配置文件中)。

在示例工具中,`File2Chars.exe` 是可配置的(要查看如何配置,请在不带参数的情况下运行它)。第一个参数指定要处理的二进制文件(EXE 或 DLL)。参数 `/e` 表示输出已加密。这里的加密相当简单:在输出字节数组中,每连续 n 个字节与接下来的 n 个字节交换,其中 `n` 是工具命令行中的最后一个参数。当然,在实际应用中可以使用更复杂的加密方法。参数 `/r` 应用于引用的 DLL。如果使用此参数,则工具将输出字节数组放置到 `ReflectProtect` 输出目录中带有 DLLX 扩展名的单独文件中。`File2Chars.exe` 工具的使用可以在 `LauncherLib` 项目的预构建和后构建事件命令行中看到。

`LauncherLib` 项目包含 `ClsPrepare` 类(与 `File2Chars` 共享)负责加密/解密操作,`UsefulAppBytes` 类包含 `static readonly byte[] bts` 变量(即“`File2Chars.exe WpfDirectoryTreeApp.exe`”预构建事件命令的输出),以及 `Launcher` 类,其方法 `Launcher.Run()` 被非托管代码调用后,将在内存中启动有用应用程序。后者是应用程序启动的焦点。该方法执行以下操作

  • 从其 `string` 参数中提取有用应用程序模式(在我们的例子中是 WPF)及其主要类型(`WpfDirectoryTreeApp.App` 和 `WpfDirectoryTreeApp.MainWindow`)
  • 将适当的处理程序分配给 `AppDomain.CurrentDomain.AssemblyResolve` 事件,以及
  • 从字节数组加载有用应用程序的主程序集并启动其适当的方法(通过调用 `private` 方法 `Launcher.LaunchUsefulApp()`)。

非托管应用程序 `ReflectProtectedApp.exe` 包含文件 `_ArrayContainer`,其中包含 `LauncherLib.dll` 作为加密的字节数组(`LauncherLib` 项目构建过程中“`File2Chars.exe LauncherLib.dll /e 11`”的构建后事件命令的输出)。`ProcessingLib.lib` 静态链接到 `ReflectProtectedApp.exe`。非托管 `ProcessingLib.lib` (文件 `Processing.cpp`) 的 `Run()` 方法将 CLR COM 对象加载到内存中,并借助它从非托管进程运行托管的 `Launcher.Run()` 方法。该方法由 `ReflectProtectedApp` 主函数调用。`Run()` 方法支持所有版本的 .NET Framework。

要生成受保护的应用程序和 DLLX 文件,我们需要构建 Protection.sln 解决方案。为了成功构建,请检查以下事项:Protection 目录的路径不应包含任何空格,并且 mscorlib.tlb 文件位于其通常位置,即 `C:\Windows\Microsoft.NET\Framework\v4.0.30319` 目录。如果最后一个条件不满足,请更改 ProcessingLib 项目的 Processing.cpp 文件中的文件路径。

解决方案构建结果将放置在 `.\bin\ReflectProtected` 目录中。文件 `ReflectProtectedApp.exe` 构成受保护的可执行文件,而 `*.dllx` 文件代表加密的引用 DLL。`System.Windows.Interactivity.dll` 未加密,因为它是一个标准的 Microsoft DLL。受保护的 `ReflectProtectedApp.exe` 应用程序(除了图标——我特意为受保护的应用程序分配了不同的图标)看起来和行为与其未受保护的模拟 `WpfDirectoryTreeApp.exe` 完全相同。

演示

演示 ZIP 文件包含在发布模式下构建 `Protection.sln` 解决方案后 `.\bin\ReflectProtected` 目录中的文件。我们可以用反射器检查这些 EXE 和 DLLX 文件,发现反射器无法揭示它们的原始代码。

讨论

所提出的技术提供了一定程度的反反射托管代码保护。尽管经过这种保护后,.NET 二进制文件仍然比普通的非托管代码更容易受到攻击,但这种保护大大增加了逆向工程的难度。这项技术可以改进,例如,可以使用更复杂的加密算法,并且托管启动器程序集可以使用更巧妙的方式加载 EXE 和引用的 DLL,例如,从网络获取它们。类似的方法可以用于将所有自定义 EXE、DLL 和资源文件(如果有的话)打包到一个非托管 EXE 文件中(例如参见 [4])。`LauncherLib` 还可以提供支持某些许可机制的功能。

与有用应用程序的原始大小相比,受保护 EXE 文件的大小并不高。托管启动器程序集和相对较小的非托管部分会增加受保护 EXE 文件的大小。这两个单元的大小几乎是固定的,与有用应用程序的大小无关。

设计一个实用程序来为有用的托管应用程序生成非托管的反反射保护可能可行。但我不认为这是一个非常好的主意。通常,有用应用程序的作者对反反射保护感兴趣。因此,他/她可以轻松维护一个带有多个项目的专用解决方案,并能够根据需要进行调整和优化,而不是使用一个“不灵活”的实用程序。

结论

本文介绍了保护 .NET 可执行程序集免受反射逆向工程的方法。通过在非托管进程中创建 .NET CLR COM 对象并从 CLR 对象调用托管方法来实现保护。该技术阻止反射器直接揭示托管程序集的原始代码。这种保护机制绝不会影响有用的托管应用程序。除了代码保护目的外,所提出的技术还可用于优化应用程序打包、部署和许可。

参考文献

  • [1] Jeffry Richter。应用微软 .NET 框架编程,2002。
  • [2] Richard Grimes。使用 Visual Studio.NET 开发应用程序,2002。
  • [3] Ranjeet Chakraborty。为 .NET 公共语言运行时创建宿主应用程序。CodeProject。
  • [4] SteveLi-Cellbi。一种将您的 .NET 代码打包到单个可执行文件中的简单方法。CodeProject。
  • [5] Mattias Högström。CLR 宿主 - 自定义 CLR。CodeProject。
© . All rights reserved.