linux文件系统-访问权限与文件安全性

Unix操作系统从一开始就在其文件系统中引入了文件、访问权限等概念,并在此基础上实现了有利于提高文件安全性的机制。从那以后这些概念和机制就一直被继承下来并进一步得到改进和完善。即使在经过了很多年后的今天,而且在计算机系统的安全性已经成为一个突出问题的情况下,这一套机制仍然不失其先进性。尽管还存在一些缺点和需要进一步改进的地方,从总体上说还是瑕不掩瑜。与当今正在广泛使用的其他操作系统相比,可以说Unix的安全性总的来说至少不会差于这些系统;如果考虑到近年来在Unix及linux中已经作出的改进以及不难作出的进一步改进,可以说Unix在安全性方面与任何其他系统相比都不逊色。同时,从当前流行的其他操作系统中,人们不难看出它们受Unix影响的或明或暗的痕迹。

Unix文件系统的访问权限是一种二维结构。就同一个用户来说,对一个文件的访问分成读、写和执行三种形式,因而形成三种不同的权限;而就同一中访问方式来说,则又可因访问者的身份属于文件主、文件主的同组人以及其他用户(称为other)而分别决定允许与否。这样一块就有9中组合,可以用9个二进制位来表示。早期的Unix是在16位结构的PDP-11机器上开发出来的,所以从那时起就一直用一个16位短整型数据结构来表示一个文件的访问模式,而将其中的低9位用于访问权限。当时比较流行的是八进制表示法,所以正好将这9位表示成3个八进制位从高到低分别用于文件主(u)、文件主的同组人(g)以及其他(o),而每个八进制数位中的三个二进制位则从高到低分别用于读、写和执行三种权限。这种表示方法一致沿用至今,例如在命令“chmod 644 file1”中的644就是这样三个8进制位。此外,这种把访问者区分为文件主、同组人以及其他用户,根据访问者的身份而分别决定其访问权限的方案称为“discretionary access control”,简写为DAC。

这个方案的实施分成几个方面。首先,除用户名外,每个用户还授予一个(在系统范围内)唯一的用户号UID,并且总是属于某一而用户组,而每个用户组则唯一的组号gid,这些信息记录在相当于一个小数据库的文件/etc/passwd中。其次,每个文件的索引节点中记录着文件主的uid、gid以及文件访问模式。还有,在每个进程的task_struct结构中相应地设置了uid和gid等字段。每当用户通过登录进入系统并创立第一个shell进程时,就从/etc/passwd中根据用户名查得其uid和gid,并将其设置到该shell进程的task_struct结构中,从此以后便代代相传。最后,也是最重要的是,内核在执行用户进程访问文件的请求的访问权限。(实际上,进程的task_struct结构中还有euid、egid、suid、sgid、fsuid以及fsgid等字段,下面还要解释)。此外,uid为0的用户为超级用户,而超级用户对任何文件都具有与文件主相同的权限。还要注意,用户名与用户号并不是一对一的关系,多个用户,甚至所有用户,都可以对应到同一个用户号。

由于超级用户的进程对任何文件都具有与文件主相同的权限,实际上可以对任何文件为所欲为,这就带来了危险(这里还没有考虑有人非法取得特权用户权限所引起的问题)。所以,有时候需要通过一个进程的用户号和组号来改变(限制)其访问权限。由此引申出了进程的真实用户号、真实组号和当前的有效用户号、有效组号的概念。相应的,在进程的task_struct结构中也增设了euid(表示effective uid)和egid两个字段,并且提供了setuid、seteuid等系统调用。另一方面,在改变有效用户号时往往需要把原来的有效用户号暂时保存起来,以便以后恢复,所以在task_struct结构中又增设可suid(表示saved uid)和sgid两个字段,这样,在task_struct结构中就有了三个用户号和三个组号,即uid、euid、suid以及gid、egid、sgid。后来,在开发和使用网络文件系统NFS的守护神(即服务进程)的过程中认识到,在网络环境下对文件的访问还需要一个不同的用户号,因此有增加一个fsuid和一个fsgid。通常fsuid与euid相同,而fsgid和egid相同,但是在特殊的情况下可以不同。这里要指出,一般而言,只有特权用户以及具有特权用户权限的进程(见下面的所谓set_uid文件和进程)才能通过系统调用来改变其用户号和组号,这些系统调用的结果都是使进程的权限更受限制;在相反的方向上,则最多是恢复到原有的水平,所以一个非特权用户进程是不能通过setuid或seteuid得到特权用户的权限的,这一点跟我们头脑中一个普通用户可以通过shell命令su变成特权用户的印象可能不一致。这里面的原因是su是一个set_uid可执行程序,它的文件主是root,即特权用户,所以当普通用户执行su的过程中就自动具有了特权用户的权限,这正是我们接下去要讨论的。

在前述二维访问权限机制的框架中,让我们考虑一个问题,即一个普通用户怎样才可以改变它自己的口令。我们知道,有关用户的名称、用户号、组号、口令等信息都保存在文件/etc/passwd中。这个文件的主人只能是超级用户,因为只有超级用户才是系统中最核心、权利最大的用户,通常就是系统的管理员。除超级用户外,其他所有的用户对这个文件都不应该由写权限,因为那样的话每个用户都可以通过修改这个文件、将自己的用户号改成0而变成特权用户了。所以,除文件主以外,所有其他用户对/etc/passwd都只能有读而不能有写权限。这显然是合理的,而且只能如此。可是,这样一来,一个普通用户就不能通过运行一个什么程序来改变自己的口令了,因为改口令意味着改变/etc/passwd中的内容。怎样解决这个矛盾呢?早期的Unix采用了一种当时看起来很巧妙的办法,就是在一些特殊用途的可执行文件上加一个标记,使得任何用户在执行这个文件(程序)时就暂时有了与该文件的文件主(通常是超级用户)相同的权限。这样,只要把用来改变口令的程序(/bin/passwd)加上这种标记,普通用户在执行这个程序的时候就能拉大旗作虎皮,暂时有了特权用户的权限,可以改变/etc/passwd的内容了。一旦执行完毕,则又回到原来的权限,又是普通用户了。这样的可执行文件,就成为set_uid文件,而加上这种文件上的标记,则是在文件模式中的一个标志位S_ISUID。与此类似,还有一个标志位S_ISGID,可以理解为对S_ISUID标志位的推广。有时候人们称S_ISUID标志位为s位,因为在用命令 ls -l 列目录时把表示这种文件对文件主的可执行权限的字符x变成了s。在当时,这个办法却是很巧妙、很有效,据说,at&t还为此申请了专利。可是,近年来去发现这种set_uid文件给黑客们带来了可乘之机,简直已经成了Unix(以及linux)在安全性检查的万恶之源,后面我们还会回到这个问题上来。

