[转]CLR 全面透彻解析 - 将 APTCA 程序集迁移到 .NET Framework 4

原文链接:http://msdn.microsoft.com/zh-cn/magazine/ee336023.aspx

 

 

将 APTCA 程序集迁移到 .NET Framework 4

Mike Rousos

在 Microsoft .NET Framework 4 中,公共语言运行时 (CLR) 安全模型发生了不少变化。其中一项变化,即采用 Level2 透明性(与 Silverlight 的安全模型非常相似)很可能影响 AllowPartiallyTrustedCallers (APTCA) 库的作者。

该影响源于 APTCA 的基础工作在 CLR v4 中发生了变化。APTCA 属性保持了向部分受信任调用方提供完全可信任库的功能,但具体的提供方式发生了变化,因此,有可能需要对 APTCA 库代码进行一些修改。

注意:本篇文章中的 v2 是指 CLR v2,它包括从 .NET Framework versions 2.0 到 3.5 SP1 的所有版本。

V4 之前的 APTCA

在 v4 之前,通过对所有入口点的完全信任隐式链接要求,所有签名程序集都受到保护,不提供给部分受信任调用方。这意味着,任何部分受信任代码在尝试访问强名称的程序集时都因为安全性异常而失败。这会阻止部分受信任调用方恶意调用(存在潜在风险的)完全受信任代码。

将 AllowPartiallyTrustedCallers 属性添加到签名的完全受信任库后,可对这些库移除这些隐式链接要求,从而可供部分受信任方使用。因此,APTCA 库通过 APTCA 公开的方法允许部分受信任代码以受控的方式访问特权操作。APTCA 作者有责任确保通过这种方式只向部分受信任方公开安全操作,任何存在潜在风险的操作都应通过隐式链接要求或完整要求加以保护。

V4 中的 APTCA

AllowPartiallyTrustedCallers 属性已发生变化。v4 再不用考虑链接要求。事实上,曾在 v2 中存在的对签名库的隐式链接要求已不复存在。而 v4 中所有完全受信任程序集在默认情况下都属于 SecurityCritical。另一方面,v4 中所有部分受信任程序集在默认情况下均属于 SecurityTransparent。正如下一部分的透明性概述中所言,SecurityTransparent 代码将无法调用 SecurityCritical 代码。

因此,新 v4 透明性系统对完全受信任代码提供与旧的链接要求相同的保护;由于 SecurityCritical 和 SecurityTransparent 是自动应用的透明性级别,部分受信任代码默认情况下无法调用完全受信任的库。

您可能猜到,v4 中对 AllowPartiallyTrustedCallers 的更改与此有关。在 v4 中,APTCA 的变化是从应用 SecurityCritical 的程序集取消了自动 SecurityCritical 行为。因此程序集默认为 SecurityTransparent,但允许 APTCA 程序集作者在必要时对特定类型和方法应用更加具体的 SecurityCritical 和 SecuritySafeCritical 属性。

透明性速成教程

熟悉 Silverlight 安全模型的读者对于 SecurityTransparent 和 SecurityCritical 等透明性属性的作用并不会感到陌生,因为新 v4 透明性模型与 Silverlight 安全模型非常相似。

让我们先来了解一下三个主要的透明性属性:SecurityTransparent、SecuritySafeCritical 和 SecurityCritical。

SecurityTransparent:标记为 SecurityTransparent 的代码从安全性角度而言是可靠的。它不能完成任何危险操作,例如声明权限、执行无法验证的代码或调用本机代码。它也不能直接调用 SecurityCritical 代码。

  • 如上文所述,出于安全的考虑,所有部分受信任代码都强制为 SecurityTransparent。这也是 APTCA 库的默认透明性。

SecurityCritical:与 SecurityTransparent 不同,SecurityCritical 代码能够执行任何所需操作。它能够执行声明、调用本机代码和其他操作。它能够调用其他方法,且不受透明性标记的限制。

  • 只有完全受信任代码才能为 SecurityCritical。事实上,(非 APTCA)完全受信任代码默认情况下属于 SecurityCritical,从而保护其免受透明的部分受信任调用方的调用。

