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

运行时编译的加密代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (26投票s)

2013年1月21日

GPL3

14分钟阅读

viewsIcon

97228

downloadIcon

4044

加密您的 C# 类并在运行时编译它们(隐藏 EXE 文件中的代码)。

引言

本文首先介绍了一个用 .NET 2010 和 c# 4.0 编写的精简实用工具的代码,该工具结合运行时编译和数据加密的能力,从普通的 c# 类创建加密类,然后解释如何在 .NET c# 项目中使用这些加密类。

我说“精简”,意味着该实用程序在输入正确、所有数据无误且符合要求时工作良好。由于我的懒惰(这是在雨雪天气里的业余时间所做的),我只实现了基本的错误检查例程、异常捕获等。

本文的主要目的是提供一种方法,通过 ILdasm 等反汇编器读取 EXE 文件来隐藏代码,从而使人难以阅读。它不能阻止有人在运行时在内存中看到解密后的代码,并且,正如一位在此方面有经验的 CodeProject 用户所指出的,在解密过程中,它会在临时文件夹中创建已解密为明文代码的文件;因此,如果您的目的是使用此解决方案作为代码混淆的替代方案,请注意,代码隐藏在 EXE 文件(或 DLL)中,而不在内存或临时文件中。

背景

理解这个小型应用程序可能需要两个概念

  1. 运行时编译,
  2. Rijndael 加密算法(即 AES [高级加密标准] 的 128 位变体)。

如果您想深入了解运行时编译的概念,我认为以下文章很有帮助

另一方面,这里提供的 Rijndael 加密算法是我以前用 C# 编写的,如果需要,可以轻松地将其更改为不同的算法,以满足特定的需求。
了解 Rijndael 加密算法并非理解本程序所必需。

使用代码(基本用法)

如果我们想在代码中使用加密类,使用此解决方案,那么我们应该遵循一些简单的步骤

  1. 首先:在一个新项目或现有项目中,编写一个类(或一组类),从这里开始称为“原始类”,以后将被加密,并且为了方便使用(但不是必需的),将其保存到一个文件(可以包含多个类和多个命名空间在同一个文件中)。
  2. 第二:加密该“原始类”并创建一个包含加密代码和一些方法的新类来为我们完成繁重的工作(这个类将被称为“管理类”):这将很容易通过本文顶部可下载的“类加密器”小型实用程序完成。
  3. 第三:将刚刚创建的管理类包含在我们的项目中,代替原始类,并使用管理类的方法来实例化原始类或调用原始类中的方法。

First

我猜想阅读本文的每个人都很有能力临时写一个小的测试类,但是,对于最懒惰的复制粘贴爱好者,我们可以使用这个“原始类

// this is the "original class"
using System;
namespace MyMath {
    public class BasicMath {
        public int add(int a, int b)            {return a + b;}
        public int sub(int a, int b)            {return a - b;}
}}

第二种

现在我们运行“类加密器”程序

在上面的白色文本框中,我们应该粘贴原始类的代码,然后在接下来的细长白色文本框中提供一个加密密钥(密码)。这样就足够运行程序并获得一个可用的管理类了。

输入类使用 Rijndael 算法和提供的加密密钥进行加密,生成包含加密代码(字节数组形式)和一些方便使用的方法的管理类(关于这些方法的详细信息将在本文的第三部分之后给出)。

在选项组中,我们有两个文本框和三个复选框

文本框 1(命名空间):生成代码中包含管理类的命名空间(可以留空,在这种情况下将创建一个没有命名空间的类)。

文本框 2(类名):要创建的管理类的名称。如果留空,则默认类名为“MyManagementClass”。