除这两个标志位以外,早期Unix还为可执行文件定义了一个粘滞(sticky)标志位。对于一些频繁运行的程序,可以把这个标志位设成1,使得内核在这个程序运行完毕后尽可能将其映像保存在内存汇总不予释放,这样下一次需要启动这个程序运行时就不需要再从磁盘装入了。不过,现在的Unix和linux多已采用虚存管理,所以这个标志位现在已经没什么意义了。

前面说过,文件的模式是以一个16位无符号整数表示的,其中9位已经用于对三种不同用户的访问权限,现在又用去了3位。这样还剩下4位,用来表示文件的类型。不过,由于只剩下4位,要为每种文件类型都分配一个标志位已不可能了,所以表示文件类型的这4位是编码的。对文件类型和上述几个标志位的定义在include/linux/stat.h,但是另一个文件include/linux/sysv_fs.h中有几行注释提供了比较详细的说明:

/* The admissible values for i_mode are listed in  :
 * #define S_IFMT  00170000  mask for type
 * #define S_IFREG  0100000  type = regular file
 * #define S_IFBLK  0060000  type = block device
 * #define S_IFDIR  0040000  type = directory
 * #define S_IFCHR  0020000  type = character device
 * #define S_IFIFO  0010000  type = named pipe
 * #define S_ISUID  0004000  set user id
 * #define S_ISGID  0002000  set group id
 * #define S_ISVTX  0001000  save swapped text even after use
 * Additionally for SystemV:
 * #define S_IFLNK  0120000  type = symbolic link

注意,这里的数字均为八进制,其中S_IFMT并不代表一种文件类型,而只是对文件类型的屏蔽位段。对低9位的定义则为:

#define S_IRWXU 00700
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100

#define S_IRWXG 00070
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010

#define S_IRWXO 00007
#define S_IROTH 00004
#define S_IWOTH 00002
#define S_IXOTH 00001

这个16位的文件模式存储在每个文件的索引节点中,而每个进程则在其task_struct结构中有uid、euid等说明其身份的信息。这就是判定一个进程是否有权对某个文件进行某种访问的基础。对访问权限的判定主要是由函数permission完成的,读者在path_walk的代码中已经看到过了,在那里的for循环中对路径中的每一个节点调用这个函数,其代码如下:

int permission(struct inode * inode,int mask)
{
	if (inode->i_op && inode->i_op->permission) {
		int retval;
		lock_kernel();
		retval = inode->i_op->permission(inode, mask);
		unlock_kernel();
		return retval;
	}
	return vfs_permission(inode, mask);
}

参数mask为代表着所要求的访问方式的标志位,定义如下:

#define MAY_EXEC 1
#define MAY_WRITE 2
#define MAY_READ 4

对于一般的文件系统就分成这么三种方式。网络文件系统NFS的情况特殊,除这三种方式以外还定义了MAY_TRUNC、MAY_LOCK等方式以及这些方式的若干组合,不过NFS不在本书要讨论的范围内。

如果具体的文件系统通过其inode_operations结构中函数指针permission提供了特定的访问权限判定函数,那就把事情交给它了,否则就执行一般的vfs_permission。

就ext2文件系统而言,共有三个inode_operations结构,即ext2_file_inode_operations、ext2_dir_inode_operations以及ext2_fast_symlink_inode_operations,根据具体inode结构所代表的节点性质而在ext2_read_inode中将其i_op指针设置成指向这三者之一。可是,这三个结构中都没有提供专门的permission操作(函数指针permission为NULL),所以执行vfs_permission,其代码如下:

permission=>vfs_permission


/*
 *	permission()
 *
 * is used to check for read/write/execute permissions on a file.
 * We use "fsuid" for this, letting us set arbitrary permissions
 * for filesystem access without changing the "normal" uids which
 * are used for other things..
 */
int vfs_permission(struct inode * inode,int mask)
{
	int mode = inode->i_mode;

	if ((mask & S_IWOTH) && IS_RDONLY(inode) &&
		 (S_ISREG(mode) || S_ISDIR(mode) || S_ISLNK(mode)))
		return -EROFS; /* Nobody gets write access to a read-only fs */

	if ((mask & S_IWOTH) && IS_IMMUTABLE(inode))
		return -EACCES; /* Nobody gets write access to an immutable file */

	if (current->fsuid == inode->i_uid)
		mode >>= 6;
	else if (in_group_p(inode->i_gid))
		mode >>= 3;

	if (((mode & mask & S_IRWXO) == mask) || capable(CAP_DAC_OVERRIDE))
		return 0;

	/* read and search access */
	if ((mask == S_IROTH) ||
	    (S_ISDIR(inode->i_mode)  && !(mask & ~(S_IROTH | S_IXOTH))))
		if (capable(CAP_DAC_READ_SEARCH))
			return 0;

	return -EACCES;
}

这里用到的一些红操作分别定义于fs.h和stat.h:

#define S_ISLNK(m)	(((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m)	(((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m)	(((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m)	(((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m)	(((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m)	(((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m)	(((m) & S_IFMT) == S_IFSOCK)


#define IS_RDONLY(inode) ((inode)->i_sb->s_flags & MS_RDONLY)

IS_RDONLY表示节点所在的文件系统,即磁盘设备,是按只读方式安装的。在这样的磁盘上,对常规文件、目录以及符号链接这三种节点都不能写。但是,即使是在按只读方式安装的文件系统中,如果节点所代表的是FIFO文件、socket等等特殊文件,或者设备文件(块设备或字符设备都一样),那就未必是不可写的。为什么呢?因为对这些文件的写访问实际上不会或者不一定写到该节点所在的磁盘上去。

其次IS_IMMUTABLE的定义为:

#define IS_IMMUTABLE(inode)	((inode)->i_flags & S_IMMUTABLE)