SecuritySafeCritical:SecuritySafeCritical 代码起着桥梁的作用,它允许透明代码调用关键方法。SecuritySafeCritical 代码与 SecurityCritical 代码的权限相同,但它可由 SecurityTransparent 代码调用。因此,SecuritySafeCritical 代码必须以安全方式公开基础 SecurityCritical 方法(以避免一些部分受信任的恶意代码尝试通过 SecuritySafeCritical 层攻击这些方法),这一点极为重要。

  • 与 SecurityCritical 代码一样,SecuritySafeCritical 代码必须完全受信任。

图 1 描述了 SecurityTransparent、SecuritySafeCritical 和 SecurityCritical 代码之间的内在关系。


图 1 SecurityTransparent、SecuritySafeCritical 和 SecurityCritical 代码之间的内在关系

注意:除了图中所示的转换关系外,所有透明性级别都可以访问自身以及任何重要性级别较低的代码(例如,SecuritySafeCritical 代码可以访问 SecurityTransparent 代码)。AllowPartiallyTrustedCallers 属性使整个程序集默认情况下是 SecurityTransparent,因此,程序集作者必须将需要执行特权操作的方法专门标记为 SecurityCritical 或 SecuritySafeCritical。如果不进行这样的标记,APTCA 作者将会发现其代码因 MethodAccessExceptions、TypeAccessExceptions 和其他错误导致执行失败,这些错误指示 APTCA 库正在尝试从 SecurityTransparent 代码调用危险 API。

此处仅对该模型进行了简介,如需了解详细信息,请访问 MSDN 文档和 Andrew Dai 之前的文章“CLR 全面透彻解析”(文章地址:msdn.microsoft.com/magazine/ee677170.aspx)。

从 V2 迁移到 V4:要应用的属性

从 v2 APTCA 程序集迁移到 v4 的大部分工作涉及标识正确的透明属性并将其应用到需要它们的方法。下列指导原则对属性的选择进行了说明。

SecurityTransparent:不能执行任何安全敏感操作的代码应属于 SecurityTransparent。

与其他透明性设置不同,SecurityTransparent 行为在 APTCA 程序集中是默认行为属性,因此无需对其进行显式标记。如果无其他属性,代码将默认为透明。

透明代码的一个优势在于其安全性(因为不允许执行危险操作),因此对它的安全性审查没有 SecurityCritical 尤其是 SecuritySafeCritical 代码那么严格。建议您尽量对代码使用 SecurityTransparent。

在 SecurityTransparent 代码中禁止执行下列操作:

  • 调用 SecurityCritical 方法
  • 声明权限或权限集
  • 使用无法验证的代码
  • 调用非托管代码
  • 重写 SecurityCritical 的虚拟方法
  • 实现 SecurityCritical 接口
  • 从任何非 SecurityTransparent 类型派生

SecuritySafeCritical:如果代码可以由部分受信任调用方调用,但又需要能够调用潜在危险的 API,则此代码应标记为 SecuritySafeCritical。通常,要求权限的方法属于此类,因为它们代表介于部分受信任代码和特权操作之间的受保护边界。

由于 SecuritySafeCritical 代码允许部分受信任调用方间接访问危险 API,这是一个非常强大的属性,应对其应用持谨慎和保守的态度。SecuritySafeCritical 代码必须以特定、安全的方式对其调用方公开 SecurityCritical 功能。通常,一个较好的做法是让 SecuritySafeCritical 代码包含要求,以确保调用方能够访问 SecuritySafeCritical 代码将使用的特定资源。对 SecuritySafeCritical 代码而言,验证输入和输出也非常重要,从而确保不传递无效的值,以及任何返回的信息能够安全地交给部分受信任调用方。

由于可能存在安全风险,建议尽量少用 SecuritySafeCritical 代码。

SecurityCritical:如果向部分受信任调用方公开代码存在风险,则此代码应标记为 SecurityCritical。之前受链接要求保护的方法可能需要此属性。

SecurityCritical 代码的危险性低于 SecuritySafeCritical,因为透明调用方(部分受信任方)不能直接对其进行调用。然而,此代码能够执行很多高安全性操作。因此,为了将对安全审查的需求降至最低,最好也尽量少用 SecurityCritical 代码。

一般原则是尽可能将代码标记为 SecurityTransparent。其他代码则应标记为 SecurityCritical,除非明确期望透明代码将通过其访问 SecurityCritical 代码,则将该代码标记为 SecuritySafeCritical。

使用 SecAnnotate.exe