复选框 1(创建独立类):如果未选中,则生成的代码将包含“using MyRuntime;”,其中 MyRuntime 是一个类(在源文件 zip 包中提供,并在本文下方适当的部分有更详细的描述),该类包含加密/解密例程,以及编译加密代码和创建加密代码的方法;这在我们使用许多加密管理类时很有用(这样加密管理代码将只在一个地方)。另一方面,如果选中此复选框,则所有必需的解密例程都将包含在输出文件中;这在我们只使用一个加密管理类时很有用,这样我们就无需使用第二个文件来管理加密等。

复选框 2(创建便捷方法):如果未选中,管理类中将只提供两个方法:一个用于实例化类(其名称必须作为参数传递),另一个用于在加密类中执行方法;这对于创建小型管理类并避免在管理类中留下加密类名称的任何线索很有用。包含在加密代码中的类名会添加到注释行中,供您参考。您可以在部署解决方案之前删除此行。另一方面,如果选中此复选框,则对于包含在加密代码中的每个类,将创建两个方法(一个用于实例化类,一个用于在该类中执行方法)。

复选框 3(使用安全代码):如果选中,则包含加密密钥(密码)的字节数组将不包含在生成的代码中,并且每个方法将额外包含一个参数:加密密钥。

在按下“加密”按钮后,底部输出的灰色文本框将填充管理类代码。只需单击一次即可选中所有内容,然后复制/粘贴到项目的新类文件中。

第三部分

生成的代码

现在我们将检查生成的管理类的代码。

如果我们在此处上面提供的所有“MyMath”类代码复制/粘贴为原始类,并使用“asd”作为密码(是的,是的,它太短了,但我很懒……您现在应该知道了)。在“选项”中,我们可以添加“myspace”作为命名空间。将复选框留空,以便生成的代码最容易理解。

按“加密”按钮,我们将得到以下代码

using System;
using MyRuntime;
namespace myspace 
{
    public class MyManagementClass 
    {
        private readonly byte[] ecode = new byte[] { 71, 15, 247, 187, 16, 123, 74, 190, 123, 19, 
          215, 31, 108, 14, 79, 190, 138, 174, 233, 55, 25, 168, 78, 187, 86, 45, 66, 190, 
          137, 187, 49, 6, 184, 194, 126, 206, 175, 175, 123, 36, 247, 230, 218, 133, 177, 
          203, 63, 22, 147, 73, 33, 12, 128, 177, 146, 17, 150, 108, 209, 83, 105, 52, 141, 
          95, 209, 32, 45, 24, 69, 131, 51, 189, 63, 244, 42, 189, 30, 169, 116, 30, 251, 
          21, 126, 84, 62, 40, 136, 20, 59, 177, 95, 88, 149, 150, 117, 85, 236, 41, 133, 
          212, 160, 173, 160, 231, 0, 74, 7, 53, 102, 38, 106, 2, 56, 133, 31, 17, 194, 25, 
          233, 75, 213, 40, 64, 128, 61, 143, 110, 218, 247, 213, 237, 242, 212, 200, 196, 
          184, 63, 129, 102, 91, 27, 145, 160, 174, 196, 91, 224, 204, 40, 69, 149, 180, 
          247, 219, 122, 171, 212, 144, 93, 119 };
        private readonly byte[] key = new byte[] { 97, 115, 100 };
 
        public object NewClass(string classname)
        {
            System.Reflection.Assembly asm = RuntimeCodeCompiler.CompileEncryptedCode(ecode, key);
            return asm.CreateInstance(classname);
        }
        public object CallMethod(string classname, string methodname, params object[] methodparameters)
        {
            System.Reflection.Assembly asm = RuntimeCodeCompiler.CompileEncryptedCode(ecode, key);
            return RuntimeCodeCompiler.CallMethod(asm, classname, methodname, methodparameters);
        }
 
        // Available classes:  MyMath.BasicMath; 
    }
}

这很简单:在类开头有两个字节数组:ecodekey,后面是两个方法和一个注释。

byte[] ecode 可以很长,包含原始类的加密代码。