在较新的linux(Unix)版本中,除访问权限外又给每个文件加上了一些属性,不可更改即使其中之一。这些属性也像访问权限一样以标志位的形式存储在文件的索引节点中,但是不像访问权限那样区分文件主、文件主的同组用户以及公众,而是另成系统,并且凌驾于访问权限上。而且,一旦设置了这些属性,即使是特权用户也不能在系统还是正常的多用户环境下运行时将这些属性去掉。这样,就算有黑客偷到了特权用户的口令,对这些文件也就无能为力了。如果一个文件被设置成了不可更改,那么即使是超级用户通过chmod把文件的访问模式设置成科协也无济于事。显然,这是因为安全性考虑而作的改进和增强。表示这些属性的标志位定义如下:

/*
 * Inode flags
 */
#define	EXT2_SECRM_FL			0x00000001 /* Secure deletion */
#define	EXT2_UNRM_FL			0x00000002 /* Undelete */
#define	EXT2_COMPR_FL			0x00000004 /* Compress file */
#define EXT2_SYNC_FL			0x00000008 /* Synchronous updates */
#define EXT2_IMMUTABLE_FL		0x00000010 /* Immutable file */
#define EXT2_APPEND_FL			0x00000020 /* writes to file may only append */
#define EXT2_NODUMP_FL			0x00000040 /* do not dump file */
#define EXT2_NOATIME_FL			0x00000080 /* do not update atime */
/* Reserved for compression usage... */
#define EXT2_DIRTY_FL			0x00000100
#define EXT2_COMPRBLK_FL		0x00000200 /* One or more compressed clusters */
#define EXT2_NOCOMP_FL			0x00000400 /* Don't compress */
#define EXT2_ECOMPR_FL			0x00000800 /* Compression error */
/* End compression flags --- maybe not all used */	
#define EXT2_BTREE_FL			0x00001000 /* btree format dir */
#define EXT2_RESERVED_FL		0x80000000 /* reserved for ext2 lib */

#define EXT2_FL_USER_VISIBLE		0x00001FFF /* User visible flags */
#define EXT2_FL_USER_MODIFIABLE		0x000000FF /* User modifiable flags */

其中有些属性是为其他目的而设的(如压缩),我们在这里只关心与安全性有关的属性。例如,EXT2_APPEND_FL表示对文件的写访问只能添加在文件的末尾,而不能改变文件中已有的内容。读者会问,在打开文件时不是就有个添加模式吗?为什么这里又要来一个添加属性呢?答案很简单,打开文件时的添加模式是用户进程自愿的,而文件的添加属性却是强制的。

所以,只要inode结构中的i_flag里面的S_IMMUTABLE标志位1,那就剥夺了所有用户对这个文件的写访问权(见163行),而与文件所设置的访问权限以及访问者的身份无关。

还有个属性EXT2_NODUMP_FL,意图是使可执行文件在运行中访问内存出错(越界访问等)时不要生成dump文件。从Unix的早期版本开始可执行文件在运行过程中因访问内存侵权而出错时都会把当时的内存映射卸载到一个磁盘文件汇总(名为core,因为早期的计算机采用磁芯存储器),使程序员可以使用调试工具(如gdb等)来重建起发生问题时的场景,这对于软件的维护显然是有好处的。可是,在实践中却发现,这也给怀有恶意的黑客提供了可乘之机。为了在某些情况下不让产生dump文件,在task_struct结构中增设了一个标志位dumpable,在某些情况下(例如通过setuid设置了有效用户号)就将这个标志位清0。同时,又设置了一个系统调用prctl,其用途之一就是将dumpable标志设置成1或0,可是这还是不能解决防止恶意攻击的问题。在一些特殊的应用环境(如银行)中,对一些特殊的可执行程序,需要完全杜绝其产生dump文件的可能性,这就是设置EXT2_NODUMP_FL属性及标志位的意图。此外,对于某些特殊文件,不能像对一般的文件那样每次访问后就要打下时间戳,标志位EXT2_NOATIME_FL就是为此目的而设置的。总之,对于传统的Unix文件系统而言,这些属性(标志位)都是体制外的,所以不能纳入原先的框架中,而其中有一些是为增强文件系统的安全性而设置的。

回到permission的代码中,下面就是访问权限的比对了。这里mode是取自inode结构中的文件访问模式,即前述的16位无符号短整型;mask则为所要求的访问方式,即MAY_EXEC、MAY_WRITE或MAY_READ,实际上只用了最低3位。前面说过,当前进程的fsuid是专用于文件访问目的的有效uid,通常与进程的euid想用,但是在使用网络文件系统时可能会不同。如果当前进程的fsuid与文件主的uid相同,那么要比对的是mode中用于文件主的访问权限,所以把mode右移6位,把用于文件主的三个标志位移动到最低的3位中。如果当前进程的fsuid与文件主的uid不同,那就要检查一下当前右移3位。判定是否同组比判定是否文件主要复杂一些,是通过一个函数in_group_p来完成的,其代码如下:

permission=>vfs_permission=>in_group_p

/*
 * Check whether we're fsgid/egid or in the supplemental group..
 */
int in_group_p(gid_t grp)
{
	int retval = 1;
	if (grp != current->fsgid)
		retval = supplemental_group_member(grp);
	return retval;
}

如果进程的fsgid与文件主的组号相同,那就成了。可是即使这二者不同也还有可能实际上是相等的,因为一个用户(从而一个进程)可以同时属于若干个组,后面会讲解进程的博客,可以看看。sched.h中对task_struct的定义,在task_struct结构中有个数组groups,其大小为常数NGROUPS,该常数为32。当然,一个用户(进程)未必会那么社会化,所以在task_struct中还有个计数器ngroups。与此相应,还提供了系统调用get_groups和set_groups。(只有得到授权的进程才可以set_groups)。所以,如果fsgid与文件主的组号不同,就要进一步拿这个数组中的其他候补组号跟文件主的组号相比,函数的代码如下:

permission=>vfs_permission=>in_group_p=>supplemental_group_member


static int supplemental_group_member(gid_t grp)
{
	int i = current->ngroups;

	if (i) {
		gid_t *groups = current->groups;
		do {
			if (*groups == grp)
				return 1;
			groups++;
			i--;
		} while (i);
	}
	return 0;
}

最后,如果当前进程却是不属于文件主的同组人,那就是属于其他用户了。此时mode不需要移位,因为要比对的3位已经在最低的位置上了。

