安全性在操作系统中应该是相当容易实现的,对吗?就是说,为任意对象指定某个安全级别所需要做的所有工作只是一个简单的函数调用,例如GrantAccessTo或者DenyAccessTo,对吗?
不幸的是,Windows NT安全性应用程序接口(API)看起来并不是那么简单。它包含了过多的与安全性有关的函数,并且仅仅为一个用户打开一个对象的工作就已经非常复杂了。
要正确使用安全性API,需要按照下面几个层次去理解:
第一个层次是理解数据结构:访问控制表(ACLs),访问控制元素(ACEs),安全性描述符(SDs)和安全性IDs (SIDs)。
第二个层次是理解ACLs的语义(虽然不需要理解它们是如何工作的)。根据ACLs建立顺序的不同,对同一个用户的访问可能允许,也可能不允许。
第三个层次是理解操作系统本身如何使用安全性。可以将安全性API理解成为服务器应用程序提供用来保护对象不被未授权的用户访问的服务集合,同事务日志帮助设备驱动程序和应用程序记录错误以及确认事件提供服务的方法相同。
在这些服务只被第三方应用程序使用时,相当容易理解安全性是如何工作的。然而,Windows NT是一个安全的操作系统,此外,基于Windows NT的网络也非常依赖于安全性。因此,安全性结合到系统的方法就显得很模糊。
谁需要安全?
在进入任何细节之前,先要明确为什么会需要安全性。如果不是处于下面的情况,就根本不必为安全性担心:
正在写一个服务器应用程序,即几个用户都可以访问的一个应用程序,而且此服务器应用程序只限于为这些用户的一个子集提供数据结构。
注意这是一个相当广泛的定义。下面是满足条件的几个例子程序。
对于单机(没有连网的计算机),编写一个服务,Windows NT启动以后一直运行,并且有多个用户在此计算机上登录及退出。该服务提供的信息只对少数用户可见。例如,想收集使用模式或者登录数据,就可能只限于管理员访问该数据。
许多特权限制在系统级。例如,系统注册表受保护,使得只有具有特殊特权的用户能够向系统中添加设备驱动程序。这是由于安全性的原因。例如,一个恶意的用户能够利用设备驱动程序监控用户输入的能力窃取其它用户的工作。安全性也有助于系统的稳定。设想一个未经授权的用户安装了一个粗制滥造的设备驱动程序。当其它用户工作时,这样的驱动程序会造成系统崩溃。通过将注册新设备驱动程序的权利限制给可信赖的用户,能够防止Windows NT的计算机上出现这种情况。
许多在网络上工作得与单机上同样好的服务器程序得益于与安全性系统的某种挂钩。例如,一个数据库服务器可能同时为几个用户服务,某些用户不允许查看给定数据库中的某些数据。假设公司内的每个人都能够查询员工数据库。管理人员需要访问员工的全部信息,而其他人则应该只能看到工作头衔和办公室编号。如果将包含工资和奖金信息的字段限制于管理人员访问,就可以允许公司内所有的人使用同一个数据库,而不会危及安全性和机密。
安全性的微观视图
安全性的一个问题是使用安全性API毫无新奇刺激可言。其他人编写的代码可以旋转茶壶、在窗口中显示动画、弹出很酷的Windows 95控件、通过MAPI来回发送数据等等,而安全性编程却总是令人厌烦的事情。
Windows NT安全性表现的非常复杂,相比之下,从微观水平上看则比较简单。每个Windows NT域(或域组)保存有一个该域所知道的用户的数据库。用户想要在Windows NT的域中工作,必须先使用一个用户名和口令证明自己。一旦安全性系统证明了口令,用户就会被关联到一个访问令牌,识别用户的内部数据结构。
关于Windows NT下安全性必须知道的首要事情是它是以用户为中心的;也就是说,试图访问受保护对象的每一行代码都必须与一个特殊用户关联,该用户必须用口令向客户机证明自己的身份。每次安全性检查都要依靠用户鉴别。例如,编写代码阻止Microsoft Excel访问一个对象是不可能的。可以保护一个对象防止Joe Blow运行Microsoft Excel访问,但是如果允许Carla Vip访问该对象,她可以使用Microsoft Excel或者其它喜欢的程序访问,只要Carla使用只有自己知道的口令向客户机证明自己的身份就行。
安全性API虽然看起来很复杂,但是却只完成两件事:
审核:每次对特定的对象试图有特定的操作时生成一个日志条目。
限制对象访问:客户程序调用的函数,可能会成功,失败返回错误代码5(访问被拒绝),或者因其它原因失败,取决于服务器如何指定特权。
用户可能不是直接看到错误信息,而是一个对话框,上面写着:“你没有特权将鸡蛋从纸盒中拿走。”弹出此对话框的程序内部可能包含下面的代码行:
if (!RemoveEggsFromCarton() && GetLastError() == ACCESS_DENIED)
AfxMessageBox("You do not have the privilege to remove the eggs from the carton");
安全性机制
Windows NT使用两种导致访问尝试失败返回错误5的机制:确认权限和确认特权。权限属于对象上的行为,比如挂起线程权限或读文件权限。权限总是与特定对象和已知用户相关联。例如,读文件权限必须与文件(权限应用在此文件上)和有或没有权限的用户相关联。同样,挂起线程权限除非与特定的线程和用户关联否则没有用。
特权是预先定义好的属于系统上操作的权限。例如,特权有调试程序、备份和恢复存储设备以及装入驱动程序。特权以用户为中心,而不是对象。
为了使两者之间区分得更清楚,可以看一下实现权限和特权的数据结构:权限在叫作访问控制表(ACL)的数据结构中指定。ACL通常与对象相关。用户用访问令牌表示。当用户试图访问受保护的对象时,其访问令牌与对象的ACL检查。访问令牌包含代表用户的唯一标识符(安全性ID,或SID)。ACL中的每个权限与一个SID相关;这样,安全性子系统就知道了与每个用户相关的权限。
另一方面,特权在访问令牌中编码,所以没有相关联的对象。要确定用户是否允许做某个与特权有关的操作,安全性子系统检查访问令牌。
此外,权限需要行为的说明(干什么的权限?例如,读文件或者挂起线程),而特权不需要(用户或者有特权,或者没有)。与特权相随的操作隐含在特权本身中。
特权在访问令牌中编码的原因是大多数特权不考虑安全性需求。例如,允许备份存储设备的用户必须能够绕过文件安全性。为了允许用户访问而给硬盘上每个单独的文件都加入一个新的ACE是不可行的。这样,备份存储设备的代码首先检查试图备份的用户是否拥有备份特权;如果有,单个文件的安全性就被忽略。
能够与访问令牌相关的特权集被牢固加密,不能被应用程序展开。服务器程序能够使用特殊的权限和普通的映射实现自定的安全性规则。
有两种类型的ACL:自由决定的(DACL)和系统的(SACL)。DACL管制对象访问,SACL管制审核。
控制访问
在大多数情况下,错误5是由Windows NT特有的叫作AccessCheck的Win32函数内部产生的。此函数的输入有用户的访问令牌、需要的特权和ACL。ACL主要是小数据结构(叫作访问控制元素,或ACE)的列表,每个数据结构定义一个用户或一组用户、一个权限集合以及允许或拒绝的信息。例如,ACL中可能有一个ACE写着“从纸盒中拿走鸡蛋的权限明确地拒绝给与用户Elephant和Bozo”,后面一个ACE写着“从纸盒中拿走鸡蛋的权限明确地准予给与用户Betty Crocker以及CHEFS组中所有用户”。
ACL与对象相关,可以在服务器程序中动态创建。例如,如果一个文件对象与一个ACL相关,不管何时有应用程序试图打开该文件对象,ACL就会被查询以决定是否允许运行应用程序的用户打开文件。
AccessCheck函数被许多系统函数内部调用,例如,CreateFile(用户试图在NTFS分区或命名管道上打开文件时)和OpenFileMapping。然而,Win32服务器程序能够直接调用AccessCheck,保护想保护的任何对象。
注意安全性API函数只被服务器程序调用;客户不需要或直接使用安全性。客户曾经看到的Windows NT安全性就是错误5。这使得Windows NT安全性可以不必考虑客户运行的软件。需要的是服务器在域的安全性数据库中确认客户以及将从客户收到的请求翻译成服务器端函数调用的能力。此函数或者隐含调用AccessCheck,或者根据服务器端AccessCheck的输出发送或不发送其结果。
Windows NT security中容易混淆的部分是对AccessCheck的调用可能是非常模糊的。例如,Windows NT监控设备驱动程序安装的功能是一个非常模糊的概念。当试图添加设备驱动程序时用户要访问哪个“对象”?系统在哪里调用AccessCheck以及必要时在哪里将错误信息显示给用户?
在设备驱动程序的例子中,答案还不是太困难:因为设备驱动程序和系统通过注册表(Windows NT通过浏览注册表子树,解释每个条目,尝试执行在单独注册表项中指定的驱动程序二进制文件而装入设备驱动程序)交互,Windows NT保护的对象是注册表项,它在Windows NT中是可以得到的对象。在Win32 API层,任何操作注册表的尝试将会翻译成注册表工作的函数,例如RegOpenKey内部调用AccessCheck。
除了注册表保护外,驱动程序二进制文件也有安全性问题。一个因访问注册表被拒绝而落空的黑客仍然能够用添加了额外功能的驱动程序副本取代原有的驱动程序执行文件。这一过程不需要访问注册表,所以Windows NT如何防止这类问题呢?相当简单,通过要求驱动程序二进制文件存放在NTFS分区并限制对其访问。这样,取代驱动程序二进制文件的企图(在Win32 API层上调用DeleteFile或CreateFile时不可避免地被终止)会被AccessCheck抓住,恶意的黑客就不走运了。
系统提供的其它安全性对象可能难于说明。例如,怎样阻止用户访问被保护的网络共享?怎样阻止打开远程计算机上的服务控制管理器?系统层如何使Windows NT无懈可击?如何使一些安全性函数自己失败返回错误5,访问被拒绝?设想如果应用程序能够自由操作自己的访问令牌或调用安全性函数改变对象的特权将会出现什么情况?这种情况下,仅仅修改ACL和令牌中的项目就能够简单的绕过安全性。。这样,必须有某种“元安全性”,一种保护安全特性自己不被错误利用的机制。如何实现?
基于AccessCheck的安全性实现的一个结果是安全性严重依赖于只允许以众所周知的入口点访问安全性对象的体系结构。例如,Windows 3.1家族操作系统中的文件系统包括许多不同入口点:中断21h(与文件系统交互),中断13h(与磁盘设备驱动交互),以及几种类型的提供对文件系统访问的C运行库和Windows API函数(如OpenFile和_fopen)。从安全性观点来看,在OpenFile内部实现中调用AccessCheck这样的函数毫无用处,应用程序可以简单的调用_fopen绕过文件安全性。只有打开文件操作的所有不同调用都翻译成一个“安全性”调用才行;如果有一个执行安全性检查而另一个不执行,就会有安全性问题。
16位Windows系统中这种“开放文件系统”结构对提供如加密软硬件的公司来说是主要的麻烦。
在编写安全性服务器程序时,将程序设计的无懈可击是绝对必要的;也就是说,必须防止客户可以访问关键数据的所有方法。安全性系统的挑战之一是使关键数据无懈可击。这可能是一件相当复杂的工作,就象前面的例子中,单独保护注册表入口对于保护整个计算机的设备驱动程序是不够的。
访问权限类型
使用安全性API,系统能够帮助管制对几乎任何种类对象的访问。但“访问”的含义是什么?是不是谈论数据库字段时所使用的访问类型,还是与访问其它窗口的消息循环完全不同的某种类型?
这就是为什么“访问”在安全性API中是一个相当普遍的术语。不是像“打开、关闭、读取和写入对象的权限”这种牢固加密的访问类型,Windows NT中的访问被定义为掩码中位的集合。安全性子系统将用户访问掩码中的位与对象访问掩码中的位进行匹配。例如,这使得我们能够设计一个员工数据库,管理员可以读写工资和奖金的信息,经理可以读但不能写,其他人不能读写访问。
以相同的方式,应用程序能够定义自己的访问类型。例如,如果程序想保护一个可以共享(从几个用户都能够调用函数操作屏幕上对象的意义上)的OpenGL对象,可以为OpenGL对象能够完成的所有操作(如旋转、拉伸、反弹和移动)定义唯一的访问权限,并且为每个需要对图象进行操作的用户指定这些权限的唯一子集。
安全性API能够以三组权限工作:
标准权限(为每种对象类型提供相同操作的权限)。
特殊权限(对每个对象类型有特殊意义的权限。两种不同类型的对象可能有相同的权限掩码位,但有对权限意义的不同翻译)。
普通权限,概略的占位符(象GENERIC_READ和GENERIC_WRITE此类的权限,几乎适用于所有对象类型,但对不同对象类型有不同的意义)。普通权限被映射为标准权限和特殊权限。这一机制允许服务器不用实际定义操作就可以建立“读”和“写”的概念。服务器程序能够用普通权限工作,不管读写是对文件还是对数据库对象,对象自己能够确定普通权限如何翻译成特殊权限。