byte[] key 包含加密密钥(从字符串转换为字节数组)。等等!在抱怨安全性等问题之前,请温和地看一下上面“复选框 3(使用安全代码)”的描述。

object NewClass(string classname) 是用于实例化加密代码(ecode)中原始类的方法。在加密表单代码时,我也发现这很有用:将 Form1.cs 和 Form1.Designer.cs 放在同一个文件中,然后加密它们,创建一个只包含它们的管理类;然后,在 Program.cs 中将 'Application.Run(new Form1());' 替换为 'Application.Run(new MyManagementClass().NewClass("Form1"));'。通过这样做,我能够运行一个简单的程序,其中唯一可见的代码是管理类和简短的“program.cs”文件中的Main类。

object CallMethod(string classname, string methodname, params object[] methodparameters) 应用于调用加密类中的方法。参数应该是不言自明的:类名、方法名以及所有方法接受的参数列表(逗号分隔)。

// Available classes: 是一个注释(哇!)它显示了加密代码(ecode)中所有类的名称。我只把它放在那里作为参考,以便在我想测试它时,类名就在我眼前。如果您不喜欢它,只需删除它。(选中“使用安全代码”复选框时,不显示此注释)。

更多细节

现在让我们更详细地检查一下这两个方法是如何工作的。

NewClass 方法

private object NewClass(string classname)
{
    System.Reflection.Assembly asm = RuntimeEncryptedCodeCompiler.CompileEncryptedCode(ecode, key);
    return asm.CreateInstance(classname);  
}

很简单:使用 CompileEncryptedCode 方法创建一个 Assembly,并从中创建一个实例。

public static System.Reflection.Assembly CompileEncryptedCode(byte[] ecode, byte[] key)
{
    byte[] dcode = ecode.RijndaelEncryption(key, false);
    string code = System.Text.Encoding.UTF8.GetString(dcode);
    return CompileCode(code);
}

CompileEncryptedCode 是 'MyRuntimeCompiler.cs' 文件中 class RuntimeEncryptedCodeCompiler 的一个静态方法:它使用 Rijndael 加密算法解密密钥,然后将字节数组转换为字符串(包含原始代码),然后编译代码。加密例程和运行时代码编译例程都可以在 RuntimeEncryptedCodeCompiler 的私有部分找到。我根据规范从头开始用 c# 编写了加密算法(我为什么要这样做?因为我喜欢编写算法,而且它很美,当然),而对于运行时编译,我搜索了互联网,并从 StackOverflow 网站复制/修改/改编了一些代码(参见上面“背景”部分中提到的该网站的第一个链接)。

CallMethod 方法

private object CallMethod(string classname, string methodname, params object[] methodparameters)
{
    System.Reflection.Assembly asm = RuntimeEncryptedCodeCompiler.CompileEncryptedCode(ecode, key);
    return RuntimeEncryptedCodeCompiler.CallMethod(asm, classname, methodname, methodparameters);
}

与前一个方法一样,首先创建一个 Assembly 对象,然后调用上面提到的同一 RuntimeEncryptedCodeCompiler 类的静态方法,创建一个所需的原始类的实例,然后使用反射调用所需的(使用 Type -> InvokeMember)方法,如以下代码所示。

public static object CallMethod(Assembly ass, string classname, string methodname, object[] methodparameters)
{
    object obj = ass.CreateInstance(classname);
    object result = obj.GetType().InvokeMember(methodname, 
      System.Reflection.BindingFlags.InvokeMethod, null, obj, methodparameters);
    return result; 
}
使用生成的代码

一旦先前生成的“管理类”安全地保存在项目内的 .cs 文件中,就可以轻松地调用“原始类”的方法,现在它已加密在“管理类”中。

myspace.MyManagementClass tc = new myspace.MyManagementClass();
object bm = tc.CallMethod("MyMath.BasicMath", "add", 4, 7);

第一行我们创建“管理类”的一个新实例,此处称为 MyManagementClass