常数S_IRWXO的值为7,所以比对的是此时mode中最低的3位。比对的结果相符时,permission返回0,;要是不符呢?一般而言就失败了。但是还有例外。首先,如果当前进程得到了授权,允许其CAP_DAC_OVERRIDE,即可以凌驾于文件系统的访问权限控制机制DAC之上,则基本上不受其限制。不过,前面159行和163行中检查的两种情况不在内。实际上,IS_IMMUTABLE要有另一种授权(CAP_LINUX_IMMUTABLE)的进程才能设置。所以,这种进程就好像是捧着“尚方宝剑”的钦差大臣,这才是真正意义上的超级用户。可惜超级用户和特权用户这两个词都已经用于uid为0的用户,所以我们在本书中称此类进程为授权进程。等一下我们还要回到这个话题上来。

除了拥有CAP_DAC_OVERRIDE授权的进程之外,还要一种特殊情况,那就是另一种授权CAP_DAC_READ_SEARCH,拥有这种特权的进程可以读任何文件,并且可以搜索任何目录节点,所以,代码中的177行检查所要求的是否读访问或者对目录节点的搜索。这里要提醒读者,搜索目录节点时所要求的访问方式为执行而不是读,所以在path_walk的for循环汇总对每个目录节点调用permission时的参数为MAY_EXEC,而不是MAY_READ。

如前所述,用户进程在一定条件下可以通过系统调用来设置其用户号,有关的系统调用由setuid、setfsuid、seteuid、setreuid。其中setuid是标准的设置用户号调用,内核中与之相应的函数为sys_setuid,代码如下:



		
/*
 * setuid() is implemented like SysV with SAVED_IDS 
 * 
 * Note that SAVED_ID's is deficient in that a setuid root program
 * like sendmail, for example, cannot set its uid to be a normal 
 * user and then switch back, because if you're root, setuid() sets
 * the saved uid too.  If you don't like this, blame the bright people
 * in the POSIX committee and/or USG.  Note that the BSD-style setreuid()
 * will allow a root program to temporarily drop privileges and be able to
 * regain them by swapping the real and effective uid.  
 */
asmlinkage long sys_setuid(uid_t uid)
{
	int old_euid = current->euid;
	int old_ruid, old_suid, new_ruid;

	old_ruid = new_ruid = current->uid;
	old_suid = current->suid;
	if (capable(CAP_SETUID)) {
		if (uid != old_ruid && set_user(uid) < 0)
			return -EAGAIN;
		current->suid = uid;
	} else if ((uid != current->uid) && (uid != current->suid))
		return -EPERM;

	current->fsuid = current->euid = uid;

	if (old_euid != uid)
		current->dumpable = 0;

	if (!issecure(SECURE_NO_SETUID_FIXUP)) {
		cap_emulate_setxuid(old_ruid, old_euid, old_suid);
	}

	return 0;
}

一般超级用户(其euid为0)都具有CAP_SETUID授权,此时在569行和573行把当前进程的euid、suid、fsuid都设立成新的uid。注意,这里吧suid也设置成新的uid,实在是个败笔,因为task_struct结构中的suid本意在于save uid,即在暂时改变euid时可以记住原来的euid是什么,以便以后恢复。而这里吧suid也设置成了uid,就失去了它的作用,并且用户号改变的历史也被一笔勾销,以后无法恢复成超级用户了。代码的原作者在函数前面加了注释,也谈到了这个问题。可是,超级用户在调用setuid时把其suid也设置成新的uid,这是在POSIX标准中规定了的,明知道不合理也只能如此。正因为这样,在BSD(以及linux)中另外提供了系统调用seteuid和setreuid来避免这个缺点。相比之下,对于不具备CAP_SETUID授权的进程,则只能设置当前进程的euid和fsuid,但是只有在新的uid就是进程的真实uid或者suid时才能进行。

每当进程改变其euid时,其task_struct结构中的标志位dumpable就被清0,这样进程在访问出错时就不会产生dump文件了、

如果当前进程具有CAP_SETUID授权,并且新的uid又与原来的真实用户号不同,则连进程的真实用户号也要改变,这是通过set_user实现的:

sys_setuid=>set_user


static int set_user(uid_t new_ruid)
{
	struct user_struct *new_user, *old_user;

	/* What if a process setreuid()'s and this brings the
	 * new uid over his NPROC rlimit?  We can check this now
	 * cheaply with the new uid cache, so if it matters
	 * we should be checking for it.  -DaveM
	 */
	new_user = alloc_uid(new_ruid);
	if (!new_user)
		return -EAGAIN;
	old_user = current->user;
	atomic_dec(&old_user->processes);
	atomic_inc(&new_user->processes);

	current->uid = new_ruid;
	current->user = new_user;
	free_uid(old_user);
	return 0;
}

后面的进程相关博客会提到,内核中有个杂凑表uidhash_table,各个进程的task_struct结构按其用户号uid的杂凑值挂入杂凑表的某个队列中。这样,根据给定的uid就可以很快找到所有属于该用户的进程。同时,在task_struct结构中有个指针user,指向一个user_struct数据结构,这个数据结构就好像task_struct与杂凑队列之间的连接件,进程的task_struct结构就是通过它挂入杂凑队列。现在,既然当前进程要改换门庭了,就要从原来的杂凑队列中脱链并将其user_struct结构释放,然后另行分配一个user_struct结构并挂入另一个队列。函数free_uid的代码如下:

sys_setuid=>set_user=>free_uid

void free_uid(struct user_struct *up)
{
	if (up && atomic_dec_and_lock(&up->__count, &uidhash_lock)) {
		uid_hash_remove(up);
		kmem_cache_free(uid_cachep, up);
		spin_unlock(&uidhash_lock);
	}
}

函数alloc_uid的作用与free_uid正好相反,我们就不列出它的代码了。

我们在前面讲过,超级用户的进程通常是得到某些授权的。相比之下,一般用户则得不到任何授权,它所有的只是文件系统的访问权限机制DAC所赋予的基本权利,而且这些基本权利也有可能得不到兑现,因为文件的属性如IS_IMMUTABLE等是凌驾于DAC之上的。可想而知,当一个超级用户进程改变其uid至某一普通用户时,其授权也要发生一些变化。

对进程的授权是独立于文件系统的访问权限控制之外,并且凌驾于其上的机制。为此目的在task_struct结构中设置了cap_effective、cap_inheritable,和cap_permitted三个字段,其类型为kernel_cap_t,目前实际上是32位无符号整数。每一种授权(capability)都用一个标志位来表示,目前共定义了29中授权,所以32位无符号整数就够用了。这些表示为(和授权)的定义如下:

/**
 ** POSIX-draft defined capabilities. 
 **/

/* In a system with the [_POSIX_CHOWN_RESTRICTED] option defined, this
   overrides the restriction of changing file ownership and group
   ownership. */

#define CAP_CHOWN            0

/* Override all DAC access, including ACL execute access if
   [_POSIX_ACL] is defined. Excluding DAC access covered by
   CAP_LINUX_IMMUTABLE. */

#define CAP_DAC_OVERRIDE     1

/* Overrides all DAC restrictions regarding read and search on files
   and directories, including ACL restrictions if [_POSIX_ACL] is
   defined. Excluding DAC access covered by CAP_LINUX_IMMUTABLE. */

#define CAP_DAC_READ_SEARCH  2
    
/* Overrides all restrictions about allowed operations on files, where
   file owner ID must be equal to the user ID, except where CAP_FSETID
   is applicable. It doesn't override MAC and DAC restrictions. */

#define CAP_FOWNER           3

/* Overrides the following restrictions that the effective user ID
   shall match the file owner ID when setting the S_ISUID and S_ISGID
   bits on that file; that the effective group ID (or one of the
   supplementary group IDs) shall match the file owner ID when setting
   the S_ISGID bit on that file; that the S_ISUID and S_ISGID bits are
   cleared on successful return from chown(2) (not implemented). */

#define CAP_FSETID           4

/* Used to decide between falling back on the old suser() or fsuser(). */

#define CAP_FS_MASK          0x1f

/* Overrides the restriction that the real or effective user ID of a
   process sending a signal must match the real or effective user ID
   of the process receiving the signal. */

#define CAP_KILL             5

/* Allows setgid(2) manipulation */
/* Allows setgroups(2) */
/* Allows forged gids on socket credentials passing. */

#define CAP_SETGID           6

/* Allows set*uid(2) manipulation (including fsuid). */
/* Allows forged pids on socket credentials passing. */

#define CAP_SETUID           7


/**
 ** Linux-specific capabilities
 **/

/* Transfer any capability in your permitted set to any pid,
   remove any capability in your permitted set from any pid */

#define CAP_SETPCAP          8

/* Allow modification of S_IMMUTABLE and S_APPEND file attributes */

#define CAP_LINUX_IMMUTABLE  9

/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */

#define CAP_NET_BIND_SERVICE 10

/* Allow broadcasting, listen to multicast */

#define CAP_NET_BROADCAST    11

/* Allow interface configuration */
/* Allow administration of IP firewall, masquerading and accounting */
/* Allow setting debug option on sockets */
/* Allow modification of routing tables */
/* Allow setting arbitrary process / process group ownership on
   sockets */
/* Allow binding to any address for transparent proxying */
/* Allow setting TOS (type of service) */
/* Allow setting promiscuous mode */
/* Allow clearing driver statistics */
/* Allow multicasting */
/* Allow read/write of device-specific registers */
/* Allow activation of ATM control sockets */

#define CAP_NET_ADMIN        12

/* Allow use of RAW sockets */
/* Allow use of PACKET sockets */

#define CAP_NET_RAW          13

/* Allow locking of shared memory segments */
/* Allow mlock and mlockall (which doesn't really have anything to do
   with IPC) */

#define CAP_IPC_LOCK         14

/* Override IPC ownership checks */

#define CAP_IPC_OWNER        15

/* Insert and remove kernel modules - modify kernel without limit */
/* Modify cap_bset */
#define CAP_SYS_MODULE       16

/* Allow ioperm/iopl access */
/* Allow sending USB messages to any device via /proc/bus/usb */

#define CAP_SYS_RAWIO        17

/* Allow use of chroot() */

#define CAP_SYS_CHROOT       18

/* Allow ptrace() of any process */

#define CAP_SYS_PTRACE       19

/* Allow configuration of process accounting */

#define CAP_SYS_PACCT        20

/* Allow configuration of the secure attention key */
/* Allow administration of the random device */
/* Allow examination and configuration of disk quotas */
/* Allow configuring the kernel's syslog (printk behaviour) */
/* Allow setting the domainname */
/* Allow setting the hostname */
/* Allow calling bdflush() */
/* Allow mount() and umount(), setting up new smb connection */
/* Allow some autofs root ioctls */
/* Allow nfsservctl */
/* Allow VM86_REQUEST_IRQ */
/* Allow to read/write pci config on alpha */
/* Allow irix_prctl on mips (setstacksize) */
/* Allow flushing all cache on m68k (sys_cacheflush) */
/* Allow removing semaphores */
/* Used instead of CAP_CHOWN to "chown" IPC message queues, semaphores
   and shared memory */
/* Allow locking/unlocking of shared memory segment */
/* Allow turning swap on/off */
/* Allow forged pids on socket credentials passing */
/* Allow setting readahead and flushing buffers on block devices */
/* Allow setting geometry in floppy driver */
/* Allow turning DMA on/off in xd driver */
/* Allow administration of md devices (mostly the above, but some
   extra ioctls) */
/* Allow tuning the ide driver */
/* Allow access to the nvram device */
/* Allow administration of apm_bios, serial and bttv (TV) device */
/* Allow manufacturer commands in isdn CAPI support driver */
/* Allow reading non-standardized portions of pci configuration space */
/* Allow DDI debug ioctl on sbpcd driver */
/* Allow setting up serial ports */
/* Allow sending raw qic-117 commands */
/* Allow enabling/disabling tagged queuing on SCSI controllers and sending
   arbitrary SCSI commands */
/* Allow setting encryption key on loopback filesystem */

#define CAP_SYS_ADMIN        21

/* Allow use of reboot() */

#define CAP_SYS_BOOT         22

/* Allow raising priority and setting priority on other (different
   UID) processes */
/* Allow use of FIFO and round-robin (realtime) scheduling on own
   processes and setting the scheduling algorithm used by another
   process. */

#define CAP_SYS_NICE         23

/* Override resource limits. Set resource limits. */
/* Override quota limits. */
/* Override reserved space on ext2 filesystem */
/* NOTE: ext2 honors fsuid when checking for resource overrides, so 
   you can override using fsuid too */
/* Override size restrictions on IPC message queues */
/* Allow more than 64hz interrupts from the real-time clock */
/* Override max number of consoles on console allocation */
/* Override max number of keymaps */

#define CAP_SYS_RESOURCE     24

/* Allow manipulation of system clock */
/* Allow irix_stime on mips */
/* Allow setting the real-time clock */

