SELinux socket访问控制

内核socket架构介绍

socket编程的用户态接口是:

1
2
int 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
2
SYSCALL_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);
在对应子类的create函数中,子类根据type的不同,绑定不同的处理函数指针。以af_unix.c为例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
switch (sock->type) {
case SOCK_STREAM:
sock->ops = &unix_stream_ops;
break;
/*
* Believe it or not BSD has AF_UNIX, SOCK_RAW though
* nothing uses it.
*/
case SOCK_RAW:
sock->type = SOCK_DGRAM;
fallthrough;
case SOCK_DGRAM:
sock->ops = &unix_dgram_ops;
break;
case SOCK_SEQPACKET:
sock->ops = &unix_seqpacket_ops;
break;
default:
return -ESOCKTNOSUPPORT;
}
跟访问控制有关的信息都存储在struct sock的sk_security成员指针中。 # socket访问控制 SELinux实现了面向主客体的访问控制模型,即所谓的TEAC(Type Enforcement Access Control)。针对文件,SELinux通过在文件系统的扩展属性上设置标签(安全上下文),当进程(主体)访问该文件客体时,SELinux提供的`avc_has_perm`函数会通过查询内存中的policydb,以获取对应的决策结果。针对socket这一类客体,SELinux的工作原理类似,每个socket object都会被打上对应的标签,从而在系统调用的时候对其进行权限判断。 ## 打标签 所有类型的socket,对socket打标签的方式都是基本一致的。下面以AF_UNIX socket为例解释Linux内核如何实现socket object打标签。 由于socket object没有文件实体,所以没法像文件一样,通过给文件设置文件系统的扩展属性来实现。socket的标签设置在socket抽象层实现,具体即net/socket.c文件,通过LSM的接口调用SELinux层的接口实现“打标签”。
graph TD;
    A["__sock_create"] --> B["security_socket_create"];
    B --> C["pf->create"];
    C --> D["security_socket_post_create"]

security_socket_createsecurity_socket_post_create对应的SELinux hook函数如下:

1
2
LSM_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
    3
    u32 newsid;
    secclass = socket_type_to_security_class(family, type, protocol);
    rc = socket_sockcreate_sid(tsec, secclass, &newsid);

这里需要注意的是newsid仅仅是一个局部变量,可见该hook点,并未对socket的标签信息进行保存。那么:

  1. socket object的标签是在哪儿生成的呢?
  2. socket标签的值具体是什么呢?

标签的生成

第一个问题的答案可以在security_socket_post_create函数中找到。

1
2
3
sksec = sock->sk->sk_security;
sksec->sclass = sclass;
sksec->sid = sid;

socket object的标签(SID)存储在struct sock结构体的sk_security的sid成员中。关于SID的内容可以参考[[详解SELinux SID]]。
下图显示了security_socket_createsecurity_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的标签。获取的方法,分两步:

  1. 如果进程设置了sockcreate属性,则使用该属性指定的标签
  2. 否则通过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
2
3
4
5
6
// in security/selinux/hooks.c, selinux_setprocattr函数
if (!strcmp(name, "exec")) {
tsec->exec_sid = sid;
} else if (!strcmp(name, "sockcreate")) {
tsec->sockcreate_sid = sid;
} ...

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
3
struct 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
4
struct 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之外,还针对一些网络属性进行了访问控制。主要包含三种语句:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// for IPv4
c = policydb->ocontexts[OCON_NODE];
while (c) {
if (c->u.node.addr == (addr & c->u.node.mask))
break;
c = c->next;
}

// for IPv6
c = policydb->ocontexts[OCON_NODE6];
while (c) {
if (match_ipv6_addrmask(addrp, c->u.node6.addr,
c->u.node6.mask))
break;
c = c->next;
}

有了node的标签,SELinux就可以使用avc_has_perm了。

node的标签为node_t, 其class属性跟随了socket object的class属性。
1
2
3
4
5
struct sk_security_struct *sksec = sk->sk_security;
err = avc_has_perm(&selinux_state,
sksec->sid, sid,
sksec->sclass, node_perm, &ad);

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
2
3
4
5
6
c = policydb->ocontexts[OCON_NETIF];
while (c) {
if (strcmp(name, c->u.name) == 0)
break;
c = c->next;
}

netif的控制有两个比较有意思的地方,下面依次道来。

如何实现单网卡的绑定

网上搜索了好久,大多是实现如何从指定的网卡发送报文(所谓的绑定)。因为操作系统通常会按照连接或网络的连通性,自动选择发送数据的网卡。也有不少提及通过SO_BINDTODEVICE绑定网卡的实现。但缺乏关键代码。最终通过下面的代码实现了demostration,摘取关键代码:

1
2
3
4
5
6
7
8
9
address.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
8
struct 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
2
case 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进行访问控制。