在.NET环境禁止别人调用代码

作者:俞伟明 发文时间:2003.08.18 12:00:49

提纲

……………………………
一、禁止未经授权的用户
二、强名称程序集
三、测试
四、安全漏洞?
……………………………

内容

现在,你应该早已试着用.NET框架编写过“Hello World”程序——也许用C#,也许用VB.NET,甚至可能用托管VC++。其实对于.NET来说,用哪一种语言并不重要,因为.NET允许用一种语言编写的.NET程序方便地调用其他语言或作者写的代码。然而,既然代码共享变得如此方便,我们怎样来保证自己的代码不被未经授权的用户使用?

在宣传.NET框架时,微软提出的一个卖点是代码访问安全(CAS,Code Access Security)的概念。由CLR(Common Language Runtime,公共语言运行时环境)执行的每一段代码都在一个安全上下文内执行,代码的授权以各种标识信息为基础,这些标识信息也是程序集的强名称(Strong Name)的构成元素,包括:

▲ 文件名字:程序集的文件名称,例如MyAssembly.DLL或MyProgram.EXE等。
▲ 区域性:程序集的目标区域环境,如en、fr或fr-CA。
▲ 版本:程序集的版本号。
▲ 公用密钥:构造程序集时如果指定了RSA签名文件,绑定到程序集的公用密钥。

本文讨论的主要是最后一项——公用密钥。

一、禁止未经授权的用户

管理员可以在上述任意标识信息的基础上,执行一个安全策略,授予或取消程序集的各种执行权限。例如,管理员可以禁止某个程序集访问Internet,或禁止程序集删除本地硬盘的文件。对于管理员来说,全面地控制代码在自己的机器上可以做什么、不可以做什么无疑是很受欢迎的。但是,当开发者和管理员的出发点不同时,问题就出现了。

考虑一下这种情形:你开发了一个软件,它要读写一个加密的、格式私有的数据文件——这是该软件保持优势的关键所在。现在,随着.NET的流行,你把软件升级到了.NET平台并予以发布。假设你的软件有一个类专门负责私有数据文件的所有I/O操作,问题出现了:只要运用ILDASM.EXE之类的命令行工具和程序逻辑工具,其他人能够方便地分析程序集的原数据,获得所有方法及其参数的详细说明。对于一个有经验的程序员来说,也许只需数分钟时间就能够了解如何运用该I/O类来操作你私有的数据文件。显然,我们不希望出现这种情形。

其实,这只是一个简单的例子,促使人们设法保护自己代码的原因不可胜数。但无论出于什么原因,保护代码最好的办法就是避免未经授权的用户直接调用某些API函数和类。

二、强名称程序集

文章开头已经提到,程序集可以通过用一个RSA签名文件签名的办法实现强名称(Strong Naming)。我们创建一个公用/私有密钥对,把它保存到文件,所有用同一签名文件构造的程序集将拥有同样的公用密钥,可以相信这些程序集来自同一开发者。任何其他人都不能构造出带有同样公用密钥的签名文件,除非别人得到了你的RSA签名文件。

假设我们要为A公司创建RSA签名文件,先进入命令行环境(最好使用VS.NET提供的快捷方式,它会自动配置路径信息)。假设构造VS.NET项目的根是C:\test,转到C:\test目录,然后执行命令:SN -k SecureProducts.snk。不带参数执行SN命令可获得SN的使用帮助。

在C:\test目录下生成SecureProducts.snk签名文件后,下面我们构造一个程序集,然后用SecureProducts.snk签名文件把它签名。

启动VS.NET,创建一个C#的类库项目,将它命名为SecureAssembly。打开AssemblyInfo.cs,将版本号设置为1.0.0.0,修改AssemblyKeyFile属性,使其指向刚才创建的签名文件。这两行属性修改后应当类如:

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyKeyFile(@"C:\test\SecureProducts.snk")]

接下来删除项目中默认创建的Class1.cs类,另外创建一个新类SecuredClass。为SecuredClass加入一个GetTopSecretInformation方法,代码如下:

public class SecuredClass {
    public SecuredClass() { }
    public string GetTopSecretInformation(){
        return "Secret Code:000111";
    }
}

为了不让GetTopSecretInformation方法提供的信息落入竞争对手的手中,我们要保证只有自己的代码(带有正确公用密钥的代码)才能够创建SecuredClass类的实例并执行GetTopSecretInformation方法。

为此,我们要获得A公司完整的公用密钥。这要用到另一个命令行工具secutil,它能够从已经编译好的程序集获得安全信息。在命令行环境中,进入SecureAssembly\obj\debug目录,执行:Secutil -hex -strongname SecureAssembly.dll > secutiloutput.txt。命令执行的结果是创建一个文本文件secutiloutput.txt,内容类如:

Microsoft (R) .NET Framework SecUtil 1.0.3705.0
Copyright (C) Microsoft Corporation 1998-2001. All rights reserved.
Public Key =
0x0024…0AC
Name =
SecureAssembly
Version =
1.0.0.0
Success

选中公用密钥(前缀0x除外,共320个字母。0x表示十六进制),将它复制到剪贴板。然后将公用密钥以属性的形式放入SecuredClass类定义的最前面,例如:

[StrongNameIdentityPermission(SecurityAction.LinkDemand,
PublicKey="0024…0AC")]
public class SecuredClass

这些操作的目的是:我们告诉CLR,任何试图访问该类的程序(无论是静态访问还是通过对象实例化),都必须有指定的公用密钥(LinkDemand枚举值)。如果没有,CLR将抛出异常。注意,为了让SecuredClass顺利编译,SecuredClass.cs的开头要加上using System.Security.Permissions语句。

如果没有保存在SecureProducts.snk文件中的对应的私有密钥,任何人无法构造出拥有该公用密钥的程序集。因此,一定要保证签名文件本身的安全。

三、测试

下面来测试一下这种办法是否确实有效。我们要构造一个控制台应用来引用上面的类,创建SecuredClass的实例并输出机密字符串。下面是控制台应用的主要代码:

static void Main(string[] args)
{
    SecureAssembly.SecuredClass secClass;
    secClass = new SecureAssembly.SecuredClass();
    Console.WriteLine("锦囊秘籍:{0}", secClass.GetTopSecretInformation());
}

在不对该程序签名的情况下运行,输出如图一所示。可以看到,CLR拒绝执行SecuredClass类的GetTopSecretInformation方法。

img_1
图一

现在创建另外一个控制台应用(或修改原来的应用),这一次在AssemblyInfo.cs中设定:

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyKeyFile(@"c:\test\SecureProducts.snk")]

再编译、运行程序,得到图二的输出结果。

img_2
图二

四、安全漏洞?

还有一个必须关注的问题是:用户可能通过其他方式获得程序代码中的字符串。例如,进入命令行环境,转到SecureAssembly\obj\debug目录,执行ILDASM SecureAssembly.dll。你可以毫不费力地找到IL(中间语言)形式的方法定义,如图三。

img_3
图三

这不是太简单了吗?我们花了很大力气加以保护的机密信息竟然可以通过这种方式获得!无论是公用密钥、签名文件,都挡不住一个简单的免费工具ILDASM!

如果你确实担心有人偷看代码中的字符串,解决办法是加密字符常量,或对整个程序进行模糊处理。本文介绍的技术能够有效地防止其他人调用程序集,如果你要保护的重点是程序中的字符串而不是程序逻辑,单纯运用密钥/签名文件是不够的。

(责任编辑:赵纪雷)

Contributors: FHL