【SEAndroid】适配 netlink_xxx_socket 的 SELinux 权限

字数 848阅读 539

版权声明:本文为 gfson 原创文章,转载请注明出处。
注:作者水平有限,文中如有不恰当之处,请予以指正,万分感谢。

一. 概述

1.1 Linux 的 netlink 机制

Linux 的 netlink 机制是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。

1.2 实现背景

  • 用户空间有一个应用 scpd 通过自定义协议 NETLINK_SCPD 创建 netlink socket,相关代码如下:
...
#define NETLINK_SCPD    28  /* scpd communition*/
...
nlfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SCPD);
...
  • 内核中使用对应的协议号 28 创建 netlink socket,相关代码如下:
...
#define NETLINK_SCPD 28
...
nl_sk = netlink_kernel_create(&init_net, NETLINK_SCPD, &cfg);
...
  • 使用了相同协议号的用户程序 scpd 和内核就可以开始通信了,这个时候只需要配置如下 SELinux 权限即可:
type scpd, domain;
type scpd_exec, exec_type, file_type;
init_daemon_domain(scpd)
allow scpd self:capability { dac_override };
allow scpd serial_device:chr_file open;
allow scpd serial_device:chr_file read;
allow scpd serial_device:chr_file write;
allow scpd serial_device:chr_file ioctl;
allow scpd device:dir write;
allow scpd device:dir add_name;
allow scpd device:sock_file create;
allow scpd device:sock_file setattr;
  • 可以看到,上述的 SELinux 权限并没有对特定的协议号有限制,换而言之,任何一个程序只要能够访问 socket,有上述的 SELinux 权限,即可通过协议号 28 来访问内核中特定的内容。
  • 了解上述的背景后,我们的目的是通过 SELinux 限制程序对协议号 28 的访问,既:

只允许配置了特定 SELinux 权限的程序可以通过协议号 28 访问内核,其他程序就算可以访问 socket,如果该程序没有针对协议号 28 配置 SELinux 权限,那么这个程序就不能使用这个协议号 28 访问内核。

二. SELinux 对 netlink 的扩展

为了解决上述问题,我们先来研究一下 kernel 中 netlink 的实现,看看其中有没有 socket 对 netlink socket 的 SElinux 扩展。内核中通过 netlink_kernel_create 来创建 netlink socket,我们从这个函数开始分析。

  1. netlink_kernel_create [ kernel\include\linux\Netlink.h ]
static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{
    return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}
  • __netlink_kernel_create [ kernel\net\netlink\Af_netlink.c ]
struct sock *
__netlink_kernel_create(struct net *net, int unit, struct module *module,
            struct netlink_kernel_cfg *cfg)
{
    struct socket *sock;
    ...
    if (sock_create_lite(PF_NETLINK, SOCK_DGRAM, unit, &sock))
        return NULL;
    ...
}
  • sock_create_lite [ kernel\net\Socket.c ]
int sock_create_lite(int family, int type, int protocol, struct socket **res)
{
    int err;
    struct socket *sock = NULL;
    err = security_socket_create(family, type, protocol, 1);
    ...
}
  • security_socket_create [ kernel\security\Security.c ]
int security_socket_create(int family, int type, int protocol, int kern)
{
    return security_ops->socket_create(family, type, protocol, kern);
}
  • security_ops 的初始化
    • selinux_init [ kernel\security\selinux\Hooks.c ]

static __init int selinux_init(void)
{
...
if (register_security(&selinux_ops))
panic("SELinux: Unable to register with kernel.\n");
...
}

  - **register_security** [ kernel\security\Security.c ]

int __init register_security(struct security_operations *ops)
{
...
security_ops = ops;
...
}

  - **selinux_ops** [ kernel\security\selinux\Hooks.c ]

static struct security_operations selinux_ops = {
...
.socket_create = selinux_socket_create,
...
}

  - 所以,`security_ops = selinux_ops`,`security_ops->socket_create = selinux_ops->socket_create = selinux_socket_create`。

- **selinux_socket_create** [ kernel\security\selinux\Hooks.c ]

static int selinux_socket_create(int family, int type,
int protocol, int kern)
{
const struct task_security_struct *tsec = current_security();
u32 newsid;
u16 secclass;
int rc;

if (kern)
    return 0;

secclass = socket_type_to_security_class(family, type, protocol);
rc = socket_sockcreate_sid(tsec, secclass, &newsid);
if (rc)
    return rc;

return avc_has_perm(tsec->sid, newsid, secclass, SOCKET__CREATE, NULL);

}


