首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第53期->技术专题
期刊号: 类型: 关键词:
CLR 中凭据(Evidence)相关信息获取原理浅析

作者:Flier Lu <flier at nsfocus.com>
出处:http://www.blogcn.com/User8/flier_lu/blog/3783526.html
主页:http://flier_lu.blogone.net/
日期:2004-11-05

我在前一篇文章《CLR 中代码访问安全检测实现原理》(后文中简称为【文1】)中简单介绍了 CLR 中是如何实现 CAS (代码访问安全) 检测,其中提到在对 Assembly/AppDomain 进行权限验证时,是直接从与其绑定的安全描述符中获取其权限集的,然后与需要检查的权限进行集合操作,验证权限是否存在于目标对象的权限集中。
    本文中将简要分析 CLR 是如何获取并构造这个与 Assembly/AppDomain 绑定的权限集的,以及如何使用之。

    通过【文1】的简要介绍我们可以知道,Java 和 IE 实际上是运行在一个基于位置的安全模型上。
    一个组件在 IE 中能够有多大权限,取决于此组件从哪个区域 (Zone) 被加载,使用者可以通过在 IE 安全中为不同 Zone 设置不同的安全权限集,限制不同位置组件的权限。但就如前文所述,IE 这种安全模型有个致命缺陷是缺少一个可信任的调用链,造成恶意代码通过被信任组件执行越权操作的可能性。
    Java 的安全模型则相对要完善一些,类型 (Class) 由类加载器 (ClassLoader) 将之与某个代码源 (CodeSource) 绑定,在加载时通过安全策略 (Policy) 获取类型的权限集 (PermissionSet),最终完成类型与权限集的绑定。在需要进行检测时,可以显式通过访问控制器 (AccessControl) 验证当前调用链上类的权限是否符合要求。JVM 安全模型的具体实现细节这里就不详细展开解释了,有兴趣的朋友可以参考《Java 安全》一书。

    CLR 的安全模型与 Java 的设计思路非常相似,可以说是在其基础上的一个扩展设计。

    首先,CLR 扩展了 Java 和 IE 中位置的概念,不再仅仅将组件所在位置作为组件的身份依据,而是引入了凭据 (Evidence) 这个概念。凭据实际上是一种广义上的代码源的概念,它不仅包括 Java 和 IE 中代码位置和证书,还可以包括组件内容的 Hash 值、发布商签名、来源站点地址等等多种粒度多个角度的证明代码身份的信息类型,同时提供了灵活的扩展机制。
    为了灵活性和可扩展性 CLR 又将凭据进一步细分为 Host 凭据和 Assembly 凭据两类:前者包括组件由 CLR 宿主 (host) 载入时显式获得的凭据信息;后者包括用户自定义的嵌入 Assembly 的特殊凭据。
    Host Evidence 包括以下类型的信息:

以下为引用:

    Hash                  Assembly 内容的 Hash 值
    StrongName            Assembly 的强签名
    Publisher             Assembly 发布者的 X509 证书

    ApplicationDirectory  应用程序路径
    Zone                  Assembly 来源区域
    Site                  Assembly 来源站点
    Url                   Assembly 来源 URL




    Hash, StrongName, Publisher 是 Assembly 内容的不同层面的凭据;ApplicationDirectory, Zone, Site, Url 则是 Assembly 位置的不同层面的凭据。通过这些不同角度不同粒度的凭据信息,CLR 代码可以很灵活的对代码权限进行限定。
    Assembly Evidence 则可以根据用户实际需求自行扩展,如可以增加一个代码作者证书的凭据。

    相对来说 Java 的安全模型就较为简单,只有一个固化了的对 URL 和证书提供支持的 CodeSource 类提供代码凭据。而 CLR 中与 CodeSource 类对应的是 Evidence 类,封装了对一组凭据信息的支持。

    Assembly 在载入时,可以由载入 Assembly 的宿主显式指定 Evidence,也可以由 fusion 在载入 Assembly 时自动获取载入位置、强签名等信息,生成 Evidence;而 AppDomain 在创建的时候,必须显式指定需要设置的 Evidence,否则会自动重用当前 AppDomain 的 Evidence。载入后的 Assembly 和创建的 AppDomain 可以通过 Evidence 属性访问与之绑定的凭据信息。

    下面将对 Assembly.Load 和 AppDomain.CreateDomain 这两组方法进行简单的实现原理分析,了解 Evidence 的创建过程。

    CLR 中并没有一个严格意义上与 JVM ClassLoader 对应的类型,而是将其功能分布在 Assembly, AppDomain 等多个类型上。例如虽然可以使用 Assembly.Load 和 AppDomain.Load 等众多方法将指定 Assembly 载入到特定的 AppDomain 中,但实际上最终都是通过调用 Assembly.InternalLoad 方法,使用 Fusion 的底层支持完成载入工作。而 JVM 的 ClassLoader 则给予用户相对更多一些的灵活性,自身的功能内聚性更强。
    首先来看看 Assembly 的载入过程:
