在传统的操作系统级安全模型中,安全管理的粒度都是 Principal-based 层面的。用户从认证登陆成功开始,就获得此帐号的所有权限,而其运行的程序,也自动被授予帐号及其所在组的所有权限。例如我在《DACL, NULL or not NULL》一文中介绍的,NT 用户从登陆到系统建立 Session 开始,就缺省使用相同权限,新建进程自动获得父进程的权限,除非程序本身手动进行限制。而 *nix 系统下面的思路也是类似,只不过从 Token 编程了各种 uid/gid 等等。
这种 Principal-based 的安全模型,在以主机为中心的孤立环境中是非常合适的,而且足够简单和高效。但随着网络的普遍使用,这种安全模型开始受到挑战,最直接的就是如何处理从网络上运行程序的策略问题。按照现有模型,所有程序都会自动获得最大权限集,但这显然是不现实的,安全管理粒度过于粗放。
因此在 Java 和 IE 等涉及网络的应用程序中,提出了新的基于位置的安全模型。一个程序运行时获得的权限,并不由其父进程或者说宿主来决定(这些程序往往用于较高权限),而是由其程序所在位置觉得具有多少权限。Java 中将之简化为对代码源的安全策略限定,如在策略文件中指定所有来自 www.nsfocus.com 的程序都有写 c:\temp 目录的权限,而来自其他网址的程序只能读取 c:\temp 目录内容。而 IE 中则更进一步,将这些来源分类为本机(my computer)、内网(intranet)、外网(internet)、可信站点(trusted)和不可信站点(untrusted)。
如果说 Principal-based 的安全模型中,关键因素是:我是谁(当前帐号)、我要访问什么(目标资源)、我要怎么访问(操作类型);则在 基于位置的安全模型中变成了:我来自哪里(代码来源)、我要访问什么(目标资源)、我要怎么访问(操作类型)。关键因素虽然只有一个我是谁到我来自哪里的转变,但其控制粒度能够大大提升。
但是这样的基于位置的安全模型还是存在其问题,因为组件之间的可信程度是不同的。例如一个本机控件因为来源于本机,受到系统的信任,被赋予很高的权限。同时一个恶意代码来源于不受信任的位置,无法执行某项操作。如果权限限定完整的话,本来不会出现问题,但因为某些权限依赖关系的管理混乱,造成恶意代码可以通过受信任代码执行本不应允许他执行的功能。这也是众多 IE 相关漏洞的问题根本所在,其受到 IE 现有安全模型的单级信任机制的限制,注定无法彻底解决。
要彻底解决此类问题,归根结底必须建立一个可信链验证机制,也就是说执行某个操作的时候,必须检查此操作所有的上级操作组件是否拥有安全权限,而不仅仅只检测最后一级的组件。这一思路正是 CLR 中代码访问安全检测的设计思路,其关键因素增加了一个:调用我的人都有哪些,他们是否有相应权限的检测。
例如我在建立一个文件的时候,可以强制性检测调用链上的所有组件是否都拥有操作此文件的权限:
以下内容为程序代码:
public void CreateFile()
{
// Create a new FileIO permission object
FileIOPermission perm = new FileIOPermission(FileIOPermissionAccess.Write, @"C:\SomeFile.txt");
try {
// Demand the FileIOPermission
perm.Demand( );
} catch (SecurityException se) {
// Callers do not have necessary permission
}
// Method implementation...
}
FileIOPermission 类描述了我需要检测的权限,对 C:\SomeFile.txt 文件可写;FileIOPermission.Demand() 则执行这一权限的检测工作,遍历此方法调用链上的所有组件,检测他们是否有次权限。这样一来就可以从理论上避免恶意代码通过调用可信组件执行越权操作的问题。具体的权限定义和使用方法,这里就不详细介绍了。下面就这种检测如何实现做一个结构上的简要分析。
System.Security.CodeAccessPermission
System.Security.Permissions.EnvironmentPermission
System.Security.Permissions.FileDialogPermission
System.Security.Permissions.FileIOPermission
System.Security.Permissions.ReflectionPermission
System.Security.Permissions.RegistryPermission
System.Security.Permissions.SecurityPermission
System.Security.Permissions.UIPermission
System.Security.Permissions.IsolatedStoragePermission
System.Security.Permissions.IsolatedStorageFilePermission
System.Security.Permissions.StrongNameIdentityPermission
System.Security.Permissions.PublisherIdentityPermission
System.Security.Permissions.SiteIdentityPermission
System.Security.Permissions.UrlIdentityPermission
System.Security.Permissions.ZoneIdentityPermission
为进行代码访问权限检测,CLR 缺省定义了以上这些权限类型。首先他们都是从 CodeAccessPermission 类型继承出来,其次就其意义可进一步分为代码访问权限和代码身份权限。代码访问权限定义代码将如何去访问资源,如读写文件、弹出对话框等等;代码身份权限则定义访问此资源的组件必须符合什么样的身份,如只能是本地文件、或者只能是 nsfocus.com 发布的组件等等。
虽然权限种类众多,但各种子类只负责定义自身权限的特性以及如何对自身权限验证,而所有的调用链遍历和验证工作,都是由 CodeAccessPermission.Demand() 方法完成的:
以下内容为程序代码:
public void CodeAccessPermission.Demand()
{
CodeAccessSecurityEngine engine = SecurityManager.GetCodeAccessSecurityEngine();
if ((engine != null) && !this.IsSubsetOf(null))
{
StackCrawlMark mark = StackCrawlMark.LookForMyCallersCaller;
engine.Check(this, ref mark);
}
}
可以看到 CodeAccessPermission.Demand 方法,实际上是将验证操作转发给安全管理器 SecurityManager 的代码访问安全引擎 CodeAccessSecurityEngine 类型的 Check 方法完成的。
以下内容为程序代码:
internal class CodeAccessSecurityEngine
{
internal virtual void Check(CodeAccessPermission cap, ref StackCrawlMark stackMark)
{
if (!PreCheck(cap, null, 1, ref stackMark, PermissionType.DefaultFlag))
{
Check(PermissionToken.GetToken(cap), cap, ref stackMark, -1, ((cap is IUnrestrictedPermission) ? 1 : 0));
}
}
internal virtual void Check(CodeAccessPermission cap, ref StackCrawlMark stackMark, PermissionType permType)
{
int num1 = 0;
if (CodeAccessSecurityEngine.GetResult(permType, out num1))
{
return;
}
if (this.PreCheck(cap, null, 1, ref stackMark, permType))
{
CodeAccessSecurityEngine.SetResult(permType, num1);
return;
}
this.Check(PermissionToken.GetToken(cap), cap, ref stackMark, -1, ((cap is IUnrestrictedPermission) ? 1 : 0));
}
[MethodImpl(MethodImplOptions.InternalCall)]
private void Check(PermissionToken permToken, CodeAccessPermission demand, ref StackCrawlMark stackMark, int checkFrames, int unrestrictedOverride);
}
CodeAccessSecurityEngine 内部类的 Check 方法,将最终调用通过 Unmanaged 代码实现的内部方法进行安全检测。rotor 中的 COMCodeAccessSecurityEngine 类型 (ComCodeAccessSecurityEngine.cpp) 实现了这个检测逻辑。
COMCodeAccessSecurityEngine::Check 函数 (ComCodeAccessSecurityEngine.cpp:683) 通过调用 COMCodeAccessSecurityEngine::CheckInternal 函数 (ComCodeAccessSecurityEngine.cpp:697) 填充一个堆栈遍历请求结构 CasCheckWalkData 的内容,最终将请求转发给 StandardCodeAccessCheck 函数 (ComCodeAccessSecurityEngine.cpp:563) 完成检测。此结构的指针将作为堆栈遍历回调函数的参数传递给回调函数进行实际权限验证,而 StandardCodeAccessCheck 只是负责调用全局堆栈遍历支持 StackWalkFunctions 函数(StackWalk.cpp:512),以 CodeAccessCheckStackWalkCB 函数 (ComCodeAccessSecurityEngine.cpp:449) 为回调函数,以 CheckInternal 函数填充的 CasCheckWalkData 结构为参数,通过现成的堆栈遍历支持 Thread::StackWalkFrames 完成堆栈遍历。
通过堆栈遍历实现代码访问安全检测调用流程如下:
以下为引用:
CodeAccessSecurityEngine::Check 内部调用定义,由下面的函数实现
COMCodeAccessSecurityEngine::Check 转发检测请求 (ComCodeAccessSecurityEngine.cpp:683)
COMCodeAccessSecurityEngine::CheckInternal 填充 CasCheckWalkData 结构 (ComCodeAccessSecurityEngine.cpp:697)
StandardCodeAccessCheck 执行堆栈遍历
Thread::StackWalkFrames 遍历当前线程堆栈
CodeAccessCheckStackWalkCB 检测当前组件权限 (ComCodeAccessSecurityEngine.cpp:449)
因此现在 CAS 检测的问题被分为两个部分:如何遍历调用堆栈;如何检测某个组件是否拥有权限。
过于如何遍历调用对象,因为涉及到比较复杂的堆栈帧类型处理,这里