ROOT 的本质是什么

所谓 root 的本质,就是 当前任务访问系统资源的能力

在一般的实时操作系统中,这里的任务(task)指的就是线程,是计算机 CPU 对程序进行调度的最小单位。

如果我们要执行一条命令 cat /etc/passwd,当前用户运行了可执行文件/bin/cat,并且新生成一个子进程,在该进程中读取了/etc/passwd文件。

要看所执行的 cat /etc/passwd 是否能够成功,其实是看在内核中下面这些条件能否满足:

  • 当前的线程是否可访问、执行cat可执行文件,是否可以创建新进程;
  • 新进程如何继承当前进程的属性,这决定了新线程是否可以访问 /etc/passwd

访问控制是什么

在计算机安全领域,访问控制表示操作系统对某个主体(subject)访问或者执行某种操作的约束,主体可以是线程或者进程,操作可以是访问文件、目录、TCP/UDP 端口、共享内存段、IO 设备等对象。这类约束可以抽象成两大类,一类可以由对象的属主对自己的访问者进行管理,称为自主访问控制(DAC);另外一类由操作系统统一管理,称为强制访问控制(MAC)。

DAC

DAC 即 Discretionary Access Control,这种权限管理机制的主体是用户,因为权限的控制是自主的,因此称为自主访问控制。

在没有使用 SELinux 的操作系统中,决定一个资源是否能被访问的因素是:某个资源是否拥有对应用户的权限(读、写、执行),只要访问这个资源的进程符合以上的条件就可以被访问,而最致命问题是,root 用户不受任何管制,系统上任何资源都可以无限制地访问。

DAC 有两种自主访问控制策略,分别是文件权限码和访问控制列表 ACL (Access Control List)。

文件权限码就是我们常说的9位权限码,分别表示当前用户(user/owner)、用户组(group) 和其他用户(other) 对应的 读、写、执行 (rwx) 访问权限,可以参考 chmod。实际上在 Linux 操作系统中在前面还增加了三位,分别是:

  • S_ISUID (04000): SETUID 位,用于在 exeve 系统调用时设置进程的有效用户ID(effective user ID);
  • S_ISGID (02000): SETGID 位,和 SETUID 类似,从父目录中继承;
  • S_ISVTX (01000): sticky bit,即防删除位,防止其他用户删除公共文件,通常用于/tmp目录下;

通过文件权限码可以实现一定程度上的自主访问控制,但是对于多用户系统而言只能通过用户组去管理,无法控制某个文件可以让用户A访问而不让用户B访问。ACL 就是为了实现这个目标而出现的。例如,需要单独给某个用户添加文件的读权限如下:

1
$ setfacl -m u:evilpan:r /etc/passwd

具体命令可以参考 setfacl,值得一提的是,ACL需要内核和文件系统的支持。

MAC

MAC 即 Mandatory Access Control,这种权限管理机制的主体是进程,是用于将系统中的信息分密级和类进行管理,以保证每个用户只能访问到那些被标明可以由他访问的信息的一种访问约束机制。