以下内容为程序代码:

public sealed class AppDomain : ...
{
  public Assembly Load(string assemblyString)
  {
    StackCrawlMark mark = StackCrawlMark.LookForMyCaller;
    return Assembly.InternalLoad(assemblyString, null, ref mark);
  }

  public Assembly Load(string assemblyString, Evidence assemblySecurity)
  {
    StackCrawlMark mark = StackCrawlMark.LookForMyCaller;
    return Assembly.InternalLoad(assemblyString, assemblySecurity, ref mark);
  }
}

public class Assembly : ...
{
  public static Assembly Load(string assemblyString)
  {
    StackCrawlMark mark = StackCrawlMark.LookForMyCaller;
    return Assembly.InternalLoad(assemblyString, null, ref mark);
  }

  public static Assembly Load(string assemblyString, Evidence assemblySecurity)
  {
    StackCrawlMark mark = StackCrawlMark.LookForMyCaller;
    return Assembly.InternalLoad(assemblyString, assemblySecurity, ref mark);
  }
}



    AppDomain.Load 和 Assembly.Load 方法都是通过指定的 Assembly 名字将之载入,与之对应的还有 xxx.LoadFile, xxx.LoadFrom 等方法,不过因为主要流程区别不大,这儿就不详细分析了。

    Assembly.InternalLoad 方法 (bcl\system\reflection\assembly.cs:1022) 主要工作是完成对参数的验证和目标组件访问权限的验证。因为载入 Assembly 是一项需要严格受限的特性,只有受到高度信任的程序才能执行此功能。此外 InternalLoad 方法还会根据目标 Assembly 的所在,检查当前代码调用链是否具有访问目标文件的权限。最后会调用内部方法 Assembly.nLoad 完成实际的 Assembly 的载入。(过于内部方法的相关概念,请参考我另外一篇文章 InternalCall 的使用与实现》)
以下内容为程序代码:

public class Assembly : ...
{
  internal static Assembly InternalLoad(string assemblyString, Evidence assemblySecurity, ref StackCrawlMark stackMark)
  {
    if (assemblyString == null)
      throw new ArgumentNullException("assemblyString");

    AssemblyName name = new AssemblyName();
    name.Name = assemblyString;
    return Assembly.InternalLoad(name, true, assemblySecurity, ref stackMark);
  }

  internal static Assembly InternalLoad(AssemblyName assemblyRef, bool stringized, Evidence assemblySecurity, ref StackCrawlMark stackMark)
  {
    if (assemblyRef == null)
      throw new ArgumentNullException("assemblyRef");

    assemblyRef=(AssemblyName)assemblyRef.Clone();

    // 经验载入代码的调用链是否都具有控制凭证的权限
    // ControlEvidence 权限一般只被授予完全信任的代码
    // 因此 Assembly 的载入工作一般只在受信任的宿主代码中执行
    if (assemblySecurity != null)
      new SecurityPermission( SecurityPermissionFlag.ControlEvidence ).Demand();

    // 验证代码来源地址的有效性,如果是本地访问,将自动添加 file:// 协议头
    String codeBase = AppDomain.VerifyCodeBase(assemblyRef.CodeBase);

    if (codeBase != null)
    {
      // 是否载入本地文件
      if (String.Compare( codeBase, 0, s_localFilePrefix, 0, 5, true, CultureInfo.InvariantCulture) != 0)
      {
        // 网络方式加载组件则验证调用链的网络访问权限
        IPermission perm = CreateWebPermission( assemblyRef.EscapedCodeBase );

        if (perm == null)
        {
          BCLDebug.Assert( false, "Unable to create System.Net.WebPermission" );
          return null;
        }

        perm.Demand();
      }
      else
      {
        // 载入本地文件时,只需要验证对目标文件是否有读权限即可
        System.Security.Util.URLString urlString = new System.Security.Util.URLString( codeBase, true );

        new FileIOPermission( FileIOPermissionAccess.Read, urlString.GetFileName() ).Demand();
      }
    }

    // 调用内部方法完成实际的 Assembly 载入工作
    return nLoad(assemblyRef, codeBase, stringized, assemblySecurity, true, null, ref stackMark);
  }

