本文主要讲述 FreeBSD 5.0 操作系统中新增的重要安全机制,即强制访问控制机制( MAC )的使用与源代码分析,主要包括强制访问控制框架及多级安全( MLS )策略两部分内容。这一部分讲述要将 MAC 框架与 MLS 策略用起来,应该做的一些工作,以及如何有效使用它们的问题。
强制访问控制(英文缩写 MAC )是实现操作系统安全的一个重要的方法,现在几乎所有的安全操作系统都采用强制访问控制作为其核心安全机制之一。强制访问控制是对操作系统的各种客体(如文件、 socket 、系统 FIFO 、 SCD 、 IPC 等)进行细粒度的访问控制,即当用户或用户程序访问系统的某个客体时,强制访问控制机制对这种访问的安全性进行检查。与自主访问控制不同,强制访问控制对用户及用户程序的行为进行限制,从而达到更高的安全级别。
强制访问控制是一种机制,它对用户与用户程序对客体的访问进行检查,但什么样的访问是安全的呢?这就需要引入安全策略的概念。安全策略可以认为是一组检查条件,它为每次访问的主体(用户或用户程序)和被访问的客体(如文件等)定义一个安全标记,再根据主体和客体的安全标记来决定这次访问是否安全。目前已经开发出多种安全策略,其中 MLS 用得最多。 MLS 是多级安全的意思,它最早用于军事领域。它的基本思想是定义一些安全级,如从低到高分普通、机密、绝密等安全级,要求高安全级别的信息不能泄露给低安全级别的用户,这样就要求低安全级的主体不能读高安全级的客体,同时高安全级的主体不能写低安全级的客体。详情请参见有关资料。
与 Linux 一样, FreeBSD 系统是开放源代码的操作系统,而且 FreeBSD 的结构清晰,安全性好,所以使用也很广泛。从 5.0RC2 版开始, FreeBSD 内核开始引入强制访问控制机制。它在内核中实现了一个灵活通用 MAC 框架,这个框架对 FreeBSD 内核中几乎所有的核心对象进行了访问控制,并且这个框架设计合理、接口简洁,使得我们可以很方便地开发各种安全策略模块并将之挂接到系统,从而按我们自己的策略对系统进行安全控制。另外,系统还提供了包括 MLS 策略模块在内的多个策略模块供我们选用。
本文从使用与源代码的分析这两方面详细讲述 FreeBSD 系统中的强制访问控制机制,内容主要包括 MAC 框架和 MLS 策略,相信会对对此部分内容感兴趣的朋友有所启发。
1 FreeBSD 5.0中强制访问控制机制的使用
FreeBSD 5.0 RC2 版本发布时,内核源代码中已经包含了 MAC 框架和一些 MAC 策略模块(如 MLS 策略模块、用于开发及实验的 MAC_NONE 策略模块、完整性模块 MAC_BIBA 等),但是这个版本的内核并没有正式对 MAC 提供支持,所以缺省情况下,在编译内核的时候没有把实现 MAC 框架的代码编译进去。另外,我们将以 MLS 策略为例,说明怎样利用 MAC 框架加载策略模块。为了让 MLS 策略真正实用,还需要配置系统的扩展文件属性等,我们将对之进行一定的介绍。最后我们将介绍如通过控制台命令以及在程序中如何使用系统调用接口对文件或进程的 MAC 标记进行操作。
1.1 重新编译内核
如上所述,由于 FreeBSD 5.0 RC2 版中的 MAC 框架还处于开发阶段,所以缺省情况下,内核二进制代码中并没有包括对 MAC 框架的支持。要使内核支持 MAC 框架,必须配置并重新编译内核,具体方法如下。
到 /usr/src/sys/i386/conf 下,把 GENERIC 文件复制成另一个文件如 yxd_kernel (不要改动 GENERIC 文件),然后在 yxd_kernel 文件中找到 makeoptions 这个部分,仿照已有的格式,添加一行(详细信息请参见 "FreeBSD Developers' Handbook" 中的 "TrustedBSD MAC FrameWork" 章节):
options MAC
这个选项将打开 MAC 编译开关,这样编译时将把相应的 MAC 框架代码编译进内核。
另外,为了支持文件及目录的 MAC 标记的存储,必须让内核支持扩展文件系统(缺省情况下也不支持)。要达到这个目的,必须在 yxd_kernel 中加入如下两个编译选项:
mac_mls_load = "YES"
值得注意的一点是,当我们修改并重新编译了 mac_mls.ko 这个模块后,必须把它从 /usr/src/sys/modules/mac_mls 目录拷贝到 /boot/kernel 目录,因为系统启动的时候是从 /boot/kernel 目录寻找模块的。
1.3 配置扩展文件系统
当我们做完 1.1 和 1.2 两步后, MLS 模块就已经在系统中起作用了,我们可以使用 "getfmac 文件 / 目录名 " 来得到文件或目录的 MAC 标记(形如 " 文件 / 目录名 : mls/low" ,详细的介绍请参阅 1.4 节),但此时的标记是一种伪标记,也就是说,这个标记是 MLS 策略给每个文件的缺省标记,且这个标记在关闭系统的时候并没有被存储。但是按照我们的需要,我们希望每个文件或目录的 MAC 标记应该有 " 非易逝性 " 的标记,也就是说我们希望这个标记与文件一样被存储在磁盘上。这就需要引入扩展文件系统,使得每个文件或目录的 MAC 标记存储在对应文件或目录的扩展属性中。为了便于理解,下面对 FreeBSD 中的扩展文件系统进行一点简要的介绍。
简单地说,扩展文件系统是对现有文件系统的一种扩充。扩展属性( EA , Extended Attribute )是相对于传统文件系统的 inode 节点中已存储的文件或目录的标准属性而言的,它可以为 inode 节点中存储的文件或目录增加额外的信息,这些信息再由系统根据 inode 结点访问到。每个文件系统的扩展属性( EA )是一组( name , value )对。一个 inode 节点(文件或目录)可能定义了某种属性,也可能没有定义。如果 inode 节点定义了某个属性,则它可能包含 0 个,也可能包含多个字节的这种属性的数据。这与一般 shell 的环境变量类似。这样,扩展属性机制就为 TrustedBSD 的各种安全特征( ACL 、 MAC 和能力等),提供了存储访问安全信息的简单方法。一些非安全应用也可以根据需要创建扩展属性,例如文件的校验和、密钥等。扩展属性可以动态地增加新的扩展属性而不必改变文件系统在磁盘上的存储格式。
如果使用的是 MLS 策略,那么返回的 MAC 标记为 mls/[single_mac]([mac_range]) ,其中 mls 为策略名,后面用 "/" 与标记数据隔开。 [single_mac] 为单一的 MAC 标记,可能为 "low" 或 "high" 或 "equal" 。 [mac_range] 为一个 MAC 标记范围,往往主体(进程)有一个范围。一个 MAC 标记的例子为: mls/low(low-high) 。
在应用程序中,可以使用的系统调用请参见 /usr/src/sys/sys/mac.h 文件中 #ifndef _KERNEL 这个块中提供的函数原型,值得注意的是, man page 中的有些接口参数及返回值类型有误,请以 mac.h 文件中的类型为准。
下面是几个最常用的函数的使用方法:
-
mac_get_proc , mac_get_file 的用法:
struct mac myMac;
char ss[60] = "mls";
myMac.m_string = ss;
myMac.m_buflen = 60;
mac_get_proc(&myMac);
mac_get_file("/tmp/test",&myMac);
-
mac_set_proc , mac_set_file 的用法:
struct mac myMac;
myMac.m_string = "mls/high";
myMac.m_buflen = strlen(myMac.m_string);
mac_set_proc(&myMac);
mac_set_file("/tmp/test",&myMac);
1.5 小结
由于 FreeBSD 5.0RC2 版才刚刚开始非正式地支持 MAC ,所以针对普通用户而言,要将其真正用起来,要做的工作还比较多比较烦琐的。对于 MLS 策略而言,还有很多值得完善的地方,比如现有的 MLS 策略不支持对用户设置 MAC 标记,主体对客体访问的时候限制过多,还有 MLS 不支持可信进程等等,都是下一个版本需要完善的。
在使用上,从我个人的使用经验来看,比较不好用。当然,这个问题不能全怪系统, MLS 策略导致易用性降低已经是不争的事实,相信经过一段时间的开发,最终会给我们一个满意的答案。
但是瑕不掩玉,撇开 MLS ,单看 MAC 框架,应该说是设计得很完美的:良好的结构、清晰的逻辑、非常简洁的接口,这些优点应该来说可以给每一个对它有兴趣的人一个大大的惊喜。即使是 MLS 策略,由于其作为一个 KLD 模块来实现,所以十分便于我们对它进行修改和扩充,相信经过一段时间, MLS 模块亦会成为我们的惊喜。如果大家有兴趣了解 MAC 框架及 MLS 策略的实现,请接着往下阅读。
本文主要讲述 FreeBSD 5.0 操作系统中新增的重要安全机制,即强制访问控制机制( MAC )的使用与源代码分析,主要包括强制访问控制框架及多级安全( MLS )策略两部分内容。这一部分较系统地对 MAC 框架及 MLS 策略的源代码进行分析。
2 MAC框架与MLS策略源代码分析
与本文相关的源代码文件主要有两个,即 /usr/src/sys/kern/kern_mac.c 和 /usr/src/sys/security/mac_mls/mac_mls.c 。另外还有一些头文件如 mac.h 、 mac_policy.h 等。
2.1 MAC框架整体结构
下面是 MAC 框架的示意性结构图,当用户控制台或用户程序通过系统调用对内核对象进行访问的时候,由于内核代码中相应的位置插入了 MAC 框架的检查函数,于是内核就会调用 MAC 框架的相应检查函数来做安全性检查。 MAC 框架会依次调用每个挂接在 MAC 框架上的安全策略,以决定访问是否安全。另外,其它可能涉及到安全问题的系统事件,如初始化各种安全标记、初始化各种内核对象等,也会通知 MAC 框架,由它做出相应的处理。
从图中我们也可以看到,安全策略作为一个独立的 KLD 模块,可以独立于内核进行编译,再在使用的时候挂接到 MAC 框架上。要判断一次访问是否安全, MAC 框架会调用所有的安全策略,只有当所有的安全策略均表示同意, MAC 框架才会授权这次访问。
2.2 安全标记
安全标记是由 MAC 框架和各个安全策略定义的一组数据,用于描述主体或客体的安全信息,安全标记与内核描述主客体的其它数据一起存储在内核中。要实现强制访问控制,首先必须为主客体定义安全标记。不同的策略由于判断的依据不一样,可能定义的标记也不相同。作为 MAC 框架,当安全策略向它注册的时候,它必须把该策略使用的安全标记附加到各个内核对象上去,这样当需要调用该策略做安全性检查的时候,才能为策略提供它们自己定义和理解的安全标记。我们先给出 MAC 框架与 MLS 策略定义的安全标记,再对之作进一步的解释。
MAC 框架中安全标记的定义是这样的:
struct label {
int l_flags;
union {
void *l_ptr;
long l_long;
} l_perpolicy[MAC_MAX_POLICIES];
};
其中 l_flags 是一个标志,被 MAC 框架用来判断是否初始化了整个标记数据结构。 l_perpolicy 数组为每个策略定义了一个联合,这样当策略向 MAC 框架注册时,它们既可以用一个 long 类的整数作为它们自己的安全标记,也可以使用联合中的指针指向一个它们自己定义的标记数据结构。 MLS 策略选择了后者。
MLS 策略定义的安全标记是这们的:
struct mac_mls_element {
u_short mme_type;
u_short mme_level;
u_char mme_compartments[MAC_MLS_MAX_COMPARTMENTS >> 3];
};
struct mac_mls {
int mm_flags;
struct mac_mls_element mm_single;
struct mac_mls_element mm_rangelow, mm_rangehigh;
};
在 mac_mls 结构中,定义了一个单一标记( mm_single )和一个标记范围( mm_rangelow , mm_rangehigh ),主客体既可以使用单一标记来标识单一安全级,也可以使用标记范围来标识一个安全级范围,还可以同时使用二者。如第 1 章中我们使用 getfmac 得到的输出 "mls/high" 表明该文件使用的是值为 "high" 的单一安全标记。再比如我们使用 getpmac 得到的输出 "mls/low(low-high)" 表明进程同时使用了单一标记和标记范围。至于究竟使用的是哪种标记,由 mm_flags 标识。
mac_mls_element 定义一个标记,它定义的标记功能很强大,既支持安全类型( mme_type 变量,值为 LOW 、 HIGH 、 EQUAL 和 UNDEFINE ),也支持多达 256 个级别的安全级(当 mme_type 的值为 LEVEL 时, mme_level 变量有效,由它定义安全级),同时还使用 mme_compartments 数组支持域( field ),后续章节将具体讲述 MLS 策略是怎样使用它所定义的标记的。
当用户试图打开一个文件时,内核就会试图打开一个 vnode ,在打开之前会调用这个函数,用于检查主体是否有权限打开这个 vnode 。 MAC_CHECK 宏定义如下:
#define MAC_CHECK(check, args...) do {
struct mac_policy_conf *mpc;
error = 0;
MAC_POLICY_LIST_BUSY();
LIST_FOREACH(mpc, &mac_policy_list, mpc_list) {
if (mpc->mpc_ops->mpo_ ## check != NULL)
error = error_select(
mpc->mpc_ops->mpo_ ## check (args),
error);
}
MAC_POLICY_LIST_UNBUSY();
} while (0)
我们可以看到,代码中使用 LIST_FOREACH 宏来遍历安全策略链表中的每个策略,然后调用其在 mpc_ops 中注册的检查函数 mpo_##check 。 mac_policy_ops 这个结构中定义了很多事件处理函数,进行细粒度的访问控制,限于篇幅,在此不一一列出。这些函数共分四类:第一类是策略本身的初始化和销毁函数;第二类是对安全标记的操作函数,包括各种内核对象的安全标记的初始化与销毁函数,以及获取与设置安全标记的接口函数;第三类是对文件系统、网络系统及进程对象的标记进行操作的事件处理函数;第四类是检查对各个内核对象的访问是否安全的检查函数。详见 mac_policy.h 。
2.4 安全策略的注册
分析 MLS 策略,我们可以发现,安全策略向 MAC 框架注册是一件很简单的事情。 mac_mls.c 文件中花了大量篇幅定义了所有的事件处理函数,如前所述,这些函数是安全策略的核心,用于判断访问的安全性等。定义完这些函数后,在源代码的最后,把这些函数的地址填入 mac_policy_ops 结构中,再用一个 MAC_POLICY_SET 宏把 mac_policy_ops 结构挂接到上面提到的 mac_policy_list 链表中去就可以了,如下:
MAC_POLICY_SET(&mac_mls_ops, trustedbsd_mac_mls, "TrustedBSD MAC/MLS", MPC_LOADTIME_FLAG_NOTLATE, &mac_mls_slot);
mac_policy_modevent 函数的核心代码如下:
case MOD_LOAD:
if (mpc->mpc_loadtime_flags & MPC_LOADTIME_FLAG_NOTLATE &&
mac_late) {
printf("mac_policy_modevent: can't load %s policy "
"after booting", mpc->mpc_name);
error = EBUSY;
break;
}
error = mac_policy_register(mpc);
break;
case MOD_UNLOAD:
/* Don't unregister the module if it was never registered. */
if ((mpc->mpc_runtime_flags & MPC_RUNTIME_FLAG_REGISTERED) != 0)
error = mac_policy_unregister(mpc);
else
error = 0;
break;
当模块被 LOAD 的时候,会调用 mac_policy_register 函数,它首先检查该模块是否已注册,如果没有,就会执行下面的代码: LIST_INSERT_HEAD(&mac_policy_list, mpc, mpc_list);
其中 mpc 为我们上面提到的 mac_policy_conf 结构的地址,这样到此为止,就把 MLS 策略添加到 MAC 框架的策略列表中去了。
2.5 MLS策略源代码分析
根据 MLS 策略,高安全级别的主体不能写低安全级别的客体,而低安全级别的主体则不能读高安全级别的客体,只有主客体的安全级别相同,才能既读又写。 MLS 定义了三种标记比较函数:
mac_mls_dominate_single(a,b) :用于检查 a 的单一标记的安全级是否高于 b 的单一标记的安全级。这个函数主要用于对内核变量的读、写、查询等事件进行安全性检查。
我们只解释一下最后一个 case ,即当 a 、 b 的类型均为 MAC_MLS_TYPE_LEVEL 时,也就是它们都使用一个数( 0 到 254 )标识它们的安全级时,判断是如何进行的。首先:
#define MAC_MLS_BIT_TEST(b, w) ((w)[(((b) - 1) >> 3)] & (1 << (((b) - 1) & 7)))
我们可以看到,在比较二者安全级之前,对它们的 mme_compartments 进行了位测试。 mme_compartments 是一个 256bit 的数据(由 uchar[32] 数组定义),每个 bit 都可以设置为 1 来表示该对象属于某一个安全域,当然也可以设置多个 bit 表示这个对象属于多个安全域。 MLS 要求如果 a 可以访问 b 的话, b 所属的安全域必须要是 a 的子集,否则访问被拒绝。对 mme_compartments 进行测试的目的也即是要判断 b 中设置了 1 的那些 bit 在 a 中有否设置。
2.6 对MAC框架及MLS策略的扩充
最后我们对 MAC 框架作一点改进。前面提过, MAC 框架目前并不支持对用户的安全级进行设置,也没有提供存储用户安全级信息的地方,我们想对之做改进,以使其更实用。我们改进的思想是,拦截系统的 setuid 系统调用,在系统作 setuid 的时候(用户登录、运行 su 命令等),读入用户安全级别信息并作相应设置。我们做了如下工作:
-
在 mac_policy.h 文件中的 mac_policy_ops 结构中增加一个新函数 mpo_get_cred_from_file() ,其参数和返回值与 mpo_create_cred() 完全一样。
-
在 mac_mls.c 文件中增加一个新函数 mac_mls_get_cred_from_file() ,其参数和返回值与 mac_mls_create_cred() 完全一样。在这个函数里,增加在内核中读写用户安全级配置文件的语句,从而把用户的安全级在这里读到标记中。然后在对 mac_policy_ops 结构的设置语句中,增加一条语句: .mpo_get_cred_from_file = mac_mls_get_cred_from_file 。
-
在 kern_prot.c 文件中的 setuid() 函数里 crcopy() 函数替换为我们自己的 mac_crcopy() 函数,并在调用这个函数之前加一条语句: newcred->cr_uid = uid; 。
mac_crcopy() 函数的代码如下:
void mac_crcopy(struct ucred *dest,struct ucred *src)
{
uid_t uid = dest->cr_uid;
KASSERT(crshared(dest) == 0,("crcopy of shared ucred"));
bcopy(&src->cr_startcopy,&dest->cr_startcopy,
(unsigned)((caddr_t)&src->cr_endcopy -
(caddr_t)&src->cr_startcopy));
uihold(dest->cr_uidinfo);
uihold(dest->cr_ruidinfo);
if(jailed(dest))
prison_hold(dest->cr_prison);
#ifdef MAC
dest->cr_uid = uid;
mac_get_cred(src,dest);
dest->cr_uid = src->cr_uid;
#endif
}
修改相应的头文件,并重新编译内核。
2.7 小结
上面分析了 MAC 框架与 MLS 策略的主要内容,限于篇幅,我们只给出主要逻辑,如果您想要了解更为详细的技术细节,请阅读其源代码或联系我。另外, MAC 框架和 MLS 策略还用到了一些很重要的技术,如系统控制( sysctl )机制、锁等,我们也没有提到,如有兴趣的读者可以继续钻研。