为了帮助正确应用透明性属性,可以使用新的 .NET Framework SDK 工具 Security Annotator (SecAnnotate.exe)。此工具使用用户的二进制数据(或二进制集合),并提供有关在何处应用透明性属性的指导。该工具在将 APTCA 库迁移到 v4 时非常有帮助。

SecAnnotate 的工作原理是数次遍历目标二进制数据,查找根据 CLR 规则需要标上透明性属性的方法。在随后的遍历中,该工具会根据之前遍历中建议的修改而查找必要的属性。例如,考虑下面一小段代码(假定其来自 APTCA 程序集):

static void Method1()
{
      Console.WriteLine("In method 1!");
      Method2();
}

static void Method2()
{
      PermissionSet ft = new PermissionSet(PermissionState.Unrestricted);
      ft.Assert();
      DangerousAPI();
      PermissionSet.RevertAssert();
}

 

SecAnnotate.exe 将立即注意到 Method2 不能为透明,因为它声明了一些权限。第一次遍历后,工具将确定 Method2 不是 SecurityCritical 就是 SecuritySafeCritical(除非透明代码需要专门访问此方法,否则它很有可能属于 SecurityCritical)。

第一次遍历二进制数据时,Method1 似乎并未引起工具的注意。但在第二次遍历时,工具将注意到 Method1 正在调用 Method2,正如 SecAnnotate 在第一次遍历时所建议的,Method2 成为 SecurityCritical。鉴于此,Method1 同样应该属于 SecurityCritical(作者也可根据自己的判断将其归于 SecuritySafeCritical)。经过两次遍历后,SecAnnotate 建议将这两种方法都标记为 SecurityCritical。

理解 SecAnnotate.exe 输出

Security Annotator 的输出是 XML 文件,其中包含发现的问题以及建议的解决方法。有时,Security Annotator 可能在后续的遍历中推翻之前的建议。如果出现这种情况,XML 文件将显示两次的建议。此时,您需要查看遍历编号,以清楚哪一项是最新建议,即正确的建议。

例如,让我们来考虑一下图 2 中的 Security Annotator 输出。注意:在方法 Logging.MethodA 的批注标记下有两个元素:一个 SecuritySafeCritical 标记和一个 SecurityCritical 标记。这表示 SecAnnotate 在分析过程中建议此方法使用 SecurityCritical 和 SecuritySafeCritical 属性。

图 2 Security Annotator 输出

<requiredAnnotations>
    <assembly name="Logging">
      <type name="Logging">
        <method name="MethodA()">
          <annotations>
            <safeCritical>
              <rule name="MethodsMustOverrideWithConsistentTransparency">
                <reason pass="2" sourceFile="d:\repro\aptca\logging.cs" sourceLine="67">Critical method Logging.MethodA()’ is overriding transparent or safe critical method ‘Logging.MethodA()’ in violation of method override rules.  Logging.MethodA()’ must become transparent or safe-critical in order to override a transparent or safe-critical virtual method or implement a transparent or safe-critical interface method.</reason>
              </rule>
            </safeCritical>
            <critical>
              <rule name="TransparentMethodsMustNotSatisfyLinkDemands">
                <reason pass="1" sourceFile="d:\repro\aptca\logging.cs" sourceLine="68">Security transparent method Logging.MethodA()’ satisfies a LinkDemand for ‘FileIOPermissionAttribute’ on method ‘Logging.set_LogLocation(System.String)’.  Logging.MethodA()’ should become critical or safe-critical in order to call ‘Logging.set_LogLocation(System.String)’.</reason>
              </rule>
            </critical>
          </annotations>
        </method>
      </type>
    </assembly>
  </requiredAnnotations>

 

对 SecurityCritical 元素的解释表示由于此方法正在调用受链接要求保护的内容,因此它不是 SecurityCritical 就是 SecuritySafeCritical。SecAnnotate.exe 默认情况下建议选择 SecurityCritical,因为它更加安全。注意:此处的遍历属性值为 1,这表示此建议来自 SecAnnotate.exe 对代码的第一次遍历。

接着对 SecuritySafeCritical 的建议表示 MethodA 正在重写透明基方法,因此必须为 SecurityTransparent 或 SecuritySafeCritical(必须具有与基方法相同的可访问性)。综合考虑此信息和之前的建议,SecAnnotate.exe 建议 MethodA 采用 SecuritySafeCritical。