- **socket_type_to_security_class** [ kernel\security\selinux\Hooks.c ]

static inline u16 socket_type_to_security_class(int family, int type, int protocol)
{
switch (family) {
...
case PF_NETLINK:
switch (protocol) {
case NETLINK_ROUTE:
return SECCLASS_NETLINK_ROUTE_SOCKET;
case NETLINK_FIREWALL:
return SECCLASS_NETLINK_FIREWALL_SOCKET;
case NETLINK_SOCK_DIAG:
return SECCLASS_NETLINK_TCPDIAG_SOCKET;
case NETLINK_NFLOG:
return SECCLASS_NETLINK_NFLOG_SOCKET;
case NETLINK_XFRM:
return SECCLASS_NETLINK_XFRM_SOCKET;
case NETLINK_SELINUX:
return SECCLASS_NETLINK_SELINUX_SOCKET;
case NETLINK_AUDIT:
return SECCLASS_NETLINK_AUDIT_SOCKET;
case NETLINK_IP6_FW:
return SECCLASS_NETLINK_IP6FW_SOCKET;
case NETLINK_DNRTMSG:
return SECCLASS_NETLINK_DNRT_SOCKET;
case NETLINK_KOBJECT_UEVENT:
return SECCLASS_NETLINK_KOBJECT_UEVENT_SOCKET;
default:
return SECCLASS_NETLINK_SOCKET;
}
...
}

return SECCLASS_SOCKET;

}

  - 从以上代码中,我们可以看出,**在源码中对 netlink socket 根据协议号是有相对应的 security class 的**。
  - Linux 中已经实现了对 netlink socket 的源码扩展和 security class 类的扩展。每一个协议号 NETLINK_XXX 对应于一个 SECCLASS_NETLINK_XXX_SOCKET,如果没有为某一个协议号定义对应的 security class,则默认使用 SECCLASS_NETLINK_SOCKET。
  - 所以,**正是由于其默认使用的是 SECCLASS_NETLINK_SOCKET,如果一个协议号没有定义对应的 security class,则其他可以访问 SECCLASS_NETLINK_SOCKET 的应用程序便都可以访问这个协议号**。为了安全考虑,保证特定的程序才可以访问这个协议号,需要对源码进行修改。
  - 接下来,我们需要做的事情分为两步:
    - 自定义协议号 28 的 security class。
    - 为 scpd 程序配置访问协议号 28 的 SELinux 权限。

# 三. 适配协议号 28 的 SELinux 权限
我们定义协议号 28 的名称为 NETLINK_SCPD。
## 3.1 在 SELinux 的配置文件中定义协议号的 security class。
- 在文件 **security_classes** [ external/sepolicy ] 中定义类名:

class netlink_scpd_socket

- 在文件 **access_vectors** [ external/sepolicy ] 中定义操作类别:

class netlink_scpd_socket
inherits socket
{
nlmsg_read
nlmsg_write
}


## 3.2 在 kernel 中实现对 NETLINK_SCPD 的扩展
- 在文件 **netlink.h** [ kernel/include/uapi/linux ] 中定义协议号:

define NETLINK_SCPD 28 /* scpd communition*/

- 在文件 **Hooks.c** [ kernel\security\selinux\Hooks.c ] 中对协议号自定义 security class。

static inline u16 socket_type_to_security_class(int family, int type, int protocol)
{
switch (family) {
...
case PF_NETLINK:
switch (protocol) {
...
case NETLINK_KOBJECT_UEVENT:
return SECCLASS_NETLINK_KOBJECT_UEVENT_SOCKET;
case NETLINK_SCPD:
return SECCLASS_NETLINK_SCPD_SOCKET;
default:
return SECCLASS_NETLINK_SOCKET;
}
...
}

return SECCLASS_SOCKET;

}

- 在文件 **classmap.h** [ kernel\security\selinux\include ] 中根据 `netlink_scpd_socket` 生成 `SECCLASS_NETLINK_SCPD_SOCKET`。

struct security_class_mapping secclass_map[] = {
...
{ "netlink_ip6fw_socket",
{ COMMON_SOCK_PERMS,
"nlmsg_read", "nlmsg_write", NULL } },
{ "netlink_scpd_socket",
{ COMMON_SOCK_PERMS,
"nlmsg_read", "nlmsg_write", NULL } },
...
}


## 3.3 配置访问 netlink_scpd_socket 的 SELinux 权限
- 在文件 **scpd.te** [ device/qcom/sepolicy/common ] 中配置 scpd 对 netlink_scpd_socket 的权限。

