SELinux源码分析(Federa Core 8)
第一章 SELinux(Security Enhance Linux,简称SELinux)简介
1.1 SELinux的起源
SELinux是一个面向政府和行业的产品,由NSA、Network Associates、Tresys以及其他组织设计和开发。尽管NSA将其作为一个补丁集引入,但从2.6版开始,它就被加入到Linux内核中。
GUN/Linux非常安全,但它也非常动态:所做的更改会为操作系统带来新的漏洞,这些漏洞可能被攻击者利用,尽管人们都非常关心阻止授权访问,但是发生入侵后会发生什么呢?
1.2访问控制
大多数操作系统使用访问控制来判断一个实体(用户或程序)是否能够访问给定资源。基于 UNIX的系统使用一种自主访问控制(Discretionary Access Control,简称DAC)的形式。此方法通常根据对象所属的分组来限制对对象的访问。例如,GNU/Linux 中的文件有一个所有者、一个分组和一个权限集。权限定义谁可以访问给定文件、谁可以读取它、谁可以向其写入,以及谁可以执行它。这些权限被划分到三个用户集中,分别表示用户(文件所有者)、分组(一个用户组的所有成员)和其他(既不是文件所有者,又不是该分组的成员的所有用户)。
很多这样的访问控制都会带来一个问题,因为所利用的程序能够继承用户的访问控制。这样,该程序就可以在用户的访问层进行操作。与通过这种方式定义约束相比,使用最小特权原则更安全,程序只能执行完成任务所需的操作。例如,如果一个程序用于响应 socket 请求,但不需要访问文件系统,那么该程序应该能够监听给定的socket,但是不能访问文件系统。通过这种方式,如果该程序被攻击者利用,其访问权限显然是最小的。这种控制类型称为强制访问控制(MAC)。
另一种控制访问的方法是基于角色的访问控制(RBAC)。在 RBAC 中,权限是根据安全系统所授予的角色来提供的。角色的概念与传统的分组概念不同,因为一个分组代表一个或多个用户。一个角色可以代表多个用户,但它也代表一个用户集可以执行的权限。
SELinux 将 MAC 和 RBAC 都添加到了 GNU/Linux 操作系统中。下一节将探讨 SELinux 实现,以及如何将安全增强透明地添加到 Linux 内核中。
1.3 Linux安全模块(Linux Security Module,简称LSM)
Linux安全模块(LSM)提供了两类对安全钩子函数的调用:一类管理内核对象的安全域,另一类仲裁对这些内核对象的访问。对安全钩子函数的调用通过钩子来实现,钩子是全局表security_ops中的函数指针,这个全局表的类型是security_operations结构,这个结
构定义在include/linux/security.h这个头文件中,这个结构中包含了按照内核对象或内核子系统分组的钩子组成的子结构,以及一些用于系统操作的顶层钩子。在内核源代码中很容易找到对钩子函数的调用:其前缀是security_ops->。对钩子函数(Hooks)的详细说明留到后面。
在内核引导的过程中,Linux安全模块(LSM)框架被初始化为一系列的虚拟钩子函数,以实现传统的UNIX超级用户机制。当加载一个安全模块时,必须使用register_security()(在linux/security.h中声明,security.c中定义)函数向Linux安全模块(LSM)框架注册这个安全模块:这个函数将设置全局表security_ops,使其指向这个安全模块的钩子函数指针,从而使内核向这个安全模块询问访问控制决策。一旦一个安全模块被加载,就成为系统的安全策略决策中心,而不会被后面的register_security()函数覆盖,直到这个安全模块被使用unregister_security()函数向框架注销:这简单的将钩子函数替换为缺省值,即Dummy.c中的Dummy函数,系统回到UNIX超级用户机制。另外,Linux安全模块(LSM)框架还提供了函数mod_reg_security()和函数mod_unreg_security(),使其后的安全模块可以向已经第一个注册的主模块注册和注销,但其策略实现由主模块决定:是提供某种策略来实现模块堆栈从而支持模块功能合成,还是简单的返回错误值以忽略其后的安全模块。(需要主模块进行调用)这些函数都提供在内核源代码文件security/security.c中。这种方式的设计主要为了实现模块的叠加,实现多种模块对系统的安全进行检查。目前该功能主要是实现,capability和SELinux的叠加。
Linux内核现在对POSIX.1e capabilities的一个子集(提供一部分钩子函数)提供支持。Linux安全模块(LSM)设计的一个需求就是把这个功能移植为一个可选的安全模块。POSIX.1e capabilities提供了划分传统超级用户特权并赋给特定的进程的功能,即传统超级用户不允许的权限,可以通过POSIX.1e capabilities授予,大大提高了系统安全的灵活性。Linux安全模块(LSM)保留了用来在内核中执行capability检查的现存的capable()接口,但把capable()函数简化为一个Linux安全模块(LSM)钩子函数的包装(secondary_ops),从而允许在安全模块中实现任何需要的逻辑。Linux安全模块(LSM)还保留了task_struck结构中的进程capability集(一个简单的位向量),而并没有把它移到安全域中去。Linux内核对capabilities的支持还包括两个系统调用:capset()和capget()。Linux安全模块(LSM)同样保留了这些系统调用但将其替换为对钩子函数的调用,使其基本上可以通过security()系统调用来重新实现。Linux安全模块(LSM)已经开发并且移植了相当部分的capabilities逻辑到一个capabilities安全模块中,但内核中仍然保留了很多原有capabilities的残余。这些实现方法都最大程度的减少了对Linux内核的修改影响,并且最大程度保留了对原有使用capabilities的应用程序的支持,同时满足了设计的功能需求。以后要使capabilities模块完全独立,剩下要做的主要步骤是:把位向量移到task_struct结构中合适的安全域中,以及重新定位系统调用接口。目前的操作系统中还没有在这方面做改进,为了减少LSM架构对原内核的影响,这部分暂且不懂。
1.4接口说明:给内核开发人员和安全研究人员使用的钩子
Linux安全模块(LSM)对于内核开发人员和安全研究人员的价值就在于:可以使用其提供的接口将现存的安全增强系统移植到这一框架上,从而能够以可加载内核模块的形式提供给用户使用;或者甚至可以直接编写适合自己需要的安全模块。Linux安全模块(LSM)提供的接口就是钩子,其初始化时所指向的虚拟函数实现了缺省的传统UNIX超级用户机制,模块编写者必须重新实现这些钩子函数来满足自己的安全策略。下面简要介绍Linux安全模块(LSM)提供的钩子,详细情况请参考源代码,特别是include/linux/security.h头文
件中security_operations结构的定义。至于具体如何根据自己需要的安全策略编写安全模块,可以参考SELinux,DTE,LIDS等系统的安全模块实现。
首先是任务钩子,Linux安全模块(LSM)提供了一系列的任务钩子使得安全模块可以管理进程的安全信息并且控制进程的操作。模块可以使用task_struct结构中的安全域来维护进程安全信息;任务钩子提供了控制进程间通信的钩子,例如kill();还提供了控制对当前进程进行特权操作的钩子,例如setuid();还提供了对资源管理操作进行细粒度控制的钩子,例如setrlimit()和nice()。
Hook int (*task_create) (unsigned long clone_flags); int (*task_alloc_security) (struct task_struct * p); void (*task_free_security) (struct task_struct * p); int (*task_setuid) (uid_t id0, uid_t id1, uid_t id2, int flags);//特权操作的Hook int (*task_post_setuid) (uid_t old_ruid /* or fsuid */ , uid_t old_euid, uid_t old_suid, int flags); int (*task_setgid) (gid_t id0, gid_t id1, gid_t id2, int flags); int (*task_setpgid) (struct task_struct * p, pid_t pgid); int (*task_getpgid) (struct task_struct * p); int (*task_getsid) (struct task_struct * p); void (*task_getsecid) (struct task_struct * p, u32 * secid); int (*task_setgroups) (struct group_info *group_info); int (*task_setnice) (struct task_struct * p, int nice);//资源管理的Hook int (*task_setioprio) (struct task_struct * p, int ioprio); int (*task_getioprio) (struct task_struct * p); int (*task_setrlimit) (unsigned int resource, struct rlimit * new_rlim);//资源管理的int (*task_setscheduler) (struct task_struct * p, int policy, struct sched_param * lp); int (*task_getscheduler) (struct task_struct * p); int (*task_movememory) (struct task_struct * p); int (*task_kill) (struct task_struct * p, struct siginfo * info, int sig, u32 secid);//控制进程间通信的Hook int (*task_wait) (struct task_struct * p); int (*task_prctl) (int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5); void (*task_reparent_to_init) (struct task_struct * p); void (*task_to_inode)(struct task_struct *p, struct inode *inode); 图1.1 任务钩子
其次是程序装载钩子。很多安全模块,包括Linux capabilities,SELinux,DTE都需要有在一个新程序执行时改变特权的能力。因此Linux安全模块(LSM)提供了一系列程序装载钩子,用在一个execve()操作执行过程的关键点上。linux_binprm结构中的安全域允许安全模块维护程序装载过程中的安全信息;提供了钩子用于允许安全模块在装载程序前初始化安全信息和执行访问控制;还提供了钩子允许模块在新程序成功装载后更新任务的安全信
息;还提供了钩子用来控制程序执行过程中的状态继承,例如确认打开的文件描述符。(不是可信启动)
int (*bprm_alloc_security) (struct linux_binprm * bprm); void (*bprm_free_security) (struct linux_binprm * bprm); void (*bprm_apply_creds) (struct linux_binprm * bprm, int unsafe); void (*bprm_post_apply_creds) (struct linux_binprm * bprm); int (*bprm_set_security) (struct linux_binprm * bprm); int (*bprm_check_security) (struct linux_binprm * bprm); int (*bprm_secureexec) (struct linux_binprm * bprm); 图1.2 程序装载钩子
再次是进程间通信IPC钩子。安全模块可以使用进程间通信IPC钩子来对System V IPC的安全信息进行管理,以及执行访问控制。IPC对象数据结构共享一个子结构kern_ipc_perm,并且这个子结构中只有一个指针传给现存的ipcperms()函数进行权限检查,因此Linux安全模块(LSM)在这个共享子结构中加入了一个安全域。为了支持单个消息的安全信息,Linux安全模块(LSM)还在msg_msg结构中加入了一个安全域。Linux安全模块(LSM)在现存的ipcperms()函数中插入了一个钩子,使得安全模块可以对每个现存的Linux IPC权限执行检查。由于对于某些安全模块,这样的检查是不足够的,Linux安全模块(LSM)也在单个的IPC操作中插入了钩子。另外还有钩子支持对通过System V消息队列发送的单个消息进行细粒度的访问控制。
int (*ipc_permission) (struct kern_ipc_perm * ipcp, short flag); int (*msg_msg_alloc_security) (struct msg_msg * msg); void (*msg_msg_free_security) (struct msg_msg * msg); int (*msg_queue_alloc_security) (struct msg_queue * msq); void (*msg_queue_free_security) (struct msg_queue * msq); int (*msg_queue_associate) (struct msg_queue * msq, int msqflg); int (*msg_queue_msgctl) (struct msg_queue * msq, int cmd); int (*msg_queue_msgsnd) (struct msg_queue * msq, struct msg_msg * msg, int msqflg); int (*msg_queue_msgrcv) (struct msg_queue * msq, struct msg_msg * msg, struct task_struct * target, long type, int mode); 图1.3 进程通信IPC钩子
下面是文件系统钩子。对于文件操作,定义了三种钩子:文件系统钩子,inode结点钩子,以及文件钩子。Linux安全模块(LSM)在对应的三个内核数据结构中加入了安全域,分别是:super_block结构,inode结构,file结构。超级块文件系统钩子使得安全模块能够控制对整个文件系统进行的操作,例如挂载,卸载,还有statfs()。Linux安全模块(LSM)在permission()函数中插入了钩子,从而保留了这个函数,但是也提供了很多其他inode结
点钩子来对单个inode结点操作进行细粒度访问控制。文件钩子中的一些允许安全模块对read()和write()这样的文件操作进行额外的检查;还有文件钩子允许安全模块控制通过socket IPC接收打开文件描述符;其他的文件钩子对像fcntl()和ioctl()这样的操作提供细粒度访问控制。
int (*sb_alloc_security) (struct super_block * sb); void (*sb_free_security) (struct super_block * sb); int (*sb_copy_data)(struct file_system_type *type, void *orig, void *copy); int (*sb_kern_mount) (struct super_block *sb, void *data); int (*sb_statfs) (struct dentry *dentry); int (*sb_mount) (char *dev_name, struct nameidata * nd, char *type, unsigned long flags, void *data); int (*sb_check_sb) (struct vfsmount * mnt, struct nameidata * nd); int (*sb_umount) (struct vfsmount * mnt, int flags); void (*sb_umount_close) (struct vfsmount * mnt); void (*sb_umount_busy) (struct vfsmount * mnt); void (*sb_post_remount) (struct vfsmount * mnt, unsigned long flags, void *data); void (*sb_post_mountroot) (void); void (*sb_post_addmount) (struct vfsmount * mnt, struct nameidata * mountpoint_nd); int (*sb_pivotroot) (struct nameidata * old_nd, struct nameidata * new_nd); void (*sb_post_pivotroot) (struct nameidata * old_nd, struct nameidata * new_nd); 图1.4 super_block文件系统钩子
int (*inode_alloc_security) (struct inode *inode); void (*inode_free_security) (struct inode *inode); int (*inode_init_security) (struct inode *inode, struct inode *dir, char **name, void **value, size_t *len); int (*inode_create) (struct inode *dir, struct dentry *dentry, int mode); int (*inode_link) (struct dentry *old_dentry, struct inode *dir, struct dentry *new_dentry); int (*inode_unlink) (struct inode *dir, struct dentry *dentry); int (*inode_symlink) (struct inode *dir, struct dentry *dentry, const char *old_name); int (*inode_mkdir) (struct inode *dir, struct dentry *dentry, int mode); int (*inode_rmdir) (struct inode *dir, struct dentry *dentry); int (*inode_mknod) (struct inode *dir, struct dentry *dentry, int mode, dev_t dev); int (*inode_rename) (struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry); int (*inode_readlink) (struct dentry *dentry); int (*inode_follow_link) (struct dentry *dentry, struct nameidata *nd); int (*inode_permission) (struct inode *inode, int mask, struct nameidata *nd); int (*inode_setattr) (struct dentry *dentry, struct iattr *attr); int (*inode_getattr) (struct vfsmount *mnt, struct dentry *dentry); void (*inode_delete) (struct inode *inode); int (*inode_setxattr) (struct dentry *dentry, char *name, void *value, size_t size, int flags); void (*inode_post_setxattr) (struct dentry *dentry, char *name, void *value, size_t size, int flags); int (*inode_getxattr) (struct dentry *dentry, char *name); int (*inode_listxattr) (struct dentry *dentry); int (*inode_removexattr) (struct dentry *dentry, char *name); const char *(*inode_xattr_getsuffix) (void); int (*inode_getsecurity)(const struct inode *inode, const char *name, void *buffer, size_t size, int err); int (*inode_setsecurity)(struct inode *inode, const char *name, const void *value, size_t size, int flags); int (*inode_listsecurity)(struct inode *inode, char *buffer, size_t buffer_size); 图1.5 inode节点钩子
int (*file_permission) (struct file * file, int mask); int (*file_alloc_security) (struct file * file); void (*file_free_security) (struct file * file); int (*file_ioctl) (struct file * file, unsigned int cmd, unsigned long arg); int (*file_mmap) (struct file * file, unsigned long reqprot, unsigned long prot, unsigned long flags, unsigned long addr, unsigned long addr_only); int (*file_mprotect) (struct vm_area_struct * vma, unsigned long reqprot, unsigned long prot); int (*file_lock) (struct file * file, unsigned int cmd); int (*file_fcntl) (struct file * file, unsigned int cmd, unsigned long arg); int (*file_set_fowner) (struct file * file); int (*file_send_sigiotask) (struct task_struct * tsk, struct fown_struct * fown, int sig); int (*file_receive) (struct file * file); 图1.6 文件钩子
接下来是网络钩子。对网络的应用层访问使用一系列的socket套接字钩子来进行仲裁,这些钩子基本覆盖了所有基于socket套接字的协议。由于每个激活的用户socket套接字有
伴随有一个inode结构,所以在socket结构或是更底层的sock结构中都没有加入安全域。socket套接字钩子对有关进程的网络访问提供了一个通用的仲裁,从而显著扩展了内核的网络访问控制框架(这在网络层是已经由Linux内核防火墙netfilter进行处理的)。例如sock_rcv_skb钩子允许在进入内核的包排队到相应的用户空间socket套接字之前,按照其目的应用来对其进行仲裁。另外Linux安全模块(LSM)也为IPv4,UNIX域,以及Netlink协议实现了细粒度的钩子,以后还可能实现其他协议的钩子。网络数据以包的形式被封装在sk_buff结构(socket套接字缓冲区)中游历协议栈,Linux安全模块(LSM)在sk_buff结构中加入了一个安全域,使得能够在包的层次上对通过网络层的数据的安全信息进行管理,并提供了一系列的sk_buff钩子用于维护这个安全域的整个生命周期。硬件和软件网络设备被封装在一个net_device结构中,一个安全域被加到这个结构中,使得能够在设备的层次上维护安全信息。
int (*socket_create) (int family, int type, int protocol, int kern); int (*socket_post_create) (struct socket * sock, int family, int type, int protocol, int kern); int (*socket_bind) (struct socket * sock, struct sockaddr * address, int addrlen); int (*socket_connect) (struct socket * sock, struct sockaddr * address, int addrlen); int (*socket_listen) (struct socket * sock, int backlog); int (*socket_accept) (struct socket * sock, struct socket * newsock); void (*socket_post_accept) (struct socket * sock, struct socket * newsock); int (*socket_sendmsg) (struct socket * sock, struct msghdr * msg, int size); int (*socket_recvmsg) (struct socket * sock, struct msghdr * msg, int size, int flags); int (*socket_getsockname) (struct socket * sock); int (*socket_getpeername) (struct socket * sock); int (*socket_getsockopt) (struct socket * sock, int level, int optname); int (*socket_setsockopt) (struct socket * sock, int level, int optname); int (*socket_shutdown) (struct socket * sock, int how); int (*socket_sock_rcv_skb) (struct sock * sk, struct sk_buff * skb); int (*socket_getpeersec_stream) (struct socket *sock, char __user *optval, int __user *optlen, unsigned len); int (*socket_getpeersec_dgram) (struct socket *sock, struct sk_buff *skb, u32 *secid); int (*sk_alloc_security) (struct sock *sk, int family, gfp_t priority); void (*sk_free_security) (struct sock *sk); void (*sk_clone_security) (const struct sock *sk, struct sock *newsk); int (*unix_stream_connect) (struct socket * sock, struct socket * other, struct sock * newsk); int (*unix_may_send) (struct socket * sock, struct socket * other); void (*sk_getsecid) (struct sock *sk, u32 *secid); void (*sock_graft)(struct sock* sk, struct socket *parent); int (*inet_conn_request)(struct sock *sk, struct sk_buff *skb, struct request_sock *req); void (*inet_csk_clone)(struct sock *newsk, const struct request_sock *req); void (*inet_conn_established)(struct sock *sk, struct sk_buff *skb); void (*req_classify_flow)(const struct request_sock *req, struct flowi *fl); 图1.7 网络钩子
最后是其他的钩子。Linux安全模块(LSM)提供了两种其他系列的钩子:模块钩子和顶层的系统钩子。模块钩子用来控制创建,初始化,清除内核模块的内核操作。系统钩子用来控制系统操作,例如设置主机名,访问I/O端口,以及配置进程记帐。虽然现在的Linux内核通过使用capability检查对这些系统操作提供了一些支持,但是这些检查对于不同操作差别很大并且没有提供任何参数信息。
1.5模块说明:给普通用户使用的现成的安全功能
Linux安全模块(LSM)对于普通用户的价值就在于:可以提供各种安全模块,由用户选择适合自己需要加载到内核,满足特定的安全功能。Linux安全模块(LSM)本身只提供增强访问控制策略的机制(只提供机制,策略由用户层提供),而由各个安全模块实现具体特定的安全策略。下面简要介绍一些已经实现的安全模块。
SELinux,这是一个Flask灵活访问控制体系在Linux上的实现,并且提供了类型增强,基于角色的访问控制,以及可选的多级安全策略。SELinux原来是作为一个内核补丁实现的,现在已经使用Linux安全模块(LSM)重新实现为一个安全模块。SELinux可以被用来限制进程为最小特权,保护进程和数据的完整性和机密性,并且支持应用安全需求。(内核补丁和编译入内核模块,都是需要重新编译内核,是需要对内核进行修改,不是在内核启动后,采用动态可加载的方式进行处理的)
DTE Linux。这是一个域和类型增强在Linux上的实现。就像SELinux一样,DTE Linux原来是作为一个内核补丁实现的,现在已经使用Linux安全模块(LSM)重新实现为一个安全模块。当这个安全模块被加载到内核上时,类型被赋给对象,域被赋给进程。DTE策略限制域之间和从域到类型的访问。
Openwall 内核补丁的LSM移植。Openwall内核补丁提供了一系列的安全特性集合来保护系统免受例如缓冲区溢出和临时文件竞争这样的攻击。有安全模块正在被开发出来以支持Openwall补丁的一个子集。
POSIX.1e capabilities。Linux内核中已经存在有POSIX.1e capabilities逻辑,但是Linux安全模块(LSM)把这个逻辑划分到了一个安全模块中。这样的修改使得不需要的用户可以从他们的内核中把这个功能略去;也使得capabilities逻辑的开发可以脱离内核开发获得更大的独立性。
LIDS。这是中国人谢华刚发起的项目。开始时作为一个入侵检测系统开发,后来逐渐演变为使用访问控制系统的形式来进行入侵预防,它通过描述一个给定的程序可以访问哪些文件来进行访问控制。同样的,LIDS原来是作为一个内核补丁实现的并附带了一些管理工具,现在已经使用Linux安全模块(LSM)重新实现为一个安全模块。(安全模块的方式是可加载的??)
当然还有缺省的传统超级用户机制。这个安全模块是Linux安全模块(LSM)缺省的,实现了传统的UNIX超级用户特权机制。
第二章 LSM安全架构启动
2.1 LSM的注册层次
SELinux的出现着实扰乱了文件系统的进度,不过送算慢慢搞清楚了其中的来龙去脉。下面将通过2.6.20内核中的security代码进行一番简单的分析。
初次接触LSM时,其间复杂的模块加载方式和顺序很是让人头疼。光其中的ops操作就有security_ops, capability_ops, secondary_ops, selinux_ops, rootplug_ops, selinux_ops, original_ops,再有init函数security_init, capability_init, rootplug_init, selinux_init中的注册关系register_secrity反反复复的来回赋值,最后究竟ops操作都是什么,已经成了一团乱麻。为此,不得不从系统的启动过程开始,寻找其间的因果顺序,事情的经过是这样的:LSM系统的初始化发生在系统内核初始化阶段,在src/init/main.c的start_kernel()里,其位置如下所示: fork_init(num_physpages); proc_caches_init(); buffer_init(); unnamed_dev_init(); key_init(); security_init(); vfs_caches_init(num_physpages); radix_tree_init(); signals_init(); 图 2.1
在unnamed_dev_init(), key_init()之后,vfs_caches_init之前,其在内核中的位置层次也基本上如此。
security_init()的具体实现在/src/security/security.c中, /** * security_init - initializes the security framework * * This should be called early in the kernel initialization sequence. */ int __init security_init(void) { printk(KERN_INFO \ \ if (verify(&dummy_security_ops)) { printk(KERN_ERR \ \ } return -EIO; } security_ops = &dummy_security_ops; do_security_initcalls(); return 0; 图 2.2
在此,很有必要了解一下dummy_security_ops的定义,在src/security/dummy.c中,给出了其定义。
struct security_operations dummy_security_ops;
定义后的dummy_security_ops并没有初始化,也就是说,它是security_operations的一个结构,该结构里是一系列的指针,每个指针都指向一个函数,而这些函数,就是security框架所能覆盖的领域,通过修改函数指针,可以达到为原先的系统通过钩子增加一个安全过滤层的目的。即,系统的这些调用,先通过你设计的函数过滤相关操作,再有你的函数调用原先的实现,实现增加一层的目的,这和NT中注册操作的回调函数相似。
下面是security_operations的定义,它没有数据成员,所有的成员都是函数指针: struct security_operations { int (*ptrace) (struct task_struct * parent, struct task_struct * child); int (*capget) (struct task_struct * target, kernel_cap_t * effective, kernel_cap_t * inheritable, kernel_cap_t * permitted); int (*capset_check) (struct task_struct * target, kernel_cap_t * effective, kernel_cap_t * inheritable, kernel_cap_t * permitted); void (*capset_set) (struct task_struct * target, kernel_cap_t * effective, kernel_cap_t * inheritable, kernel_cap_t * permitted); int (*capable) (struct task_struct * tsk, int cap); int (*acct) (struct file * file); int (*sysctl) (struct ctl_table * table, int op); int (*quotactl) (int cmds, int type, int id, struct super_block * sb); int (*quota_on) (struct dentry * dentry); int (*syslog) (int type); int (*settime) (struct timespec *ts, struct timezone *tz); int (*vm_enough_memory) (long pages);
int (*bprm_alloc_security) (struct linux_binprm * bprm); void (*bprm_free_security) (struct linux_binprm * bprm);
void (*bprm_apply_creds) (struct linux_binprm * bprm, int unsafe); void (*bprm_post_apply_creds) (struct linux_binprm * bprm); int (*bprm_set_security) (struct linux_binprm * bprm); int (*bprm_check_security) (struct linux_binprm * bprm);
int (*bprm_secureexec) (struct linux_binprm * bprm);
int (*sb_alloc_security) (struct super_block * sb); void (*sb_free_security) (struct super_block * sb); int (*sb_copy_data)(struct file_system_type *type, void *orig, void *copy);
int (*sb_kern_mount) (struct super_block *sb, void *data); int (*sb_statfs) (struct dentry *dentry);
int (*sb_mount) (char *dev_name, struct nameidata * nd, char *type, unsigned long flags, void *data);
int (*sb_check_sb) (struct vfsmount * mnt, struct nameidata * nd); int (*sb_umount) (struct vfsmount * mnt, int flags); void (*sb_umount_close) (struct vfsmount * mnt); void (*sb_umount_busy) (struct vfsmount * mnt); void (*sb_post_remount) (struct vfsmount * mnt, unsigned long flags, void *data); void (*sb_post_mountroot) (void);
void (*sb_post_addmount) (struct vfsmount * mnt, struct nameidata * mountpoint_nd); int (*sb_pivotroot) (struct nameidata * old_nd, struct nameidata * new_nd);
void (*sb_post_pivotroot) (struct nameidata * old_nd, struct nameidata * new_nd);
int (*inode_alloc_security) (struct inode *inode); void (*inode_free_security) (struct inode *inode);
int (*inode_init_security) (struct inode *inode, struct inode *dir, char **name, void **value, size_t *len); int (*inode_create) (struct inode *dir,
struct dentry *dentry, int mode); int (*inode_link) (struct dentry *old_dentry,
struct inode *dir, struct dentry *new_dentry); int (*inode_unlink) (struct inode *dir, struct dentry *dentry); int (*inode_symlink) (struct inode *dir,
struct dentry *dentry, const char *old_name); int (*inode_mkdir) (struct inode *dir, struct dentry *dentry, int mode); int (*inode_rmdir) (struct inode *dir, struct dentry *dentry); int (*inode_mknod) (struct inode *dir, struct dentry *dentry, int mode, dev_t dev);
int (*inode_rename) (struct inode *old_dir, struct dentry *old_dentry,
struct inode *new_dir, struct dentry *new_dentry); int (*inode_readlink) (struct dentry *dentry);
int (*inode_follow_link) (struct dentry *dentry, struct nameidata *nd);
int (*inode_permission) (struct inode *inode, int mask, struct nameidata *nd);
int (*inode_setattr) (struct dentry *dentry, struct iattr *attr); int (*inode_getattr) (struct vfsmount *mnt, struct dentry *dentry); void (*inode_delete) (struct inode *inode); int (*inode_setxattr) (struct dentry *dentry, char *name, void *value, size_t size, int flags); void (*inode_post_setxattr) (struct dentry *dentry, char *name, void *value, size_t size, int flags); int (*inode_getxattr) (struct dentry *dentry, char *name); int (*inode_listxattr) (struct dentry *dentry); int (*inode_removexattr) (struct dentry *dentry, char *name); const char *(*inode_xattr_getsuffix) (void);
int (*inode_getsecurity)(const struct inode *inode, const char *name, void *buffer, size_t size, int err);
int (*inode_setsecurity)(struct inode *inode, const char *name, const void *value, size_t size, int flags);
int (*inode_listsecurity)(struct inode *inode, char *buffer, size_t buffer_size);
int (*file_permission) (struct file * file, int mask); int (*file_alloc_security) (struct file * file); void (*file_free_security) (struct file * file);
int (*file_ioctl) (struct file * file, unsigned int cmd, unsigned long arg); int (*file_mmap) (struct file * file, unsigned long reqprot, unsigned long prot, unsigned long flags); int (*file_mprotect) (struct vm_area_struct * vma, unsigned long reqprot, unsigned long prot);
int (*file_lock) (struct file * file, unsigned int cmd); int (*file_fcntl) (struct file * file, unsigned int cmd, unsigned long arg);
int (*file_set_fowner) (struct file * file);
int (*file_send_sigiotask) (struct task_struct * tsk, struct fown_struct * fown, int sig); int (*file_receive) (struct file * file);
int (*task_create) (unsigned long clone_flags); int (*task_alloc_security) (struct task_struct * p); void (*task_free_security) (struct task_struct * p);
int (*task_setuid) (uid_t id0, uid_t id1, uid_t id2, int flags); int (*task_post_setuid) (uid_t old_ruid /* or fsuid */ , uid_t old_euid, uid_t old_suid, int flags); int (*task_setgid) (gid_t id0, gid_t id1, gid_t id2, int flags); int (*task_setpgid) (struct task_struct * p, pid_t pgid);
int (*task_getpgid) (struct task_struct * p); int (*task_getsid) (struct task_struct * p);
void (*task_getsecid) (struct task_struct * p, u32 * secid); int (*task_setgroups) (struct group_info *group_info); int (*task_setnice) (struct task_struct * p, int nice); int (*task_setioprio) (struct task_struct * p, int ioprio); int (*task_getioprio) (struct task_struct * p);
int (*task_setrlimit) (unsigned int resource, struct rlimit * new_rlim); int (*task_setscheduler) (struct task_struct * p, int policy, struct sched_param * lp);
int (*task_getscheduler) (struct task_struct * p); int (*task_movememory) (struct task_struct * p); int (*task_kill) (struct task_struct * p, struct siginfo * info, int sig, u32 secid); int (*task_wait) (struct task_struct * p);
int (*task_prctl) (int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
void (*task_reparent_to_init) (struct task_struct * p);
void (*task_to_inode)(struct task_struct *p, struct inode *inode); int (*ipc_permission) (struct kern_ipc_perm * ipcp, short flag); int (*msg_msg_alloc_security) (struct msg_msg * msg); void (*msg_msg_free_security) (struct msg_msg * msg);
int (*msg_queue_alloc_security) (struct msg_queue * msq); void (*msg_queue_free_security) (struct msg_queue * msq); int (*msg_queue_associate) (struct msg_queue * msq, int msqf int (*msg_queue_msgctl) (struct msg_queue * msq, int cmd); int (*msg_queue_msgsnd) (struct msg_queue * msq, struct msg_msg * msg, int msqflg); int (*msg_queue_msgrcv) (struct msg_queue * msq, struct msg_msg * msg, struct task_struct * target, long type, int mode); int (*shm_alloc_security) (struct shmid_kernel * shp); void (*shm_free_security) (struct shmid_kernel * shp); int (*shm_associate) (struct shmid_kernel * shp, int shmflg); int (*shm_shmctl) (struct shmid_kernel * shp, int cmd); int (*shm_shmat) (struct shmid_kernel * shp, char __user *shmaddr, int shmflg);
int (*sem_alloc_security) (struct sem_array * sma); void (*sem_free_security) (struct sem_array * sma);
int (*sem_associate) (struct sem_array * sma, int semflg); int (*sem_semctl) (struct sem_array * sma, int cmd); int (*sem_semop) (struct sem_array * sma, struct sembuf * sops, unsigned nsops, int alter); int (*netlink_send) (struct sock * sk, struct sk_buff * skb); int (*netlink_recv) (struct sk_buff * skb, int cap);
/* allow module stacking */
int (*register_security) (const char *name,
struct security_operations *ops); int (*unregister_security) (const char *name,
struct security_operations *ops); void (*d_instantiate) (struct dentry *dentry, struct inode *inode);
int (*getprocattr)(struct task_struct *p, char *name, void *value, size_t size); int (*setprocattr)(struct task_struct *p, char *name, void *value, size_t size); int (*secid_to_secctx)(u32 secid, char **secdata, u32 *seclen); void (*release_secctx)(char *secdata, u32 seclen);
#ifdef CONFIG_SECURITY_NETWORK int (*unix_stream_connect) (struct socket * sock, struct socket * other, struct sock * newsk); int (*unix_may_send) (struct socket * sock, struct socket * other);
int (*socket_create) (int family, int type, int protocol, int kern); int (*socket_post_create) (struct socket * sock, int family, int type, int protocol, int kern); int (*socket_bind) (struct socket * sock, struct sockaddr * address, int addrlen); int (*socket_connect) (struct socket * sock, struct sockaddr * address, int addrlen); int (*socket_listen) (struct socket * sock, int backlog);
int (*socket_accept) (struct socket * sock, struct socket * newsock); void (*socket_post_accept) (struct socket * sock, struct socket * newsock); int (*socket_sendmsg) (struct socket * sock, struct msghdr * msg, int size); int (*socket_recvmsg) (struct socket * sock, struct msghdr * msg, int size, int flags); int (*socket_getsockname) (struct socket * sock);
int (*socket_getpeername) (struct socket * sock); int (*socket_getsockopt) (struct socket * sock, int level, int optname); int (*socket_setsockopt) (struct socket * sock, int level, int optname); int (*socket_shutdown) (struct socket * sock, int how); int (*socket_sock_rcv_skb) (struct sock * sk, struct sk_buff * skb); int (*socket_getpeersec_stream) (struct socket *sock, char __user *optval, int __user *optlen, unsigned len); int (*socket_getpeersec_dgram) (struct socket *sock, struct sk_buff *skb, u32 *secid); int (*sk_alloc_security) (struct sock *sk, int family, gfp_t priority); void (*sk_free_security) (struct sock *sk); void (*sk_clone_security) (const struct sock *sk, struct sock *newsk); void (*sk_getsecid) (struct sock *sk, u32 *secid); void (*sock_graft)(struct sock* sk, struct socket *parent); int (*inet_conn_request)(struct sock *sk, struct sk_buff *skb, struct request_sock *req); void (*inet_csk_clone)(struct sock *newsk, const struct request_sock *req); void (*inet_conn_established)(struct sock *sk, struct sk_buff *skb); void (*req_classify_flow)(const struct request_sock *req, struct flowi *fl); #endif /* CONFIG_SECURITY_NETWORK */
#ifdef CONFIG_SECURITY_NETWORK_XFRM int (*xfrm_policy_alloc_security) (struct xfrm_policy *xp, struct xfrm_user_sec_ctx *sec_ctx); int (*xfrm_policy_clone_security) (struct xfrm_policy *old, struct xfrm_policy *new); void (*xfrm_policy_free_security) (struct xfrm_policy *xp); int (*xfrm_policy_delete_security) (struct xfrm_policy *xp); int (*xfrm_state_alloc_security) (struct xfrm_state *x, struct xfrm_user_sec_ctx *sec_ctx, u32 secid); void (*xfrm_state_free_security) (struct xfrm_state *x); int (*xfrm_state_delete_security) (struct xfrm_state *x); int (*xfrm_policy_lookup)(struct xfrm_policy *xp, u32 fl_secid, u8 dir); int (*xfrm_state_pol_flow_match)(struct xfrm_state *x, struct xfrm_policy *xp, struct flowi *fl); int (*xfrm_decode_session)(struct sk_buff *skb, u32 *secid, int ckall); #endif /* CONFIG_SECURITY_NETWORK_XFRM */ /* key management security hooks */ #ifdef CONFIG_KEYS int (*key_alloc)(struct key *key, struct task_struct *tsk, unsigned long flags); void (*key_free)(struct key *key); int (*key_permission)(key_ref_t key_ref, struct task_struct *context, key_perm_t perm);
#endif /* CONFIG_KEYS */ };
dummy_security_ops的初始化是在上面给出的security_init中verify(&dummy_security_ops)函数实现的,它将参数XXX_ops中的所有空指针的函数,初始化为dummy.c中定义的dummy_XXX类型对应函数体,每个函数体只提供一个表示成功的返回值(即所有的检查都通过,不对原来的执行过程实行任何影响),而不执行任何操作。具体参见src/security/dummy.c。
dummy_security_ops初始化完成后,通过security_ops=&dummy_security_ops,使得security_ops指向一个不做任何操作的过滤层。再通过do_security_initcalls()加载具体配置的安全过滤模块。
do_security_initcalls()函数的实现:
static void __init do_security_initcalls(void) { initcall_t *call; call = __security_initcall_start; while (call < __security_initcall_end) { (*call) (); call++; } } 在系统中,security_initcall.init的函数总共有三个,分别为:capability_init(), rootplug_init()和selinux_init()。其中,selinux只能第一个注册(即作为security_ops上的第一个过滤层),否则注册会失败。selinux注册后,original_ops=secondary_ops=dummy_ops; securuty_ops=selinux_ops。前两者的注册方式完全一致,并且,在当前代码实现中,两者只能注册一个,另外一个在第一个注册成功后,即使以模块方式加载也会失败。例如,capability_init()注册后,secondary_ops=capability_ops; original_ops=dummy_ops; security_ops=selinux_ops。此时如果再调用rootplug_init(),将因为security_ops!=dummy_security_ops而调用register_security失败,然后因为secondary_ops != original_ops而在调用security_ops->register_security即selinux_register_security函数中返回出错,原因为几经存在了一个第二层模块。这表示do_security_initcalls函数中的while循环只执行两次。具体代码不在这里列出,有兴趣者可以到src/security/目录下分别查看capability.c, root_plug.c和selinux/hooks.c。
因此可见,主要得代码实现都是在selinux中实现的。以我所感兴趣的selinux_mount和selinux_umount为例,其代码如下:
static int selinux_mount(char * dev_name,
struct nameidata *nd, char * type,
unsigned long flags, void * data) { int rc; rc = secondary_ops->sb_mount(dev_name, nd, type, flags, data); if (rc)
return rc;
if (flags & MS_REMOUNT) return superblock_has_perm(current, nd->mnt->mnt_sb, FILESYSTEM__REMOUNT, NULL); else return dentry_has_perm(current, nd->mnt, nd->dentry, FILE__MOUNTON); }
static int selinux_umount(struct vfsmount *mnt, int flags) { int rc; }
rc = secondary_ops->sb_umount(mnt, flags); if (rc) return rc;
return superblock_has_perm(current,mnt->mnt_sb,
FILESYSTEM__UNMOUNT,NULL);
由上面的ops赋值关系可知,secondary_ops->sb_mount和secondary_ops->sb_umount无论capability和root_plug是否加载,因为其在这两者中都没有定义实现,所以,为dummy_XXX函数,相关的代码如下:
static int dummy_sb_mount (char *dev_name, struct nameidata *nd, char *type, unsigned long flags, void *data) { return 0; }
static int dummy_sb_check_sb (struct vfsmount *mnt, struct nameidata *nd) { return 0; }
static int dummy_sb_umount (struct vfsmount *mnt, int flags) { return 0; }
static void dummy_sb_umount_close (struct vfsmount *mnt) { return; }
static void dummy_sb_umount_busy (struct vfsmount *mnt) { return; }
static void dummy_sb_post_remount (struct vfsmount *mnt, unsigned long flags, void *data) { return; }
static void dummy_sb_post_mountroot (void) { return; }
然后给出一定的pemmision的判断。permmision判断通过查找avc的hash表实现,它是以数据库的形式在内核维护的。在selinux中,ss目录下的文件负责维护security所有安全检查需要的数据库,对于数据库的详细操作和保存和加载,请参考src/security/selinux/ss/policydb.c中的相关函数。
2.2 LSM Hooks
上一章已经对LSM安全模块的注册做了很详细的介绍。下面对LSM Hooks的函数功能做详细的介绍,因为我们的主要工作是在该系统上添加新的功能,必须把该系统的功能分析清楚。而LSM系统的最重要的部分就是Hook函数。因为要加强整个系统的安全,从可执行文件的启动,到进程的运行,再到进程对文件等资源的访问,直到进程的结束。
系统调用为用户空间提供了内核的一个抽象的接口,可以通过截获系统调用来实施访问控制。实际上,2.6版本的内核是不允许内核模块重写系统调用表。截获系统调用表是需要付出代价的,需要代码复制,可能不能准确的表达上下文所需要的安全策略决策。LSM的策略执行点刚好在对象被访问之前。如果访问被拒绝,就会强制返回一个错误码。在内核底层进行截获,可以访问整个内核空间,能够更加细粒度的进行访问控制。具体的访问过程如下图所示:
图2.3 LSM Hooks访问过程
从上图所示,LSM Hooks的策略检查是在内核的底层,在标准linux的安全检查之后。
2.3 相关的内核数据结构
LSM Hooks的加入,对内核中关键的数据结构都有部分的修改,具体如下图所示,左边的表示内核中的数据结构,右边是对象,内核中的数据结构和对象都是相对应的。为了能够更好的了解LSM Hooks的执行原理和功能,我们首先要了解一下内核中的关键数据结构。本节的主要内容是对LSM Hooks修改的内核数据结构,以及LSM Hooks函数中所使用到的内核数据结构做一个简要的介绍。
图 2.5
2.3.1 Task_struct(进程结构)
在操作系统中,数据结构是代表不同的实体,为了管理进程,操作系统必须对每个进程所作的事情进行清楚的描述。在Linux系统中,每个进程都由Task_struct数据结构来定义,task_struct就是我们通常所说的PCB(Process Control Block,简称PCB)。它包含了进程的所有信息,在任何时候操作系统都能跟踪这个结构的信息,这个结构是linux内核汇总最重要的数据结构。
它是对进程控制的唯一手段,也是最有效的手段。当我们调用fork()时,系统会为我们产生一个task_struct结构。然后从父进程那里继承一些数据,并把新的进程插入到进程树中,以待进行进程管理。在task_struct中包括以下信息:1.进程状态,记录进程在等待,运行,或死锁;2.调度信息,由哪个调度函数调度,怎样调度等;3.进程的通信状况;4.进程链接信息,因为要插入进程树,必须有联系父子兄弟指针;5.时间信息,比如计算好执行的时间,以便cpu分配;6.标号,决定改变进程归属;7.可以读写打开的一些文件信息;8.进程上下文和内核上下文;9.处理器上下文;10.内存信息,因为每个PCB都是这样的,只有这些结构,才能满足一个进程所有要求。
2.3.2 Linux_binprm
在装入二进制是需要用到结构linux_binprm,这个结构保存着一些装入代码时需要的信息:
详细的定义如下:
struct linux_binprm{
char buf[128];/*读入文件时用的缓冲区*/
unsigned long page[MAX_ARG_PAGES];//页面数组指针 MAX_ARG_PAGES定义为32 unsigned long p; //最大参数个数所占用的空间
int sh_bang; //表示可执行文件的性质,当可执行文件时一个shell脚本是置为1 struct inode * inode;/*映像来自的节点*/即二进制文件的inode节点 int e_uid, e_gid;
int argc, envc; /*参数数目,环境数目*/
char * filename; /* 二进制映像的名字,也就是要执行的文件名 */ unsigned long loader, exec;
int dont_iput; /* binfmt handler has put inode */ };
其它域的含义在后面的do_exec()代码中做进一步解释。
Linux所提供的系统调用名为execve(),可是,C语言的程序库在此系统调用的基础上向应用程序提供了一整套的库函数,包括execve()、execlp()、execle()、execv()、execvp(),它们之间的差异仅仅是参数的不同。下面来介绍execve()的实现。
系统调用execve()在内核的入口为sys_execve(),其代码在arch/i386/kernel/process.c:
2.3.3 Super_block(超级块)
超级块(superblock)中包含的信息描述了文件系统的布局。和引导块一样,超级块的大小也是固定的1024字节。超级块的主要功能是给出文件系统各个部分的大小。如果给定块大小和i节点数,我们可以很容易地算出i节点位图和存放i节点所需的块数。例如,假设块大小为1KB,每个位图块有1024字节(8192位),可以记录8192个i节点的状态(实际上第一块只能处理8191个i节点,因为第0号i节点并不存在,但我们在位图中也会为它保留一位)。如果有10000个i节点,则需要用到两个位图块。由于每个i节点要占用64个字节,因此1KB的块最多能存放16个i节点。如果有64个i节点,则需要4个磁盘块来存储。
启动文件系统时,根设备中的超级块会被读入内存。同样,当挂装其他的文件系统时,它们的超级块也会被读入内存。在内存的超级快中,有些字段是磁盘上的超级块所没有的,如设备的只读标志和字节顺序标志位、位图中第一个空闲位的位置(用于加速位图的访问)以及该超级块来自于哪一个设备等。
每个文件系统只有一个超级块,它是对文件系统总体信息的描述。 Super_block数据结构: struct super_block { struct list_head s_list; /* Keep this first */指向超级块链表的指针 dev_t s_dev; /* search index; _not_ kdev_t */设备标识符 unsigned long s_blocksize;//以字节为单位的块大小 unsigned char s_blocksize_bits;//以位为单位的块大小 unsigned char s_dirt;//修改脏标志 unsigned long long s_maxbytes; /* 文件大小的上限*/ struct file_system_type *s_type;//文件系统的类型 const struct super_operations *s_op;//超级块方法 struct dquot_operations *dq_op;//磁盘限额方法 struct quotactl_ops *s_qcop;//限额控制方法 struct export_operations *s_export_op;//导出方法 unsigned long s_flags;//挂载标志 unsigned long s_magic;//文件系统魔数 struct dentry *s_root;//目录挂载点 struct rw_semaphore s_umount;//卸载信号量 struct mutex s_lock;//超级块信号量 int s_count;//引用计数 int s_syncing;//文件系统同步标志
int s_need_sync_fs;//尚未同步标志 atomic_t s_active;//活动引用计数 #ifdef CONFIG_SECURITY void *s_security;//super_block的安全域 #endif struct xattr_handler **s_xattr;// struct list_head s_inodes; /* 所有的inode */ struct list_head s_dirty; /* 脏节点链表 */ struct list_head s_io; /* 回写链表 */ struct hlist_head s_anon; /* 匿名目录项anonymous dentries for (nfs) exporting */
struct list_head
s_files;//被分配文件链表
struct block_device *s_bdev;//相关设备 struct mtd_info *s_mtd;
struct list_head s_instances;//该类型文件系统
struct quota_info s_dquot; /* 限额相关选项Diskquota specific options */ int s_frozen; wait_queue_head_t s_wait_unfrozen; char s_id[32]; void
/* Informational name */
/*文件系统特殊信息 */
*s_fs_info;
/*
* The next field is for VFS *only*. No filesystems have any business * even looking at it. You had been warned. */
struct mutex s_vfs_rename_mutex; /* 重命名信号量Kludge */ /* Granularity of c/m/atime in ns. Cannot be worse than a second */ u32 s_time_gran; /* * Filesystem subtype. If non-empty the filesystem type field * in /proc/mounts will be \ */ char *s_subtype;//文件系统的子版本号 };
由前面所阐述的内容,可以得出在每个文件系统中,超级块有且只能有一个,大小为1024个字节。超级块中剩余的空间不能用来做存储。
2.3.4 Inode(索引节点)
文件的i节点的主要功能是给出文件数据块所在的位置,存放关于文件的一般信息。在打开一个文件时,先找到它的i节点,并把它装入到内存的inode表中,而且它会一直呆在那儿,直至文件被关闭。在内存中的inode表中有一些字段是磁盘i节点所没有的,如i节点所在的设备。这样,当该i节点被修改后,文件系统才知道应该把它写会到什么地方。
struct inode { struct hlist_node i_hash; struct list_head i_list; struct list_head i_sb_list; struct list_head i_dentry; unsigned long i_ino; atomic_t i_count; unsigned int i_nlink; uid_t i_uid; gid_t i_gid; dev_t i_rdev; unsigned long i_version; loff_t i_size; #ifdef __NEED_I_SIZE_ORDERED seqcount_t i_size_seqcount; #endif struct timespec i_atime; struct timespec i_mtime; struct timespec i_ctime; unsigned int i_blkbits; blkcnt_t i_blocks; unsigned short i_bytes; umode_t i_mode; spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */ struct mutex i_mutex; struct rw_semaphore i_alloc_sem; const struct inode_operations *i_op; const struct file_operations *i_fop; /* former ->i_op->default_file_ops */ struct super_block *i_sb; struct file_lock *i_flock; struct address_space *i_mapping; struct address_space i_data; #ifdef CONFIG_QUOTA struct dquot *i_dquot[MAXQUOTAS]; #endif struct list_head i_devices; union { struct pipe_inode_info *i_pipe;
struct block_device *i_bdev; struct cdev *i_cdev; }; int i_cindex; __u32 i_generation;
#ifdef CONFIG_DNOTIFY unsigned long i_dnotify_mask; /* Directory notify events */ struct dnotify_struct *i_dnotify; /* for directory notifications */ #endif
#ifdef CONFIG_INOTIFY struct list_head inotify_watches; /* watches on this inode */ struct mutex inotify_mutex; /* protects the watches list */ #endif unsigned long i_state; unsigned long dirtied_when; /* jiffies of first dirtying */ unsigned int i_flags; atomic_t i_writecount; #ifdef CONFIG_SECURITY void #endif void };
*i_security;//安全域结构指针
*i_private; /* fs or device private pointer */
2.3.5 File(文件)
文件对象描述进程怎样与一个打开的文件进行交互。文件对象是在文件被打开时创建的,由一个file结构组成,其中包含的字段如下图所示。注意,文件对象在磁盘上没有对应的映像,因此file结构中没有设置“脏”字段来表示文件对象是否已被修改。
存放在文件对象中的主要信息是文件指针,即文件中当前的位置,下一个操作将在该位置发生。由于几个进程可能同时访问同一个文件,因此文件指针必须存放在文件对象而不是索引节点对象中。
struct file { union { struct list_head fu_list; struct rcu_head fu_rcuhead;
} f_u; //用于通用文件对象链表的指针 struct path f_path;
#define f_dentry f_path.dentry//与文件相关的目录对象 #define f_vfsmnt f_path.mnt//含有该文件的已安装文件系统 const struct file_operations *f_op;//指向文件操作表的指针 atomic_t f_count;//文件对象的引用计数器 unsigned int f_flags;//当打开文件时所指定的标志 mode_t f_mode;//进程访问模式 loff_t f_pos;//当前的文件位移 struct fown_struct f_owner;//通过信号进行I/O事件通知的数据 unsigned int f_uid, f_gid;//用户的UID和用户的GID struct file_ra_state f_ra;//文件预读状态 unsigned long f_version;//版本号,每次使用后自动递增 #ifdef CONFIG_SECURITY
void *f_security;//指向文件对象的安全指针 #endif void *private_data;//指向特定文件系统或设备驱动程序所需要的数据的指针
#ifdef CONFIG_EPOLL struct list_head f_ep_links;//文件的时间轮询等待者链表的头 spinlock_t f_ep_lock;//保护f_ep_links链表的自旋锁 #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping;//指向文件地址空间对象的指针 };
2.3.6 dentry(目录项)(《深入理解LINUX内核》第474页)
存放目录项(也就是文件的特定名称)与对应文件进行链接的有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。《深入理解LINUX内核》第474页。
struct dentry { atomic_t d_count;//引用计数器 unsigned int d_flags; /* 目录项高速缓存标志*/ spinlock_t d_lock; /* 保护目录项的自旋锁*/ struct inode *d_inode; /* 与文件名关联的索引节点 */ struct hlist_node d_hash; /* lookup hash list */ struct dentry *d_parent; /* 父目录的目录项对象*/ struct qstr d_name;//文件名 struct list_head d_lru; /* 用于未使用目录项链表指针 */ union {
*/
struct list_head d_child; /*对目录而言,用于同一父目录中的目录项链表指针
struct rcu_head d_rcu;//回收目录项对象时,由RCU描述符使用 } d_u;
struct list_head d_subdirs; /* 对目录而言,子目录项链表的头 */ struct list_head d_alias; /*用于与同一索引节点(别名)相关的目录项链表指针 */ unsigned long d_time; /* 由d_revalidate方法使用 */ struct dentry_operations *d_op;//目录项方法 struct super_block *d_sb; /*文件的超级块对象 */ void *d_fsdata; /*依赖于文件系统的数据 */ #ifdef CONFIG_PROFILING struct dcookie_struct *d_cookie; /* 指向内核配置文件使用的数据结构指针 */ #endif };
int d_mounted;//对目录而言,用于记录安装该目录项的文件系统数的计数器 unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 存放短文件名的空间*/
2.3.7 Sk_buff(套接字缓冲区)
套接字缓冲区用结构体struct sk_buff表示,它用于在网络子系统中的各层之间传递数据,处于一个核心地位,非常之重要。
《Linux设备驱动程序》第三版第522页
2.3.8 Net_device
Net_device结构位于网络驱动程序层的最核心地位。Net_device数据结构的完整描述在《Linux设备驱动程序》第三版499页。
2.3.9 Kern_ipc_perm(《深入理解LINUX内核》第783页)
消息队列中,重要的数据结构:
struct kern_ipc_perm { };
spinlock_t lock;//保护IPC资源描述符的自旋锁 int deleted;//如果资源已被释放,则设置该标志 key_t key;//IPC的关键字 uid_t uid;//属主用户ID gid_t gid;//属主组ID uid_t cuid;//创建者用户ID gid_t cgid;//创建者组ID mode_t mode; //许可权掩码 unsigned long seq;//位置使用序号 void *security;//安全结构指针
2.3.10 Msg_msg(《深入理解LINUX内核》第791页)
每一个消息都有一个msg_msg结构与之对应
struct msg_msg { };
struct list_head m_list; //用于消息链表的指针 long m_type; //消息类型 int m_ts; /*消息正文的大小*/ struct msg_msgseg* next;//消息的下一部分 void *security;//安全数据结构指针
2.4 安全结构
在介绍Hooks中介绍。
2.5 Hooks函数 2.5.1 Task Hooks
要详细了解Task Hooks,必须对进程管理有所了解。下面会对进程管理进行简短的介绍。
进程在操作系统中执行特定的任务。而程序是存储在磁盘上包含可执行机器指令和数据的静态实体。进程或者任务是处于活动状态的计算机程序。
进程是一个随执行过程不断变化的实体。和程序要包含指令和数据一样,进程也包含程序计数器和所有CPU寄存器的值,同时它的堆栈中存储着如子程序参数、返回地址以及变量之类的临时数据。当前的执行程序,或者说进程,包含着当前处理器中的活动状态。Linux是一个多处理操作系统。进程具有独立的权限与职责。如果系统中某个进程崩溃,它不会影响到其余的进程。每个进程运行在其各自的虚拟地址空间中,通过核心控制下可靠的通讯机制,它们之间才能发生联系。
进程在生命期内将使用系统中的资源。它利用系统中的CPU来执行指令,在物理内存来放置指令和数据。使用文件系统提供的功能打开并使用文件,同时直接或者间接的使用物理设备。Linux必须跟踪系统中每个进程以及资源,以便在进程间实现资源的公平分配。如果系统有一个进程独占了大部分物理内存或者CPU的使用时间,这种情况对系统中的其它进程是不公平的。
系统中最宝贵的资源是CPU,通常系统中只有一个CPU。Linux是一个多处理操作系统,它最终的目的是:任何时刻系统中的每个CPU上都有任务执行,从而提高CPU的利用率。如果进程个数多于CPU的个数,则有些进程必须等待到CPU空闲时才可以运行。多处理是的思路很简单;当进程需要某个系统资源时它将停止执行并等待到资源可用时才继续运行。单处理系统中,如DOS,此时CPU将处于空等状态,这个时间将被浪费掉。在多处理系统中,因为可以同时存在多个进程,所以当某个进程开始等待时,操作系统将把CPU控制权拿过来并交给其它可以运行的进程。调度器负责选择适当的进程来运行,Linux使用一些调度策略以保证CPU分配的公平性。
在介绍hook函数之前还必须介绍一下task的安全结构: struct task_security_struct { struct task_struct *task; /* 指向原task对象 */
};
u32 osid; /* 执行该进程的SID,用于安全执行检查*/ u32 sid; /* 当前进程的SID */ u32 exec_sid; /* exec SID */ u32 create_sid; /* fscreate SID */ u32 keycreate_sid; /* keycreate SID */ u32 sockcreate_sid; /* fscreate SID */
u32 ptrace_sid; /* SID of ptrace parent */
下面就对Task相关的hook函数,它们大部分都是调用task_has_perm进行权限检查的。static int task_has_perm(struct task_struct *tsk1,struct task_struct *tsk2,u32 perms),第一个参数是主体进程,第二个参数是客体进程,第三个参数表示申请要检查的权限。返回值为0时,表示检查通过。否则不同过。下面对具体的函数进行一一介绍::
int (*task_create) (unsigned long clone_flags);
该函数在调用fork时调用。它调用static int task_has_perm(struct task_struct *tsk1,struct task_struct *tsk2,u32 perms)主体和客体都是当前进程,perms是所要申请检查的权限。
int (*task_alloc_security) (struct task_struct * p); void (*task_free_security) (struct task_struct * p);
动态分配或释放安全结构的内存。分配时,除了task成员不同,其它的数据成员的值都和当前进程(即父进程)一样。
int (*task_setuid) (uid_t id0, uid_t id1, uid_t id2, int flags);//特权操作的Hook
既然setuid只影响当前进程,而selinux访问控制不是基于linux的身份属性的,所以selinux不需要对该操作进行访问控制。但是,selinux不能控制权能hook使用CAP_SETUID和CAP_SETGID。因为capability(权能)是和selinux独立的,互相不影响。
int (*task_post_setuid) (uid_t old_ruid /* or fsuid */ , uid_t old_euid, uid_t old_suid, int flags);
不作任何操作
int (*task_setgid) (gid_t id0, gid_t id1, gid_t id2, int flags); 同task_setuid都需要,原因也一样。
int (*task_setpgid) (struct task_struct * p, pid_t pgid);
对PROCESS_SETPGID权限进行检查,主体和客体分别为current和p int (*task_getpgid) (struct task_struct * p);
对PROCESS__GETPGID权限进行检查,主体和客体分别为current和p int (*task_getsid) (struct task_struct * p); PROCESS_GETSESSION。
void (*task_getsecid) (struct task_struct * p, u32 * secid);
调用selinux_get_task_sid(p,secid),该函数的功能获取进程的sid。当selinux关闭时,sid被赋值为0。
int (*task_setgroups) (struct group_info *group_info); 同set_uid。
int (*task_setnice) (struct task_struct * p, int nice);//资源管理的Hook 不作任何操作,只是检查进程的PROCESS__SETSCHED权限。 int (*task_setioprio) (struct task_struct * p, int ioprio);
只检查当前进程对进程p是否有设置io优先级(PROCESS__SETSCHED)权限
int (*task_getioprio) (struct task_struct * p);
只检查当前进程对进程p是否有PROCESS__GETSCHED权限。
int (*task_setrlimit) (unsigned int resource, struct rlimit * new_rlim);//资源管理的Hook 对硬件资源限制的设置进行控制。
int (*task_setscheduler) (struct task_struct * p, int policy,struct sched_param * lp); 设置进程的调度策略,直接对PROCESS_SETSCHED权限进行检查。 int (*task_getscheduler) (struct task_struct * p);
获取进程的调度策略,对PROCESS_GETSCHED权限进行检查。 int (*task_movememory) (struct task_struct * p); 对PROCESS__SETSCHED权限进行检查。
int (*task_kill) (struct task_struct * p, struct siginfo * info, int sig, u32 secid);//控制进程间通信的Hook
该函数的功能是对杀死进程进行权限检查,如果参数secid不为0,则利用secid作为主体进行策略检查,否则直接利用当前进程作为主体进行检查。
int (*task_wait) (struct task_struct * p);
对p->exit_signal权限进行检查,主体和客体分别为当前进程current和p。 int (*task_prctl) (int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
对当前进程没有任何影响,故不作任何检查。
void (*task_reparent_to_init) (struct task_struct * p);
将进程p安全域的sid赋值给osid,sid被覆盖为SECINITSID_KERNEL void (*task_to_inode)(struct task_struct *p, struct inode *inode);
为inode节点设置进程相关的安全属性,将task的sid赋值给inode的sid。并将initialized成员置为1。
2.5.2 Program Loading Hooks
要详细了解selinux启动的钩子,必须要对linux程序的加载有所了解。下面是对linux程序加载的简单介绍。
一个进程在内存中主要占用了以下几个部分,分别是代码段、数据段、BSS,栈,堆,等参数。其中,代码、数据、BSS的内容是可执行文件中对应的内容,加载程序并不是把它们的内容从可执行程序中填充到内存中,而是将它们的信息(基地址、长度等)更新到进程控制块(task_struct)中,当CPU第 一次实际寻址执行的时候,就会引起缺页中断,操作系统再将实际的内容从可执行文件中复制内容到物理内存中。
堆的内容是程序执行中动态分配的,所以加载程序 只是将它的起始地址更新到进程控制块中,执行过程中遇到动态分配内存的操作的时候再在物理内存分配实际的页。参数区在新进程加载的时候要存入环境变量和命令行参数列表。栈在程序加载时候存入的内容就是环境参数列表和命令行参数列表的指针和命令行参数的个数。
关于linux_binprm结构在前面已经介绍,下面就具体介绍一下它所附加的安全结构: struct bprm_security_struct { struct linux_binprm *bprm; /*指向bprm对象*/ u32 sid; /* 进程转换SID */ unsigned char set;
char unsafe;//为bprm_post_apply_creds()提供共享失败信息,该信息来自于bprm_apply_creds() };
下面是主要hook函数的说明:
int (*bprm_alloc_security) (struct linux_binprm * bprm);//分配安全域 void (*bprm_free_security) (struct linux_binprm * bprm);//释放安全域
上述函数给安全结构动态分配内存,初始化为默认值,sid赋值为SECINITSID_UNLABELED,set赋值为0,unsafe未被初始化。并将返回的指针赋值给linux_binprm的void *security,该安全域主要是为了记录安全信息的,以供后面使用。 void (*bprm_apply_creds) (struct linux_binprm * bprm, int unsafe);
该函数的功能是计算并设置进程的转换属性,该属性的转换是根据旧的属性和set_security所设置的bprm_security值。执行该操作是会调用进程锁,不会返回一个错误结果。 void (*bprm_post_apply_creds) (struct linux_binprm * bprm);
在bprm_apply_creds操作之后执行,也需要进程锁进行互斥操作,这个hook函数能够执行进程的状态改变,如当属性改变后,哪些已经打开的文件描述符不能够再访问。 int (*bprm_set_security) (struct linux_binprm * bprm);
该函数的功能是设置安全域中的信息,主要是安全域中sid。它的信息来源于当前进程的安全信息和二进制文件的inode中的安全信息。Set成员表示该安全域是否被设置。Sid默认为当前进程的sid。如果当前进程的exec_sid成员为空,则需要通过默认sid转换算出sid,否则为默认值。
接着就是检验设置的正确性,如果sid为默认值,需要检验是否存在域转换,如果不存在域转换,成功设置,否则返回错误。如果sid不为默认值,则产生了域转换,需要检验产生了域转换,否则返回错误。同时还需检验是否具有FILE_ENTRYPOINT权限,如果不具有,则返回错误值。 int (*bprm_check_security) (struct linux_binprm * bprm);
在调用该函数之前必须调用set_security,在set_security函数已做了权限检查,该函数没有什么需要处理的。 int (*bprm_secureexec) (struct linux_binprm * bprm); 检查程序是否被安全执行,对那些产生域转换的进程进行检查。该检查发生在进程已成功启动。
2.5.3 IPC Hooks
按照管理先简单介绍一下进程通信IPC: Linux下进程间通信的几种主要手段简介: 1.管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
2.信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
上面对IPC的通信原理做了详细的介绍,下面对IPC内核数据结构的安全域进行详细的分析:
kern_ipc_perm的安全域: struct ipc_security_struct { struct kern_ipc_perm *ipc_perm; /*指向所属对象 */ u16 sclass; /* 该主体的安全类型*/ u32 sid; /* IPC资源的sid */ };
msg_msg的安全结构: struct msg_security_struct { struct msg_msg *msg; /*指向所属对象*/ u32 sid; /*消息的sid*/ }; 从上面看IPC通信所涉及的安全信息比较少。对IPC Hook函数介绍之前还需要对一个重要的函数进行介绍:static int ipc_has_perm(struct kern_ipc_perm *ipc_perms,u32 perms),它是调用avc_has_perm对所申请的权限进行检查,该主体为当前的进程,而客体为ipc_has_perm的第一个参数。第三种对avc_has_perm进行了详细的介绍。
int (*ipc_permission) (struct kern_ipc_perm * ipcp, short flag);
在该函数中,首先对S_IRUGO,S_IWUGO两个信号量的标志,转化为selinux的权限。接着调用ipc_has_perm进行权限校验。
int (*msg_msg_alloc_security) (struct msg_msg * msg); void (*msg_msg_free_security) (struct msg_msg * msg);
给msg_msg分配安全结构msg_security_struct,并将其sid初始化为
SECINITSID_UNLABELED。释放函数的功能就是释放该结构所占用的内存空间。
int (*msg_queue_alloc_security) (struct msg_queue * msq); void (*msg_queue_free_security) (struct msg_queue * msq);
消息队列的安全操作,kern_ipc_perm是消息队列中的一个结构数据成员。该操作是给消息队列结构,即kern_ipc_perm分配安全域空间。一个消息队列只包含一个kern_ipc_perm实例。首先是给调用ipc_alloc_security分配ipc_security_struct安全域,其中sclass成员和ipc_perm成员都是冲过参数传递进去的,而sid成员设置为task的sid。然后检查当前进程是否有创建消息队列的权限,如果没有释放所申请的空间。
int (*msg_queue_associate) (struct msg_queue * msq, int msqflg);
响应msgget系统调用请求时检查MSGQ_ASSOCIATE权限。该函数只会在返回已存在的消息队列标识是调用,如果是新的消息队列被创建,则不调用。int (*msg_queue_msgctl) (struct msg_queue * msq, int cmd);
当对消息队列执行控制操作时,根据cmd检查权限。cmd包括:
IPC_INFO,MSG_INFO,IPC_STAT,MSG_STAT,IPC_SET,IPC_RMID,当cmd为MSG_INFO,检查当前进程是否有SYSTEM_IPC_INFO权限,客体标签为SECINITSID_KERNEL.其它都是根据cmd设置相应的权限值,并检查当前进程对消息队列是否拥有这些权限。
int (*msg_queue_msgsnd) (struct msg_queue * msq, struct msg_msg * msg, int msqflg);
消息插入队列时进行权限检查。该函数涉及到三个安全域,分别是:当前进程的安全域,消息队列的安全域,消息的安全域。首先检查消息是否打上标签,如果没有打上标签,则先通过security_transition_sid计算出消息的sid。该函数会在后面章节详细讲到。接着需要检查当前进程是否有权限写该队列,如果有写的权限,下一步就需要检查当前进程是否有权限发送该消息,如果通过检查,最后还需要检查该消息是否能够插入到消息队列中。至此,当所有的检查都通过后,才会返回成功值,如果其中有一个不通过,就会返回错误值。
int (*msg_queue_msgrcv) (struct msg_queue * msq, struct msg_msg * msg, struct task_struct * target,long type, int mode);
该函数检查消息队列的接受信息的权限。同上面函数一样,涉及到三个对象的安全域,它们分别为,当前进程,消息队列,接受的消息。第一步是检查进程是否对消息队列拥有读的权限。第二步是检查进程是否有对消息结构的权利。
2.5.4 File System Hooks
操作系统中负责管理和存储文件信息的软件机构称为文件管理系统,简称文件系统。文件系统由三部分组成:与文件管理有关的软件、被管理的文件以及实施文件管理所需的数据结构。从系统角度来看,文件系统是对文件存储器空间进行组织和分配,负责文件的存储并对存入的文件进行保护和检索的系统。具体地说,它负责为用户建立文件,存入、读出、修改、转储文件,控制文件的存取,当用户不再使用时撤销文件等。
Linux的第一个版本是基于Minix文件系统的。当 Linux成熟时,引入了扩展文件系统(Ext FS) ,它包含了几个重要的扩展,但提供的性能不令人满意。在 1994 年引入了第二扩展文件系统(second Extended Filesystem,Ext2) 其特点为存取文件的性能极好,对于中小型的文件更显示出优势;它除了包含几个新的特点外,还相当高效和强健,经过逐步改进EXT3文件系统已成为广泛使用的 Linux文件系统。
Linux文件系统由两大模块组成:虚拟文件系统和实际的文件系统。虚拟文件系统为用户空间程序提供文件系统接口的调用,也是保证系统内核各种文件系统实现能够共存的抽象层。
Linux文件系统框架如下:
VFS使得Linux可以支持多个不同的文件系统,每个表示一个VFS 的通用接口。由于软件将Linux 文件系统的所有细节进行了转换,所以Linux核心的其它部分及系统中运行的程序将看到统一的文件系统。Linux 的虚拟文件系统允许用户同时能透明地安装许多不同的文件系统。Linux 2.6 内核中,几乎支持所有现存的文件系统格式,例如ext3, ext2, xia, minix, umsdos, msdos, vfat, proc, smb, ncp, iso9660, sysv, hpfs, affs, ufs等等。
虚拟文件系统是在内核实现的一个软件层,它既为用户空间程序提供文件系统接口的调用,也是保证系统内核各种文件系统实现能够共存的抽象层。
VFS系统调用如open(2), stat(2), read(2), write(2), chmod(2)等在进程上下文中被调用。文件系统锁在文档Documentation/filesystems/Locking中描述。 目录结构缓存(dcache):
VFS实现了open(2), stat(2), chmod(2)以及其它类似的系统调用。VFS利用传递给这些调用的文件路径参数在目录入口缓存(dcache 或者 目录机构缓存)中进行查找。通过一种快速的转换机制将文件路径转化为特定的目录入口。目录结构缓存只存在在RAM中而从不写入磁盘,他们只是为了提高性能而创建的。
目录结构缓存是整个文件空间的视图,但是绝大多数计算机并没有足够的空间将所有的目录结构同时放在RAM中,因此某些缓存将会丢弃。为了解决目录结构中的路径,VFS也许需要按照路径结构重新创建该缓存,并加载inode节点。这通过查找inode节点实现。 Inode节点对象:
一个独立的目录结构通常会有一个指向一个inode的指针。Inodes是文件系统的对象,例如普通文件,目录,FIFO等。他们或者存在于磁盘上(快设备文件系统)或者内存中。被请求访问的inode从磁盘加载道内存,在修改后再写入磁盘。一个独立的inode可以同时作为多个目录结构的指针目标(如硬链接时会出现这种情况)。
查找一个inode时,需要通过VFS对inode的父目录inode调用looup()方法。该方法在inode所属的具体文件系统中实现。一旦VFS请求了目录结构(同时inode),我们就可以通过open(2)打开文件,或者stat(2)查看inode数据。stat(2)操作很简单,一旦VFS获得了目录结构,它就察看inode数据,并将其中某些数据传给用户空间。 文件对象:
打开一个文件还需要其它的操作:获取文件结构(内核实现的文件描述表)。最新分配的文件结构被初始化为一个指向目录结构的指针和一组文件操作函数集合。这些数据从inode获得。然后调用特定的文件系统实现的open()文件操作执行相关的任务。这是VFS交换功能的一个实现。文件结构被加入进程的文件表述表。
读、写和关闭文件(以及其它相关的VFS操作)通过用户空间的文件描述表获取对应的文件结构,然后调用请求的文件结构来实现需要得功能。一旦文件被打开,目录结构将保持
在使用状态,表示VFS inode处于被访问状态。
Super Block Hooks
有关于super_block数据结构,在前面已经详细的描述过,这里就不再重复了。下面就对super_block的安全结构进行详细介绍:
struct superblock_security_struct { struct super_block *sb; /* 指向所属super_block结构*/ struct list_head list; /*superblock_security_struct链表 */ u32 sid; /* SID of file system superblock*/ u32 def_sid; /* default SID for labeling */ u32 mntpoint_sid; /*文件系统挂载点的安全上下文 */ unsigned int behavior; /*打标签操作*/ unsigned char initialized; /*初始化标志*/ unsigned char proc; /*proc文件系统*/ struct mutex lock;//信号锁 struct list_head isec_head;//inode安全域链表 spinlock_t isec_lock;//自旋锁 };
介绍完了安全结构,我们开始介绍Super Block Hooks: int (*sb_alloc_security) (struct super_block * sb); void (*sb_free_security) (struct super_block * sb);
上述两函数主要功能是为super_block结构动态分配和释放安全数据成员的内存,由于superblock_security_struct数据成员比较多,其中包括链表和信号量,所以初始化也就比较复杂,下面截取了部分源码,能够很清楚的表明各个数据成员的默认值,其中sbsec是新动态分配的安全结构:
mutex_init(&sbsec->lock); INIT_LIST_HEAD(&sbsec->list); INIT_LIST_HEAD(&sbsec->isec_head); spin_lock_init(&sbsec->isec_lock); sbsec->sb = sb; sbsec->sid = SECINITSID_UNLABELED; sbsec->def_sid = SECINITSID_FILE; sbsec->mntpoint_sid = SECINITSID_UNLABELED;
int (*sb_copy_data)(struct file_system_type *type,void *orig, void *copy);
把数据从orig拷贝到copy中,如果是二进制挂载数据,则直接进行拷贝。(具体这个还没有弄清楚
int (*sb_kern_mount) (struct super_block *sb, void *data); 首先调用superblock_doinit为data建立sb,这个函数细节比较复杂,会在后面进行详细介绍。初始化成功后,对挂载操作进行权限检查,主体是当前进程,客体是sb,申请检查的权限是FILESYSTEM_MOUNT。
int (*sb_statfs) (struct dentry *dentry);
该函数的首先进行审计数据结构进行初始化,审计系统部分,我们这里就忽略不讲,因为这不是我们任务的重点。这一点在前面我没有提及,文章后半部分也不会提及。主体是当前进程,客体是dentry?d_sb,对FILESYSTEM_GETATTR权限进行检查。
int (*sb_mount) (char *dev_name, struct nameidata * nd,char *type, unsigned long flags, void *data);
该函数的参数比较多,如果是重新挂载,就直接检查FILESYSTEM_REMOUNT权限,主体是当前进程,客体是nd?mnt?mnt_sb。否则,对FILE_MOUNTON权限进行检查,主体是当前进程,客体是nd?dentry。是调用dentry_has_perm,最终是调用inode_has_perm,客体的最终是nd?dentry-->d_inode。
int (*sb_umount) (struct vfsmount * mnt, int flags);
卸载要比挂载简单,只对FILESYSTEM__UNMOUNT权限进程检查就行了。主体是当前进程,客体是mnt?mnt_sb。
void (*sb_umount_close) (struct vfsmount * mnt); void (*sb_umount_busy) (struct vfsmount * mnt); void (*sb_post_remount) (struct vfsmount * mnt, unsigned long flags, void *data); void (*sb_post_mountroot) (void); void (*sb_post_addmount) (struct vfsmount * mnt, struct nameidata * mountpoint_nd); int (*sb_pivotroot) (struct nameidata * old_nd, struct nameidata * new_nd); void (*sb_post_pivotroot) (struct nameidata * old_nd, struct nameidata * new_nd); 上述函数是在security_operations中有,但selinux没有改写,后面如果出现这种函数,就直接跳过,不再獒述。
Inode Hooks
dentry在文件系统中是一个很重要的数据结构,文件系统的钩子函数中很多都涉及到该数据结构。为了跟好的理解钩子函数,必须对dentry做简单的介绍。dentry的中文名称是目录项,是linux文件系统中某个索引节点(inode)的链接。这个索引节点可以是文件,也可以是目录。
struct dentry {
atomic_t d_count; //目录项对象使用计数器 unsigned int d_flags; //目录项标志
struct inode * d_inode; //与文件名关联的索引节点 struct dentry * d_parent; //父目录的目录项对象 struct list_head d_hash; //散列表表项的指针 struct list_head d_lru; //未使用链表的指针
struct list_head d_child; //父目录中目录项对象的链表的指针
struct list_head d_subdirs;//对目录而言,表示子目录目录项对象的链表 struct list_head d_alias; //相关索引节点(别名)的链表
int d_mounted; //对于安装点而言,表示被安装文件系统根项 struct qstr d_name; //文件名
unsigned long d_time; /* used by d_revalidate */ struct dentry_operations *d_op; //目录项方法 struct super_block * d_sb; //文件的超级块对象 vunsigned long d_vfs_flags;
void * d_fsdata;//与文件系统相关的数据
unsigned char d_iname [DNAME_INLINE_LEN];// 存放短文件名 };
Inode对应于物理磁盘上的具体对象,dentry是一个内存实体,其中的d_inode成员指向对应的inode节点。也就是说,一个inode可以在运行的时候链接多个dentry,而d_count记录了这个链接的数量。
Inode(索引节点)
struct inode_security_struct {
struct inode *inode; /* 所属的主体 */ struct list_head list; /*inode_security_struct的链表 */ u32 task_sid; /* 创建该节点的进程(如果刚开始就存在于文件系统中,怎么办)*/ u32 sid; /* 所属主体的sid */ u16 sclass; /*该主体的安全类*/ unsigned char initialized; /* 初始化标识 */ struct mutex lock; unsigned char inherit; /*从父目录项继承来的sid*/ };
int (*inode_alloc_security) (struct inode *inode); void (*inode_free_security) (struct inode *inode); 分配安全结构,并进行初始化。初始化的值如下: mutex_init(&isec->lock); INIT_LIST_HEAD(&isec->list); isec->inode = inode; isec->sid = SECINITSID_UNLABELED; isec->sclass = SECCLASS_FILE; isec->task_sid = tsec->sid; inode->i_security = isec;
上述代码中isec值是inode_security_struct结构指针。
int (*inode_init_security) (struct inode *inode, struct inode *dir, char **name, void **value, size_t *len);
该函数涉及到进程、inode、和super_block三个对象,因此它的安全检查也涉及到三个安全域。它们分别被初始化为tsec = current->security; dsec = dir->i_security; sbsec = dir->i_sb->s_security; 接着是计算inode的sid,当tsec的create_sid数据成员不为0,且sbsec的behavior数据成员不为SECURITY_FS_USE_MNTPOINT时,inode的sid为tsec?create_sid。否则,通过security_transition_sid函数计算出inode的sid。如果sbsec已经初始化,则直接把上一阶段获得的sid赋值给inode。否则,错误退出。最后通过sid利用security_sid_to_context给value和len赋值。
int (*inode_create) (struct inode *dir, struct dentry *dentry, int mode);
调用may_create来判断是否拥有创建新节点的权限。Inode的创建涉及到进程,inode节点和超级块,安全检查涉及到它们的安全域。新inode的创建,必须给它安全域的数据成员赋值。首先,需要检查进程对目录的DIR_ADD_NAME和DIR_SEARCH权限。获取新sid的流程和上面一个函数一样。接着就需要检查进程对新sid的FILE_CREATE权限。最
后,需要检查新sid对超级块,即文件系统的FILESYSTEM_ASSOCIATE权限。这个过程中需要检查多个主客体之间不同的权限。
int (*inode_link) (struct dentry *old_dentry,struct inode *dir, struct dentry *new_dentry);
int (*inode_unlink) (struct inode *dir, struct dentry *dentry);
上面两个函数主要是正对文件系统的链接和释放链接操作进行检查,它们都是调用may_link实现自己功能的。may_link函数的功能是检查一个进程是否拥有链接,删除链接和删除文件或目录的权限。may_link涉及到三个安全域,当前进程的安全域和连个inode安全域。根据may_link的第三个参数决定申请检查的权限,主体是进程,客体是inode,检查的权限为DIR_REMOVE_NAME或DIR_ADD_NAME。该次检查后,接着又另外一次检查,它的主体是当前进程,客体是dentry的inode,权限时FILE_LINK,FILE_UNLINK,DIR_RMDIR。该链接是硬链接。
int (*inode_symlink) (struct inode *dir,struct dentry *dentry, const char *old_name);
该函数是检查符号链接,即软链接的权限。软链接需要创建新的文件,所以调用may_create函数。May_create函数在前面已经讲到了。
int (*inode_mkdir) (struct inode *dir, struct dentry *dentry, int mode); 创建新的inode节点,所以调用may_create。
int (*inode_rmdir) (struct inode *dir, struct dentry *dentry); 在inode_link函数中已经讲到了。
int (*inode_mknod) (struct inode *dir, struct dentry *dentry, int mode, dev_t dev); 调用may_create函数。
int (*inode_rename) (struct inode *old_dir, struct dentry *old_dentry, struct inode *new_dir, struct dentry *new_dentry);
调用may_rename(old_inode, old_dentry, new_inode, new_dentry),dentry是inode在内存中的映像,要给inode改名,内存映像也需要改名。首先检查的是进程对原目录的DIR_REMOVE_NAME和DIR_SEARCH权限,接着是对原dentry的FILE_RENAME权限进行检查,第三步是对dentry的DIR_REPARENT权限进行检查,第四步是对新目录的DIR_ADD_NAME,DIR_SEARCH和DIR_REMOVE_NAME等权限进行检查。最后,还需要检查当前进程对新dentry的删除权限。(这个rename操作怎么这么复杂具体的过程是什么)
int (*inode_readlink) (struct dentry *dentry);
调用dentry_has_perm来进行FILE_READ权限进行检查。dentry_has_perm最终是调用inode_has_perm来进行权限检查的,主体是当前进程,客体是dentry的inode对象。
int (*inode_follow_link) (struct dentry *dentry, struct nameidata *nd); 检查主体对客体通过符号链接查询路径名权限。符号链接需要二次读取文件,所以也是对FILE_READ权限进行检查。
int (*inode_permission) (struct inode *inode, int mask, struct nameidata *nd); 主体当前进程,客体是对象,权限为file_mask_to_av(inode?i_mode,mask) int (*inode_setattr) (struct dentry *dentry, struct iattr *attr);
根据iattr的ia_valid数据成员来决定程序的执行流程,如果ia_valid数据成员中ATTR_FORCE为真,则直接返回0,不作任何权限检查。否则,只要ia_valid中有
ATTR_MODE,ATTR_UID,ATTR_GID,ATTR_ATIME_SET,ATTR_MTIME_SET中的一项,就需要对FILE_SETATTR权限进行检查。最后还得对 FILE_WRITE权限进行检查。
int (*inode_getattr) (struct vfsmount *mnt, struct dentry *dentry);
只需要检查进程对dentry的inode节点的FILE_GETATTR权限即可。
int (*inode_setxattr) (struct dentry *dentry, char *name, void *value,size_t size, int flags); 该函数的主要功能是对设置扩展属性进行权限检查,由于涉及到进程,超级块,和inode节点,因此比较复杂。如果我们不能识别所设置的属性值,就简单的对FILE_SETATTR权限做检查。否则,就需要对检查当前进程对inode的FILE_RELABLELFROM权限进行检查。接着通过security_context_to_sid查找到value的sid,并检查当前进程对该sid是否具有FILE_RELABELTO的权限。下一步是需要对当前的类型转换进行验证,看是否合法。最后,还需要检查新sid对超级块是否具有FILESYSTEM_ASSOCIATE权限。 void (*inode_post_setxattr) (struct dentry *dentry, char *name, void *value, size_t size, int flags); 成功设置扩展属性后,对inode安全域进行更新。再次调用上一个函数中调用的security_context_to_sid,查找出新的sid,并把inode安全域的sid设置为新的sid。 int (*inode_getxattr) (struct dentry *dentry, char *name); int (*inode_listxattr) (struct dentry *dentry); 上述两个函数都是对文件的扩展属性的读权限进行检查,所以都是对FILE_GETATTR权限进行检查。并不需要做任何其它操作。 int (*inode_removexattr) (struct dentry *dentry, char *name); 首先需要识别扩展属性,如果扩展属性没有定义,就直接对FILE_SETATTR权限进行检查。(这里貌似有写问题) const char *(*inode_xattr_getsuffix) (void);
获取inode扩展属性的后缀
int (*inode_getsecurity)(const struct inode *inode, const char *name, void *buffer, size_t size, int err);
获取inode的安全上下文,并把它放在buffer中。
int (*inode_setsecurity)(struct inode *inode, const char *name, const void *value, size_t size, int flags);
设置inode的安全上下文,该安全上下文保存在value当中。
int (*inode_listsecurity)(struct inode *inode, char *buffer, size_t buffer_size);
将扩展属性XATTR_NAME_SELINUX输出到buffer当中。
File Hooks
File的安全结构如下:(解决了file结构不确定的问题了) struct file_security_struct { struct file *file; /* 指向所属主体 */ u32 sid; /* 打开文件的描述符sid */ u32 fown_sid; /* 文件所有者的sid */ };
int (*file_permission) (struct file * file, int mask);
通过文件的掩码mask,计算出所要检查的权限,再通过file_has_perm进行权限检查。最后含需要验证socket文件是否已经打上标签,先查看该文件是不是socket文件,如果是socket文件,先查看文件是否打上标签,如果没有,则根据inode的SID给它打上标签。
int (*file_alloc_security) (struct file * file);
给file的安全结构分配内存,并初始化。Sid和fown_sid数据成员都赋值为当前进程的sid。
void (*file_free_security) (struct file * file); 和其它释放内存一样。
int (*file_ioctl) (struct file * file, unsigned int cmd, unsigned long arg); 根据cmd参数,检查当前进程对file结构相应的权限。
int (*file_mmap) (struct file * file,unsigned long reqprot, unsigned long prot,unsigned long flags, unsigned long addr,unsigned long addr_only);
mmap执行文件的内存操作,并将映射放入进程的地址空间。file_mmap函数的功能是在执行mmap之前进行权限检查。
int (*file_mprotect) (struct vm_area_struct * vma,unsigned long reqprot,unsigned long prot);
在改变内存访问权限之前进行权限检查。
int (*file_lock) (struct file * file, unsigned int cmd);
调用file_has_perm检查当前进程对file对象的FILE_LOCK权限。
int (*file_fcntl) (struct file * file, unsigned int cmd,unsigned long arg);
根据cmd的值,调用file_has_perm检查当前进程对file对象的特定权限。
int (*file_set_fowner) (struct file * file);
设置file的安全结构的fown_sid数据成员。
int (*file_send_sigiotask) (struct task_struct * tsk, struct fown_struct * fown, int sig); 检查file向进程tsk的发送io信号的权限。
int (*file_receive) (struct file * file);
该钩子函数允许安全模块去控制进程通过socket IPC接收一个打开的文件描述符。
2.5.5 Network Hooks
安全结构:
struct sk_security_struct { struct sock *sk; /* 指向所属主体对象*/ u32 sid; /*对象 SID */ u32 peer_sid; /* 对等对象的SID*/ #ifdef CONFIG_NETLABEL
u16 sclass; /* sock安全类型*/ enum { /* 网络标签状态*/ NLBL_UNSET = 0, NLBL_REQUIRE, NLBL_LABELED, } nlbl_state; spinlock_t nlbl_lock; /* 保护网络标签状态 */ #endif };
int (*unix_stream_connect) (struct socket * sock,struct socket * other, struct sock * newsk);
在建立UNIX流连接时,检查UNIX_STREAM_SOCKET__CONNECTTO权限,主体是sock,客体对象是other。并为newsk分配sid。
int (*unix_may_send) (struct socket * sock, struct socket * other);
在连接或发送一个数据报是进行策略检查。主体是sock,客体是other,检查的权限是SOCKET_SENDTO.
int (*socket_create) (int family, int type, int protocol, int kern); 创建套接字时,对SOCKET_CREATE进行检查,主体是当前进程。客体的sid:newsid = tsec->sockcreate_sid ? : tsec->sid。
int (*socket_post_create) (struct socket * sock, int family, int type, int protocol, int kern);
套接字创建成功后,设置套接字的安全结构,由于套接字是一种特殊的文件,所以不仅要设置inode_security_struct,还需要设置sk_security_struct安全结构。
int (*socket_bind) (struct socket * sock,struct sockaddr * address, int addrlen); 检查SOCKET_BIND权限,主体是当前进程,客体是sock。
int (*socket_connect) (struct socket * sock,struct sockaddr * address, int addrlen); 先调用socket_has_perm检查SOCKET_CONNECT权限,主体是当前进程,客体是sock。如果是TCP或DCCP套接字,还需检查name_connect的权限。主体是sock的inode,客体是端口,客体类型是inode的客体类型。权限码是TCP_SOCKET__NAME_CONNECT 或DCCP_SOCKET__NAME_CONNECT。
int (*socket_listen) (struct socket * sock, int backlog); 调用socket_has_perm,检查SOCKET_LISTEN权限,主体是当前进程,客体是sock。客体对象是sock的客体类型。
int (*socket_accept) (struct socket * sock, struct socket * newsock); 调用socket_has_perm,检查SOCKET_ACCEPT权限,主体是当前进程,客体是sock。并设置newsock的inode的安全结构。
int (*socket_sendmsg) (struct socket * sock,struct msghdr * msg, int size); 先检查当前进程对sock的SOCKET_WRITE的权限,接着验证套接字是否是NetLabel标签。实现该功能的是selinux_netlbl_inode_permission(struct inode *inode,int mask),查找文件的inode,并查看该套接字是否标记为被NetLabel保护,然后验证该套接字是否已经打上标记,如果没有,就用inode的sid立即给套接字打上标签。
int (*socket_recvmsg) (struct socket * sock,struct msghdr * msg, int size, int flags); 检查当前进程对sock的SOCKET_READ的访问权限。
int (*socket_getsockname) (struct socket * sock);
检查当前进程对sock的SOCKET_GETATTR的权限。
int (*socket_getpeername) (struct socket * sock);
调用socket_has_perm检查当前进程对sock的SOCKET_GETATTR权限。
int (*socket_getsockopt) (struct socket * sock, int level, int optname);
调用socket_has_perm检查当前进程对sock的SOCKET_GETOPT权限。 int (*socket_setsockopt) (struct socket * sock, int level, int optname);
先调用socket_has_perm检查当前进程对sock的SOCKET_SETOPT权限。接着调用selinux_netlbl_socket_setsockopt(),不允许用户删除NetLabel。该函数首先检查setsockopt()函数,如果用户企图替换套接字IP选项和套接字的NetLabel,就阻止该操作的继续执行。
int (*socket_shutdown) (struct socket * sock, int how);
调用socket_has_perm检查当前进程对sock的SOCKET_SHUTDOWN权限。
int (*socket_sock_rcv_skb) (struct sock * sk, struct sk_buff * skb);
int (*socket_getpeersec_stream) (struct socket *sock, char __user *optval, int __user *optlen, unsigned len);
int (*socket_getpeersec_dgram) (struct socket *sock, struct sk_buff *skb, u32 *secid);
int (*sk_alloc_security) (struct sock *sk, int family, gfp_t priority); void (*sk_free_security) (struct sock *sk);
给sock分配安全域或释放安全域,安全域为sk_security_struct
void (*sk_clone_security) (const struct sock *sk, struct sock *newsk);
void (*sk_getsecid) (struct sock *sk, u32 *secid);
void (*sock_graft)(struct sock* sk, struct socket *parent);
int (*inet_conn_request)(struct sock *sk, struct sk_buff *skb,
struct request_sock *req);
void (*inet_csk_clone)(struct sock *newsk, const struct request_sock *req);
void (*inet_conn_established)(struct sock *sk, struct sk_buff *skb);
void (*req_classify_flow)(const struct request_sock *req, struct flowi *fl);
2.5.6 Other Hooks 2.6 SELinux 初始化
第三章 AVC
3.1 设计目标
缓存决策结果,提高查询效率
3.2 数据结构
下面是AVC模块中重要的数据结构: struct avc_entry { u32 ssid; u32 tsid; u16 tclass; struct av_decision avd; atomic_t used; /* used recently */ };
struct avc_node { struct avc_entry ae; struct list_head list; struct rcu_head rhead; };
struct avc_cache { struct list_head slots[AVC_CACHE_SLOTS]; spinlock_t slots_lock[AVC_CACHE_SLOTS]; /* lock for writes */ atomic_t lru_hint; /* LRU hint for reclaim scan */ atomic_t active_nodes; u32 latest_notif; /* latest revocation notification */
};
struct avc_callback_node { int (*callback) (u32 event, u32 ssid, u32 tsid, u16 tclass, u32 perms, u32 *out_retained); u32 events; u32 ssid; u32 tsid; u16 tclass; u32 perms; struct avc_callback_node *next; };
为了能够更好的了解AVC模块中数据结构之间的关系,我们来详细的分析一下它们的初始化工作,初始化功能主要在avc_init函数中实现,下面是它的源码:
void __init avc_init(void) { int i; for (i = 0; i < AVC_CACHE_SLOTS; i++) { INIT_LIST_HEAD(&avc_cache.slots[i]); spin_lock_init(&avc_cache.slots_lock[i]); } atomic_set(&avc_cache.active_nodes, 0);//将avc_cache的active_nodes设置为0 atomic_set(&avc_cache.lru_hint, 0);//将avc_cache的lru_hint设置为0,它们必须是原子操作 avc_node_cachep = kmem_cache_create(\ 0, SLAB_PANIC, NULL);//给avc_node非配内存 audit_log(current->audit_context, GFP_KERNEL, AUDIT_KERNEL, \} 因为avc_cache中slots数据成员是一个结构数组,所以采取循环方式,对它的数据成员进行初始化。它的每一项是一个双向链表,调用INIT_LIST_HEAD()便可以对其进行赋值为空链表。和它一起初始化的还有自旋锁。从上面的函数我们还不能看出avc_node和avc_cache之间的关系,必须通过它们的操作,如avc_insert,avc_lookup等,才能理解其中联系。它们是通过list_head数据结构联系在一起,avc_cache中的list_head数组中的中存储的是avc_node数据成员。这种设计很巧妙,有很多设计都是我们平时不能想象的,如通过数据成员指针,获取结构的指针。AVC整体的存储架构是Hash表。通过Hash表的思想,就能够比较容易的理解这段代码。
3.3 重要的操作
AVC的重要操作都在security/selinux/avc.c中。其中大多是和Hash表相关的操作。
3.4 接口
AVC提供了从安全服务器获得的访问策略的缓冲区(cache),提高了安全机制的运行性能。它提供了hook函数高效检查授权的接口,提供了安全服务器管理cache的接口。与hook函数的接口定义在include/avc.h中,与服务器的接口定义在include/avc_ss.h中,AVC代码在avc.c中。(这些接口应该很好找的) Avc接口函数主要是被Hooks函数调用,下面就看几个例子:
static int selinux_task_create(unsigned long clone_flags) { int rc; rc = secondary_ops->task_create(clone_flags); if (rc) return rc; return task_has_perm(current, current, PROCESS__FORK); }
static int task_has_perm(struct task_struct *tsk1, struct task_struct *tsk2, u32 perms) { struct task_security_struct *tsec1, *tsec2; tsec1 = tsk1->security; tsec2 = tsk2->security; return avc_has_perm(tsec1->sid, tsec2->sid, SECCLASS_PROCESS, perms, NULL); }
上面举了几个简单的例子,下面就详细的介绍这些接口的功能。调用接口avc_init()上面已经介绍了。下面来介绍调用接口:avc_has_perm()该函数非常重要,是selinux Hooks和AVC的查询接口,下面我就对它的进行详细讲解: 首先我们来看一下源码:
int avc_has_perm(u32 ssid, u32 tsid, u16 tclass,
u32 requested, struct avc_audit_data *auditdata) { struct av_decision avd; int rc; rc = avc_has_perm_noaudit(ssid, tsid, tclass, requested, 0, &avd); avc_audit(ssid, tsid, tclass, requested, &avd, rc, auditdata); return rc; }
该函数的功能是检查权限并执行合适的统计检查。参数ssid是主体的安全标识,参数
tsid,客体的安全标识,参数tclass是客体安全类型,参数requested是申请检查的权限,参数auditdata是附加的审计数据。由源码我们可以看到该函数调用两个函数,一个是avc_has_perm_noaudit,一个是avc_audit,两个函数把整个功能划分成两部分,一部分是仲裁权限的,另一部分是负责审计。从源码中还可以看出函数的返回值与后者无关,完全由前者决定。
为了集中注意力,讲解的重点,审计部分先放在一边。下面是avc_has_perm_audit的源码:
int avc_has_perm_noaudit(u32 ssid, u32 tsid, { u16 tclass, u32 requested, unsigned flags, struct av_decision *avd) struct avc_node *node; struct avc_entry entry, *p_ae; int rc = 0; u32 denied; rcu_read_lock(); node = avc_lookup(ssid, tsid, tclass, requested); if (!node) { rcu_read_unlock(); rc = security_compute_av(ssid,tsid,tclass,requested,&entry.avd); if (rc) goto out; rcu_read_lock(); node = avc_insert(ssid,tsid,tclass,&entry); } p_ae = node ? &node->ae : &entry; if (avd) memcpy(avd, &p_ae->avd, sizeof(*avd)); denied = requested & ~(p_ae->avd.allowed); if (!requested || denied) { if (selinux_enforcing || (flags & AVC_STRICT)) rc = -EACCES; else if (node) avc_update_node(AVC_CALLBACK_GRANT,requested, ssid,tsid,tclass); }
rcu_read_unlock(); out: return rc; }
首先加上读保护锁,接着调用avc_lookup()查询AVC缓冲向量,如果返回值为空,表示该权限以前没有被查找过,通过调用安全服务器(Security Server)提供的接口security_compute_av()(如上图红色字体所示),获得策略决策avd。并把查询的结果插入到AVC缓冲区中。接着,把所获得策略决策保存到参数avd中,并检查权限是否得到允许。如果请求为空或者denied的参数不为0,且系统处于enforcing模式或者flags为AVC_STRICT标志是,返回拒绝消息。当系统不是enforcing模式且flags为没有含AVC_STRICT标志时,在avc node查询命中时,需要对调用avc_update_node进行更新。如果请求不为空或者denied的参数为0,表示权限获得允许,返回值为0,即表示成功。
最后调用rcu_read_unlock()解除读锁。
avc_init和avc_has_perm是LSM Hooks和AVC最重要的接口,还有其它一些接口,如avc_audit(),因为是实现审计功能,所以在这里没有介绍。Avc_has_perm_noaudit(),在介绍avc_has_perm中已经介绍,avc_add_callback(),avc_get_hash_stats(),这两个函数不是很重要,如果需要了解可以自己阅读源码。
第四章 SELinux安全策略
内核空间的SELinux主要由访问向量缓存(AVC)、安全服务器、安全策略实施机构和SELinuxfs伪文件系统组成。
SELinux的安全服务器:维护主客体的标识信息,根据安全策略以及主客体标识信息计算访问向量。
SELinux的访问向量缓存:维护访问向量,接受安全策略实施机构的查询。 SELinux策略库:存放系统安全管理员定义的安全策略。
SELinux安全策略实施机构:获取主客体的安全信息,通过查询访问向量缓存控制主体对客体的访问。
SELinuxfs:提供用户程序与内核SELinux组件交互的接口。
SELinux的客体管理器:接受用户空间应用程序的策略查询请求。(这个是什么东东) LYSLinux的安全策略?? XML语言配置
4.1 访问控制简介 4.1.1自主访问控制 4.1.2 Capability 4.1.3强制访问控制 4.1.4基于角色的访问控制
标记决策:不经过avc,直接访问安全服务器。客体创建时调用。 访问决策:需要同过avc。
4.2 相关的数据结构 4.2.1 数据结构基础
哈希表(Hash)简介:
一般的线性表,树中,记录在结构中的相对位置是随机的,即和记录的关键字之间不存在确定的关系,因此,在结构中查找记录时需进行一系列和关键字的比较。这一类查找方法建立在“比较“的基础上,查找的效率依赖于查找过程中所进行的比较次数。
理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。 哈希表最常见的例子是以学生学号为关键字的成绩表,1号学生的记录位置在第一条,10号学生的记录位置在第10条...
有了大概的了解,我们下面会讲到后面会用到的一些基本概念。
若结构中存在关键字和K相等的记录,则必定在f(K)的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function),按这个思想建立的表为散列表。
对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称冲突。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数H(key)和处理冲突的方法将一组关键字映象到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“象” 作为记录在表中的存储位置,这种表便称为散列表,这一映象过程称为散列造表或散列,所得的存储位置称散列地址。
若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。
上面对哈希表进行了简单的介绍,有了上面的知识准备,下面我们对哈希表的结构,及
实现进行详细介绍。
下面的数据结构是典型的哈希表中都会用到的, struct hashtab_node {
void *key;//关键字 void *datum;//数据 struct hashtab_node *next;//指向下一个节点 };
struct hashtab {
struct hashtab_node **htable; /* 哈希表 */ u32 size; /* 哈希表的桶数,一般为素数 */ u32 nel; /* 哈希表中元素的个数 */ u32 (*hash_value)(struct hashtab *h, const void *key); /* 哈希函数 */ int (*keycmp)(struct hashtab *h, const void *key1, const void *key2); /* 关键字比较 */ };
Hash表的存储空间都是动态分配,从hashtab结构中可以看到htable是一个二级指针,初始化的时候分配了size个hashtab_node指针,组成一个指针数组。数组的大小在初始化就决定了。把它们用空间图形表示出来,可以很清楚理解他们之间的关系。图形如下图所示:
hashtab指针数组hashtab_nodevoid *keyvoid *datumSidtab_node *nextvoid *keyvoid *datumSidtab_node *nextvoid *keyvoid *datumSidtab_node *nextSidtab_node ** htableU32 sizeU32 nelHash_value functionKeycmp functionhashtab_node *hashtab_node *hashtab_node *hashtab_node *...hashtab_node *void *keyvoid *datumSidtab_node *nextvoid *keyvoid *datumSidtab_node *nextvoid *keyvoid *datumSidtab_node *next 图 Hash表的结构图
Hash表和其它链表一样,最重要两个操作是插入和查找。
链表:
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
相比较顺序结构,链表比较方便插入和删除操作。它包括单链表,循环链表和双向链表。
线性表的链式存储表示的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素 与其直接后继数据元素 之间的逻辑关系,对数据元素 来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。由这两部分信
息组成一个\结点\(如下图所示),表示线性表中一个数据元素 。
根据情况,也可以自己设计链表的其它扩展。但是一般不会在边上附加数据,因为链表的点和边基本上是一一对应的(除了第一个或者最后一个节点,但是也不会产生特殊情况)。不过有一个特例是如果链表支持在链表的一段中把前和后指针反向,反向标记加在边上可能会更方便。
对于非线性的链表,可以参见相关的其他数据结构,例如树、图。另外有一种基于多个线性链表的数据结构:跳表,插入、删除和查找等基本操作的速度可以达到O(nlogn),和平衡二叉树一样。
其中存储数据元素信息的域称作数据域(设域名为data),存储直接后继存储位置的域称为指针域(设域名为next)。指针域中存储的信息又称做指针或链。
位图:
#define MAPTYPE u64 /* 每个node中位图的一部分*/ #define MAPSIZE (sizeof(MAPTYPE) * 8) /* 位图中bit位*/ #define MAPBIT 1ULL /*位图中一个bit位 */
struct ebitmap_node { u32 startbit; /* 所有位图初始位置 */ MAPTYPE map; /*部分位图*/ struct ebitmap_node *next;//指向下一个位图节点 };
struct ebitmap { struct ebitmap_node *node; /*位图中第一个位图节点*/ u32 highbit; /* 位图中最高的位置*/ };
从结构中可以很明显的看出ebitmap的存储结构是单链表结构,struct ebitmap是链表头结点,后面所有的节点类型都是struct ebitmap_node结构的节点,作为一种位图存储结构,除了有链表的一些操作外,还有一些位图特点的操作,这些位图管理函数在selinux/ss/ebitmap.c文件中。
4.2.2 策略库Policydb
策略库policydb结构是用来存储策略文件,是整个策略服务器存储信息的中心,有关于策略的操作都是基于该策略库的。作为策略数据的存储结构,它具有了数据存储的所有特点,数据存储的有序性,高效查找性等。在介绍它的各种操作之前,我们必须了解它所存储的内容以及各数据成员的含义。 struct policydb { /* 符号表 */ struct symtab symtab[SYM_NUM];//以哈希表作为存储结构
#define p_commons symtab[SYM_COMMONS] #define p_classes symtab[SYM_CLASSES] #define p_roles symtab[SYM_ROLES]
#define p_types symtab[SYM_TYPES] #define p_users symtab[SYM_USERS] #define p_bools symtab[SYM_BOOLS] #define p_levels symtab[SYM_LEVELS] #define p_cats symtab[SYM_CATS]
/* 符号名字以(value-1)来索引 */ char **sym_val_to_name[SYM_NUM];
#define p_common_val_to_name sym_val_to_name[SYM_COMMONS] #define p_class_val_to_name sym_val_to_name[SYM_CLASSES] #define p_role_val_to_name sym_val_to_name[SYM_ROLES] #define p_type_val_to_name sym_val_to_name[SYM_TYPES] #define p_user_val_to_name sym_val_to_name[SYM_USERS] #define p_bool_val_to_name sym_val_to_name[SYM_BOOLS] #define p_sens_val_to_name sym_val_to_name[SYM_LEVELS] #define p_cat_val_to_name sym_val_to_name[SYM_CATS]
/* class, role, and user属性,以(value-1)为索引 */ struct class_datum **class_val_to_struct;//数组 struct role_datum **role_val_to_struct;//数组 struct user_datum **user_val_to_struct;//数组 /* 类型裁决访问向量和转换 */ struct avtab te_avtab;//哈希表作为存储 /* 角色转换*/ struct role_trans *role_tr;//链表作为存储结构 /*布尔值,以value-1为索引 */ struct cond_bool_datum **bool_val_to_struct;//数组 /* 类型裁决条件访问向量和转换 */ struct avtab te_cond_avtab;//哈希表 /* 利用conditional链表索引te_cond_avtab */ struct cond_node* cond_list;//链表 /* 角色允许 */ struct role_allow *role_allow;//链表 /* 初始化SID的安全上下文,没标识的文件系统,TCP和UDP端口号,网络接口和节点安全上下文*/ struct ocontext *ocontexts[OCON_NUM];//数组,每一个数据项是链表作为存储结构 /* 文件系统中的文件安全上下文,不支持采用持久的标签或采取固定标签行为 */ struct genfs *genfs;//链表 /* 域转换 */ struct range_trans *range_tr;//链表 /* 违反规则的type -> attribute */ struct ebitmap *type_attr_map;//位图 unsigned int policyvers; };
Policydb数据结构庞大,需要对他们进行一一介绍: (1) symtab(符号表)数组
该符号表的每一项都是以Hash表作为存储结构,关键字key是字符串,数据datum是对应的数据结构,p_commons是common_datum,p_classes是class_datum,p_roles是role_datum,p_types是type_datum,p_users是user_datum,p_bools是cond_bool_datum,p_levels是level_datum,p_cats是cat_datum。这些结构的定义在security/selinux/ss/policydb.h中。
(2) sym_val_to_name
它是一个数组,它的每个成员都是一个字符串数组,对它的查询是通过,value-1来查询的。由于很多常量定义都是宏定义,客体类型的定义,每一个宏都代表一个数字,和数组的下标相关联。由于客体类别是事先都定义好了的,所以可以用数组来存储。它是通过符号表数组来初始化的,初始化后就不再改变。字符数组中的字符串是symtab中Hash表的key值。和symtab数组一一对应。
(3) class,role,user,bool属性转换表
struct class_datum **class_val_to_struct,struct role_datum **role_val_to_struct,struct user_datum **user_val_to_struct,struct cond_bool_datum **bool_val_to_struct,这四个转换表都是根据p_classes,p_roles,p_users,p_bools,属性转换表中存储的是符号表中的数据成员。
(4) te强制访问控制策略
te强制访问控制策略,存储在te_avtab结构和te_cond_avtab结构。他们所采取的存储是一样的,都是采取Hash表作为存储结构。key是avtab_key结构,datum是avtab_datum结构。这些结构在4.2.6节会详细介绍。
(5) Role_trans角色转换
该数据成员是以单链表作为存储结构。链表结构在4.2.1中已经详细介绍了。 (6) Role_allow
Role_allow策略,采取单链表存储结构。 (7) Cond_list
单链表作为存储结构,条件策略,一个cond节点代表策略中的一个条件块。它包含了一个条件表达式,表达式当前的状态和两个规则链表。 struct cond_node { int cur_state; struct cond_expr *expr;//布尔表达式 struct cond_av_list *true_list;//布尔值为真的节点 struct cond_av_list *false_list;//布尔值为假的节点 struct cond_node *next; };
和avtab相关联的数据结构: struct cond_av_list { struct avtab_node *node; struct cond_av_list *next; };
(8) Ocontexts
初始化安全上下文。单链表作为存储结构 (9) Genfs