第二行代码,我们调用已加密“原始类”的方法。我们通过调用“管理类”的“CallMethod”方法来实现这一点,传递以下参数:类名(如果“原始类”中存在命名空间,则包含命名空间)、方法名以及该方法所需的参数列表。然后我们从该方法获取返回对象(object bm),并且我们还可以(可选地)将其转换为更适合我们需求的特定类型。

使用代码(可选用法)

上面描述的是最简单的情况,它包含了运行时编译加密代码的所有基本功能;勾选表单上的可选功能会在输出中添加一些有用的代码,但不会改变刚才描述的基本概念。

创建便捷方法

如果我们勾选“创建便捷方法”复选框,将会创建更多方法:对于“原始类”文本中包含的每个类,将有一个创建者和一个调用每个 said 类加密方法的方法。在我们的例子中(“MyMath”类),将创建一个创建者方法和一个调用者方法。

public object NewMyMath_BasicMath() // the creator (no need to pass class name)
public object CallMyMath_BasicMathMethod (string methodname, params object[] methodparameters) // the method caller (no need to pass class name) 

这些方法充当了前面提到的 NewClassCallMethod 方法,在这种情况下,它们没有被创建。

public object NewMyMath_BasicMath()
{
    System.Reflection.Assembly asm = RuntimeEncryptedCodeCompiler.CompileEncryptedCode(ecode, key);
    return asm.CreateInstance("MyMath.BasicMath");
}
 
public object CallMyMath_BasicMathMethod(string methodname, params object[] methodparameters)
{
    System.Reflection.Assembly asm = RuntimeEncryptedCodeCompiler.CompileEncryptedCode(ecode, key);
    return RuntimeEncryptedCodeCompiler.CallMethod(asm, "MyMath.BasicMath", methodname, methodparameters);
}
创建独立类

如果我们勾选“创建独立类”复选框,那么输出的“管理类”将不再依赖于“RuntimeEncryptedCodeCompiler”类:这是通过将所有解密例程包含在“管理类”中来实现的。这些例程包括前面提到的“CompileEncryptedCode”方法以及 Rijndael 加密/解密方法。

CompileEncryptedCode”是对我在 StackOverflow 网站上找到的代码(参见上面“背景”部分中提到的该网站的第一个链接)的修改版本(增加了加密功能)。它主要解密作为参数传递的代码,并将其转换为 UTF8 字符串,然后借助“CSharpCodeProvider”返回一个 System.Reflection.Assembly

Rijndael 加密算法在此不作解释,因为它不是本文的主要话题,而且互联网上有大量的相关文档。

如果您想查看“CompileEncryptedCode”方法的代码或检查加密例程,请在源文件(顶部可下载的 zip 文件)中找到“RuntimeEncryptedCodeCompiler”类;您也可以(当然)在“类加密器程序”的输出“管理类”中找到它们。

使用安全代码

如果我们勾选“使用安全代码”复选框,则“管理类”中的 'private readonly byte[] key' 将被注释掉,并且所有方法将有一个 'byte[] key' 作为第一个参数。在这种情况下,我们需要在每次调用“管理类”的方法时提供密码。

public object NewClass(byte[] key, string classname)
{
    System.Reflection.Assembly asm = RuntimeEncryptedCodeCompiler.CompileEncryptedCode(ecode, key);
    return asm.CreateInstance(classname);
}
public object CallMethod(byte[] key, string classname, string methodname, params object[] methodparameters)
{
    System.Reflection.Assembly asm = RuntimeEncryptedCodeCompiler.CompileEncryptedCode(ecode, key);
    return RuntimeEncryptedCodeCompiler.CallMethod(asm, classname, methodname, methodparameters);
} 