  [MethodImpl(MethodImplOptions.InternalCall)]
  private static Assembly nLoad(AssemblyName fileName, string codeBase, bool isStringized, Evidence assemblySecurity, bool throwOnFileNotFound, Assembly locationHint, ref StackCrawlMark stackMark);
}



    实现 Assembly.nLoad 的 AssemblyNative::Load 函数 (vm\AssemblyNative.cpp:57),实际上只是为 Fusion 的 AssemblySpec 类做初始化工作。AssemblyNative::Load 函数从参数传入的 AssemblyName 对象等信息中,获取 Assembly 名字、强签名 (StrongName)、代码基 (CodeBase)等信息,初始化 AssemblySpec 类,最终调用 AssemblySpec::LoadAssembly 函数完成 Assembly 的加载。
    这里获取的强签名信息 (Public Key, Public Key Token)和 Code Base 是后面生成 Evidence 的重要组成信息,但如果用户在调用 Assembly.Load 时指定了 Evidence 的话 (fIsStringized == true),则部分信息获取工作将被跳过。
以下内容为程序代码:

Object* AssemblyNative::Load(AssemblyNameBaseObject* assemblyNameUNSAFE,
                             StringObject* codeBaseUNSAFE,
                             BYTE fIsStringized,
                             Object* securityUNSAFE,
                             BYTE fThrowOnFileNotFound,
                             AssemblyBaseObject* locationHintUNSAFE,
                             StackCrawlMark* stackMark)
{
  // 建立一个需要对 GC 操作进行保护的对象结构
  struct _gc
  {
      ASSEMBLYNAMEREF assemblyName;
      STRINGREF       codeBase;
      ASSEMBLYREF     locationHint;
      OBJECTREF       security;
  } gc;

  gc.assemblyName    = (ASSEMBLYNAMEREF) assemblyNameUNSAFE;
  gc.codeBase        = (STRINGREF)       codeBaseUNSAFE;
  gc.locationHint    = (ASSEMBLYREF)     locationHintUNSAFE;
  gc.security        = (OBJECTREF)       securityUNSAFE;

  // 保障 gc 结构中的对象不会被 GC 自动回收
  // 实际上类似于 C# 中的 pin 对象保护机制
  GCPROTECT_BEGIN(gc);

  // 处理没有显式指定 Evidence 的情况
  if (!fIsStringized)
  {
    if (gc.assemblyName->GetPublicKeyToken() != NULL)
    {
      // 获取并复制 public key token
    }

    else if (gc.assemblyName->GetPublicKey() != NULL)
    {
      // 获取并复制 public key
    }
  }

  // 定义用于 Assembly 实际载入工作的 AssemblySpec 对象
  AssemblySpec spec;

  if (gc.codeBase != NULL)
  {
    // 获取并复制 code base
  }

  if (gc.assemblyName->GetSimpleName() != NULL)
  {
    // 获取并转换 Assembly 的名字
    LPSTR psSimpleName = ...

    // 获取父 Assembly 的 IAssembly 引用
    Assembly *pRefAssembly = ...

    // 在指定 Evidence 时用名字初始化描述符,否则用获取的安全信息初始化之
    if (fIsStringized)
      hr = spec.Init(psSimpleName);
    else
      hr = spec.Init(psSimpleName, &sContext, pStrongName, dwStrongName, dwFlags);

    spec.GetCodeBase()->SetParentAssembly(pRefIAssembly);
  }
  else
    spec.SetCodeBase(pCodeBase, dwCodeBase);


  // 实际完成 Assembly 的载入工作
  HRESULT hr = spec.LoadAssembly(&pAssembly, &Throwable, &gc.security);

  if ((!Assembly::ModuleFound(hr)) && psSimpleName && pCodeBase)
  {
    // 当用户同时指定 Assembly 的名字和 Code Base 时
    // 尝试通过 Code Base 完成通过名字无法完成的载入操作
  }
}



    AssemblySpec::LoadAssembly 函数 (vm\AssemblySpec.cpp:436) 比较复杂,主要负责从缓存或 Fusion 的搜索路径下,定位并载入 Assembly 的文件,最终调用 BaseDomain::LoadAssembly 函数分析并载入 Assembly 的内容。
    AssemblySpec::LoadAssembly 函数伪代码如下:
以下内容为程序代码:

HRESULT AssemblySpec::LoadAssembly(Assembly** ppAssembly,
                                   OBJECTREF* pThrowable, /*= NULL*/
                                   OBJECTREF* pExtraEvidence, /*= NULL*/
                                   BOOL fPolicyLoad) /*= FALSE*/
{
  IAssembly* pIAssembly = NULL;
  Assembly *pAssembly;

  // 在当前 AppDomain 的缓存中寻找是否以及加载过此 Assembly
  if((pAssembly = GetAppDomain()->FindCachedAssembly(this)) != NULL)
  {
    *ppAssembly = pAssembly;
    return S_FALSE;
  }

  // 尝试寻找并载入 Assembly 的目标文件
  if(S_FALSE == GetAppDomain()->BindAssemblySpec(this, &pFile, &pIAssembly, &pAssembly, pExtraEvidence, pThrowable))
  {
    // 当 Assembly 是通过 AppDomain.AssemblyResolve 事件被载入时
    // 获取并保存 Assembly 的强签名公钥

    *ppAssembly = pAssembly;
    return S_OK;
  }

  if (pIAssembly)
  {
    // 处理同一内存模块 (HMODULE) 对应多个 Assembly 对象的问题
    //...
  }

  Module* pModule;

  // 在当前 AppDomain 中分析并载入 Assembly 的实际内容
  HRESULT hr = GetAppDomain()->LoadAssembly(pFile, pIAssembly, &pModule, &pAssembly, pExtraEvidence, fPolicyLoad, pThrowable);

  // 载入成功则将 assembly 放入当前 AppDomain 的缓存中
  if(SUCCEEDED(hr))
  {
    *ppAssembly = pAssembly;
    /*HRESULT hrLoose =*/ GetAppDomain()->AddAssemblyToCache(this, pAssembly);
  }
  return hr;
}



    这里 fusion 定位并载入 Assembly 文件的流程就不详细解释了,有兴趣的朋友可以参看 Fusion 项目组成员 Jufeng Zhang 的 blog。

    BaseDomain::LoadAssembly 函数 (vm\AppDomain.cpp:3738) 是实际完成 Assembly 内容分析与载入的所在,伪代码如下:
以下内容为程序代码:

HRESULT BaseDomain::LoadAssembly(PEFile *pFile,
                                 IAssembly* pIAssembly,
                                 Module** ppModule,
                                 Assembly** ppAssembly,
                                 OBJECTREF *pExtraEvidence,
                                 BOOL fPolicyLoad,
                                 OBJECTREF *pThrowable)
{
  if (pFile->IsSystem() && this != SystemDomain::System())
  {
    // 总是将系统库加载到系统 AppDomain 中

    return SystemDomain::System()->LoadAssembly(...);
  }

  if((pModule = FindModule(pFile->GetBase()) != NULL)
  {
    // 处理已经载入过此模块的情况
    //...

    return S_FALSE;
  }

  if(m_AssemblyLoadLock.Find(pFile->GetBase()) != NULL)
  {
    // 处理递归载入 Assembly 的情况
    //...

    return XXX;
  }

  // 处理防止递归载入 Assembly 的代码
  //...

  // 分配 Assembly 安全描述符
  AssemblySecurityDescriptor *pSecDesc = AssemSecDescHelper::Allocate(SystemDomain::GetCurrentDomain());

  // 如果显式指定了 Evidence 则使用此凭据初始化安全描述符
  if (pExtraEvidence!=NULL)
  {
    if(*pExtraEvidence!=NULL)
      pSecDesc->SetAdditionalEvidence(*pExtraEvidence);
  }

  // 处理跨 AppDomain 共享 Assembly 的情况
  //...

  // 如果 Assembly 不是跨 AppDomain 共享,则构造一个新的 Assembly 对象
  if (pAssembly == NULL)
    CreateAssemblyNoLock(pFile, pIAssembly, &pAssembly);

  // 获取并设置安全描述符信息
  pAssembly->GetSharedSecurityDescriptor()->SetManifestFile(pFile);
  pSecDesc = pSecDesc->Init(pAssembly);
  if (pAssembly->IsSystem())
      pSecDesc->GetSharedSecDesc()->SetSystem();

  // 建立 Module 对象
  //...

  pSecDesc->AddDescriptorToDomainList();
}



    其中对跨 AppDomain 共享的 Assembly 需要有特殊的处理,但因为与整体流程无关,这里就不展开分析了。

    AssemblySecurityDescriptor::Init 函数 (vm\Security.cpp:3126) 从目标 Assembly 对象获取与之绑定的安全描述符,并尝试将自己加入到其共享列表中,如已经有相同的存在则获取相同的那个描述符。而最后调用的 AddDescriptorToDomainList 方法,则进一步将此安全描述符绑定到当前的 AppDomain 的共享安全描述符列表中。

    这样一来,Assembly 的安全描述符绑定流程就大概清楚了。

    相对于 Assembly 绑定时获取 Evidence 相关信息和绑定安全描述符的工作来说,AppDomain 建立的工作较为简单。
以下内容为程序代码:

public sealed class AppDomain : ...
{
  public static AppDomain CreateDomain(string friendlyName)
  {
    return AppDomain.CreateDomain(friendlyName, null, null);
  }

  public static AppDomain CreateDomain(string friendlyName, Evidence securityInfo)
  {
    return AppDomain.CreateDomain(friendlyName, securityInfo, null);
  }

  [SecurityPermission(SecurityAction.Demand, ControlAppDomain=true)]
  public static AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup info)
  {
    if (friendlyName == null) throw new ArgumentNullException(Environment.GetResourceString("ArgumentNull_String"));
    if (securityInfo != null) new SecurityPermission(SecurityPermissionFlag.ControlEvidence).Demand();

    AppDomain domain = AppDomain.CreateBasicDomain();
    AppDomain.RemotelySetupRemoteDomain(domain, friendlyName, info, securityInfo, (securityInfo == null ? AppDomain.CurrentDomain.InternalEvidence : null), AppDomain.CurrentDomain.GetSecurityDescriptor());
    return domain;
  }

  [MethodImpl(MethodImplOptions.InternalCall)]
  internal static AppDomain CreateBasicDomain();
}



    从上面代码片断可以看到,建立 AppDomain 的过程实际上分为创建和设置两个步骤。
    创建 AppDomain 的步骤由内部方法 AppDomain.CreateBasicDomain 完成,实现上是通过调用 AppDomainNative::CreateBasicDomain 函数 (vm/AppDomainNative.cpp:48),进一步调用 SystemDomain::NewDomain 函数 (vm/AppDomain.cpp:2480),创建并初始化新的 AppDomain 类型 (vm/AppDomain.hpp:1103),最终获得一个可以跨 AppDomain 操作目标对象的透明代理。
以下内容为程序代码:

Object* AppDomainNative::CreateBasicDomain()
{
  AppDomain *pDomain = NULL;

  SystemDomain::NewDomain(&pDomain);

  return OBJECTREFToObject(pDomain->GetAppDomainProxy());
}



    而设置 AppDomain 的步骤由 AppDomain.RemotelySetupRemoteDomain 方法 (bcl/system/appdomain.cs:1288) 完成,负责将创建 AppDomain 时指定的凭证 (providedSecurityInfo) 和创建时的当前 AppDomain 的凭证 (creatorsSecurityInfo) 打包到一起,序列化后通过 AppDomain.InternalRemotelySetupRemoteDomain 方法 (bcl/system/appdomain.cs:1382),将凭证传递给新建 AppDomain。InternalRemotelySetupRemoteDomain 方法则通过建立 AppDomain 时获得的透明代理,将序列化后的凭证集合传递给目标 AppDomain 的 InternalRemotelySetupRemoteDomainHelper 方法 (bcl/system/appdomain.cs:1347),最终调用内部方法 AppDomain.SetupDomainSecurity 完成安全凭证的设置工作。调用路径如下:

以下为引用:

AppDomain.CreateDomain
  AppDomain.CreateBasicDomain - 建立新的 AppDomain 并返回透明代理
    AppDomainNative::CreateBasicDomain - 实现 AppDomain.CreateBasicDomain
      SystemDomain::NewDomain
      AppDomain::GetAppDomainProxy
  AppDomain.RemotelySetupRemoteDomain - 设置创建的 AppDomain,序列化凭证集合
    AppDomain.InternalRemotelySetupRemoteDomain - 通过透明代理调用目标 AppDomain
      AppDomain.InternalRemotelySetupRemoteDomainHelper - 在目标 AppDomain 领域内反序列化凭证集合
        AppDomain.SetupDomainSecurity - 设置 AppDomain 的安全凭据
          AppDomainNative::SetupDomainSecurity - 实现 AppDomain.SetupDomainSecurity




    AppDomainNative::SetupDomainSecurity 函数 (vm/AppDomainNative.cpp:76) 根据传入 Evidence 情况不同,对指定 AppDomain 的安全描述符进行设置。
以下内容为程序代码:

void AppDomainNative::SetupDomainSecurity(AppDomainBaseObject* refThisUNSAFE,
                                          StringObject* strFriendlyNameUNSAFE,
                                          Object* providedEvidenceUNSAFE,
                                          Object* creatorsEvidenceUNSAFE,
                                          void* parentSecurityDescriptor)
{
  // 对传入的参数进行 GC 保护操作
  struct _gc
  {
      APPDOMAINREF    refThis;
      STRINGREF       strFriendlyName;
      OBJECTREF       providedEvidence;
      OBJECTREF       creatorsEvidence;
  } gc;

  gc.refThis          = (APPDOMAINREF) refThisUNSAFE;
  gc.strFriendlyName  = (STRINGREF)    strFriendlyNameUNSAFE;
  gc.providedEvidence = (OBJECTREF)    providedEvidenceUNSAFE;
  gc.creatorsEvidence = (OBJECTREF)    creatorsEvidenceUNSAFE;

  // 建立应用程序安全描述符
  ApplicationSecurityDescriptor *pCreatorSecDesc = (ApplicationSecurityDescriptor*)parentSecurityDescriptor;

  BOOL resolveRequired = FALSE;
  OBJECTREF orEvidence = NULL;

  // 创建 AppDomain 时是否指定了 Evidence
  if (gc.providedEvidence == NULL)
  {
    // 对缺省 AppDomain 使用缺省的安全信息
    if (pCreatorSecDesc->GetProperties(CORSEC_DEFAULT_APPDOMAIN) != 0)
      pDomain->GetSecurityDescriptor()->SetDefaultAppDomainProperty();

    orEvidence = gc.creatorsEvidence;
  }
  else
  {
    if (Security::IsExecutionPermissionCheckEnabled())
      resolveRequired = TRUE;

    orEvidence = gc.providedEvidence;
  }

  // 设置 AppDomain 安全描述符中的凭据
  pDomain->GetSecurityDescriptor()->SetEvidence( orEvidence );

  // 实际进行安全描述符的解析工作
  if (resolveRequired)
    pDomain->GetSecurityDescriptor()->Resolve();

  //...
}



    在创建并绑定了 Assembly/AppDomain 相关的安全描述符后,我们来看看这些安全描述符是如何被使用的。

    Assembly/AppDomain 相关的安全描述符,是进一步获取凭据相关信息的工具。为了效率等原因,Fusion 没有选择在加载 Assembly 或创建 AppDomain 时直接载入所有凭据相关信息,而是通过安全描述符的 GetEvidence 和 Resolve 方法提供延迟载入优化。使用者需要用到凭据相关信息时,访问 Assembly.Evidence 和 AppDomain.Evidence 属性,就会被动引发延迟的凭据相关信息载入操作。