#define CAP_SYS_TIME         25

/* Allow configuration of tty devices */
/* Allow vhangup() of tty */

#define CAP_SYS_TTY_CONFIG   26

/* Allow the privileged aspects of mknod() */

#define CAP_MKNOD            27

/* Allow taking of leases on files */

#define CAP_LEASE            28

代码的作者已经加了详尽的注释,我们这里就不做解释了。定义中的数值为标志位的位置,如CAP_CHOWN的定义为0,对授权的检查是capable


/*
 * capable() checks for a particular capability.  
 * New privilege checks should use this interface, rather than suser() or
 * fsuser(). See include/linux/capability.h for defined capabilities.
 */

static inline int capable(int cap)
{
#if 1 /* ok now */
	if (cap_raised(current->cap_effective, cap))
#else
	if (cap_is_fs_cap(cap) ? current->fsuid == 0 : current->euid == 0)
#endif
	{
		current->flags |= PF_SUPERPRIV;
		return 1;
	}
	return 0;
}

这里的cap_raised是个宏操作,与之有关的定义如下:
 

#define cap_t(x) (x)

#define CAP_TO_MASK(x) (1 << (x))

#define cap_raised(c, flag)  (cap_t(c) & CAP_TO_MASK(flag))

全局变量cap_bset则设置成CAP_INIT_EFF_SET,即全部标志位均为1。

当进程改变其uid时,要通过cap_emulate_setxuid检查并可能改变其授权情况,除非在编译内核前将一个常数SECUREBITS_DEFAULT中的SECURE_NO_SETUID_FIXUP标志位设成1,表示可以忽略对进程的授权机制。函数的代码如下:

sys_setuid=>cap_emulate_setxuid

  
/* 
 * cap_emulate_setxuid() fixes the effective / permitted capabilities of
 * a process after a call to setuid, setreuid, or setresuid.
 *
 *  1) When set*uiding _from_ one of {r,e,s}uid == 0 _to_ all of
 *  {r,e,s}uid != 0, the permitted and effective capabilities are
 *  cleared.
 *
 *  2) When set*uiding _from_ euid == 0 _to_ euid != 0, the effective
 *  capabilities of the process are cleared.
 *
 *  3) When set*uiding _from_ euid != 0 _to_ euid == 0, the effective
 *  capabilities are set to the permitted capabilities.
 *
 *  fsuid is handled elsewhere. fsuid == 0 and {r,e,s}uid!= 0 should 
 *  never happen.
 *
 *  -astor 
 *
 * cevans - New behaviour, Oct '99
 * A process may, via prctl(), elect to keep its capabilities when it
 * calls setuid() and switches away from uid==0. Both permitted and
 * effective sets will be retained.
 * Without this change, it was impossible for a daemon to drop only some
 * of its privilege. The call to setuid(!=0) would drop all privileges!
 * Keeping uid 0 is not an option because uid 0 owns too many vital
 * files..
 * Thanks to Olaf Kirch and Peter Benie for spotting this.
 */
extern inline void cap_emulate_setxuid(int old_ruid, int old_euid, 
				       int old_suid)
{
	if ((old_ruid == 0 || old_euid == 0 || old_suid == 0) &&
	    (current->uid != 0 && current->euid != 0 && current->suid != 0) &&
	    !current->keep_capabilities) {
		cap_clear(current->cap_permitted);
		cap_clear(current->cap_effective);
	}
	if (old_euid == 0 && current->euid != 0) {
		cap_clear(current->cap_effective);
	}
	if (old_euid != 0 && current->euid == 0) {
		current->cap_effective = current->cap_permitted;
	}
}

代码中的cap_clear是个宏操作,定义如下:

#define cap_clear(c)         do { cap_t(c) =  0; } while(0)

结合代码作者所加的注释,读者应该可以读懂这段代码。举例来说,如果一个超级用户进程通过setuid将进程的有效用户号euid从0改变成某个普通用户的用户号(非0),则进程的有效授权cap_effective变成0,但是cap_permitted并未改变。以后,当这个进程恢复原先的euid时,就将进程的cap_permitted复制到cap_effective中。但是,setuid将进程的uid、euid记忆suid全部改变,所以进程的cap_permitted和cap_effective二者都被清0,以后就不能恢复了。不过这里还有个例外,那就是进程的task_struct结构中有个标志位keep_capabilities,可以通过系统调用prctl将这个标志位预先设成1,这样就可以避免将cap_permitted清0(注意第458行的if前面并没有else,所以仍会将cap_effective清0)。

读者可能会因为曾经使用shell命令su升格成超级用户而得出一个错觉,似乎普通用户的进程也可以通过系统调用setuid将自己的用户号设置成0而变成超级用户进程。其实,/bin/su是个属于超级用户的set uid可执行程序,普通用户的进程在执行这个程序时就有了超级用户的身份。在检查了口令以后,它就fork出一个新的shell进程。这个新shell进程的父进程具有超级用户的身份,所以它也成了超级用户进程。至于原来的shell进程从原先的shell进程手中接管了终端的键盘呵呵显示屏;而从用户界面看,则似乎原来的shell进程升格成了超级用户进程。

在常规的访问权限控制机制DAC的基础上,有些Unix变种版本(如AIX,Solaris等)作了一个重要的改进。叫做访问控制单(access control list),缩写为ACL。在实现了ACL的系统中,每个文件可以伴随存储一份访问控制单,里面有一些访问控制项(access control entry),可以为具体的用户规定对基本访问权限的修正。例如,可以这样规定;当用户A属于用户组g1时就剥夺它的读写权限;对于用户B则永远增加写访问权,而不论其是否为文件主或同组人。显然,对于商务应用这是很有意义的,可以改善文件系统的安全性。当前的linux版本正在朝实现ACL迈进,在一些数据结构中已经设置了用于ACL的结构成分(如ext2_inode结构中的i_file_acl和i_dir_acl),以及ext2_acl_entry的数据结构,但是其代码则尚未实现,所以在函数permission中并未访问目标节点的ACL。

我们在前面看到了当前进程改变用户号时授权的改变,可是这些授权最初时怎么来的呢?让我们看当前进程通过exec执行一个可执行文件时的情况,因为每一个进程初始的授权最终都可以追溯到这里,例如,当一个用户login进入系统时,系统就会fork出一个进程并让它执行/bin/bash(或csh等),而这个shell进车仓就成为该用户启动的所有进程的祖先。在后面的进程博客中,我们会看到do_execve中要先通过prepare_binprm设置一个linux_binprm结构,在这个数据结构中同样有cap_effective、cap_permitted个cap_inheritable三个字段,分别与task_struct结构中的三个字段相对应。函数prepare_binprm中与授权有关的处理如下:

sys_execve=>do_execve=>prepare_binprm

int prepare_binprm(struct linux_binprm *bprm)
{
......
	bprm->e_uid = current->euid;
	bprm->e_gid = current->egid;

	if(!IS_NOSUID(inode)) {
		/* Set-uid? */
		if (mode & S_ISUID)
			bprm->e_uid = inode->i_uid;

		/* Set-gid? */
		/*
		 * If setgid is set but no group execute bit then this
		 * is a candidate for mandatory locking, not a setgid
		 * executable.
		 */
		if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP))
			bprm->e_gid = inode->i_gid;
	}

	/* We don't have VFS support for capabilities yet */
	cap_clear(bprm->cap_inheritable);
	cap_clear(bprm->cap_permitted);
	cap_clear(bprm->cap_effective);

	/*  To support inheritance of root-permissions and suid-root
         *  executables under compatibility mode, we raise all three
         *  capability sets for the file.
         *
         *  If only the real uid is 0, we only raise the inheritable
         *  and permitted sets of the executable file.
         */

	if (!issecure(SECURE_NOROOT)) {
		if (bprm->e_uid == 0 || current->uid == 0) {
			cap_set_full(bprm->cap_inheritable);
			cap_set_full(bprm->cap_permitted);
		}
		if (bprm->e_uid == 0) 
			cap_set_full(bprm->cap_effective);
	}

	memset(bprm->buf,0,BINPRM_BUF_SIZE);
	return kernel_read(bprm->file,0,bprm->buf,BINPRM_BUF_SIZE);
}

我们先从631行看起,等一下还要回到它前面的几行。三个字段全部清0,这就是普通用户得到的授权。然后,如果进程在执行该可执行程序时有效用户号为0,则将linux_binprm结构中的cap_inheritable和cap_effective设成全1;如果进程的真实用户号为0但有效用户不为0,则仅将cap_inheritable设成全1.也就是说,超级用户进程最初时具有全部授权。但是在开始运行过程后超级用户进程可以通过系统调用capset来减少授权,改变以后进程的用户号仍旧为0,还是超级用户进程,但是授权却减小了。注意,capset只能减少而不能增加一个进程已有的授权,所以是单向的。

回到前面的第615行至628行,可以看到(可执行文件的)模式mode中的S_ISGID标志位怎样影响着linux_binprm结构中用户号e_uid和组号e_gid。

设置好linux_binprm结构,并且装入了可执行文件的映像以后,内核会通过一个函数compute_creds,根据linux_binprm结构中的内容来设置当前进程的task_struct结构找那个相应内容,这个函数的代码如下:

lload_aout_binary=>compute_creds或load_elf_binary=>compute_creds


/*
 * This function is used to produce the new IDs and capabilities
 * from the old ones and the file's capabilities.
 *
 * The formula used for evolving capabilities is:
 *
 *       pI' = pI
 * (***) pP' = (fP & X) | (fI & pI)
 *       pE' = pP' & fE          [NB. fE is 0 or ~0]
 *
 * I=Inheritable, P=Permitted, E=Effective // p=process, f=file
 * ' indicates post-exec(), and X is the global 'cap_bset'.
 *
 */

void compute_creds(struct linux_binprm *bprm) 
{
	kernel_cap_t new_permitted, working;
	int do_unlock = 0;

	new_permitted = cap_intersect(bprm->cap_permitted, cap_bset);
	working = cap_intersect(bprm->cap_inheritable,
				current->cap_inheritable);
	new_permitted = cap_combine(new_permitted, working);

	if (bprm->e_uid != current->uid || bprm->e_gid != current->gid ||
	    !cap_issubset(new_permitted, current->cap_permitted)) {
                current->dumpable = 0;
		
		lock_kernel();
		if (must_not_trace_exec(current)
		    || atomic_read(¤t->fs->count) > 1
		    || atomic_read(¤t->files->count) > 1
		    || atomic_read(¤t->sig->count) > 1) {
			if(!capable(CAP_SETUID)) {
				bprm->e_uid = current->uid;
				bprm->e_gid = current->gid;
			}
			if(!capable(CAP_SETPCAP)) {
				new_permitted = cap_intersect(new_permitted,
							current->cap_permitted);
			}
		}
		do_unlock = 1;
	}


	/* For init, we want to retain the capabilities set
         * in the init_task struct. Thus we skip the usual
         * capability rules */
	if (current->pid != 1) {
		current->cap_permitted = new_permitted;
		current->cap_effective =
			cap_intersect(new_permitted, bprm->cap_effective);
	}
	
        /* AUD: Audit candidate if current->cap_effective is set */

        current->suid = current->euid = current->fsuid = bprm->e_uid;
        current->sgid = current->egid = current->fsgid = bprm->e_gid;

	if(do_unlock)
		unlock_kernel();
	current->keep_capabilities = 0;
}

前面在prepare_binprm设置了linux_binprm结构中的这些授权以后,还要与进程当前已有的授权进行一些整合。这里的cap_intersect和cap_combine都是inline函数,还有个cap_issubset则为宏操作,均定义如下:

static inline kernel_cap_t cap_combine(kernel_cap_t a, kernel_cap_t b)
{
     kernel_cap_t dest;
     cap_t(dest) = cap_t(a) | cap_t(b);
     return dest;
}

static inline kernel_cap_t cap_intersect(kernel_cap_t a, kernel_cap_t b)
{
     kernel_cap_t dest;
     cap_t(dest) = cap_t(a) & cap_t(b);
     return dest;
}


#define cap_issubset(a,set)  (!(cap_t(a) & ~cap_t(set)))

也就是说,对于普通用户整合以后仍为0,而对于超级用户则整合以后为当前的cap_permitted与当前的cap_inheritable的逻辑和。如果这个逻辑和不是当前cap_permitted的一个子集,则意味着执行给定可执行程序时的授权将比进程现有的授权有所提高,所以把变量cap_raised设成1,。当然,如果已经通过capset将当前进程的cap_inheritable设置成0,则这种授权的回升就不可能发生了。授权的回升一般是允许的,但是在第674行所列的五种情况下则是有条件的允许。如果当前的cap_permitted中不包括CAP_SETPCAP就不允许了。这里IS_NOSUID是个宏定义,表示inode所在的文件系统在安装时在super_block结构中将MS_NOSUID标志位设成了1,使该文件系统中所有可执行文件的set_uid标志位都作废了。

