3.套接字(socket)
UNIX 的哲学是“万物皆文件”。但是起源于 BSD 的套接字打破了这个哲学。等到 UNIX的捍卫者想要将套接字统一进文件时,为时已晚㊀ ,程序员已经熟悉了套接字,改不回来了。
从安全的角度考虑, UNIX 上原生的套接字确实有些问题。 UNIX 在套接字上几乎没有访问控制。作者只能查到两处:一处是只允许代表 root 用户的进程绑定(bind)特权端口,即 tcp和 udp 的端口号在 1024 以下的端口;另一处是发生在 UNIX 本地域的套接字操作中,服务器端bind 操作会创建一个套接字文件,在客户端 connect 操作中会判断客户进程对此套接字文件是否有写许可。但是此处的套接字文件只是一种文件,并不是套接字。
SELinux 虽然弥补了这一缺失,却矫枉过正了。它定义的套接字子类别过多,各类别上的操作也过细。
和文件一样,套接字也有很多种,它们有一些共有的操作许可,其中有一些和文件的一样,如: ioctl、 read、 write、 create、 getattr、 setattr、 lock、 relabelfrom、 relabelto、 append,还有一些是套接字特有的,如: bind、 connect、 listen、 accept、 getopt、 setopt、 shutdown、 recvfrom、sendto、 recv_msg、 send_msg、 name_bind。
(1)套接字与文件共有的操作许可
UNIX 的哲学“万物皆文件”,并非凭空刻意为之。套接字中有很多和文件相同的操作类型。SELinux 让套接字和文件共享一些操作类型,但是做得有些生硬,一些对套接字没有意义的操作类型也混了进来。
以下是套接字的基本操作:
● create 创建套接字
● ioctl*
● lock*
以下操作与数据收发相关:
● read 自套接字读取消息。
● write 向套接字发送消息。
● append*。
以下操作与套接字的地址相关:
● getattr 读取套接字当前绑定地址( getsockname) ,或者读取连接端套接字地址(getpeername)。
● setattr*。
以下操作与 SELinux 相关:
● relabelfrom 改变套接字的安全上下文,使其不再是现在的值。只在 tun_socket 中用到。
● relabelto 改变套接字的安全上下文,使其变为新值。
(2)套接字特有操作许可
以下操作与连接相关:
● bind 对套接字执行系统调用 bind
● name_bind 绑定套接字到特权端口
Linux 系统为网络服务进程定义了一个端口范围,绑定范围外端口需要此项操作许可,关于端口范围可以查询/proc/sys/{ipv4,ipv6}/ip_local_port_range 文件。
● listen 对套接字执行系统调用 listen
● accept 对套接字执行系统调用 accept
● connect 对套接字执行系统调用 connect
以下操作与套接字属性相关:
● getopt 对套接字执行系统调用 getsockopt
● setopt 对套接字执行系统调用 setsockopt
以下操作与套接字的数据收发相关:
● recvfrom 自套接字读取数据包。只被 udp_socket、 tcp_socket、 rawip_socket 使用,即仅适用于主机间通信。
● sendto 向套接字发送数据包。只被 UNIX 套接字使用,即仅适用于同一主机内进程间通信。
● recv_msg*
● send_msg*
以下为其他操作:
● shutdown 对套接字执行系统调用 shutdown
(3)不同的套接字类型上的特有操作
1) tcp_socket
在客体类别“tcp_socket”上的操作包含上面列出的所有套接字与文件共有的操作和所有套接字的特有操作,此外, tcp_socket 还包含如下操作:
● connectto*
● newconn*
● acceptfrom*
● node_bind 将套接字绑定到某个网络地址上
主机可以有多个网络地址,如 127.0.0.1 是回送地址, 192.168.1.1 是内部局域网地址。这个操作限定绑定到某个地址之上。
● name_connect socket 连接到某个网络端口上
赋予端口安全上下文,限制对端口的连接操作。
2) udp_socket
客体类别 udp_socket 的操作既包含所有套接字的共有操作,还包含如下操作:
● node_bind 将 socket 绑定到某个网络地址上
3) rawip_socket
客体类别 rawip_socket 的特有操作如下: ● node_bind 将 socket 绑定到某个网络地址上4) netlink_socket
此套接字类别没有特有操作。5) netlink_route_socket
客体类别 netlink_route_socket 的特有操作如下:
● nlmsg_read 读取信息
● nlmsg_write 写入信息
6) netlink_firewall_socket
客体类别 netlink_firewall_socket 的特有操作如下:
● nlmsg_read 读取信息
● nlmsg_write 写入信息
7) netlink_tcpdiag_socket
客体类别 netlink_tcpdiag_socket 的特有操作如下:
● nlmsg_read 读取信息
● nlmsg_write 写入信息
8) netlink_nflog_socket
此套接字类别没有特有操作。9) netlink_xfrm_socket
客体类别 netlink_xfrm_socket 的特有操作如下:
● nlmsg_read 读取信息● nlmsg_write 写入信息10) netlink_selinux_socket此套接字类别没有特有操作。11) netlink_audit_socket
客体类别 netlink_audit_socket 的特有操作如下:
● nlmsg_read 读取信息
● nlmsg_write 写入信息
● nlmsg_relay 将用户态 audit 消息转发给 audit 服务
● nlmsg_readpriv 列出 audit 配置规则
● nlmsg_tty_audit 控制 tty 审计信息
12) netlink_ip6fw_socket
客体类别 netlink_ip6fw_socket 的特有操作如下:
● nlmsg_read 读取信息
● nlmsg_write 写入信息
13) netlink_dnrt_socket
此套接字类别没有特有操作。
14) netlink_kobject_uevent_socket
此套接字类别没有特有操作。
15) packet_socket
此套接字类别没有特有操作。
16) key_socket
此套接字类别没有特有操作。
17) unix_stream_socket
客体类别“unix_stream_socket”的特有操作如下:
● connectto 连接到服务 socket
● newconn*
● acceptfrom*
18) unix_dgream_socket
此套接字类别没有特有操作。
19) appletalk_socket
此套接字类别没有特有操作。
20) dccp_socket
客体类别 dccp_socket 的特有操作如下:
● node_bind 将 socket 绑定到某个网络地址上
● name_connect 连接 socket 到某个地址
21) tun_socket
客体类别“tun_socket”的特有操作如下:
● attach_queue 附加一个新队列
SELinux 不厌其烦地在套接字上细分出子类别,其实有些过犹不及。
4.进程间通信
进程间通信( Inter-Process Communication, IPC)包括信号灯( Semaphore)、消息队列
(Message Queue)、共享内存(Shared Memory)。它们具有一些共同的操作许可:
(1)进程间通信的共有操作
● create 创建
● destroy 删除
● getattr 读取属性
● setattr 设置属性
● associate 获取 IPC 对象 ID
在系统调用 semget、 msgget、 shmget 中,在获取 IPC 对象前, SELinux 会判断 associate 操
作许可。
● read 读取消息/内容
● write 写入消息/内容
● unix_read 读取
● unix_write 写入
unix_read 和 unix_write 是指传统的 UNIX 中规定的读写操作。它们和打开一个进程间通信对象时给出的操作模式相联系,是只读、只写或读写方式。 read 和 write 的功能也是读和写,其实与 unix_read 和 unix_write 是有重合的。下面看代码:
ipc/shm.c long do_shmat(int shmid, char __user *shmaddr, int shmflg, ulong *raddr, unsigned long shmlba) { … err = -EACCES; if (ipcperms(ns, &shp->shm_perm, acc_mode)) goto out_unlock; err = security_shm_shmat(shp, shmaddr, shmflg); if (err) goto out_unlock; … }上面的代码首先调了进程间通信通用的 ipcperms,然后调了共享内存专有的 security_ shm_shmat。先来看 ipcperms。
ipc/util.c int ipcperms(struct ipc_namespace *ns, struct kern_ipc_perm *ipcp, short flag) { … return security_ipc_permission(ipcp, flag); } security_ipc_permission 会调用 SELinux 的钩子函数 selinux_ipc_permission。 security/selinux/hooks.c static int selinux_ipc_permission(struct kern_ipc_perm *ipcp, short flag) { u32 av = 0; av = 0; if (flag & S_IRUGO) av |= IPC__UNIX_READ; if (flag & S_IWUGO) av |= IPC__UNIX_WRITE; if (av == 0) return 0; return ipc_has_perm(ipcp, av); }总结一下, ipcperms 会间接调用 ipc_has_perm 来判断 unix_read 和 unix_write 操作许可。下面看 security_shm_shmat,它会调用 SELinux 的钩子函数 selinux_ shm_shmat:
security/selinux/hooks.c static int selinux_shm_shmat(struct shmid_kernel *shp, char __user *shmaddr, int shmflg) { u32 perms; if (shmflg & SHM_RDONLY) perms = SHM__READ; else perms = SHM__READ | SHM__WRITE; return ipc_has_perm(&shp->shm_perm, perms); }总结一下, security_shm_shmat 会间接调用 ipc_has_perm 来判断 read 和 write 操作许可。在 SELinux 代码中,有些暗含读语义或写语义的系统调用中也会判断 read 和 write 操作,例如在系统调用 semctl 中,当参数 cmd 是 GETVAL 或 GETALL 时, SELinux 会判断 read操作许可。
security/selinux/hooks.c static int selinux_sem_semctl(struct sem_array *sma, int cmd) { int err; u32 perms; switch (cmd) { … case GETVAL: case GETALL: perms = SEM__READ; break; case SETVAL: case SETALL: perms = SEM__WRITE; break; … default: return 0; } err = ipc_has_perm(&sma->sem_perm, perms); return err; }(2)各种进程间通信上的特有操作
在信号灯上无特有操作;在消息队列上有一个特有操作 enqueue,含义是发送消息到消息队列;在共享内存上的特有操作是 lock,含义是加锁或解锁共享内存。
(3)消息队列中的消息
在消息队列的消息本身也带有安全上下文。
对消息队列中的消息, SELinux 提供了额外的操作许可。
● receive 从队列读取消息
● send 向队列写入消息
细分析 SELinux 代码你会发现,它在有的地方实在是罗嗦。以消息队列为例,为了获得消息队列对象,应用程序要调用系统调用 msgget, SELinux 会根据传入参数 msgflg 检查 unix_read和 unix_write,还会判断 associate。然后应用程序调用 msgrcv 或 msgsnd 时, SELinux 会先判断unix_read 和 unix_write,然后判断 read 和 write,最后还要在消息上判断 receive 和 send。下面看一下代码。
ipc/msg.c SYSCALL_DEFINE2(msgget, key_t, key, int, msgflg) { struct ipc_namespace *ns; struct ipc_ops msg_ops; struct ipc_params msg_params; ns = current->nsproxy->ipc_ns; msg_ops.getnew = newque; msg_ops.associate = msg_security; msg_ops.more_checks = NULL; msg_params.key = key; msg_params.flg = msgflg; return ipcget(ns, &msg_ids(ns), &msg_ops, &msg_params); } ipc/util.c int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids, struct ipc_ops *ops, struct ipc_params *params) { if (params->key == IPC_PRIVATE) return ipcget_new(ns, ids, ops, params); else return ipcget_public(ns, ids, ops, params); } ipc/util.c static int ipcget_public(struct ipc_namespace *ns, struct ipc_ids *ids, struct ipc_ops *ops, struct ipc_params *params) { … err = ipc_check_perms(ns, ipcp, ops, params); … } ipc/util.c static int ipc_check_perms(struct ipc_namespace *ns, struct kern_ipc_perm *ipcp, struct ipc_ops *ops, struct ipc_params *params) { int err; if (ipcperms(ns, ipcp, params->flg)) err = -EACCES; else { err = ops->associate(ipcp, params->flg); if (!err) err = ipcp->id; } return err; }这里的 associate 函数指针指向函数 msg_security。
ipc/msg.c static inline int msg_security(struct kern_ipc_perm *ipcp, int msgflg) { struct msg_queue *msq = container_of(ipcp, struct msg_queue, q_perm); return security_msg_queue_associate(msq, msgflg); } security_msg_queue_associate 会调用 SELinux 的钩子函数。 security/selinux/hooks.c static int selinux_msg_queue_associate(struct msg_queue *msq, int msqflg) { struct ipc_security_struct *isec; struct common_audit_data ad; u32 sid = current_sid(); isec = msq->q_perm.security; ad.type = LSM_AUDIT_DATA_IPC; ad.u.ipc_id = msq->q_perm.key; return avc_has_perm(sid, isec->sid, SECCLASS_MSGQ, MSGQ__ASSOCIATE, &ad); } 以发送为例: ipc/msg.c SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, int, msgflg) { long mtype; if (get_user(mtype, &msgp->mtype)) return -EFAULT; return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg); } ipc/msg.c long do_msgsnd(int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg) { … for (;;) { struct msg_sender s; err = -EACCES; if (ipcperms(ns, &msq->q_perm, S_IWUGO)) goto out_unlock0; … err = security_msg_queue_msgsnd(msq, msg, msgflg); if (err) goto out_unlock0; … } … } security_msg_queue_msgsnd 会调用 SELinux 的钩子函数 selinux_msg_queue_msgsnd: security/selinux/hooks.c static int selinux_msg_queue_msgsnd(struct msg_queue *msq, struct msg_msg *msg, int msqflg) { … rc = avc_has_perm(sid, isec->sid, SECCLASS_MSGQ, MSGQ__WRITE, &ad); if (!rc) /* Can this process send the message */ rc = avc_has_perm(sid, msec->sid, SECCLASS_MSG, MSG__SEND, &ad); if (!rc) /* Can the message be put in the queue? */ rc = avc_has_perm(msec->sid, isec->sid, SECCLASS_MSGQ, MSGQ__ENQUEUE, &ad); return rc; }5.网络
SELinux 对网络对象的标记比较复杂,目前大概有 4 套体系共存:通过 SELinux 网络策略标记、通过标签化的 IP 协议头(CIPSO)标记㊀ 、通过 IPSEC 标记、通过 iptable 的 secmark 标记。其中,标签化网络形式又支持在 IP 协议头没有标记的情况下,根据环境信息(IP 地址、端口号)标记网络,这又和通过 SELinux 网络策略标记重合。
为什么 SELinux 中会有这么多套体系同时作用于网络类的客体呢?作者认为原因是SELinux 在如何为网络类客体设计安全上下文上还不成熟。
为了便于理解,下面只介绍第一个方案。先思考一下网络对象是由什么组成的?首先是节点(node), 节点由 IP 地址限定。 其次, 在节点上有一个进程在收发网络包, 这被定义为“peer”。然后,当网络包到达本地端时,它要经过网卡,这被定义为网络接口“netif”。最后,当我们将控制的粒度细化时,对每一个网络包都可以实施不同的访问控制,网络包就被定义为“packet”。
(1)网络节点(node)
客体类别“node”上有以下操作。这些操作不是同时加入的。在“recvfrom”和“sendto”两个操作加入内核后,其他的操作就不再使用了。
● dccp_recv*
● dccp_send*
● tcp_recv*
● tcp_send*
● udp_recv*
● udp_send*
● rawip_recv*
● rawip_send*
● enforce_dest*
● recvfrom 接收来自网络节点的数据包
● sendto 发送数据包到网络节点
(2)网络接口(netif)
客体类别“netif”上有以下操作。这些操作不是同时加入的。在“ingress”和“egress”两个操作加入内核后,其他的操作就不再使用了。
● tcp_recv*
● tcp_send*
● udp_recv*
● udp_send*
● rawip_recv*
● rawip_send*
● dccp_recv*
● dccp_send*
● ingress 通过网络接口接收数据包
● egress 通过网络接口发送数据包
(3) peer
客体类别“peer”的引入简化了 SELinux 的逻辑㊀ 。其上有一个操作:
● recv 接收消息
(4) packet
客体类别“packet”指网络上传输的数据包。其上有五个操作,可分为两类。
1)基本
● send 发送
● recv 接收
● forward_in 向内转发
● forward_out 向外转发
2) SELinux 相关
● relabelto
标记 SELinux 的安全上下文。奇怪的是,没有 relabelfrom。