本文为CoryXie原创译文,转载及有任何问题请联系cory.xie#gmail.com。
本文分析FreeBSD 10.0【 http://xrefs.info/freebsd-10.0/ 】的MAC Framework的整体流程。
在【/usr/src/sys/security/mac/mac_framework.c】中有如下一段注释,描述了MAC Framework实现的三大功能:
也就是说,1)可以按照不同的安全保护策略,实现不同的策略模块,并通过在<security/mac/mac_policy.h> 中定义的接口向MAC Framework注册;2)各内核子系统可以通过<security/mac/mac_framework.h>中定义的与各子系统相关的接口来请求MAC Framework进行安全判定;3)MAC Framework在接收到安全判定请求时,会循序调用已经注册的各个策略模块的判定函数,实现安全判定;4)提供了用户空间接口用于设置受控资源对象的安全属性(label state)。
整体而言,FreeBSD的MAC相关代码位于【/usr/src/sys/security/】目录下面。其中mac目录是框架本身,audit目录是负责安全审计的模块(Common Criteria要求),其他目录是实现不同的MAC策略。
其中,【/usr/src/sys/security/mac/】目录如下:
可见:
通过查看【/usr/src/sys/security/mac/mac_framework.c】模块的SYSINIT()找到模块的入口如下:
因此,mac_init()会被首先调用。
本模块定义了一些模块全局变量,在这里得到了初始化。
也就是说,MAC策略模块分为动态策略模块(链入mac_policy_list且在遍历时需上锁)和静态策略模块(链入mac_static_policy_list且在遍历时无需上锁)。
MAC策略是通过mac_policy_register()向MAC Framework注册的。
该mac_policy_register()函数的前面部分:
首先是加锁mac_policy_xlock();接着判断该策略是该链入静态链表还是链入动态链表(判断原则是基于动态策略模块可以卸载,即会设置MPC_LOADTIME_FLAG_UNLOADOK,而静态策略不能动态卸载);然后再判断该策略是否已经在相应链表上(以免重复注册同样的策略)。
函数mac_policy_register()的第二部分如下:
全局变量mac_slot_offsets_free类型为int,限定了最多只支持32个不同策略,下面的注释解释了原因。因为MAC Framework维护了一个bitmask,每个注册的MAC策略在该bitmask上都对应一个位,并且卸载时该位不会被回收,从而能保证这些槽位不会被重用。这一点是跟Label相关的设计。
因此,mac_policy_register()的该段代码会根据当前mac_slot_offsets_free变量已经设置的bitmask,来找到新注册的策略模块对应的槽位(slot)。接着就设置MPC_RUNTIME_FLAG_REGISTERED表示该策略已经注册,并将其链入对应的策略链表。每个策略都可能会有策略特定的初始化操作(用于初始化策略特定的数据结构和功能),因此在将策略链入链表之后,就会调用该策略的初始化函数(mpc->mpc_ops->mpo_init(mpc)),这里的策略回调函数都接受参数mpc,是典型的用C语言实现面向对象的操作模型。
在调用mac_policy_xunlock()解锁并返回之前,对mac_policy_update()的调用很关键。
函数mac_policy_update()的代码如下:
看起来很简单,就是清空并重新计算mac_policy_count全局变量(实际上这个变量无需用这种办法更新,直接在注册或者卸载函数中递增或者递减就行)以及mac_labeled全局变量(这个倒是有必要这样更新,见后面的分析)。对动态策略列表和静态策略列表上的每一个策略,都调用mac_policy_getlabeled()函数,将该函数的返回值"按位与"到mac_labeled全局变量。这个mac_labeled全局变量比较重要,下面的注释说明了这一点。每个MAC策略都"声明"了一个bitmask,代表该策略要求哪些"对象类型(object types)"需要分配"标签(label)"。注意这里的"声明"并不是通过一个明确的结构体字段实现的,而是通过检查该MAC策略实现了哪些"标签初始化(label init)"回调函数而得出的(参见mac_policy_getlabeled())。而这个mac_labeled全局变量实际上是对所有策略"声明"的bitmask的一个总和。
下面是mac_policy_getlabeled()函数的代码:
也就是说,每个MAC策略可以支持("感兴趣")多个"对象类型(object types)",并且对所支持的对象类型都要实现一个与该对象类型相关的"标签初始化(label init)"回调函数。每个对象类型对应一个bit位(例如MPC_OBJECT_VNODE),而mac_policy_getlabeled()函数返回uint64_t,因此系统最多支持64种对象类型。目前系统定义了下列支持的对象类型:
当注册了所有的策略之后,就有了mac_labeled全局变量,从而能判断系统中对于每一种对象类型,是否有对其"感兴趣"的MAC策略。进而,每种对象类型在初始化该类型的一个对象(object)的时候,也就有可以调用该类型特定的初始化函数,该函数根据mac_labeled全局变量来判断是否需要对该对象分配一个"标签(label)"。例如,对于文件系统的vnode对象类型(对应MPC_OBJECT_VNODE),就有一个mac_vnode_init()函数可以完成对vnode对象的标签进行分配。该mac_vnode_init()函数如下:
而对这个函数的调用在VFS的getnewvnode()函数中:
如前所述,如果有任意的MAC策略对某个"对象类型(object type)"感兴趣,那么该对象类型的"对象(object)"就会有一个"标签(label)"与该对象"相关(associated)"。由于可能有多个MAC策略对一种"对象类型(object type)"感兴趣,而一个实际的"对象(object)"又只有一个与之相关的"标签(label)",这就决定了这个"标签(label)"是"通用的(generic)",即不是一个标签对应一个MAC策略,而是一个标签对应多个MAC策略。因此,这个"标签(label)"的定义如下:
从这个结构体的定义也可以看出,一个"标签(label)"是不特定于某个MAC策略的,也不特定于某种"对象类型(object type)"。然而,在分配一个对象的标签时,需要按照该对象的类型,将该标签初始化成特定于该对象类型的标签。这种初始化的过程特定于这种对象类型,因此只有这种对象类型的对象管理代码才知道如何管理这个struct label结构。由于struct lavel的l_perpolicy[]数组有MAC_MAX_SLOTS个字段,因此可以推断,对某一类型对象的对象标签管理代码会针对与对该对象类型"感兴趣"的每一种MAC策略分配一个数据结构,而这个数据结构特定于该对象类型和该MAC策略(从而建立一个对象与MAC策略的一一对应关系)。我们以前面看到的mac_vnode_init()函数中如何分配标签为例来验证这一猜测,该函数实际调用的是mac_mount_label_alloc()来分配标签:
这里的mac_labelzone_alloc()仅仅是从一个专用于label的UMA zone中分配一块内存(struct label)。
实际的对这个label的初始化在代码MAC_POLICY_PERFORM(vnode_init_label, label)中完成。MAC_POLICY_PERFORM()的定义如下:
因此,MAC_POLICY_PERFORM(vnode_init_label, label)实际上就是对所有已经注册的MAC策略,分别调用该策略的mpo_vnode_init_label(label)回调函数。对此,我们搜索可以得知,对于vnode这一对象类型,有如下MAC策略实现了这一回调函数,因此他们也就是对vnode"感兴趣"的MAC策略。
我们以mac_biba.c中实现的biba_init_label()函数来看看具体的某个MAC策略是如何初始化一个label的(实际上应该是初始化该label对应于该MAC策略的部分)。
而SLOT_SET()定义如下:
显然,这里的SLOT_SET()是要将biba_alloc()返回的指针设置到label中l_perpolicy[]数组对应于biba_slot的位置。
而这个biba_slot变量来源于下面的MAC_POLICY_SET():
还记得前面在分析mac_policy_register()时有段代码是得到这个注册的MAC策略对应的槽位slot号么?
这里将得到的slot号设置到了mpc->mpc_field_off这个地址处,因此,对应mac_biba策略而言,实际上就是更新了biba_slot这个全局变量的值。那么mac_biba策略又是啥时候调用到mac_policy_register()的呢?这就回到了FreeBSD是如何初始化模块这个问题了。下面是
再次查看到MAC_POLICY_SET()中有DECLARE_MODULE(mpname, mpname##_mod, SI_SUB_MAC_POLICY, I_ORDER_MIDDLE)就知道,这里实际上是static moduledata_t mpname##_mod中指定了mac_policy_modevent()作为模块加载或者卸载的回调函数,因此实际上在FreeBSD的初始化早期的模块初始化中,会按照SI_SUB_MAC_POLICY子系统的I_ORDER_MIDDLE顺序来初始化每个模块,从而使得mac_policy_register()被调用到。
现在回到biba_alloc():
如前面我们所猜测的那样,这是要分配一个特定于biba这个MAC策略的数据结构,即struct mac_biba,并且所分配的这个结构体的指针将会被存放在struct label的l_perpolicy[]数组对应于biba_slot的位置。有了这个数据结构之间的绑定关系,当要执行针对某个对象的安全策略检查的时候,就可以通过这个对象的l_perpolicy[]数组,找到该策略对该对象的struct mac_biba。下面我们来看看是不是这样的。
我们仍然以vnode对象类型为例来分析MAC策略是如何进行安全检查的。如下面的截图所示,在【/usr/src/sys/security/mac/mac_vfs.c】中实现了一系列针对vnode的检查入口函数,这些函数是针对vnode的不同访问方式的检查入口。
例如,mac_vnode_check_open()函数的调用位置如下:
其中,【/usr/src/sys/kern/vfs_vnops.c】中的调用如下:
那么回到mac_vnode_check_open(),我们看看具体是如何检查的:
实际上,所有这些安全检查入口,虽然参数个数各有不同,但都有一个共通的规则,就是都有一个struct ucred 结构体的指针参数(例如这里的cred),另外还有一个当前访问的"对象(object)"的指针(例如这里的vp),其他的就是与之相关的辅助参数(例如这里的accmode)。
我们看到,这里实际上是调用的MAC_POLICY_CHECK()这个宏:
这个MAC_POLICY_CHECK()宏的第一个参数是要执行的检查的标示(例如这里的vnode_check_open),这个标示在宏内与mpc_ops的相关操作组合拼出来具体要执行的回调函数。例如,这里就拼出来mpc->mpo_ops->mpo_vnode_check_open_check()。因此,MAC_POLICY_CHECK()的实际就是要执行所有策略的针对某一对象类型的检查。针对mac_biba策略,这个函数就是biba_vnode_check_open():
我们目前暂时不考虑具体的某个策略是如何判断一个访问是否被认为是安全的(即暂时不考具体的策略原则),而只考虑是如何从具体的访问中找到判断的依据(即以什么资源来判断是否安全)。分析上面的这个例子,我们可以看出:
如果判断出这个访问是不安全的,那么这个判断函数返回EACCESS(或者其他错误),否则返回0。那么,对应一个对象可能有多个MAC策略要检查,那如果任何一个策略的检查有失败,这个返回值都会被作为mac_error_select()的第一个参数,而前一个检查的结果作为mac_error_select()的第二个参数,进行最高优先级错误的判定。最后所有的判定完成后的返回值作为这一次访问安全性的总结果返回给调用者,由调用者来最终判定是否允许这一访问。