SELinux socket访问控制
内核socket架构介绍
socket编程的用户态接口是:1
2int socketpair(int domain, int type, int protocol, int fd[2]);
int socket(int domain, int type, int protocol);
domain
定义在Linux内核目录include/linux/socket.h文件中,常用的有:
我们通常用AF_
(Address Family)取代PF_
(Protocol Family)。在内核代码中也有所体现:
type
当前定义的类型为:
- SOCK_STREAM
- SOCK_DGRAM
- SOCK_RAW
protocol
决定协议的种类。例如针对AF_INET类型socket,可以有不同的网络协议,例如:SOCK_STREAM的默认协议是IPPROTO_TCP,SOCK_DGRAM的默认协议为IPPROTO_UDP。这些协议类型定义的位置,以musl为例,在include/netinet/in.h中。不过大多时候,传0就好了,例如:socket(AF_INET, SOCK_STREAM, 0)
socket和socketpair都会调用系统调用进入内核,对应的系统调用分别是:1
2SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol,int __user *, usockvec)
这两个系统调用会通过sock_create
实例化真正的内核的socket object。1
retval = sock_create(family, type, protocol, &sock);
我们从socket 的family属性可以看到,Linux希望用socket涵盖所有的通信场景。针对不同的socket使用场景,Linux内核采用了类似面向对象的实现方法。
classDiagram class `struct sock_common` class `struct sock` { + void *sk_security } class `struct socket` { + const struct proto_ops ops } `struct sock` o-- `struct sock_common` `struct socket` o-- `struct sock``sock_create`调用`__sock_create`最终通过Address Family找到对应socket类注册的create接口,实现具体对象的构造。
1 | err = pf->create(net, sock, protocol, kern); |
1 | switch (sock->type) { |
graph TD; A["__sock_create"] --> B["security_socket_create"]; B --> C["pf->create"]; C --> D["security_socket_post_create"]
security_socket_create
和security_socket_post_create
对应的SELinux hook函数如下:1
2LSM_HOOK_INIT(socket_create, selinux_socket_create),
LSM_HOOK_INIT(socket_post_create, selinux_socket_post_create)security_socket_create
通过下面的代码实现了:
- family到security class的转换
- socket标签的生成
1
2
3u32 newsid;
secclass = socket_type_to_security_class(family, type, protocol);
rc = socket_sockcreate_sid(tsec, secclass, &newsid);
这里需要注意的是newsid仅仅是一个局部变量,可见该hook点,并未对socket的标签信息进行保存。那么:
- socket object的标签是在哪儿生成的呢?
- socket标签的值具体是什么呢?
标签的生成
第一个问题的答案可以在security_socket_post_create
函数中找到。1
2
3sksec = sock->sk->sk_security;
sksec->sclass = sclass;
sksec->sid = sid;
socket object的标签(SID)存储在struct sock结构体的sk_security的sid成员中。关于SID的内容可以参考[[详解SELinux SID]]。
下图显示了security_socket_create
和security_socket_post_create
中对socket SID的两次计算。由于未赋予正确的标签(type与user,role的组合不合法),且SELinux处于permissive模式,所以内核audit子系统报了两次告警。
为什么要有两次SID计算?
第一次是在创建socket之前,检查进程是否有权限创建该socket,此时尚不具备该socket object,所以自然无法记录。直到security_socket_post_create时,socket object已经创建完毕了,此时再计算出socket的SID,并予以记录。
标签的计算
不论是security_socket_create
还是security_socket_post_create
都会调用socket_sockcreate_sid
函数来获取socket object的标签。获取的方法,分两步:
- 如果进程设置了sockcreate属性,则使用该属性指定的标签
- 否则通过security_transition_sid执行type transition
sockcreate
通过改写进程的sockcreate属性,每个进程可以决定其创建的socket object的标签属性。一旦为该属性赋值,那么socket object的type_transition不再生效。
graph TD; A["写/proc/pid/attr/sockcreate文件"] --> B["proc_pid_attr_write"]; B --> C["security_setprocattr"]; C --> D["__tsec->sockcreate_sid = xxx"];
通过下面的代码,用户态设置的属性值最终被写入到current->cred->security->sockcreate_sid成员中。
1 | // in security/selinux/hooks.c, selinux_setprocattr函数 |
type transition
关于typetransition可以参考[[28—(6 条消息) 深入理解 SELinux SEAndroid(第一部分)阿拉神农的博客 - CSDN 博客_domain_auto_trans]],也可以参考官方文档TypeRules - SELinux Wiki。
socket的type_transition和其他类型的type_transtion没有区别。例如:1
type_transition proc_t, source_t, target_t, { xxx_class }
这条语句的意思是:
由类型为proc_t创建的,源标签为source_t的客体,如果其类型为xxx_class,则其目标标签为target_t。
由于socket的起始默认标签,会继承进程的主体标签,所以如果是socket,则上面这样一条语句会变成:1
2# 这里以SOCK_STREAM类型的AF_UNIX socket为例
type_transition proc_t, proc_t, target_t, { unix_stream_socket }
如果使用refpolicy提供的宏来编写,那么就会写作:1
filetrans_pattern(proc_t, source_t, target_t, { unix_stream_socket })
filetrans_pattern
定义在policy/support/file_patterns.spt文件中。
最终socket的标签信息(SID)会被记录在struct sock的sk_security成员中。
一旦socket object有了标签信息,并且可以实施type_transition,那么我们就可以将某个进程创建的socket转换为任意我们想要的标签(type),并对其定义任意我们想要的allow规则。
AF_UNIX访问控制
AF_UNIX socket又称为Unix Domain Socket,简称UDS,中文称为域套接字。UDS还有一些比较特殊的地方。AF_UNIX socket通常用来进行操作系统内部的进程间通信。通过设置sun_family和sun_path来为UDS设置地址。从而完成客户端与服务端的绑定。1
2
3struct sockaddr_un *addr;
addr->sun_family = AF_UNIX;
strncpy(addr->sun_path, path, sizeof(addr->sun_path) - 1);
服务端进程可以调用bind,listen,accept来监听客户端的请求,并用recv和send来响应。客户端进程通过send和recv类接口发送请求。
当服务端创建好socket之后,会在目录下生成一个socket文件。而UDS除了标准的(或者通用的)socket object的权限控制之外,还可以对socket文件进行访问控制。
UDS socket文件和普通文件类似,可以设置DAC权限,也可以打标签。区别是UDS socket不能在rootfs中集成,而是在运行时动态生成的。生成之后,可以通过restorecon或者chcon等SELinux工具或接口对其设置安全上下文。UDS socket文件同样可以实现type_transition。使用refpolicy的宏接口可以如下编写1
filetrans_pattern(proc_t, dir_t, target_t, { sock_file };
需要注意以下几点:
- UDS socket文件和普通文件一样,会默认继承其所在目录的标签,所以文件的源标签要设置成目录的标签
- UDS socket的类型(class)是sock_file,如果设置不正确,则type_transtion不会生效。
配置好type_transition之后,就可以针对UDS socket文件进行访问控制了。
匿名socket
UDS还有一种特殊的socket,即匿名socket,英文称为abstract namespace socket。此时sun_path的首字符为’\0’。1
2
3
4struct sockaddr_un *addr;
addr->sun_family = AF_UNIX;
addr->sun_path[0] = '\0';
strncpy(&addr->sun_path[1], path, sizeof(addr->sun_path) - 1);
其实匿名socket比有名socket更简单,其丢失了socket文件的文件访问控制特性,只保留了socket object访问控制。
AF_INET访问控制
INET型socket除了通用的socket object之外,还针对一些网络属性进行了访问控制。主要包含三种语句:
- nodecon
- portcon
- netifcon
可以参考官方的网页NetworkStatements - SELinux Wiki。本节挑选node和netif相关的控制做简单介绍。
node
SELinux的nodecon语句在编译时,被加载到policydb中。在security/selinux/hooks.c文件中定义的系统调用hook点的实现时,通过查询policydb,获得node的标签。其实现调用流程如下:
graph TD; A["selinux_socket_bind"] --> B["sel_netnode_sid"]; B --> C["sel_netnode_sid_slow"]; C --> D["security_node_sid"]
在
security_node_sid
中,查询policydb完成了node标签的查询。1 | // for IPv4 |
有了node的标签,SELinux就可以使用avc_has_perm了。
node的标签为node_t, 其class属性跟随了socket object的class属性。
1 | struct sk_security_struct *sksec = sk->sk_security; |
nodecon语句的特殊性
nodecon是不支持模块化策略的,也就是说在.pp文件中编写nodecon编译会报错(syntax error)。通过semanage工具可以增加nodecon标签条目,例如:semanage node -a -M 255.255.255.255 -t node_t -r s0:c20.c250 -p ipv4 127.0.0.2会生成nodecon语句nodecon ipv4 127.0.0.2 255.255.255.255 system_u:object_r:node_t:s0:c20.c250。
这里还需要注意的是:node的type属性不能随便赋予。否则会出现以下错误:
netif
netif本身倒没什么特别之处,与node的控制方法类似。
graph TD; A["selinux_inet_sys_rcv_skb"] --> B["sel_netif_sid"]; B --> C["sel_netif_sid_slow"]; C --> D["security_netif_sid"];
在
security_netif_sid
中查找policydb中的netif表格。1 | c = policydb->ocontexts[OCON_NETIF]; |
netif的控制有两个比较有意思的地方,下面依次道来。
如何实现单网卡的绑定
网上搜索了好久,大多是实现如何从指定的网卡发送报文(所谓的绑定)。因为操作系统通常会按照连接或网络的连通性,自动选择发送数据的网卡。也有不少提及通过SO_BINDTODEVICE绑定网卡的实现。但缺乏关键代码。最终通过下面的代码实现了demostration,摘取关键代码:1
2
3
4
5
6
7
8
9address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
struct ifreq ifr;
memset(&ifr, 0x00, sizeof(ifr));
strncpy(ifr.ifr_name, "lo", strlen("lo")); // "lo"就是网卡名
ioctl(server_fd, SIOCGIFINDEX, &ifr);
setsockopt(server_fd, SOL_SOCKET, SO_BINDTODEVICE, (char *)&ifr, sizeof(ifr));
设置完socket之后,就可以像正常的socket一样访问和监听了。
这里要注意为socket设置的绑定地址,即address.sin_addr.s_addr
要和SO_BINDTODEVICE指定的地址相匹配,否则会出现connection failed。例如:address.sin_addr.s_addr
设置一个与ifreq指定的网卡IP地址不同的IP地址,则bind会返回connection failed的错误。
另外,SO_BINDTODEVICE实现的网卡绑定,也不适用netifcon的权限控制。
netifcon在哪些hook点生效?
netifcon的策略检查点在sel_netif_sid
中。该函数的调用者只有以下两个函数:
- selinux_inet_sys_rcv_skb
- 该函数只在fallback peer labelling生效时使用。关于fallback peer labelling,可以关注以下官方网页NB Networking - SELinux Wiki,以及一些相关的衍生阅读
- 该函数也有两个调用点,分别是:
- selinux_socket_sock_rcv_skb
- selinux_ip_forward
- selinux_ip_postroute
- 该函数在NetFilter中使用,post routing是一个NetFilter的一个chain的名字。关于NetFilter以及iptables,可以参考[[iptables + SELinux控制socket packet]]。
AF_CAN访问控制
CAN总线是有别于网络报文的另一种总线通信方式。我是用的demo程序是linux-can/can-utils: Linux-CAN / SocketCAN user space applications 中的candump。具体可以看candump.c这个文件。对CAN socket的调用方法大致如下:1
2
3
4
5
6
7
8struct ifreq ifr;
struct sockaddr_can addr;
socket(PF_CAN, SOCK_RAW, CAN_RAW);
addr.can_family = AF_CAN;
strncpy(ifr.ifr_name, can_netif_name, name_size);
bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr));
nbytes = recvmsg(sock_fd, &msg, 0);
CAN设备可以被作为一张网卡,被监听和读写。如何插入一个虚拟CAN设备,可以参考[[52—How to create a virtual CAN interface on Linux - PragmaticLinux]]。
CAN设备的访问控制和其他的对socket object对访问控制是一样的。也可以实现type_transition:1
filetrans_pattern(candump_t, candump_t, target_t, { can_socket })
需要注意的是,对于CAN设备,内核会根据其Address Family或Protocol Family识别其类型(class)。1
2case PF_CAN:
return SECCLASS_CAN_SOCKET;
所以在写策略时,class一定要写对,如果写成了其他的class,那么策略是无法正确生效的。
另外,虽然CAN设备被当成一个netif在使用,但针对netif的hook点并没有对CAN设备有针对性的部署。根据[[#netif]]中的描述,针对netif的控制只发生在和网络相关的调用路径上。所以无法针对CAN设备使用netifcon语句。
总结
本文从Unix Domain Socket(UDS,域套接字,AF_UNIX或AF_LOCAL),INET socket和CAN socket的角度阐述了内核对socket的访问控制实现方式。
针对通用socket,内核通过将标签写入内核的socket object中,并在系统调用时,对其进行主客体匹配实现访问控制。
针对不同种类的socket,内核还辅以其他相关资源的控制,例如:
- 如果是有名的域套接字还可以对socket文件进行控制,class属性为sock_file
- 如果是IP地址,还可以结合nodecon进行控制,该语句只能在monolithic策略中编写,无法在modular策略中使能
- 如果是网卡,可以结合netifcon进行控制,该语句只和网络相关的hook点相关
- CAN socket可以使用通用socket进行访问控制,不能使用netifcon进行访问控制。