正如您在代码中看到的,这两个基本方法与不带“使用安全代码”选项生成的方法几乎相同。在前一种情况下,密钥是一个全局只读变量:使用方便,但它将密码存储在代码中(因此,足够熟练的人可以找到它并用它来解密您的代码);而在这种“安全”情况下,密钥不存储在类代码中(因此应该更安全,这就是我给当前选项命名为“使用安全代码”的原因……),但是每次调用方法时,您都必须提供密钥(这对于懒惰的程序员来说可能很麻烦)。

可能还有其他许多解决方案,例如使用一个基础密码,该密码在执行过程中以某种方式进行置换/更改/解密,从而将其隐藏在代码中(我将在我的“冥想”时间考虑类似的事情,但目前我并不认为这个问题与本文的主要思想严格相关;但是如果您碰巧有一个好主意,请随时在下面的评论中分享,如果人们喜欢,我将实现它)。

使用代码(RuntimeEncryptedCodeCompiler)

在“MyRuntimeCompiler.cs”文件中有一个名为 MyRuntime 的命名空间;在该命名空间中有一个类 RuntimeEncryptedCodeCompiler。这个类是运行时编译加密代码的“核心”,也是“管理类”创建的核心(如果有人忘记了,它包含加密代码和使用它的方法)。

这个类包含三个方法

  1. public static string CreateEncryptedCodeManagingClass(string code, byte[] key, string namespacename, string classname, bool createStandAloneClass = false, bool createEasyMethods = false, bool securecode = false)”,它基本上在按下“加密”按钮(窗体中唯一的按钮)时被“类加密器”程序调用,以生成“管理类”的代码。
    此方法代码很长,但显而易见,因为它由创建大字符串组成,并逐渐附加小的代码字符串。
  2. public static System.Reflection.Assembly CompileEncryptedCode(byte[] ecode, byte[] key)”,它解密加密代码,然后编译并运行它。
    public static System.Reflection.Assembly CompileEncryptedCode(byte[] ecode, byte[] key)
    {
        byte[] dcode = ecode.RijndaelEncryption(key, false);
        string code = System.Text.Encoding.UTF8.GetString(dcode);
        return CompileCode(code);
    }  
    第一行使用 Rijndael 算法将代码解密为字节数组;第二行将此解密后的字节数组转换为 UTF8 字符串;第三行编译代码(使用下面的私有方法“CompileCode”)。
  3. public static object CallMethod(Assembly ass, string classname, string methodname, object[] methodparameters)”,它给定一个程序集,创建相应的对象并使用反射调用其中的一个方法(通常我们不需要直接调用此方法,但当管理类创建时没有选择“创建独立类”时,它会被调用)。

其他私有方法包含

  1. 非常重要的“private static Assembly CompileCode(string code)”,它接收一个包含有效 C# 代码的字符串作为参数,然后编译它(使用 'Microsoft.CSharp.CSharpCodeProvider' 类),并返回一个 Assembly 对象(通过它可以实例化一个新对象,还可以(通过反射)调用该对象中的方法)。
  2. 一系列简单的文本编辑例程,不值得解释
  3. 加密例程,实现 Rijndael 算法,这又太长了,无法在此解释(正如我之前所说:如果您有兴趣,他们写了很多关于这个主题的书籍和文章)。

关注点

有了下面解释的例程,我能想到的几个用途是:

  • 隐藏可执行文件中的代码
  • 隐藏只有在提供密码后才能工作的代码
  • 创建一个“仅供您观看”的可执行文件以显示信息(文本、图像等)
  • 通过一些小改动,可以从程序外部获取加密代码,从而允许将该代码存储在外部文件中,或者通过互联网(邮件、网站等)发送相同的代码。也许,下一个下雪的周末我会实现一些小的改动来从外部获取加密代码。

我对其他可能的用途以及改进此程序和相关源代码的新想法持开放态度。

历史

v1.00 在我开始写文章时就已经准备好了……然后,在写作过程中,一些想法涌现出来,我实现了它们,从而达到了当前的 v1.03(这是第一个发布的版本)。

© . All rights reserved.