注意:pass="2" 表示此建议来自 SecAnnotate.exe 对代码的第二次遍历。这是因为在第一次遍历中,工具不知道 MethodA 不能为透明,因此没有意识到对 SecuritySafeCritical 的要求。

由于 SecuritySafeCritical 建议源于第二次(最新)遍历,因此在此例中它是正确的批注。

SecAnnotate.exe 最佳实践

如果 SecurityCritical 和 SecuritySafeCritical 都是正确的标记,Security Annotator 优先选择代码已有的属性,其次选择 SecurityCritical,因为它的风险较小。不幸的是,这样做得到的代码虽然很安全,但在沙盒中却不可利用,因为将对部分受信任调用方堵塞所有入口点。

请牢记,如果 API 应由透明/部分受信任代码直接调用并且已考虑此点进行过安全审查,则这些 API 适用 SecuritySafeCritical。由于 Security Annotator 无法知道哪些 API 计划由部分受信任调用方调用,无法知道通过这种方式调用是否安全,因此它只会将很少的 API 标记为 SecuritySafeCritical。库作者必须手动将 SecuritySafeCritical 属性应用到某些方法,即便在使用 Security Annotator 时也如此。

由于在透明代码中发生一次受到禁止的操作,就有可能导致在 Security Annotator 的后续遍历中繁衍出许多 SecurityCritical 标记而不是导致策略性地放置 SecuritySafeCritical 标记,因此为 SecAnnotate.exe 使用 /p 命令行开关是不错的选择。开关 /p:x(x 表示数字)指示 Security Annotator 仅运行 x 次遍历,无需运行到完成所有必需的更改为止。下面是使用 Security Annotator 的一种好方法:

  1. 运行 SecAnnotate.exe /p:1 /d:<被引用程序集路径> <FileName.dll>
          a. 这将在需要时添加透明性属性,但仅针对单次遍历。如果到此为止,作者可以手动检查属性。
          b. 默认情况下,SecAnnotate.exe 仅在 GAC 中寻找它所批注的程序集的依赖关系。其他程序集必须通过 /d 开关指定路径。
  2. 使用建议属性更新库的源文件。但也要考虑出现多个属性的情况,然后选择正确的属性。在某些情况下,尽管 SecAnnotate 偏好 SecurityCritical,但 SecuritySafeCritical 才是正确的属性。
  3. 重新生成程序集,并从步骤 1 重复(不带 /p:1)。可以反复使用 /p:1 重新运行程序,但您无需这样做,因为在第一次迭代步骤 2 之后便已得到所需 SecuritySafeCritical 属性。

包含开发人员手动干预的迭代流程将产生批注正确的程序集,从而得到最多的透明代码。

确定和检查 SecuritySafeCritical API

如前文所述,SecAnnotate.exe 通常建议 API 不是 SecurityCritical 就是 SecuritySafeCritical。主要区别在于部分受信任调用方是否能够安全地调用 API。如果 API 经过所有验证可以确保基础关键或本机 API 能够被安全(例如通过要求或输入和输出验证)调用,则它可以标记为 SecuritySafeCritical,此属性有时更加理想,因此它允许 API 的调用方是透明的。另一方面,如果恶意代码有可能通过 API 访问受保护的资源,则该 API 必须保持为 SecurityCritical。

有必要仔细检查所有 SecuritySafeCritical 代码,以确定在其公开给部分受信任调用方后的安全影响。尽管 SecuritySafeCritical 和 SecurityCritical 代码都应尽量少用,但如果对于哪个属性合适心存疑虑,则 SecurityCritical 是较安全的选择。

应用透明性属性

应用透明性属性和在代码中应用任何其他 .NET 属性一样简单。MSDN 中提供了下列属性类型的使用文档:

  • SecurityTransparentAttribute
    注意:此属性只应用于程序集级别。在此情况下,它意味着程序集中的所有类型和方法都是透明的。在类型或方法级别没有必要使用此属性,因为它是 APTCA 程序集中的默认透明性设置。
  • SecuritySafeCriticalAttribute
  • SecurityCriticalAttribute

在 C# 中,属性的应用与下列代码类似:

[SecurityCritical]
public static void Method1()
{ /* Do something potentially dangerous*/ }

[SecuritySafeCritical]
public static void Method2()
{ /* Do something potentially dangerous in a safe way that can be called from partial trust */ }

 