通俗的来说,在强制访问控制下,用户(或其他主体)与文件((其他客体)都被标记了固定的安全属性(如安全级、访问权限等),在每次访问发生时,系统检测安全属性以便确定一个用户是否有权访问该文件。其中 SELinux 和 AppArmor 就是 Linux 中典型的强制访问控制实现。

在使用了 SELinux 的操作系统中,决定一个资源是否能被访问的因素除了某个资源是否拥有对应用户的权限(读、写、执行),还需要判断每一类进程是否拥有对某一类资源的访问权限,这样一来,即使进程是以 root 身份运行的,也需要判断这个进程的类型以及允许访问的资源类型才能决定是否允许访问某个资源。进程的活动空间也可以被压缩到最小。即使是以 root 身份运行的服务进程,一般也只能访问到它所需要的资源。即使程序出了漏洞,影响范围也只有在其允许访问的资源范围内。安全性大大增加。

UID

访问控制策略是根据用户和组去进行管理的。对于操作系统而言,为了方便管理,用户和组都分别对应数字 ID,即 UID 和 GID。

一般情况下 su 是一个设置了 SETUID 位的程序,并且 owner 是 root 用户。普通用户执行该程序只是上是对该文件执行了execve系统调用,也就是说,内核会根据 SETUID 位来调整当前进程的权限,这主要是通过有效用户ID去实现的,如果文件名所指的程序文件设置了 set-user-ID 位,并且底层文件系统没有以 nosuid 方式挂载(即挂载时没有使用 MS_NOSUID 标志),并且调用进程没有被 ptrace 调试,那么调用进程的有效用户 ID 将被更改为该程序文件所有者的用户 ID。

Linux中的用户ID分为 real user ideffective user id,前者用来表示进程的真实用户,后者用来表示当前所表示的有效用户。

在内核里的 task_struct 中有一个 struct cred 字段,该字段对应的结构就包含了当前任务的安全相关上下文信息,其中就有 uid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
// ...
} __randomize_layout;

Capabilities

传统 Linux 执行权限检测主要是基于 UID,而且只有两个分类,即 (effective) UID 为 0 的超级用户和其他普通用户。这样一来就会面临权限划分粒度太粗的问题,比如只想让普通用户可以访问 ping 程序,就需要给 ping 文件加上 SETUID 位,如果该可执行文件的实现存在漏洞,就可能被利用造成权限提升。

因此,从 Linux 2.2 开始,就引入了 capabilities,将超级用户的权限进行切分,并且按需要给普通用户进行分配,解决了传统 UID-0 的局限性。

capabilities 以任务(线程)为单位,还是在上面内核的 struct cred 结构体中,其相关的字段为:

1
2
3
4
5
6
7
8
9
struct cred {
// ...
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
// ...
}

从定义上看,一共有三类 capability,分别是 effective、permitted 和 inheritable,这和 UID 的设计初衷是类似的,因为进程可以被复制(fork),因此增加了 inheritable 的控制。对于每一类 capabilities,由于其类型是__u32,每项 capability 通过位与方式进行组合,因此最多可以支持 32 种 capability,其中一些常见的包括:

  • CAP_NET_RAW: 创建和使用 RAW/PACKET socket 的权限以及绑定透明代理地址的权限;
  • CAP_NET_ADMIN: 各类网关相关的操作,比如网卡接口配置、路由表修改等;
  • CAP_SETUID: 设置和修改进程 UID 的权限;
  • CAP_SYS_PTRACE: 使用 ptrace 跟踪任意其他进程的能力;
  • ….

完整的权限列表可以参考 capabilities(7)

对于系统管理员而言,更多是使用 capsh、getcap、setcap 等命令行工具,不过本质上都是通过 libcap 对系统调用进行封装实现的。

SELinux

SELinux 即 Security Enhanced Linux,是 Linux 中强制访问控制的两大实现之一(另一个是 AppArmor),作为 Linux 的拓展,最初由 NSA 开发,后集成到了开源内核主线中。

用户态

在 SELinux 中,访问控制通过 context 来描述访问权限,例如对于文件系统,可以使用 ls -Z 查看文件对应的标签:

1
2
3
4
5
6
7
8
9
generic:/ # ls -lZ /
total 2424
dr-xr-xr-x 3 root root u:object_r:cgroup:s0 0 1970-01-01 08:00 acct
lrwxrwxrwx 1 root root u:object_r:rootfs:s0 50 1970-01-01 08:00 bugreports -> /data/user_de/0/com.android.shell/files/bugreports
drwxrwx--- 6 system cache u:object_r:cache_file:s0 4096 2019-12-23 16:52 cache
lrwxrwxrwx 1 root root u:object_r:rootfs:s0 13 1970-01-01 08:00 charger -> /sbin/healthd
dr-x------ 2 root root u:object_r:rootfs:s0 40 1970-01-01 08:00 config
lrwxrwxrwx 1 root root u:object_r:rootfs:s0 17 1970-01-01 08:00 d -> /sys/kernel/debug
drwxrwx--x 38 system system u:object_r:system_data_file:s0 4096 1970-01-01 08:00 data