以下内容为程序代码:

public class AppDomain : ...
{
  private Evidence _SecurityIdentity;

  public Evidence Evidence
  {
    get
    {
      if (this._SecurityIdentity == null)
      {
        this.nForceResolve();
      }
      return this._SecurityIdentity.Copy();
    }
  }

  [MethodImpl(MethodImplOptions.InternalCall)]
  internal void nForceResolve();
}

public class Assembly : ...
{
  public virtual Evidence Evidence
  {
    get
    {
      return this.nGetEvidence().Copy();
    }
  }

  [MethodImpl(MethodImplOptions.InternalCall)]
  internal Evidence nGetEvidence();
}



    AppDomain.nForceResolve 方法和 Assembly.nGetEvidence 方法分别对应着 AppDomainNative::ForceResolve 函数 (vm\AppDomainNative.cpp:694) 和 Security::GetEvidence 函数 (vm\Security.cpp:4405)。这两个实现函数都是通过 AppDomain/Assembly 绑定的安全上下文完成延迟凭据相关信息载入的。
以下内容为程序代码:

void AppDomainNative::ForceResolve(AppDomainBaseObject* refThisUNSAFE)
{
  AppDomain* pAppDomain = ValidateArg(refThis);

  // 初始化安全管理器
  Security::InitSecurity();

  pAppDomain->GetSecurityDescriptor()->GetEvidence();
  pAppDomain->GetSecurityDescriptor()->Resolve();
}

Object* Security::GetEvidence(AssemblyBaseObject* pThisUNSAFE)
{
  Assembly *pAssembly = pThis->GetAssembly();

  OBJECTREF orEvidence = pAssembly->GetSecurityDescriptor()->GetEvidence();

  return OBJECTREFToObject(orEvidence);
}



    此处虽然 AppDomain 和 Assembly 的 GetSecurityDescriptor 方法分别会返回 ApplicationSecurityDescriptor 和 AssemblySecurityDescriptor,但他们的 GetEvidence 实现方法并不相同。

    AppDomain 安全描述符 ApplicationSecurityDescriptor 在 GetEvidence 方法 (vm/security.cpp:2548) 中,会调用 Managed 代码 AppDomain.CreateSecurityIdentity 方法 (bcl/system/appdomain.cs:1460),将可能存在的根 Assembly 的凭证,与 AppDomain 的凭证合并。伪代码如下:
以下内容为程序代码:

OBJECTREF ApplicationSecurityDescriptor::GetEvidence()
{
  // 初始化安全管理器
  Security::InitSecurity();

  // 保护参数不被 GC
  struct _gc {
      OBJECTREF rootAssemblyEvidence;
      OBJECTREF additionalEvidence;
  } gc;

  GCPROTECT_BEGIN(gc);

  // 获取根 Assembly 的 Evidence
  // 根 Assembly 是由宿主在新建 AppDomain 时设置的
  // 例如执行 CLR 程序 main 方法的 SystemDomain::ExecuteMainMethod
  // 就会设置新建 AppDomain 的根 Assembly 为启动的 Assembly
  if (m_pAppDomain->m_pRootAssembly != NULL)
  {
      gc.rootAssemblyEvidence = m_pAppDomain->m_pRootAssembly->GetSecurityDescriptor()->GetEvidence();
  }

  gc.additionalEvidence = ObjectFromHandle(m_hAdditionalEvidence);

  // 调用托管代码合并不同凭据
  return m_pAppDomain->AppDomain::CreateSecurityIdentity(gc.rootAssemblyEvidence, gc.additionalEvidence);
}