Level1 和 Level2

关于透明性和 APTCA 的最后一点说明是:可以通过使用程序集级别的属性来使用较旧的 v2 APTCA 行为而非新 v4 行为。但不建议您这么做,这是因为新模型更安全,更易于审核,并且在 Silverlight 和桌面 CLR 之间通用。当然,在迁移之前,有时在短期内还是会存在兼容性问题。在这种情况下,您可以使用 SecurityRules 属性强制程序集使用较旧的 v2 规则。

SecurityRules 属性采用 SecurityRuleSet 枚举类型的参数。SecurityRuleSet.Level1 指定兼容性。SecurityRuleSet.Level2 指定新模型,但 Level2 属性并非必需,因为它是默认设置。但它能够用于显式指示使用中的透明性规则集,并防止将来对 .NET Framework 默认规则集的更改。

在 C# 中,此属性的应用如下所示:

[assembly:SecurityRules(SecurityRuleSet.Level1)]

 

常见缺陷

APTCA 库作者在从 v2 迁移到 v4 时应注意下列常见问题:

  • SecAnnotate.exe 会建议将 LinkDemands 更改为 SecurityCritical 属性(这与对 FullTrust 的 LinkDemands 非常相似)。但如果类型(而非方法)原来由 LinkDemand 保护,这与在 v4 中向类型应用 SecurityCritical 不同。最好对类型的所有成员应用 SecurityCritical,因为这与 v2 类型级别的 LinkDemand 更为相似。
  • 注意,有些部分受信任代码应该可以满足某些低权限 LinkDemand,将这些 LinkDemand 转换为 SecurityCritical 不一定是最好的选择。如果 LinkDemand 针对的是低权限(例如,对特定安全路径的读取权限),您最好移除 LinkDemand 并将其替换为针对相同权限的完整要求。这将使部分受信任代码能够继续调用 API(但该要求将确保仅具有足够权限的部分受信任代码才能进行调用)。
  • 通常,类型级别的透明性属性也会应用到它们修改的类型的成员。最外层的属性将取代其他属性。因此,如果方法所在的类型应用了 [SecuritySafeCritical],那么对该方法应用 [SecurityCritical] 将无效。通常,[SecuritySafeCritical] 在类型级别不是一个有用的属性。如果有人日后向类型添加新成员,但并未意识到它是 SecuritySafeCritical(源于类型级别属性),就可能导致安全漏洞,这种情况很可能发生。

尽管类型级别的属性应用到他们修改的新类型的插槽成员,但不会应用到重写的成员。如果您使用类型级别的透明性属性,在需要时务必专门将属性添加到重写成员。

迁移示例

图 3 是一个在 v2 中编写的简单(但不完整)的日志库。

图 3 V2 APTCA 库

using System;
using System.IO;
using System.Security;
using System.Security.Permissions;

// This assembly is meant to be representative of a simple v2 APTCA assembly
// It has some dangerous code protected with demands/link demands
// It exposes some dangerous code in a controlled way with an assert

[assembly: AllowPartiallyTrustedCallers]
public class Logging
{
    private string logLocation = @"C:\temp\firstfoo.txt";

    public virtual string Usage()
    {
        return "This is a helpful string";
    }

    public virtual string LogLocation
    {
        get
        {
            return logLocation;
        }

        [FileIOPermissionAttribute(SecurityAction.LinkDemand, Unrestricted=true)]
        set
        {
            logLocation = value;
        }
    }

    public virtual void SetLogLocation(int index)
    {
        switch (index)
        {
            case 1: 
                LogLocation = @"C:\temp\foo.txt";
                break;
            case 2:
                LogLocation = @"D:\temp\foo.txt";
                break;
            case 3:
                LogLocation = @"D:\repro\temp\foo.txt";
                break;
            default:
                break;
        }
    }


    public virtual void DeleteLog()
    {
        FileIOPermission fp = new FileIOPermission(FileIOPermissionAccess.AllAccess, LogLocation);
        fp.Assert();
        if (File.Exists(LogLocation)) { File.Delete(LogLocation); }
        SecurityPermission.RevertAll();
    }

    // TODO : Put other APIs (creating log, writing to log, etc) here
}

public class OtherLogging : Logging
{
    public override string Usage()
    {
        LogLocation = null;
        return "This is a different useful string";
    }