对于网络端口的标签,可以用 netstat -Z查看;对于进程标签,则可以通过ps -Z查看:

1
2
3
4
5
6
7
8
9
10
generic:/ # ps -Z
LABEL USER PID PPID VSIZE RSS WCHAN PC NAME
u:r:init:s0 root 1 0 7856 1556 SyS_epoll_ 0008e458 S /init
u:r:kernel:s0 root 2 0 0 0 kthreadd 00000000 S kthreadd
u:r:platform_app:s0:c512,c768 u0_a28 1642 885 1669496 105556 binder_thr ab777494 D com.android.systemui
u:r:fingerprintd:s0 system 901 1 8260 3536 binder_thr b12da494 S /system/bin/fingerprintd
u:r:gatekeeperd:s0 system 902 1 7244 2888 binder_thr b21a1494 S /system/bin/gatekeeperd
u:r:perfprofd:s0 root 903 1 4104 1740 hrtimer_na a72f0378 S /system/xbin/perfprofd
u:r:logd:s0 logd 906 1 4644 2184 __skb_recv ac3d3584 S /system/bin/logcat
u:r:shell:s0 shell 909 884 3544 1896 sigsuspend af915698 S /system/bin/sh

context 可以分为几个部分,使用冒号:分隔,分别是:

  • user: 表示 SELinux 用户账号,与 Linux 用户账号不同,前者在 policy 中定义,包含多层级权限;
  • role: 定义了主体(subject)在特定域(domain)中可以对客体(object)进行的操作;
  • type: 定义了文件的类型;
  • sensitivity: 即最后一个字段,表示涉密等级,范围可以从c0到c1023,c3表示Top Secret。该字段仅在 MLS 模式中使用,用于高敏感度的国防军事机构,对于客户端或者一般数据服务器而言只需保留默认值。

对一系列系统资源增加标签后,系统就可以根据标签来判断访问是否应该允许,一个示例的访问拒绝日志如下:

1
type=1400 audit(18.250:15): avc: denied { getattr } for pid=939 comm="ls" path="/ueventd.rc" dev="rootfs

访问权限的判断是在内核中实现的,但是访问规则可以动态生成和更新,内核中只预置了一系列触发点。SELinux 规则(policy)通常使用自定义的高级语言去描述,目前正在开发的是 CIL(Common Intermediate Language),但使用更多的是传统的 MLS Statements,比如访问规则的定义如下:

1
rule_name source_type target_type:class perm_set;

一个具体的例子:

1
allow initrc_t acct_exec_t:file { getattr read execute };

表示允许拥有initrc_t标签类型的主体访问带有acct_exec_t标签的目标文件,访问权限为 getattr、read和write。其中类型是使用type关键字定义的,一般使用单独的file_contexts文件记录。MLS 的完整语法见 Kernel Policy Language Definition Links

对于系统管理员而言,常用的相关命令有:

  • chcon: 修改目标文件的 SELinux 标签;
  • resotrecon: 重新加载(恢复)系统文件的 SELinux 标签;
  • semanage: 实时修改当前系统的 SELinux 规则;

内核态

前面说 SELinux 是在内核中进行检查的,那么就以打开文件的操作为例来简单分析下 SELinux 的校验过程。打开文件使用的系统调用是openat,该系统调用在内核中的大致调用路径如下:

  • sys_openat
  • do_sys_open
  • do_filp_open
  • path_openat
  • do_last
  • may_open
  • inode_permission
    • do_inode_permission -> generic_permission
    • devcgroup_inode_permission
    • security_inode_permission

