Windows NT最迷人的部分之一是它的安全系统。如果你对安全方面不太关心,你可以完全忽略这部分的 Win32 API。然而, 在不少情况下,NT内置的安全特性可以帮助解决一些很有趣的问题。在这篇文章中,你将会理解到NT安全系统的意图,这些概念将会帮助你懂得它的用处,以及操作它的技术。
要注意的是,本文后的一些代码,要用管理员的身份登录才能够运行。只有管理员才拥有执行这里讨论的某些系统安全任务的权限。
NT安全性可以做到的事情
在大多数的人看来,NT是一个颇为完整和复杂的安全系统。经验丰富的UNIX程序员对UNIX操作系统的文件安全系统很熟悉,并且了解root权限的含义。不过这些系统与NT的相比都是较为简单的。接下来的例子将让你对NT的安全系统有所了解。
由文件管理器的安全菜单和安全编辑器中,我们可以看到NT中内置的安全特性。你可以写自己的代码来修改NT文件系统卷上的安全信息,而且可以做到与它一样详细。在你写的程序中, 你可以控制谁有某种类型的权限来访问NTFS卷上的文件, 就象文件管理器的工作一样。
.你可以在系统上创建一个命名管道,该管道只有其它系统上的管理员能够访问。当用户尝试着与服务器进行连接时,安全系统将进行检查,以确保只有拥有相应访问权限的用户才能访问,而拒绝没有管理员权限的用户。你可通过这种方法来让不同机器上的管理员能相互沟通,同时又确保安全性。你可以写代码来选择允许访问或者拒绝访问的用户或用户组。
.你可以创建一个带安全标记的mutex,只允许某些用户或组来访问它。这个处理可以应用在系统中的任何对象上:包括有文件、semaphores、线程、事件、文件映射中的共享内存等。对于mutex,你可以使用这个处理来防止对一个应用的同步机制的非法访问;
.你可以在某个对象(例如一个线程、mutex、管道、文件或注册键)被访问时,在事件日志中创建一个项目。你能指定访问的类型和用户,并且产生一个事件日志项目。例如, 对于某个文件,你可以设置为在用户smith成功读取该文件时,产生一个事件日志项目。
由上面的例子中,我们可以知道NT可让你对系统中的对象设置精确的安全访问权限,并且允许你监视和记录对象是怎么使用的。
NT安全系统的术语和概念
NT安全系统有大量的新概念以及描述这些概念的词汇,这里将把这些概念用简单的词语表述,以便于你理解。
你对安全系统中最常见的部分应该很熟悉了:这就是密码。你可以将密码系统看成是一栋建筑物前门的锁。密码系统的作用就象一把锁把建筑物内外的事物隔离开来。你的密码就相当于开门的钥匙,让你进入NT系统。
如果你的硬盘是用NT的文件系统格式化的话,你知道NT的安全系统还有第二层。你可以将这一层看成是建筑物里面的各个房间的锁。某些房间是打开的,任何人都可以进去,但一些是上了锁的。每个房间的锁都是有点不同的。例如,在使用NT文件格式化的卷中,如果你在上面创建了一个文件,你就可以为它设置多种不同的访问权限:
.只有文件的拥有者才能访问它
.只有某个用户才能访问它
.某些用户可以访问它
.你可以设定为某个组的成员才能访问它
.你可以设定为某个组的成员才能访问它,同时又拒绝该组中的某些用户对它的访问
.可以设置为多个组的成员均可访问它
.任何人都可以访问
可将每个受保护的文件看成是一个房间。你对文件的安全设置就象是房间的一把锁,用来决定谁允许进入房间。你还可以控制当一个人进入房间时,可允许他做的事情。例如,对于一个文件,你可以给一个用户读的权限,给另一个用户写的权限等。打开文件管理器的安全菜单,看一下其中的权限对话框,你可以尝试做不同的设置。
NT就象一个有着很多房间的建筑物。你的密码就相当于一把钥匙,让你进入到建筑物中。一旦就入了建筑物,你就可以使用自己的钥匙来进入不同的房间。一些房间是没有上锁的,有一些是上锁的,但你可以进去,另外有一些是拒绝你进入的。房间就相当与NT系统中的各个对象:文件,注册子键,线程和mutex对象等。
你还可以在房间的门口加入一个岗哨,岗哨的任务是监视谁使用房间,并且他是怎样使用的。这在NT中称为审核。当你在一个NT对象上加入一个审核时,当某些用户以某种方式来访问该对象时,该对象就会在事件日志中写入相关的项目。例如,如果你可以访问一个NT文件系统卷,并且有管理的权限,打开文件管理器并且选择一个你拥有的文件,然后在安全菜单中选择审核的选项。对于每个独立的用户或者组,你都可以设置什么时候进行审核,可以是在他们成功(或者失败,或者两者均是)打开文件、写入文件、执行文件等时候。
要了解这些安全特性,最方便的是在NT的文件系统中。通过文件管理器中的安全编辑器,你能够以图形化和简便的形式为文件设置各种不同的安全特性。这些安全细节的划分对于NT中的许多对象都是适用的。例如,你可以用同样的方式保护注册表中的项目,实际上,注册表编辑器包含有与文件管理器相似的可视化安全编辑器。你还可以为许多内部的系统对象设置安全属性。例如,你可以用这种方式来限制对一个命名管道的访问。
以下就是上面所讨论的概念的简要总结:
.一个NT系统就象一栋建筑物。你的密码就相当于一把钥匙,可让你进入建筑物。
.进入建筑物后,你就会发现有成千上万个房间。一个文件就相当于一个房间,注册表项目、命名管道和线程等也类似。
.每个房间的安全是由拥有者来设置的,可设置为只让某个人使用、一群用户使用、某个组或者任何人访问;
.每个房间还可以加入一个岗哨,负责检查和记录谁进入房间以及每个人进入后做了些什么;
现在你已经有了一些基本的概念,以下再谈一下NT在安全方面的一些特别的术语。
NT的安全术语
当你登录入一个NT系统时,系统将给你一个访问的记号。这个访问记号是你用来打开NT系统中的锁的钥匙。你的钥匙可以打开一些锁,但是有一些它打不开。你所做的每个处理都包括有一个你的访问标记的副本。
这个访问的标记用来做两件不同的事情。首先,它用来标识你的身份。例如,当你以用户“smith”的身份登录时,你的访问记号就包含了该用户的标识。访问记号还用来标识你属于的所有组。例如,在用户管理器中,你可能是Power User组,Backup Operator组和一个名为Programmers的自定义组的成员。你的访问记号就可以标识你为这些不同组的成员。
你的访问记号还包含该用户的所有权限。你所属的每个组都拥有一些相关的用户权限。如果你打开用户管理器,你将可以在用户权限菜单中看到权限的列表。例如,power users用户组拥有设置系统时间的权限,而普通的用户没有。某些用户可以关闭系统,某些不行。系统通过将你所属每个组的所有权限组合起来,得到你的权限列表,并且放在你的访问记号中。管理员还可以通过用户管理器,为每个用户分配一些特别的权限。
***************图一*****************
一个访问记号包括有一个用户ID,用户所属的组名和由所有这些组得到的用户权限列表。
系统中的大部分对象都可以有自己的锁。在NT中,一把锁被称为一个安全描述器。如果在某个对象的创建函数包含有一个安全的参数,你就可以加入一把锁。以下的对象都可以有锁:
.文件(如果它们存放在一个NT文件系统卷中)
.目录(如果它们存放在一个NT文件系统卷中)
.注册键
.进程
.线程
.Mutexes
.Semaphores
.事件
.命名管道(在系统或者网络上)
.匿名管道(仅在系统中)
.Mailslots(仅在系统中)
.控制台屏幕缓冲
.文件映射
.服务
.私有的对象
要锁上一个对象,你要创建一个安全描述器,并且在创建对象的时候将它传送给该对象。如果将安全属性参数的值设置为0,则表示系统为该对象创建一个默认的安全描述器,从而允许你访问该对象。
一个安全描述器包含有四个方面:
.一个拥有者标识符,用来标识该对象目前的拥有者是谁
.一个主要的组标识符
.一个系统访问控制列表(SACL,system access control list),包含有审核的信息
.一个自由访问控制列表(DACL,discretionary access control list),用来决定那些用户和组和可以访问,那些不可以
NT对象的拥有者可以随时更改对象的安全信息。例如,如果你拥有一个文件,并且将它设置为没有人可以访问它,以后,你还可以改变它的安全信息,因为你是它的拥有者。这就象打坏锁进入你的房间一样,虽然锁被打坏了,但是你可以换一把新锁,因为你是房间的拥有者。
*************图二******************
任何对象的安全描述器包含有用户和组的ID、控制访问权限的自由访问控制列表,以及控制审核信息的系统访问控制列表。
DACL是这把锁的核心。它用来控制谁可以访问、禁止谁访问该对象。它是一个访问控制列表,或者简称为ACL,其中包含有访问控制项目(ACE)。每项ACE都说明了一个用户或者组,以及它们的权限。例如,如果对象是一个文件,用户“smith”可允许读取该文件,这样就会有一个ACE指示用户“smith”拥有读取的权限。这也称为一个访问允许ACE,因为它允许一个用户或者组做某件事情。同样,还有一个访问拒绝ACE,用来禁止某个用户或者组访问。例如,你允许Power Users组访问一个对象,但对于“smith”用户,他虽然属于power user,但你不想他可访问到该对象,使用一个拒绝访问ACE就可以了。
SACL(System Access Control List,系统访问控制列表)也包含有ACE,不过这些ACE是用来决定谁将被审核以及审核的原因。SACL中的一个ACE被称为一个审核访问ACE。例如,当“smith”用户成功打开一个文件时,是否要创建一个审核项目呢?这个是由SACL中的一个ACE项目指定的。
**************图三****************
一个访问控制项目(ACE)指定一个用户(或者组)和所允许的访问类似。ACE都存储在ACL中。
一个ACL中的每个ACD都由三部分来组成:一个安全标识符(SID,Security Identifier),一个访问掩模和一个ACE头。SID是存储在注册表中的一个值(也可通过函数调用得到),可唯一标识用户管理器中的每个用户或者组。ACE头用来决定ACE的类型:访问控制,访问拒绝等。一个掩模有32位,用来决定用户可对对象进行的操作。有一些标准的权限应用于系统中所有的对象,还有一些对象有特别的访问权限,可在这些对象上应用。例如,以下就是一个文件对象可应用的特别和标准访问权限:
specific:
FILE_READ_DATA
FILE_WRITE_DATA
FILE_APPEND_DATA
FILE_READ_EA
FILE_WRITE_EA
FILE_EXECUTE
FILE_READ_ATTRIBUTES
FILE_WRITE_ATTRIBUTES
FILE_ALL_ACCESS
standard:
DELETE
READ_CONTROL
STANDARD_RIGHTS_ALL
STANDARD_RIGHTS_EXECUTE
STANDARD_RIGHTS_READ
STANDARD_RIGHTS_REQUIRED
STANDARD_RIGHTS_WRITE
SYNCHRONIZE
WRITE_DAC
WRITE_OWNER
generic:
GENERIC_ALL
GENERIC_EXECUTE
GENERIC_READ
GENERIC_WRITE
generic权限是预定义的标准(standard)和特定(specific)权限的混合,对于各种对象都是不同的。
访问掩模是一个32位的位掩码。在掩码中,每个standard、specific和generic权限都有一个位与之相关。开头的16位保存的是specific的权限,并且也是该访问掩码所指对象的键(例如,上面的FILE_ constants )。接着的8位保存的是standard权限。高4位保存generic权限。
*************图四****************
NT的安全特性使用了不少的新术语,不过,通过例子中的代码,你将会逐渐熟悉所有这些名词,并且也更容易理解所有这些概念。以下就是一个简要的总结:
.用户登录后,他们将得到一个访问记号。一个访问记号包含有用户的ID,用户组和用户所属组得到的用户权限
.每个对象都拥有一个安全描述器,就象它的锁一样。一个安全描述器包含有一个用户和组标识符,一个系统ACL和一个Discretionary ACL。
.一个DACL控制谁允许访问及可以对对象做什么操作
.一个SACL控制要审核谁对一个对象的某种处理
.ACL由ACE组成。每个ACE包含有一个SID,用来标识用户或者组,一个访问掩模,用来决定允许用户或者组做何种处理,还有一个ACE头,决定ACD的类型。
以下的部分提供了两个简单的例子,将所有这些术语组合在一起,并且展示了这些安全设置是如何运作的。
简单的例子
这部分的重点是谈一个文件对象的安全描述器的创建和应用。这里选择了文件对象作例子的原因是由于它是常见的而且易于理解,你也可以通过文件管理器中的安全编辑器很容易地查看代码的运行结果。不过,如果你的硬盘并不是使用NT的文件系统来格式化的话,这些代码将是没有用处的,而且安全编辑器也是不可用的。你可以采用以下三种方式来处理:
1。重新使用NTFS来格式化硬盘,并且重新安装NT,或者使用convert命令来将你的硬盘转换为NT的文件系统;
2。装入一个新的硬盘,并且使用NT的文件系统来格式化
3。在你的硬盘中分出一小部分(大概10M),并且使用NTFS来格式化,这意味着你将必须重新格式化和重装原有的分区
或者,你可以先等一下,我们还会将同样的代码应用到一个注册键中。无论你使用的是哪种文件系统,你将可以在注册表编辑器中看到安全编辑器。
以下的代码包含了一个创建新文件的程序。为新文件创建的安全描述器设置为只有“guest”的用户才可以使用这个文件,而且该用户只有读的权限。这个程序没有任何的错误检测,因此你很容易看出其中的要点。当你运行代码的时候,它将会在c:\创建一个testfile的文件。在文件管理器中选择该文件,然后选择安全菜单中权限选项。你将会看到列表中只有一个项目:只有“guest”可以读取该文件。在你运行这些代码时,你可以任意改变文件名或者用户的名字。
#include
#include
SECURITY_ATTRIBUTES sa;
SECURITY_DESCRIPTOR sd;
BYTE aclBuffer[1024];
PACL pacl=(PACL)&aclBuffer;
BYTE sidBuffer[100];
PSID psid=(PSID) &sidBuffer;
DWORD sidBufferSize = 100;
char domainBuffer[80];
DWORD domainBufferSize = 80;
SID_NAME_USE snu;
HANDLE file;
void main(void)
{
InitializeSecurityDescriptor(&sd,
SECURITY_DESCRIPTOR_REVISION);
InitializeAcl(pacl, 1024, ACL_REVISION);
LookupAccountName(0, "guest", psid,
&sidBufferSize, domainBuffer,
&domainBufferSize, &snu);
AddAccessAllowedAce(pacl, ACL_REVISION,
GENERIC_READ, psid);
SetSecurityDescriptorDacl(&sd, TRUE, pacl,
FALSE);
sa.nLength= sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = FALSE;
sa.lpSecurityDescriptor = &sd;
file = CreateFile("c:\\testfile",
GENERIC_READ | GENERIC_WRITE,
0, &sa, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL, 0);
CloseHandle(file);
}
首先看看程序的末尾,你将会发现一个对CreateFile的调用,该调用创建了一个名字为c:\testfile的文件。不过,这个调用拥有一个安全的参数,它位于参数列表的第4位。你可能在不少的NT代码中看到这个参数通常为0。0值让操作系统在创建对象的时候,使用一个默认的安全描述器。0值还会禁止继承。上面的代码使用一个SECURITY_ATTRIBUTES来代替,其中包含有一个有效的安全描述器。
安全描述器首先在第一行通过调用InitializeSecurityDescriptor函数创建。(对于本文用到的函数,可见SDK中的Win32帮助文件或者Visual C++的帮助文件得到更多的信息)。这步使用绝对(absolute)的格式创建一个安全描述器(还有第二种称为自相关的格式)。新的安全描述器在初始化时除了修改级别的信息外,没有其它的信息:没有拥有者的标识符,没有组的定义,没有SACL,也没有DACL。
下一行调用InitializeAcl来创建ACL,它将成为该安全描述器的DACL。当InitializeAcl函数返回时,pacl指向一个空的ACL。这就是说,ADL中没有ACE。如果你注释掉下面的两行,这个空的ACL就会被放到安全描述器中,并且这个安全描述器会应用到文件,这时就没有人可以访问到这个文件。这是因为discretionary ACL中没有任何的ACE。如果你不创建DACL,并且在DACL安全步骤中传入一个NULL,这样所有人都可以访问到该文件。
接着的两行创建一个ACE,并且将它加入到ACL中。对于指定的帐号名,LookupAccountName函数返回一个SID。LookupAccountName函数将在系统或者本地系统中查找特定的帐号。如果在本地找不到,它将会在域
控制器或者信任的域控制器中找。该函数返回帐号的SID,如果SID是由域控制器得到的,它还会返回一个域名,以及一个枚举值,该值用来指示帐号的类型:
SidTypeUser
SidTypeGroup
SidTypeDomain
SidTypeAlias
SidTypeWellKnownGroup
SidTypeDeletedAccount
SidTypeInvalid
SidTypeUnknown
SID是一个安全标识符,它唯一标识系统中的一个用户或者一个组。
由LookupAccountName函数返回的SID被用在一个对AddAccessAllowedAce函数的调用中,该函数用来创建一个访问允许(与访问拒绝相对)ACE,并且将它加入到当前为空的ACL中。
AddAccessAllowedAce 函数创建该SID的ACE和指定的访问掩模,并且将它加入到指定的ACL中。GENERIC_READ访问掩模为文件加入读权限。
现在ACL中包含了一个ACE,并且指定了“guest”用户可以读取该文件。这个ACL需要使用SetSecurityDescriptorDacl函数来放入到安全描述器的Discretionary ACL中。
现在安全描述器中包含了一个有效的DACL,其中有一个ACE。程序将这个安全描述器放到一个安全属性结构体中,并且将它传送给CreateFile函数。
如果你将前面的三行代码注释掉,并且在调用SetSecurityDescriptorDacl时,将DACL的pacl用NULL代替,这时任何人都可以访问该文件。没有DACL意味着任何人可以访问该文件。
编译并运行以上的代码。在你运行前,你要确保程序中指定的文件名在系统中并不存在。通过文件管理器的安全编辑器查看该新文件时,你将会发现它的权限与代码中设置得一样。
你可以很容易地修改这些代码,以加深对安全描述器的了解。例如,你可以尝试给某个组写的权限,或者给DACL加入几个ACE,或者使用AddAccessDeniedAce创建一个访问拒绝ACE。要确保访问拒绝ACE放在访问允许ACE的前面。使用SetSecurityDescriptorOwner函数来修改安全描述器中的拥有者。
另一个例子
同样的安全描述器代码可以应用到其它受保护的NT对象上,因为安全系统对于所有的对象都是一样的。例如,你可以使用它来创建注册表中的一个键值,具体见下面的代码。如果你将下面的代码和上面的作对比,你将会发现它们几乎是完全一样了,除了这里是用RegCreateKeyEx来创建一个注册表键而不是一个文件。
#include
#include
SECURITY_ATTRIBUTES sa;
SECURITY_DESCRIPTOR sd;
BYTE aclBuffer[1024];
PACL pacl=(PACL)&aclBuffer;
BYTE sidBuffer[100];
PSID psid=(PSID) &sidBuffer;
DWORD sidBufferSize = 100;
char domainBuffer[80];
DWORD domainBufferSize = 80;
SID_NAME_USE snu;
LONG result;
HKEY regKey;
DWORD disposition;
void main(void)
{
InitializeSecurityDescriptor(&sd,
SECURITY_DESCRIPTOR_REVISION);
InitializeAcl(pacl, 1024, ACL_REVISION);
LookupAccountName(0, "guest", psid,
&sidBufferSize, domainBuffer,
&domainBufferSize, &snu);
AddAccessAllowedAce(pacl, ACL_REVISION,
GENERIC_READ, psid);
SetSecurityDescriptorDacl(&sd, TRUE, pacl,
FALSE);
sa.nLength= sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = FALSE;
sa.lpSecurityDescriptor = &sd;
result = RegCreateKeyEx(HKEY_CURRENT_USER,
"junk", 0, 0, REG_OPTION_VOLATILE,
KEY_ALL_ACCESS, &sa, ?Key,
&disposition);
cout << result << endl;
RegCloseKey(regKey);
}
在运行以上代码前,先运行注册表编辑器(regedit32.exe),查看HKEY_CURRENT_USER,并且确认在HKEY_CURRENT_USER下并没有一个名字为“junk”的键值。如果有的话,请删除它,或者修改代码来创建一个不同的键。
现在可运行代码,如果没有设置为自动刷新的话,刷新注册表编辑器的显示,你将会看到一个名字为“junk”的新键。通过安全菜单中的权限选项,查看它的权限,你将会发现该键的拥有者是你或者是管理员,而“guest”是唯一可以访问它的用户。你可以改变键的名字或者是用户,再继续尝试一下,由于以上的代码创建的是一个volatile键,因此该键将在你注销或者重启系统的时候消失。