1号进程,即init进程,是特殊的,它的授权是在宏定义INIT_TASK中固定了的,其cap_effective、cap_inheritable和cap_permitted分别设置为CAP_INIT_EFF_SET、CAP_INIT_INH_SET以及CAP_FULL_SET。其中CAP_FULL_SET为全部授权,而CAP_INIT_EFF_SET和CAP_INIT_INH_SET则为除CAP_SETPCAP以外的全部授权。系统中所有其他的进程都是init进程的后裔,所以只要init进程调用capset清除其cap_effective中的CAP_SETPCAP,则以后fork出来的所有进程就都没有了这种授权。又如,只有具有CAP_LINUX_IMMUTABLE授权的进程才能改变文件的S_IMMUTABLE和S_APPEND属性,如果init进程将/etc/inetd.conf的属性加上不可改变,把/var/log/message加上S_APPEND属性,然后清除其CAP_LINUX_IMMUTABLE授权,则以后fork出来的进程永远都不能改变这两个文件的这些属性了。显然,这是对于文件系统安全性的一大改进。

前面讲过,将可执行文件设置成set_uid模式,可以使执行它的进程在执行期间将其euid暂时改成该文件的文件主的uid,实践中通常是使普通用户在执行某个可执行文件的期间变成超级用户。这在Unix的早期是一项很巧妙的发明,到现在也还有很重要的意义。但是,近年来的实践发现,对Unix系统的黑客攻击事件大多数是与此有关的。这种攻击都与可执行程序本身的缺陷有关,其中最重要的就是所谓缓冲区溢出攻击。举例来说,可能会有这样一个应用程序:

main(int argc, char ** argv)
{
    char options[128];
    ......
    if (argc > 1) {
        strcpy(options, argv[1]);
    }

}

这段程序的开头将用户提供的命令行参数拷贝到一个大小为128字节的字符数组中,但是却没有检查字符串的长度,这是一个常见的错误。一般情况下,命令行参数不至于超过128字节,所以通常不会造成问题。可是,如果碰巧(或故意)命令行参数的字符串长度为150字节呢?这时候的strcpy就越界了。由于字符数组options是在堆栈中的局部变量,一越界以后就可能吧main的返回地址也冲掉了。这一班会导致从main返回时访问出错,但是既然所要做的事情已经完成了,一般也就无所谓了。可是,黑客们有可能在经过多次尝试以后在堆栈中原先为main的返回地址的位置上由目的地植入一个返回地址(通过strcpy从命令行参数中复制进去),使得从main返回时就返回到一个特定的地方去。这个特定的地方通常也在堆栈中,并且通过类似的手段植入一小段可执行代码,例如相当于编译后的system("/bin/bash")这么二十来个字符。这么一来,当从main返回就会fork出一个shell进程出来。对于一般的可执行文件,这么做的意义似乎并不大,因为既然你能启动这个可执行文件,就说明你本来就有个shell进程。可是,如果这个可执行文件是个set uid文件呢?这时候黑客们从一个普通用户的shell进程开始,却以得到一个超级用户的shell进程而告终,因为这个shell进程是从超级用户退出之前fork的。当然,黑客们事先未必知道这个set uid的可执行文件存在着这样的问题,他们可能是通过一条shell命令find / -perm 00400 -uid 0 -type f -print 列出系统中所有属于超级用户的set uid文件,然后逐一试凑而异,他们有的是时间!但是,一旦被它们发现这么一个可乘之机,那后果就严重了。得到了一个超级用的shell进程以后,他们立即就会修改/etc/passwd,将他们的用户号改成0,或者增加一个uid为0的用户。从此以后,他们就可以如入无人之境了。堵塞这种漏洞的途径是多方面的,其中之一是把进程的堆栈段的属性改成不可执行,因为黑客们很难把什么内容植入到进程的代码段中。此外,准备用作set uid可执行文件的应用程序要精心设计、精心实现和调试,并且系统中的set uid可执行程序的数量要尽可能的减少。

另一方面,可执行程序的set uid机制是否真的必要也是个问题。我们在开头以改变用户的口令为例来说明其必要性,但那时建立在本用户的进程要直接修改/etc/passwd这么个前提之下的。早期Unix的进程间通信进制比较薄弱,所以别无他法。可是,现在的进程间通信进制已经很强,有的事情可以按照c/s模式来设计和实现的。例如,现在完全可能在系统中建立一个内核线程passwd_d作为口令服务器,每当用户要改变口令时就通过socket与它建立其连接,然后由passwd_d负责来判定该用户是否可以改变其口令,如果可以就由它来修改/etc/passwd。这样,在用户进程与文件/etc/passwd之间就间隔了一层类似防火墙那样的东西,连对/etc/passwd的读访问权也就不必有了。

计算机系统的安全性是个综合性的问题,在相当程度上是个管理问题,而从技术角度来看则主要由两个方面的问题,即文件系统的安全性与网络操作的安全性。由于篇幅限制,我们在这里基本上不涉及有关的东西。即使是文件安全性的问题,也不是一下就能讲清楚、讲全面的。

有一种说法,说由于linux内核的源代码公开而使得其安全性降低了,因为黑客们可以从源代码中去寻找漏洞或可乘之机。这种说法理所当然地遭到持相反意见的人士驳斥。公开的源代码固然使攻击的一方容易找到可乘之机,但是同时也使防守的一方易于事先防范。即使出了问题,事后的分析和解决问题也比较容易,毕竟防守方打的是人民战争。如果把攻守双方的较量比喻成作一种振荡的话,则公开源代码使振幅的衰减或收敛加快,这应该是好事,对于防守方来说,攻防双方都在明处总比都在暗中要好。事实上,最令人不安的则是黑盒子,两眼一抹黑不知道里面在干些什么。更何况,还有官匪一家,开发系统的人自己在系统中留下后门的可能。

以前,人们还只是从理论上谈论这种可能性,可是前不久报有个Microsoft的工程师承认自己却曾在Windows中留下了后门。对于一个不公开源代码的系统,你怎么知道这样的后门到底有多少呢?

你可能感兴趣的:(linux内核,文件系统,linux)