inode_permission 是在文件打开之前检查文件系统 inode 权限的操作,其中包含常规的 DAC 检查、cgroup 权限检查以及我们所关心的 SELinux 检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define call_int_hook(FUNC, IRC, ...) ({			\
int RC = IRC; \
do { \
struct security_hook_list *P; \
\
list_for_each_entry(P, &security_hook_heads.FUNC, list) { \
RC = P->hook.FUNC(__VA_ARGS__); \
if (RC != 0) \
break; \
} \
} while (0); \
RC; \
})

int security_inode_permission(struct inode *inode, int mask)
{
if (unlikely(IS_PRIVATE(inode)))
return 0;
return call_int_hook(inode_permission, 0, inode, mask);
}

struct security_hook_heads 是一个结构体,其中包含一系列链表,每个链表都对应一类 SELinux hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct security_hook_heads {
struct list_head binder_set_context_mgr;
struct list_head binder_transaction;
struct list_head binder_transfer_binder;
struct list_head binder_transfer_file;
struct list_head ptrace_access_check;
struct list_head ptrace_traceme;
struct list_head capget;
struct list_head capset;
//...
struct list_head inode_permission;
// ...
}

每个链表都是在内核启动时进行初始化的,inode_permission也不例外。在security/linux/hooks.c中定义了静态数组selinux_hooks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct security_hook_list selinux_hooks[] = {
LSM_HOOK_INIT(binder_set_context_mgr, selinux_binder_set_context_mgr),
LSM_HOOK_INIT(binder_transaction, selinux_binder_transaction),
LSM_HOOK_INIT(binder_transfer_binder, selinux_binder_transfer_binder),
LSM_HOOK_INIT(binder_transfer_file, selinux_binder_transfer_file),

LSM_HOOK_INIT(ptrace_access_check, selinux_ptrace_access_check),
LSM_HOOK_INIT(ptrace_traceme, selinux_ptrace_traceme),
LSM_HOOK_INIT(capget, selinux_capget),
LSM_HOOK_INIT(capset, selinux_capset),
// ...
LSM_HOOK_INIT(inode_permission, selinux_inode_permission),
// ...
}

因此,selinux_inode_permission 就是实际进行 SELinux 检查的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static int selinux_inode_permission(struct inode *inode, int mask)
{
const struct cred *cred = current_cred();
u32 perms;
bool from_access;
unsigned flags = mask & MAY_NOT_BLOCK;
struct inode_security_struct *isec;
u32 sid;
struct av_decision avd;
int rc, rc2;
u32 audited, denied;

from_access = mask & MAY_ACCESS;
mask &= (MAY_READ|MAY_WRITE|MAY_EXEC|MAY_APPEND);

/* No permission to check. Existence test. */
if (!mask)
return 0;

validate_creds(cred);

if (unlikely(IS_PRIVATE(inode)))
return 0;

perms = file_mask_to_av(inode->i_mode, mask);

sid = cred_sid(cred);
isec = inode->i_security;

rc = avc_has_perm_noaudit(sid, isec->sid, isec->sclass, perms, 0, &avd);
audited = avc_audit_required(perms, &avd, rc,
from_access ? FILE__AUDIT_ACCESS : 0,
&denied);
if (likely(!audited))
return rc;

rc2 = audit_inode_permission(inode, perms, audited, denied, rc, flags);
if (rc2)
return rc2;
return rc;
}

这里有几个值得注意的地方,一个是 selinux_hooks 中注册了很多回调列表,这些模块就是内核中预置的检查点;另外,在 selinux_inode_permission 函数中,使用 file_mask_to_av 来将打开文件的 flag 转换成 SELinux 对应的访问动作(Access Vector):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Convert a Linux mode and permission mask to an access vector. */
static inline u32 file_mask_to_av(int mode, int mask)
{
u32 av = 0;

if (!S_ISDIR(mode)) {
if (mask & MAY_EXEC)
av |= FILE__EXECUTE;
if (mask & MAY_READ)
av |= FILE__READ;

if (mask & MAY_APPEND)
av |= FILE__APPEND;
else if (mask & MAY_WRITE)
av |= FILE__WRITE;

} else {
if (mask & MAY_EXEC)
av |= DIR__SEARCH;
if (mask & MAY_WRITE)
av |= DIR__WRITE;
if (mask & MAY_READ)
av |= DIR__READ;
}

return av;
}