type scpd, domain;
type scpd_exec, exec_type, file_type;
init_daemon_domain(scpd)
allow scpd self:capability { dac_override };
allow scpd serial_device:chr_file open;
allow scpd serial_device:chr_file read;
allow scpd serial_device:chr_file write;
allow scpd serial_device:chr_file ioctl;
allow scpd device:dir write;
allow scpd device:dir add_name;
allow scpd device:sock_file create;
allow scpd device:sock_file setattr;
allow scpd device:dir remove_name;
allow scpd device:sock_file unlink;
allow scpd self:netlink_scpd_socket { create write bind read };


# 四. 注意事项
## 4.1 SECCLASS_NETLINK_SCPD_SOCKET 的生成原理
文件 **classmap.h** 中定义了 `netlink_scpd_socket` 以后,文件 **genheaders.c** [ kernel/scripts/selinux/genheaders ] 中会根据 **classmap.h** 中的内容动态生成文件 **flask.h** [ out\target\product\msm8909\obj\KERNEL_OBJ\security\selinux ],其中内容如下:

...

define SECCLASS_NETLINK_SCPD_SOCKET 38

...

并且,Hooks.c 中 `#include "avc.h"`,而 avc.h 中 `#include "flask.h"`,所以 Hooks.c 中可以正常引用 `SECCLASS_NETLINK_SCPD_SOCKET`。

## 4.2 定义 security class 类名要相同
在 security_classes、access_vectors、Hooks.c、classmap.h 中定义的类名要相同。
- security_classes、access_vectors和 classmap.h 中的类名一定要相同。
- classmap.h 中的类名 xxx 部分和 Hooks.c 中的 SECCLASS_XXX 部分相同。

## 4.3 eng 或 userdebug 版本测试时出现的问题
在 **eng** 或 **userdebug** 版本测试的时候,发现:
  - **正常现象**:如果没有为 scpd 配置 netlink_scpd_socket 相关权限,通过 `start scpd` 启动这个程序时,可以出现 avc denied 的限制,netlink_scpd_socket 无法正常访问。只有当配置了相应权限以后,`start scpd` 才可以正常访问 netlink_scpd_socket。
  - **奇怪现象**:不通过 `start scpd` 启动程序,而且直接在 shell 中运行 scpd,发现即使没有给 scpd 配置相应的 SELinux 权限,也可以正常的访问 netlink_scpd_socket。

> 分析:
- 通过 `start scpd` 启动时,其实是通过 init 进程启动,和开机 init 进程启动 scpd 是一样的,由于配置了 `init_daemon_domain(scpd)`,所以进程域会由 init 转换成 scpd,所以进程 scpd 的 scontext 为 `u:r:scpd:s0`。这个时候,配置的 SELinux 规则对这个进程起作用,会执行 mac 检查。
- 通过在 shell 中直接运行,我们可以发现,进程 scpd 的 scontext 为 `u:r:su:s0`,为了解释这个问题,我们看一下文件 **su.te** [ extern/sepolicy ] 的内容:

type su_exec, exec_type, file_type;
userdebug_or_eng(`
type su, domain;
domain_auto_trans(shell, su_exec, su)
...
permissive su;
...
')

上述的配置文件包含几个意思:
  - **上述 `domain_auto_trans(shell, su_exec, su)` 的意思是在 userdebug 版本或者 eng 版本,在 shell 中执行 su 时,会自动转化成 su 域**。我们可以回想一下,在 eng 版本中,默认就有 root 权限,这个是因为 adb shell 中已经自动执行了 su。而 userdebug 版本需要执行 `adb root` 或者在 shell 中执行 su,就会有 root 权限。而执行完 su 后,就从 shell 域切换到了 su 域了。
  - **上述 `permissive su` 的意思,是不对 su 域的行为进行任何的 mac 检查,即不受 SELinux 的规则约束**。换而言之,su 域既有 root 权限,而且不受 SELinux 的规则约束,那么 su 可以做任何事情。所以,上述内容只在 userdebug 和 eng 编译进去,user 版本的 su 权限还是受到了很大限制的。
  - 所以,这就是为什么 shell 中直接运行 scpd 没有权限,而必须切换到 su 后,才可以运行 scpd,同理,由于在 su 下运行的程序都是 su 域,故这种情况下,scpd 可以不受 SELinux 的规则约束。
  - 这种情况对 user 版本没有影响,因为 user 版本没有 su,就算通过非法手段放进去了 su,但是执行 su 的时候,会受到 SELinux 很大的约束,最大程度的限制了 user 版本下 su 的权限。

推荐阅读更多精彩内容