private Evidence AppDomain.CreateSecurityIdentity(Evidence rootAssemblyEvidence, Evidence additionalEvidence)
{
  Evidence evidence;

  if (rootAssemblyEvidence != null)
    evidence = rootAssemblyEvidence;
  else
    evidence = new Evidence();

  if (additionalEvidence != null)
    evidence.MergeWithNoDuplicates(additionalEvidence);

  _SecurityIdentity = evidence;
  return _SecurityIdentity;
}




    而 Assembly 安全描述符 AssemblySecurityDescriptor 在 GetEvidence 方法 (vm/security.cpp:2978) 中,则调用Managed 代码 Assembly.CreateSecurityIdentity 方法 (bcl/system/reflection/assembly.cs:880),根据 Assembly 的安全描述符中绑定的安全相关信息,生成新的凭证。
以下内容为程序代码:

OBJECTREF AssemblySecurityDescriptor::GetEvidence()
{
  // 初始化
  Security::InitSecurity();

  // 获取与安全描述符绑定的基本安全信息
  LPWSTR      wszCodebase = NULL;
  DWORD       dwZone = (DWORD) NoZone;
  BYTE        rbUniqueID[MAX_SIZE_SECURITY_ID];
  DWORD       cbUniqueID = sizeof(rbUniqueID);

  m_pAssem->GetSecurityIdentity(&wszCodebase, &dwZone, rbUniqueID, &cbUniqueID);

  // 保护参数不被 GC
  struct _gc {
      STRINGREF url;
      OBJECTREF uniqueID;
      OBJECTREF cert;
      OBJECTREF serializedEvidence;
      OBJECTREF additionalEvidence;
  } gc;

  GCPROTECT_BEGIN(gc);

  if (wszCodebase != NULL && *wszCodebase)
      gc.url = COMString::NewString(wszCodebase);
  else
      gc.url = NULL;

  if (m_pSignature && m_pSignature->pbSigner && m_pSignature->cbSigner)
      SecurityHelper::CopyEncodingToByteArray(m_pSignature->pbSigner,
                                              m_pSignature->cbSigner,
                                              &gc.cert);

  gc.serializedEvidence = GetSerializedEvidence();

  gc.additionalEvidence = ObjectFromHandle(m_hAdditionalEvidence);

  return m_pAssem->Assembly::CreateSecurityIdentity(gc.url, gc.uniqueID, dwZone, gc.cert, gc.serializedEvidence, gc.additionalEvidence);
}

private Evidence Assembly.CreateSecurityIdentity(String url,
                                                 byte[] uniqueID,
                                                 int zone,
                                                 byte[] cert,
                                                 byte[] serializedEvidence,
                                                 Evidence additionalEvidence)
{
  Evidence evidence = new Evidence();

  if (zone != -1) evidence.AddHost( new Zone((SecurityZone)zone) );

  if (url != null)
  {
    evidence.AddHost( new Url(url, true) );

    // 对非文件的 URL,单独创建一个 Site 信息对象
    if (String.Compare( url, 0, s_localFilePrefix, 0, 5, true, CultureInfo.InvariantCulture) != 0)
      evidence.AddHost( Site.CreateFromUrl( url ) );
  }

  if (cert != null) AddX509Certificate(evidence, cert);

  if (serializedEvidence != null) DecodeSerializedEvidence( evidence, serializedEvidence );

  byte[] blob = nGetPublicKey();

  if ((blob != null) && (blob.Length != 0)) AddStrongName( evidence, blob );

  evidence.AddHost(new Hash(this));

  // 如果载入 Assembly 时指定了附加的 Evidence 信息,则合并进去
  if (additionalEvidence != null)
    evidence.MergeWithNoDuplicates(additionalEvidence);

  return evidence;
}




    至此,Assembly 载入和 AppDomain 创建时的 Evidence 相关信息获取步骤,以及延迟进行的 Evidence 相关信息解析整理的流程,就大致有了一个轮廓。通过这个分析我们可以大致了解到 CLR 在载入 Assembly 和创建 AppDomain 的过程中,在哪些步骤要搜集哪些信息,以及我们如何将自定义的 Evidence 信息与之配合使用。
版权所有,未经许可,不得转载