这些宏定义在 <build>/security/selinux/av_permissions.h中,是编译内核时自动生成的。在确认该次访问需要审计后,就接着调用 audit_inode_permission -> slow_avc_audit 进行实际的判断了。因为这类访问控制判断需要频繁调用,出于性能考虑判断过程所使用的访问规则预先编译好并已经加载到内核缓存中,称为 avc (Access Vector Cache),这也是前面日志中 avc 的来源。

SEAndroid

Google 在 Android 4.4 上正式添加以 SELinux 为基础的系统安全机制,命名为SEAndroid。SEAndroid 在架构和机制上与 SELinux 完全一样,基于移动设备的特点,SEAndroid 的只是所以移植 SELinux 的一个子集。

模式

  • Disable模式:此种模式关闭SELinux检测,不进行任何SELinux权限检查,畅通无阻。
  • Permissive模式:宽容模式,当权限检查不通过时,不决绝资源访问,只打印avc log日志。
  • Enforceing模式:强制模式,此种模式下权限检查不通过时,拒绝资源访问,并打出avc log,这个是最狠模式。
1
2
3
4
5
6
7
8
#查看SeLinux状态
adb shell getenforce

#设置SeLinux模式为Enforcing(只允许Enforcing和Permissive之间切换)
adb shell 'su -c setenforce 1'

#设置SeLinux模式为Permisssive
adb shell 'su -c setenforce 0'

SEAndroid app分类

ELinux(或SEAndroid)将app划分为主要三种类型(根据user不同,也有其他的domain类型):

  • untrusted_app:第三方app,没有Android平台签名,没有system权限

  • platform_app:有android平台签名,没有system权限

  • system_app:有android平台签名和system权限

  • untrusted_app_25:第三方app,没有Android平台签名,没有system权限,其定义为 This file defines the rules for untrusted apps running with targetSdkVersion <= 25

从上面划分,权限等级,理论上:untrusted_app < platform_app < system_app 按照这个进行排序 property_contexts(系统属性)主要描述系统属性相关

seaapp_context 定义

seapp_contexts定义在system/sepolicy/seapp_contexts数据文件,如下

1
2
3
4
5
6
7
8
9
10
isSystemServer=true domain=system_server
user=system seinfo=platform domain=system_app type=system_app_data_file
user=bluetooth seinfo=platform domain=bluetooth type=bluetooth_data_file
user=nfc seinfo=platform domain=nfc type=nfc_data_file
user=radio seinfo=platform domain=radio type=radio_data_file
user=shared_relro domain=shared_relro
user=shell seinfo=platform domain=shell type=shell_data_file
user=_isolated domain=isolated_app levelFrom=user
user=_app seinfo=platform domain=platform_app type=app_data_file levelFrom=user
user=_app domain=untrusted_app type=app_data_file levelFrom=user

从上面可以看出,domain和type由user和seinfo两个参数决定。

比如:

user=system seinfo=platform,domain才是system_app

user=_app,可以是untrusted_app或platform_app,如果seinfo=platform,则是platform_app。

参考链接

Android SELinux开发入门指南之SELinux基础知识_selinux书-CSDN博客

Android/Linux Root 的那些事儿 - 有价值炮灰 (evilpan.com)

一文彻底明白linux中的selinux到底是什么 - 知乎 (zhihu.com)

正确姿势临时和永久开启关闭Android的SELinux_setenforce: couldn’t set enforcing status to ‘0’: -CSDN博客