如何在受反混淆器保护的 .NET 应用程序内存中读取代码






4.90/5 (63投票s)
本文介绍并实现了一种在内存中获取正在运行的 .NET 应用程序代码的技术(即使该应用程序被打包成非托管代码以防止二进制文件被反混淆),并提供了一个可行的示例。
引言
每个 .NET 开发人员都熟悉反编译工具,这些工具允许我们通过其托管的可执行文件恢复原始代码。但有时开发人员希望保护他们的代码不被反编译工具读取。一些反反编译代码保护机制基于这样的想法:将加密的托管代码放入非托管二进制文件中,然后在内存中解密 .NET 模块。我还在 2013 年 10 月在 CodeProject 上发表了一篇关于此主题的文章* [1]*。这篇文章受到了 CodeProject 成员的热烈欢迎(感谢大家!)。这种方法解决了问题:它保护了托管的可执行文件免遭反编译。然而,一些读者合理地认为,尽管代码不能直接从二进制文件中读取,但很可能可以访问内存中运行的托管代码。但到目前为止,还没有提出这种方法的实际实现。本文及配套代码提供了一个解决方案,即读取内存中的托管代码。
在我继续进行反(反反编译)操作之前,我想做一个重要的说明。我们今天要“破解”的反反编译保护仍然有用,不应低估。要从内存中获取代码,需要以某种方式启动应用程序并将要反编译的模块放入内存(应用程序的启动并不保证感兴趣的代码会被加载到内存中,因为代码可能被设计成在不通过某些安全障碍的情况下阻止某些模块加载到内存中)。这可能会增加破解保护的复杂性。
背景
简而言之,问题的解决方案如下:在目标应用程序启动后,另一个应用程序使用远程线程技术*[2]*将一段代码注入到目标应用程序中。注入的代码构成一个非托管 DLL。其 DllMain()
方法在 DLL_PROCESS_ATTACH
情况(“原因”)下,启动一个实现 ICorRuntimeHost
接口的 .NET CLR COM 对象*[1]*。然后,CLR 对象加载能够访问正在运行的目标代码的托管 DLL 反编译器,对其进行反编译,并将结果作为 Microsoft 中间语言(MSIL)代码提交。反编译 DLL 作为字符(字节)数组加载。加载反编译器后,CLR 对象调用已加载的托管 DLL 的方法,该方法实际通过反射检索目标应用程序的 MSIL 代码。
设计
系统操作如下方图所示
正在运行的非托管目标进程包含受保护的托管 .NET 应用程序。系统的目标是读取受保护的 .NET 应用程序的代码。为了实现这一点,将执行以下步骤。
- 托管的 CodeReaderLib.dll 能够反编译 .NET 代码并调用
CodeReaderLib.CodeReader.Reflect()
方法检索其 MSIL。从MethodInfo
对象到 MSIL 的转换是通过从*[3]*中获取的AutoDiagrammer.ILInstruction
和AutoDiagrammer.MethodBodyReader
类型执行的。CodeReaderLib.dll 应作为char
数组加载到非托管进程内存中。File2Chars.exe 实用程序*[1]*执行此工作,并在CodeReaderLib
项目的后置生成事件中被调用。* - 非托管的 TargetPlugin.dll 设计用于激活具有
ICorRuntimeHost
接口的 CLR COM 对象。然后,CLR 对象加载托管的 CodeReaderLib.dll 反编译器,并调用其CodeReaderLib.CodeReader.Reflect()
方法,执行实际的反编译和 MSIL 代码输出。这些操作在DLL_PROCESS_ATTACHED
原因下由DllMain()
调用的PluginProc()
方法中执行。 - 注入器进程 MemoryReflectorApp.exe 首先启动目标进程。在本例中,这是 ReflectProtectedApp.exe 及其在*[1]*中描述的*.dllx 受保护的 DLL 文件。ReflectProtectedApp.exe 保护 WPF 应用程序免受二进制文件反编译。然后,注入器进程使用进程内 COMInjector.dll 将 TargetPlugin.dll 注入到目标进程。注入是通过远程线程注入技术执行的。Injector.dll 的设计在*[2]*中进行了详细描述。
因此,工作流程如下:具有Injector COM 对象的注入器进程将 TargetPlugin.dll 注入到目标 ReflectProtectedApp.exe 进程中,然后 TargetPlugin.dll 的 DllMain()
创建 CLR COM 对象,并使用它调用 .NET 内存区域内的托管代码读取器反编译器。反编译器完成其工作,并在其退出后,远程线程也终止。
关于为此任务使用注入技术的注意事项。在*[2]*中,注入对象在目标进程的整个生命周期内持续运行,处理其窗口。为了确保这一点,我们创建了注入对象(非托管进程内 COM 或 COM 包装器中的托管对象),继承目标应用程序窗口并为注入对象配备进程间通信机制。对于本文的目的,这种方法似乎是过度的,因为这里我们只需要一次性反编译托管对象,生成 MSIL 代码并将其写入文件。完成这些之后,我们就不再需要访问注入对象了。当然,当受保护的托管应用程序动态加载其某些模块时,需要在目标应用程序的整个生命周期内保持注入的反编译器处于活动状态。在这种情况下,需要使用更复杂的注入技术,并在*[2]*中描述的目标应用程序主线程中创建反编译器对象。
代码示例
代码示例 MemoryReflection.sln 包含以下项目
- MemoryReflectorApp - 托管代码,启动目标应用程序 ReflectProtectedApp.exe 并使用Injector COM 对象将其注入到其中,
- Injector - 进程内 COM,实际将 TargetPlugin.dll 注入到目标进程,
- TargetPlugin - 非托管 DLL,注入到目标进程中,在那里它创建一个具有
ICorRuntimeHost
的 CLR COM 对象,并将其加载为字符数组的托管CodeReaderLib
程序集, - CodeReaderLib - 托管 DLL,实际反编译目标应用程序代码。在其后置生成事件处理程序中,它使用 File2Chars.exe 实用程序转换为字符数组,并且
- File2Chars - 托管实用程序,将文件(在本例中为 CodeReaderLib.dll )转换为字符数组。
解决方案应以 MemoryReflectorApp 作为启动项目进行构建和运行。结果文件 WpfDirectoryTreeApp_MSIL.txt 生成在输出目录中,其中包含 WpfDirectoryTreeApp
程序集所有类型的所有方法的 MSIL 代码。当然,这只是一个示例,仅提供一个程序集的方法。
注入代码到外部进程是一项非常敏感的操作,并且经常被计算机的保护软件阻止(或至少引起警报)。因此,在运行本文的代码示例或演示时,请注意这种可能性。为了成功测试示例,您应该暂时禁用此保护。
演示
演示示例解析了反编译保护的 WPF 应用程序 ReflectProtectedApp.exe 。该目标应用程序是*[1]*中描述的反反编译保护技术的产物。它引用了几个自定义的受保护 DLL(扩展名为 dllx 的文件)和标准的 System.Windows.Interactivity.dll。ReflectProtectedApp.exe 使用 ReflectProtectedApp.exe.config 配置文件。要读取目标应用程序的代码,我们必须首先使用 regsvr32.exe 实用程序注册 Injector.dll 进程内 COM 对象,然后运行 MemoryReflectorApp.exe 应用程序。批处理文件 _MemoryReflectorApp_RunMe.bat 执行这两个操作。应运行此文件来操作演示。结果生成文件 WpfDirectoryTreeApp_MSIL.txt 。它包含 WpfDirectoryTreeApp
程序集所有类型的所有方法的 MSIL 代码。
讨论
所呈现的技术为内存中的反编译问题提供了通用的解决方案。它不依赖于二进制文件加密的方式,因为在内存中运行的是普通的托管代码。为了使图景完整,生成某种广泛使用的 .NET 编程语言(如 C# 或 VB.NET)的代码将是很好的。这不是一项简单的任务,但可以在任何知名的反编译工具(例如 Lutz Roeder's Reflector 和 ILSpy)中完成,因此超出了本文的范围。
结论
本文介绍了获取正在运行的托管应用程序的 MSIL 代码。所描述的方法在有用的托管模块被非托管代码“包装”以防止它们被反编译工具作为二进制文件读取时特别有效。
参考文献
[1] Igor Ladnik. Anti-Reflector .NET Code Protection. CodeProject
[2] Igor Ladnik. Automating Windows Applications. CodeProject
[3] Sacha Barber. 200% Reflective Class Diagram Creation Tool. CodeProject
[4] Sorin Serban. Parsing the IL of a Method Body. CodeProject