    // TODO : Put other APIs (creating log, writing to log, etc) here
}

 

图 4 显示的是迁移到 v4 的同一个库,附带解释变化的注释文字(斜体)。

图 4 V4 APTCA 库

using System;
using System.IO;
using System.Security;
using System.Security.Permissions;

// This assembly is meant to be representative of a simple v2 APTCA assembly
// It has some dangerous code protected with demands/link demands
// It exposes some dangerous code in a controlled way with an assert

[assembly: AllowPartiallyTrustedCallers]
public class Logging
{
    private string logLocation = @"C:\temp\firstfoo.txt";
	
    // This API can be transparent because it does nothing dangerous.
    // Transparent APIs need no attributes because it is the default behavior of a v4
    // APTCA assembly
    public virtual string Usage()
    {
        return "This is a helpful string";
    }

    // Note that transparency attributes do not go directly on properties.
    // Instead, they go on the getters and setters (even if the getter and setter
    // get the same attributes)
    public virtual string LogLocation
    {
        get
        {
            return logLocation;
        }

        // This API is made critical because it sets sensitive data (the path to write to)
        // which partial trust code should not be able to do.
        [SecurityCritical]
        // The previous LinkDemand is removed as the SecurityCritical attribute replaces it
	//[FileIOPermissionAttribute(SecurityAction.LinkDemand, Unrestricted=true)]
        set
        {
            logLocation = value;
        }
    }

    // This API accesses a critical member (LogLocation) and, therefore, cannot be transparent
    // However, the access is done in a limited, safe way and we expect transparent code
    // should be able to call this API. Therefore, it is SecuritySafeCritical
    [SecuritySafeCritical]
    public virtual void SetLogLocation(int index)
    {
        switch (index)
        {
            case 1: 
                LogLocation = @"C:\temp\foo.txt";
                break;
            case 2:
                LogLocation = @"D:\temp\foo.txt";
                break;
            case 3:
                LogLocation = @"D:\repro\temp\foo.txt";
                break;
            default:
                break;
        }
    }

    // This API is potentially dangerous; it asserts which means it can’t be transparent
    // Because setting LogLocation is protected, however, partial trust code can safely
    // call this API. In fact, it is intended that it is safe for partial trust code
    // to call this method. Therefore, it is SecuritySafeCritical
    [SecuritySafeCritical]
    public virtual void DeleteLog()
    {
        FileIOPermission fp = new FileIOPermission(FileIOPermissionAccess.AllAccess, LogLocation);
        fp.Assert();
        if (File.Exists(LogLocation)) { File.Delete(LogLocation); }
        SecurityPermission.RevertAll();
    }

    // TODO : Put other APIs (creating log, writing to log, etc) here
}

public class OtherLogging : Logging
{
    // The logic for attributing this method is complicated and it is an example of when
    // SecAnnotate.exe can be very helpful. This API cannot be transparent because it 
    // calls a critical member (LogLocation). However, because it overrides a transparent
    // method (Usage) it cannot be critical. Therefore, the only possible annotation here
    // is SecuritySafeCritical and it is the author’s responsibility to make sure that
    // a malicious caller cannot abuse that access.
    [SecuritySafeCritical]
    public override string Usage()
    {
        LogLocation = null;
        return "This is a different useful string";
    }

    // TODO : Put other APIs (creating log, writing to log, etc) here
}

 

同步 CLR 和 Silverlight CoreCLR 安全系统

尽管将 APTCA 和 v4 中的透明性合并似乎很复杂,但合并将最终在部分受信任调用方调用系统敏感资源时提供直接和有效的保护。此外,此变化较好地协调了桌面 CLR 和 Silverlight CoreCLR 安全系统。

SecAnnotate.exe 等 SDK 工具和 FxCop 规则(可以验证透明性)有助于简化迁移。V4 APTCA 程序集更加易于审核,您只要密切关注 SecuritySafeCritical API(和它们执行的 SecurityCritical 调用)即可对程序集的安全性满怀信心。

由于透明代码通常占程序集的 80-90% 甚至更多,因此同步合并可显著减少审核的工作量。如果您希望更加深入地了解透明性,请参阅 MSDN 文档以获得更全面的解释。

 

posted @ 2010-11-26 17:16  Alen在西安  阅读(1280)  评论(0编辑  收藏  举报