Linux DAC 权限管理详解

1. 背景简介

linux下有多种权限控制的机制,常见的有:DAC(Discretionary Access Control)自主式权限控制和MAC(Mandatory Access Control)强制访问控制。

其实本质上的模型都是差不多的,参与的对象有3种:主体(subject)、客体(object)、规则(policy)。
在这里插入图片描述

权限判定过程大概如下:

  • 1、主体拥有自己的凭证来标识自己的身份。在DAC中,主体通常是进程,而凭证是进程对应用的euid和egid。
  • 2、客体拥有属性来标识自己的身份。在DAC中,客体通常是文件,而权限相关属性是文件对应的uid和gid。
  • 3、主体对客体的操作可以称之为行为。DAC的特点就是行为比较简单,行为仅包括R(读)、W(写)、X(执行)这三种。
  • 4、针对"主体对客体发起的行为",查询规则表来进行权限判定。DAC的UGO规则非常简单,把主体分为User、Group、Other三种类型,每种类型拥有自己的RWX mask。

DAC的权限控制策略是非常简洁,行为是简单的RWX三种,主体也很简单的就能被分为UGO三类。这种简洁造也就了DAC检查的开销非常小。

但是凡事都有好有坏,DAC的简洁造就了它的高效,但是过于简洁也让它的权限划分粒度过大,一旦获得了root权限,几乎就是无所不能。在CPU日益高涨的今天,性能开销已经不是问题了,权限的细粒度管理更加重要,所以诞生了MAC。MAC在DAC的基础上,把行为规则判定结果进一步细分。所以它的权限管理粒度更细,但是开销也稍大。

DAC是Linux权限管理的基础机制,我们本文的重点也是DAC。

2. 主体(subject)

2.1 用户

我们在权限管理的时候,通常做的第一件事就是创建群组(groupadd)、创建用户(useradd)。这些信息存储在以下的三个文件当中,其中最重要的信息就是UIDGID密码

  • /etc/passwd

每一行都表示的是一个用户的信息;一行有7个段位;每个段位用:号分割,例如:

beinan:x:500:500:beinan sun:/home/beinan:/bin/bash
linuxsir:x:501:502::/home/linuxsir:/bin/bash

第一字段:用户名(也被称为登录名);在上面的例子中,我们看到这两个用户的用户名分别是 beinan 和linuxsir;
第二字段:口令;在例子中我们看到的是一个x,其实密码已被映射到/etc/shadow 文件中;
第三字段:UID ;请参看本文的UID的解说;
第四字段:GID;请参看本文的GID的解说;
第五字段:用户名全称,这是可选的,可以不设置,在beinan这个用户中,用户的全称是beinan sun ;而linuxsir 这个用户是没有设置全称;
第六字段:用户的家目录所在位置;beinan 这个用户是/home/beinan ,而linuxsir 这个用户是/home/linuxsir ;
第七字段:用户所用SHELL 的类型,beinan和linuxsir 都用的是 bash ;所以设置为/bin/bash ;

useradd 会把新用户的主组设置为 /etc/default/useradd 中 GROUP 变量指定的值,再或者默认是 100。

  • /etc/group

/etc/group 的内容包括用户组(Group)、用户组口令、GID及该用户组所包含的用户(User),每个用户组一条记录;格式如下:

group_name:passwd:GID:user_list

在/etc/group 中的每条记录分四个字段:
第一字段:用户组名称;
第二字段:用户组密码;
第三字段:GID
第四字段:用户列表,每个用户之间用,号分割;本字段可以为空;如果字段为空表示用户组为GID的用户名;
我们举个例子:

root:x:0:root,linuxsir 

注:用户组root,x是密码段,表示没有设置密码,GID是0,root用户组下包括root、linuxsir以及GID为0的其它用户(可以通过/etc/passwd查看);;
  • /etc/shadow

/etc/shadow 文件的内容包括9个段位,每个段位之间用:号分割;我们以如下的例子说明:

beinan:$1$VE.Mq2Xf$2c9Qi7EQ9JP8GKF8gH7PB1:13072:0:99999:7:::
linuxsir:$1$IPDvUhXP$8R6J/VtPXvLyXxhLWPrnt/:13072:0:99999:7::13108:

第一字段:用户名(也被称为登录名),在/etc/shadow中,用户名和/etc/passwd 是相同的,这样就把passwd 和shadow中用的用户记录联系在一起;这个字段是非空的;
第二字段:密码(已被加密),如果是有些用户在这段是x,表示这个用户不能登录到系统;这个字段是非空的;
第三字段:上次修改口令的时间;这个时间是从1970年01月01日算起到最近一次修改口令的时间间隔(天数),您可以通过passwd 来修改用户的密码,然后查看/etc/shadow中此字段的变化;
第四字段:两次修改口令间隔最少的天数;如果设置为0,则禁用此功能;也就是说用户必须经过多少天才能修改其口令;此项功能用处不是太大;默认值是通过/etc/login.defs文件定义中获取,PASS_MIN_DAYS 中有定义;
第五字段:两次修改口令间隔最多的天数;这个能增强管理员管理用户口令的时效性,应该说在增强了系统的安全性;如果是系统默认值,是在添加用户时由/etc/login.defs文件定义中获取,在PASS_MAX_DAYS 中定义;
第六字段:提前多少天警告用户口令将过期;当用户登录系统后,系统登录程序提醒用户口令将要作废;如果是系统默认值,是在添加用户时由/etc/login.defs文件定义中获取,在PASS_WARN_AGE 中定义;
第七字段:在口令过期之后多少天禁用此用户;此字段表示用户口令作废多少天后,系统会禁用此用户,也就是说系统会不能再让此用户登录,也不会提示用户过期,是完全禁用;
第八字段:用户过期日期;此字段指定了用户作废的天数(从1970年的1月1日开始的天数),如果这个字段的值为空,帐号永久可用;
第九字段:保留字段,目前为空,以备将来Linux发展之用;

2.2 进程

我们在讲Linux权限管理的时候经常讲到用户,但是内核中却没有用户这个数据结构。内核中只会识别UID,至于用户名是通过在/etc/passwd文件中查找对应关系得到的。

以下是获取uid,并且根据uid查找到对应文件名的实例:

// test.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
int main()
{
    uid_t userid;
    struct passwd* pwd;
    userid=getuid();		// 使用系统调用获取到当前进程的uid
    printf("userid is %d\n",userid);
    pwd=getpwuid(userid);	// 根据uid查找`/etc/passwd`文件,得到对应的用户名和用户目录
    printf("username is %s\nuserdir is %s\n",pwd->pw_name,pwd->pw_dir);
}

// 对应输出:
$ ./test 
userid is 1000
username is ipu
userdir is /home/ipu

$ cat /etc/passwd
...
ipu:x:1000:1000:ipu:/home/ipu:/bin/bash

权限管理时真正代表用户的是进程,操作文件的也是进程,也就是说用户所拥有的文件访问权限是通过进程来体现的。

2.2.1 凭证(credentials)

用户拥有的权限,是通过进程的credentials成员来描述的。

task_stuct结构体包含credentials的定义:

struct task_struct {
    ...
	/* Process credentials: */

	/* Tracer's credentials at attach: */
	/* ptrace时attach的tracer的证书 */
	const struct cred __rcu		*ptracer_cred;

	/* Objective and real subjective task credentials (COW): */
	/* `客体`和`实际主体`的进程证书 */
	const struct cred __rcu		*real_cred;

	/* Effective (overridable) subjective task credentials (COW): */
	/* `有效的主体`(可覆盖)的进程证书 */
	const struct cred __rcu		*cred;

    ...
}

对DAC来说,最重要的就是struct cred中的uid、gid定义:

/*
 * The security context of a task
 *
 * The parts of the context break down into two categories:
 *
 *  (1) The objective context of a task.  These parts are used when some other
 *	task is attempting to affect this one.
 *
 *  (2) The subjective context.  These details are used when the task is acting
 *	upon another object, be that a file, a task, a key or whatever.
 *
 * Note that some members of this structure belong to both categories - the
 * LSM security pointer for instance.
 *
 * A task has two security pointers.  task->real_cred points to the objective
 * context that defines that task's actual details.  The objective part of this
 * context is used whenever that task is acted upon.
 *
 * task->cred points to the subjective context that defines the details of how
 * that task is going to act upon another object.  This may be overridden
 * temporarily to point to another security context, but normally points to the
 * same context as task->real_cred.
 */
/ *
 * 进程的安全上下文
 *
 * 上下文的内部分为两类:
 * (1)进程的`客体`上下文。当某些其他进程试图影响本进程时,将使用这些部分。
 * (2)`主体`上下文。当进程作用于另一个对象(文件、进程、键或其他对象)时,将使用这些详细信息。
 *
 * 请注意,此结构体的某些成员同时属于这两个类别 - 例如LSM安全指针。
 *
 * 一个进程有两个安全指针:
 * 1、`task->real_cred`指向`客体`上下文,它定义了进程的实际细节。每当该进程被其他人作用时,都会使用此上下文的客观部分。
 * 2、`task->cred`指向`主体`上下文,该上下文定义了该任务将如何作用于另一个对象的详细信息。可能会暂时覆盖它以指向另一个安全上下文,但通常指向与task->real_cred相同的上下文。
 * /

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 */
	unsigned	securebits;	/* SUID-less security management */
	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 */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */
};

// objective :本译客观,这里可翻译成客体。在本进程作被其他进程作用时,称为客体。  
// subjective :本译主观,这里可翻译成主体。本进程作为主体时,作用于其他客体(如 文件、进程、键或其他对象)。

2.2.2 uid/suid/euid/fsuid

仔细看cred结构,其中最令人疑惑的是uid/gid有四种表达方式,这其中的区别在哪里呢?

name meaning descript
uid real UID 进程原本的uid
suid saved UID 一个uid缓存
在SUID机制设置euid时,suid同时被设置成euid
在setuid()设置uid时,suid同时被设置成uid
euid effective UID 有效uid,权限判断时看的就是euid。
初始状态时,uid和euid相同,做一些权限切换时euid可能改变不等于uid了。
fsuid UID for VFS ops linux系统中特有的文件操作uid,通常情况下和euid相等。
除非调用setfsuid()设置成不一样

2.2.3 初始uid (fork())

uid的初始状态是在进程创建时,复制父进程的安全凭证:

_do_fork() → copy_process() → copy_creds():

int copy_creds(struct task_struct *p, unsigned long clone_flags)
{

	/* (1) 分配新的cred,并复制当前进程cred的内容 */
	new = prepare_creds();

	/* (2) 赋值给新建进程 */
	p->cred = p->real_cred = get_cred(new);

}

普通进程在初始状态时,uid/suid/euid/fsuid都是相同的。

2.2.4 uid权限升级 (SUID execve())

在linux日常使用时,一般我们使用普通用户来操作,bash进程是普通用户的uid,那么各种操作创建出来的新进程也是普通用户uid。

某些情况下,需要切换到root操作,使用su或者sudo命令输入对应密码就能切换到root用户权限。这种权限升级的操作是什么原理呢?

权限的升级依赖于SUID(set-user-id)机制,在文件的UGO策略中除了记录三组用户的rwx mask位,还针对x权限定义了一个补充的s位,对应UGO用户分别为SUID/SGID/SBIT。如果文件设置了SUID,那么它在执行的时候,会把进程的权限(euid)设置成文件属主的uid。我们查看sudo/su文件就是SUID被设置:

[ipu@localhost uid]$ ll /bin/sudo
---s--x--x. 1 root root 147320 Aug  9  2019 /bin/sudo
[ipu@localhost uid]$ ll /bin/su
-rwsr-xr-x. 1 root root 32128 Aug  9  2019 /bin/su

这个SUID机制就是专门为提升/切换用户权限而设计的,切换用户也必须先提升到root用户才能切换到其他用户。在这类文件被执行后,不需要验证密码,进程的euid被设置成文件属主的uid,如果文件属主是root用户当前进程就有了root权限,同时这时进程的uid和euid也不相等了。

具体的execve()代码解析如下:

do_execve() → do_execveat_common():

step 1、分配一份新的安全凭证:
→ prepare_bprm_creds() → prepare_exec_creds()

step 2.1、根据被执行文件是否有suid/sgid被职位,来使用文件属主uid/gid替代进程原来的uid/gid
 → prepare_binprm() → bprm_fill_uid()
static void bprm_fill_uid(struct linux_binprm *bprm)
{
	struct inode *inode;
	unsigned int mode;
	kuid_t uid;
	kgid_t gid;

	/*
	 * Since this can be called multiple times (via prepare_binprm),
	 * we must clear any previous work done when setting set[ug]id
	 * bits from any earlier bprm->file uses (for example when run
	 * first for a setuid script then again for its interpreter).
	 */
	bprm->cred->euid = current_euid();
	bprm->cred->egid = current_egid();

	/* (2.1.1) 如果path不支持suid则返回 */
	if (path_nosuid(&bprm->file->f_path))
		return;

	if (task_no_new_privs(current))
		return;

	inode = bprm->file->f_path.dentry->d_inode;
	mode = READ_ONCE(inode->i_mode);
	if (!(mode & (S_ISUID|S_ISGID)))
		return;

	/* Be careful if suid/sgid is set */
	inode_lock(inode);

	/* reload atomically mode/uid/gid now that lock held */
	/* (2.1.2) 从文件inode读出对应的mode、uid、gid */
	mode = inode->i_mode;
	uid = inode->i_uid;
	gid = inode->i_gid;
	inode_unlock(inode);

	/* We ignore suid/sgid if there are no mappings for them in the ns */
	if (!kuid_has_mapping(bprm->cred->user_ns, uid) ||
		 !kgid_has_mapping(bprm->cred->user_ns, gid))
		return;

	/* (2.1.3) 如果suid标志被设置,使用文件属主uid替代进程原来的euid */
	if (mode & S_ISUID) {
		bprm->per_clear |= PER_CLEAR_ON_SETID;
		bprm->cred->euid = uid;
	}

	/* (2.1.4) 如果sgid和group的x属性被设置,使用文件属主gid替代进程原来的egid */
	if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
		bprm->per_clear |= PER_CLEAR_ON_SETID;
		bprm->cred->egid = gid;
	}
}

step 2.2、
 → prepare_bprm_creds() → security_bprm_set_creds() → cap_bprm_set_creds()

int cap_bprm_set_creds(struct linux_binprm *bprm)
{

	/* (2.2.1) 把suid和fsuid同步成euid的值 */
	new->suid = new->fsuid = new->euid;
	new->sgid = new->fsgid = new->egid;

}

step 3、更新进程为新的安全凭证:
→ load_elf_binary() → install_exec_creds() → commit_creds()

注意:可以看到SUID的权限是非常大的,如果文件属主是root,不需要验证密码进程权限被提升为root权限。这类文件如果有漏洞的话,就会被攻击获取到root权限,需要特别小心。

2.2.5 uid权限降级 (setreuid()/setuid()/setresuid()/setfsuid())

在使用sudo/su进行操作时,并不希望进程一直处于root权限当中,它最终会根据配置切换到适当的权限。

例如sudo -u test cat /etc/shadow的命令执行过程:

  • 1、首先因为sudo的SUID被设置且文件属主为root,execve()执行sudo,进程权限被切换成root权限。
  • 2、接下来sudo进程运行在root权限状态,来验证当前用户密码。
  • 3、如果密码验证成功,则读取/etc/sudoers配置文件,其中规定了当前用户使用sudo命令时的权限。/etc/sudoers文件中的配置格式如下%wheel ALL=(ALL) ALL,规定了指定用户能切换到哪些用户,能运行哪些命令。
  • 4、如果当前用户允许切换到test用户,则执行sudo命令的-u test参数,调用setuid()系统调用把当前进程的euid切换成test用户。
  • 5、切换到test用户以后,如果/etc/sudoers允许执行cat命令,则继续执行cat /etc/shadow命令。

从上述的过程可以看到权限切换的关键途径,一般通过SUID机制来无密码的把权限升级到root,然后在root状态下验证用户密码,再根据配置通过setreuid()/setuid()/setresuid()系统调用到权限降级到合适用户。

setreuid()/setuid()/setresuid()系统调用可以降级权限,它没有升级的能力。它的几个基本准则是:

  • 1、进程只能设置本进程的uid。
  • 2、如果进程有root权限,那么uid/suid/euid/fsuid可以设置任意值。
  • 3、如果进程没有root权限,那么设置新的uid只能是原uid/suid/euid/fsuid中的一个值,不能是任意值。

这几个系统调用的详细解析过程:

SYSCALL_DEFINE1(setuid, uid_t, uid)
{
	struct user_namespace *ns = current_user_ns();
	const struct cred *old;
	struct cred *new;
	int retval;
	kuid_t kuid;

	/* (1) 在当前namespace下,把uid转换成kuid */
	kuid = make_kuid(ns, uid);
	if (!uid_valid(kuid))
		return -EINVAL;

	/* (2) 分配新的进程权限凭证,默认拷贝了旧的凭证值 */
	new = prepare_creds();
	if (!new)
		return -ENOMEM;
	old = current_cred();

	retval = -EPERM;
	/* (3.1) 如果当前进程有CAP_SETUID权限的能力 (一般这里指root权限,在通过SUID切换到root权限时,会赋予用户所有能力)
			设置新cred的suid和uid为传入参数uid
	 */
	if (ns_capable(old->user_ns, CAP_SETUID)) {
		new->suid = new->uid = kuid;
		if (!uid_eq(kuid, old->uid)) {
			retval = set_user(new);
			if (retval < 0)
				goto error;
		}
	/* (3.2) 如果当前进程没有CAP_SETUID(不是root用户),设置的uid也不等于原cred的uid或suid中的一个
			出错返回
	 */
	} else if (!uid_eq(kuid, old->uid) && !uid_eq(kuid, new->suid)) {
		goto error;
	}

	/* (3.3) 综合上述逻辑分为几种情况:
			1、有CAP_SETUID权限(root用户),把新cred的uid、suid、euid、fsuid全都设置成新的uid
			2、没有CAP_SETUID权限(非root用户),只能把新cred的euid、fsuid设置成原cred的uid或suid中的一个。例如原来使用SUID机制切换了权限,这里也可以切换回去。
			3、不符合以上两种条件的都是非法操作。
	 */
	new->fsuid = new->euid = kuid;

	/* (4) lsm check点 */
	retval = security_task_fix_setuid(new, old, LSM_SETID_ID);
	if (retval < 0)
		goto error;

	/* (5) 更新为新凭证 */
	return commit_creds(new);

error:
	abort_creds(new);
	return retval;
}

SYSCALL_DEFINE2(setreuid, uid_t, ruid, uid_t, euid)
{
	struct user_namespace *ns = current_user_ns();
	const struct cred *old;
	struct cred *new;
	int retval;
	kuid_t kruid, keuid;

	kruid = make_kuid(ns, ruid);
	keuid = make_kuid(ns, euid);

	if ((ruid != (uid_t) -1) && !uid_valid(kruid))
		return -EINVAL;
	if ((euid != (uid_t) -1) && !uid_valid(keuid))
		return -EINVAL;

	new = prepare_creds();
	if (!new)
		return -ENOMEM;
	old = current_cred();

	retval = -EPERM;
	/* (1.1) 如果传入ruid != -1,则更新新cred的uid值
			否则保持新cred的uid值不变
	 */
	if (ruid != (uid_t) -1) {
		new->uid = kruid;
		/* (1.2) 如果ruid != -1,还需要满足以下附加条件,才能更新新cred的uid值:
				1、有CAP_SETUID权限(root用户),可以把uid设置成任意值。
				2、没有CAP_SETUID权限(非root用户),只能把uid设置成原cred的uid或euid中的任一个。
		 */
		if (!uid_eq(old->uid, kruid) &&
		    !uid_eq(old->euid, kruid) &&
		    !ns_capable(old->user_ns, CAP_SETUID))
			goto error;
	}

	/* (2.1) 如果传入euid != -1,则更新新cred的euid值
			否则保持新cred的euid值不变
	 */
	if (euid != (uid_t) -1) {
		new->euid = keuid;
		/* (2.2) 如果euid != -1,还需要满足以下附加条件,才能更新新cred的euid值:
				1、有CAP_SETUID权限(root用户),可以把euid设置成任意值。
				2、没有CAP_SETUID权限(非root用户),只能把euid设置成原cred的uid、euid或suid中的任一个。
		 */
		if (!uid_eq(old->uid, keuid) &&
		    !uid_eq(old->euid, keuid) &&
		    !uid_eq(old->suid, keuid) &&
		    !ns_capable(old->user_ns, CAP_SETUID))
			goto error;
	}

	if (!uid_eq(new->uid, old->uid)) {
		retval = set_user(new);
		if (retval < 0)
			goto error;
	}

	/* (3) 如果ruid被成功更新,
			或者euid被成功更新,且euid不等于原uid的值
			更新suid的值为euid
	 */
	if (ruid != (uid_t) -1 ||
	    (euid != (uid_t) -1 && !uid_eq(keuid, old->uid)))
		new->suid = new->euid;

	/* (4) 同步更新fsuid为euid (linux下,大部分情况下fsuid就等于euid) */	
	new->fsuid = new->euid;

	retval = security_task_fix_setuid(new, old, LSM_SETID_RE);
	if (retval < 0)
		goto error;

	return commit_creds(new);

error:
	abort_creds(new);
	return retval;
}

SYSCALL_DEFINE3(setresuid, uid_t, ruid, uid_t, euid, uid_t, suid)
{
	struct user_namespace *ns = current_user_ns();
	const struct cred *old;
	struct cred *new;
	int retval;
	kuid_t kruid, keuid, ksuid;

	kruid = make_kuid(ns, ruid);
	keuid = make_kuid(ns, euid);
	ksuid = make_kuid(ns, suid);

	if ((ruid != (uid_t) -1) && !uid_valid(kruid))
		return -EINVAL;

	if ((euid != (uid_t) -1) && !uid_valid(keuid))
		return -EINVAL;

	if ((suid != (uid_t) -1) && !uid_valid(ksuid))
		return -EINVAL;

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	old = current_cred();

	retval = -EPERM;
	/* (1.1) 有CAP_SETUID权限(root用户),可以把uid/euid/suid设置成任意值。 */
	if (!ns_capable(old->user_ns, CAP_SETUID)) {
		/* (1.2) 如果传入ruid != -1,没有CAP_SETUID权限(非root用户),只能把uid设置成原cred的uid、euid或suid中的任一个。 */
		if (ruid != (uid_t) -1        && !uid_eq(kruid, old->uid) &&
		    !uid_eq(kruid, old->euid) && !uid_eq(kruid, old->suid))
			goto error;
		/* (1.3) 如果传入euid != -1,没有CAP_SETUID权限(非root用户),只能把euid设置成原cred的uid、euid或suid中的任一个。 */
		if (euid != (uid_t) -1        && !uid_eq(keuid, old->uid) &&
		    !uid_eq(keuid, old->euid) && !uid_eq(keuid, old->suid))
			goto error;
		/* (1.4) 如果传入suid != -1,没有CAP_SETUID权限(非root用户),只能把suid设置成原cred的uid、euid或suid中的任一个。 */
		if (suid != (uid_t) -1        && !uid_eq(ksuid, old->uid) &&
		    !uid_eq(ksuid, old->euid) && !uid_eq(ksuid, old->suid))
			goto error;
	}

	/* (2.1) 更新uid的值为传入的ruid */
	if (ruid != (uid_t) -1) {
		new->uid = kruid;
		if (!uid_eq(kruid, old->uid)) {
			retval = set_user(new);
			if (retval < 0)
				goto error;
		}
	}
	/* (2.2) 更新euid的值为传入的euid */
	if (euid != (uid_t) -1)
		new->euid = keuid;
	/* (2.3) 更新suid的值为传入的suid */
	if (suid != (uid_t) -1)
		new->suid = ksuid;
	/* (3) 同步更新fsuid为euid (linux下,大部分情况下fsuid就等于euid) */	
	new->fsuid = new->euid;

	retval = security_task_fix_setuid(new, old, LSM_SETID_RES);
	if (retval < 0)
		goto error;

	return commit_creds(new);

error:
	abort_creds(new);
	return retval;
}

SYSCALL_DEFINE1(setfsuid, uid_t, uid)
{
	const struct cred *old;
	struct cred *new;
	uid_t old_fsuid;
	kuid_t kuid;

	old = current_cred();
	old_fsuid = from_kuid_munged(old->user_ns, old->fsuid);

	kuid = make_kuid(old->user_ns, uid);
	if (!uid_valid(kuid))
		return old_fsuid;

	new = prepare_creds();
	if (!new)
		return old_fsuid;

	/* (1) 符合以下条件,把当前进程fsuid设置成传入的uid
			情况1、有CAP_SETUID权限(root用户)。
			情况2、没有CAP_SETUID权限(非root用户),且传入的uid等于原uid/suid/euid中的任意一员。
	 */
	if (uid_eq(kuid, old->uid)  || uid_eq(kuid, old->euid)  ||
	    uid_eq(kuid, old->suid) || uid_eq(kuid, old->fsuid) ||
	    ns_capable(old->user_ns, CAP_SETUID)) {
		if (!uid_eq(kuid, old->fsuid)) {
			new->fsuid = kuid;
			if (security_task_fix_setuid(new, old, LSM_SETID_FS) == 0)
				goto change_okay;
		}
	}

	abort_creds(new);
	return old_fsuid;

change_okay:
	commit_creds(new);
	return old_fsuid;
}

3. 客体(object)

对DAC模式来说,客体通常是文件。但是进程也是可以作为客体的,还记得进程有两个cred成员,task->cred是进程作为主体时的权限凭证,而task->real_cred是进程作为客体时的权限凭证。

对客体文件来说,最重要的属性是文件的属主uid和gid:

inode->i_uid
inode->i_gid

对应命令查看时的:

在这里插入图片描述

需要注意的是进程主体(subject)也是和文件耦合在一起,一是进程的执行代码是从文件中加载的,而是SUID机制能把进程的执行权限修改成文件属主。所以在DAC模型理解时,注意相互之间的概念,避免绕晕。

4. 规则(policy)

对DAC模式来说,规则通常比较简单,一般就存储在客体文件inode相关属性中。而MAC模式,规则比较复杂,需要独立的文件来存储。

4.1 UGO(User、Ggroup、Other)规则

UGO是最通用的规则了,客体文件inode->i_mode中存储了UGO 3组mask,每组mask由rwx三个行为组成。
在这里插入图片描述

UGO把操作当前文件的进程分为3种类型:

  • User用户。文件的属主,即主体进程的euid等于客体文件的uid。
  • Group同组用户。即主体进程的egid等于客体文件的gid。
  • Other用户。不满足上述两种条件的其他用户。

每组用户对当前文件的行为,又分为3种权限:

  • r(Read,读取):
    对文件而言,具有读取文件内容的权限;
    对目录来说,具有浏览目录的权限。
  • w(Write,写入):
    对文件而言,具有新增,修改,删除文件内容的权限;
    对目录来说,具有新建,删除,修改,移动目录内文件的权限。
  • x(eXecute,执行):
    对文件而言,具有执行文件的权限;
    对目录了来说该用户具有进入目录的权限。

目录权限的总结:

  • 1、目录的只读访问不允许使用cd进入目录,必须要有执行的权限才能进入。
  • 2、只有执行权限只能进入目录,不能看到目录下的内容,要想看到目录下的文件名和目录名,需要可读权限。
  • 3、一个文件能不能被删除,主要看该文件所在的目录对用户是否具有写权限,如果目录对用户没有写权限,则该目录下的所有文件都不能被删除,文件所有者除外
  • 4、目录的w位不设置,即使你拥有目录中某文件的w权限也不能写该文件

文件默认权限:umask值用于设置用户在创建文件时的默认权限,当我们在系统中创建目录或文件时,目录或文件所具有的默认权限就是由umask值决定的。对于root用户,系统默认的umask值是0022;对于普通用户,系统默认的umask值是0002。执行umask命令可以查看当前用户的umask值。
umask值一共有4组数字,其中第1组数字用于定义特殊权限,我们一般不予考虑,与一般权限有关的是后3组数字。
默认情况下,对于目录,用户所能拥有的最大权限是777;对于文件,用户所能拥有的最大权限是目录的最大权限去掉执行权限,即666。因为x执行权限对于目录是必须的,没有执行权限就无法进入目录,而对于文件则不必默认赋予x执行权限。
对于root用户,他的umask值是022。当root用户创建目录时,默认的权限就是用最大权限777去掉相应位置的umask值权限,即对于所有者不必去掉任何权限,对于所属组要去掉w权限,对于其他用户也要去掉w权限,所以目录的默认权限就是755;当root用户创建文件时,默认的权限则是用最大权限666去掉相应位置的umask值,即文件的默认权限是644

有了UGO规则,主体进程在RWX客体文件时会做对应的权限规则检查。例如,open一个文件时,几个权限校验的关键节点:

  • 第一步权限检查: 最开始对文件所在路径上每个目录项对应的inode进行执行权限检查.对应代码:path_openat->link_path_walk->may_lookup->inode_permission;
  • 第二步权限检查:如果是新建文件,对文件所在目录项的inode做写和可执行权限检查。对应代码:path_openat->do_last->lookup_open->vfs_create->may_create->inode_permission.
  • 第三步权限检查:真正打开文件之前,对文件所对应的inode做读写权限检查。对应代码:path_openat->do_last->may_open->inode_permission.

inode_permission()最后会调用acl_permission_check()来做UGO规则检查:

inode_permission() → __inode_permission() → do_inode_permission() → generic_permission() → acl_permission_check()

static int acl_permission_check(struct inode *inode, int mask)
{
	/* (1) 从i_mode中取出UGO规则 */
	unsigned int mode = inode->i_mode;

	/* (2) User用户取最高3bit规则,主体进程的euid等于客体文件的uid */
	if (likely(uid_eq(current_fsuid(), inode->i_uid)))
		mode >>= 6;
	else {
		/* (3) User用户匹配失败首先去匹配ACL规则 */
		if (IS_POSIXACL(inode) && (mode & S_IRWXG)) {
			int error = check_acl(inode, mask);
			if (error != -EAGAIN)
				return error;
		}

		/* (4) ACL匹配失败则尝试匹配Group用户规则,
				Group用户取中间3bit规则,主体进程的egid等于客体文件的gid 
		*/
		if (in_group_p(inode->i_gid))
			mode >>= 3;
	}
	/* (5) 如果以上条件都未匹配成功,则为Other用户,取最低3bit规则 */

	/*
	 * If the DACs are ok we don't need any capability check.
	 */
	/* (6) 使用规则允许的3bit和当前操作进行匹配,决定放行还是拒绝 */
	if ((mask & ~mode & (MAY_READ | MAY_WRITE | MAY_EXEC)) == 0)
		return 0;
	return -EACCES;
}

4.2 ACL(Access Control List)规则

UGO的规则非常简洁和实用,但是在使用的过程中人们发现这个分组粒度实在是太粗了,仅仅3种分组UGO。如果一个用户需要在UGO之外分配一个独有的权限,该怎么操作呢?

在普通权限中,用户对文件只有三种身份,就是属主、属组和其他人;每种用户身份拥有读(read)、写(write)和执行(execute)三种权限。但是在实际工作中,这三种身份实在是不够用。ACL 权限就是为了解决这个问题的。在使用 ACL 权限给用户 st 陚予权限时,st 既不是 /project 目录的属主,也不是属组,仅仅赋予用户 st 针对此目录的 r-x 权限。这有些类似于 Windows 系统中分配权限的方式,单独指定用户并单独分配权限,这样就解决了用户身份不足的问题。ACL是Access Control List(访问控制列表)的缩写,不过在Linux系统中,ACL用于设定用户针对文件的权限,而不是在交换路由器中用来控制数据访问的功能(类似于防火墙)。

ACL主要是以下两条命令:

# getfacl 文件名			// 查看ACL权限
# setfacl 选项 文件名		// 设定ACL权限
		选项:
		-m:设定 ACL 权限。如果是给予用户 ACL 权限,则使用"u:用户名:权限"格式赋予;如果是给予组 ACL 权限,则使用"g:组名:权限" 格式赋予;
		-x:删除指定的 ACL 权限;
		-b:删除所有的 ACL 权限;
		-d:设定默认 ACL 权限。只对目录生效,指目录中新建立的文件拥有此默认权限;
		-k:删除默认 ACL 权限;
		-R:递归设定 ACL 权限。指设定的 ACL 权限会对目录下的所有子文件生效;

例如:我们要求root/project目录的属主,权限是rwxtgroup是此目录的属组,tgroup组中拥有班级学员zhangsanlisi,权限是rwx;其他人的权限是0。这时试听学员st来了,她的权限是r-x。我们来看具体的分配命令。

[root@localhost ~]# useradd zhangsan
[root@localhost ~]# useradd lisi
[root@localhost ~]# useradd st
[root@localhost ~]# groupadd tgroup
// 添加需要试验的用户和用户组,省略设定密码的过程
[root@localhost ~]# mkdir /project #建立需要分配权限的目录
[root@localhost ~]# chown root:tgroup /project/
// 改变/project目录的属主和属组
[root@localhost ~]# chmod 770 /project/
// 指定/project目录的权限
[root@localhost ~]# ll -d /project/
drwxrwx--- 2 root tgroup 4096 1月19 04:21 /project/
// 查看一下权限,已经符合要求了
// 这时st学员来试听了,如何给她分配权限
[root@localhost ~]# setfacl -m u:st:rx /project/
// 给用户st赋予r-x权限,使用"u:用户名:权限" 格式
[root@localhost /]# cd /
[root@localhost /]# ll -d project/
drwxrwx---+ 3 root tgroup 4096 1月19 05:20 project/
// 使用ls-l査询时会发现,在权限位后面多了一个"+",表示此目录拥有ACL权限
[root@localhost /]# getfacl project
// 查看/prpject目录的ACL权限
#file: project <-文件名
#owner: root <-文件的属主
#group: tgroup <-文件的属组
user::rwx <-用户名栏是空的,说明是属主的权限
user:st:r-x <-用户st的权限
group::rwx <-组名栏是空的,说明是属组的权限
mask::rwx <-mask权限
other::--- <-其他人的权限

// 大家可以看到,st 用户既不是 /prpject 目录的属主、属组,也不是其他人,我们单独给 st 用户分配了 r-x 权限。这样分配权限太方便了,完全不用先辛苦地规划用户身份了。

ACL规则是UGO规则的补充,相当于扩充了UGO用户组,但是操作的行为RWX还是不能扩充。

ACL规则信息保存在inode当中。例如:Ext4 扩展属性(xattrs)通常存储在磁盘上的一个单独的数据块中,通过inode.i_file_acl*引用。扩展属性的第一应用是存储文件的ACL以及其他安全数据(selinux)。

在上一节acl_permission_check()的分析中我们就已经看到,在User用户匹配失败后就去匹配ACL规则,在ACL规则匹配失败以后才去匹配Group用户。我们看看具体的过程:

inode_permission() → __inode_permission() → do_inode_permission() → generic_permission() → acl_permission_check() → check_acl():

static int check_acl(struct inode *inode, int mask)
{
#ifdef CONFIG_FS_POSIX_ACL
	struct posix_acl *acl;

	if (mask & MAY_NOT_BLOCK) {
		acl = get_cached_acl_rcu(inode, ACL_TYPE_ACCESS);
	        if (!acl)
	                return -EAGAIN;
		/* no ->get_acl() calls in RCU mode... */
		if (is_uncached_acl(acl))
			return -ECHILD;
	        return posix_acl_permission(inode, acl, mask & ~MAY_NOT_BLOCK);
	}

	/* (1) 获取到acl信息 */
	acl = get_acl(inode, ACL_TYPE_ACCESS);
	if (IS_ERR(acl))
		return PTR_ERR(acl);
	if (acl) {
			/* (2) 查询主体进程在ACL规则中的权限 */
	        int error = posix_acl_permission(inode, acl, mask);
	        posix_acl_release(acl);
	        return error;
	}
#endif

	return -EAGAIN;
}

4.3 Capability规则

在UGO的使用过程中,人们还发现UGO的另一个弊端,就是行为的划分也是粗粒度的,仅仅三种行为RWX。这样会造成权限过量的情况,如果用户只需要某项行为的权限,但是你只能给他root的rwx权限,那他就拥有了所有其他行为的root rwx权限。这种拥有所有root权限的程序是攻击的首选目标,如果它有漏洞就会造成系统门户大开。

Linux引入了capabilities机制对root行为进行细粒度的控制,实现按需授权,从而减小系统的安全攻击面。

例如,把ping的权限从SUID的root权限,降为普通用户+cap_net_admin,cap_net_raw能力,功能上完全一致,但是安全风险大大降低。

  • step 1、因为 ping 命令在执行时需要访问网络,这就需要获得 root 权限,常规的做法是通过 SUID 实现的(和 passwd 命令相同)。红框中的 s 说明应用程序文件被设置了 SUID,这样普通用户就可以执行这些命令了。
    在这里插入图片描述

  • step 2、移除 ping 命令文件上的 SUID 权限。在移除 SUID 权限后,普通用户在执行 ping 命令时碰到了 “ping: socket: Operation not permitted” 错误。
    在这里插入图片描述

  • step 3、为 ping 命令文件添加 capabilities。通过 setcap 命令可以添加执行 ping 命令所需的 capabilities 为 cap_net_admin 和 cap_net_raw,被赋予合适的 capabilities 后,ping 命令又可以正常工作了,相比 SUID 它只具有必要的特权,在最大程度上减小了系统的安全攻击面。
    在这里插入图片描述

Linux 将传统上与超级用户 root 关联的特权划分为不同的单元,称为 capabilites。Capabilites 作为线程(Linux 并不真正区分进程和线程)的属性存在,每个单元可以独立启用和禁用。如此一来,权限检查的过程就变成了:在执行特权操作时,如果进程的有效身份不是 root,就去检查是否具有该特权操作所对应的 capabilites,并以此决定是否可以进行该特权操作。比如要向进程发送信号(kill()),就得具有 capability CAP_KILL;如果设置系统时间,就得具有 capability CAP_SYS_TIME。

capabilities能力的全集可以参考capabilities man page

在主体进程的credentials结构体中,定义了本进程的capability能力:

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 */
	...
}

和SUID机制一样,在文件执行的时候,把文件的capability和当前进程的capability进行综合:

do_execve() → do_execveat_common()→ prepare_binprm() → security_bprm_set_creds() → cap_bprm_set_creds():

int cap_bprm_set_creds(struct linux_binprm *bprm)
{
	const struct cred *old = current_cred();
	struct cred *new = bprm->cred;
	bool effective = false, has_fcap = false, is_setid;
	int ret;
	kuid_t root_uid;

	if (WARN_ON(!cap_ambient_invariant_ok(old)))
		return -EPERM;

	ret = get_file_caps(bprm, &effective, &has_fcap);
	if (ret < 0)
		return ret;

	/* (1) root uid等于0 */
	root_uid = make_kuid(new->user_ns, 0);

	/* (2) 判断当前进程是不是root用户,针对root权限的capability能力赋值 */
	handle_privileged_root(bprm, has_fcap, &effective, root_uid);

	/* (3) 普通用户的capability综合过程,就没有仔细分析了 */
	/* if we have fs caps, clear dangerous personality flags */
	if (__cap_gained(permitted, new, old))
		bprm->per_clear |= PER_CLEAR_ON_SETID;

	/* Don't let someone trace a set[ug]id/setpcap binary with the revised
	 * credentials unless they have the appropriate permit.
	 *
	 * In addition, if NO_NEW_PRIVS, then ensure we get no new privs.
	 */
	is_setid = __is_setuid(new, old) || __is_setgid(new, old);

	if ((is_setid || __cap_gained(permitted, new, old)) &&
	    ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) ||
	     !ptracer_capable(current, new->user_ns))) {
		/* downgrade; they get no more than they had, and maybe less */
		if (!ns_capable(new->user_ns, CAP_SETUID) ||
		    (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
			new->euid = new->uid;
			new->egid = new->gid;
		}
		new->cap_permitted = cap_intersect(new->cap_permitted,
						   old->cap_permitted);
	}

	new->suid = new->fsuid = new->euid;
	new->sgid = new->fsgid = new->egid;

	/* File caps or setid cancels ambient. */
	if (has_fcap || is_setid)
		cap_clear(new->cap_ambient);

	/*
	 * Now that we've computed pA', update pP' to give:
	 *   pP' = (X & fP) | (pI & fI) | pA'
	 */
	new->cap_permitted = cap_combine(new->cap_permitted, new->cap_ambient);

	/*
	 * Set pE' = (fE ? pP' : pA').  Because pA' is zero if fE is set,
	 * this is the same as pE' = (fE ? pP' : 0) | pA'.
	 */
	if (effective)
		new->cap_effective = new->cap_permitted;
	else
		new->cap_effective = new->cap_ambient;

	if (WARN_ON(!cap_ambient_invariant_ok(new)))
		return -EPERM;

	if (nonroot_raised_pE(new, old, root_uid, has_fcap)) {
		ret = audit_log_bprm_fcaps(bprm, new, old);
		if (ret < 0)
			return ret;
	}

	new->securebits &= ~issecure_mask(SECURE_KEEP_CAPS);

	if (WARN_ON(!cap_ambient_invariant_ok(new)))
		return -EPERM;

	/* Check for privilege-elevated exec. */
	bprm->cap_elevated = 0;
	if (is_setid ||
	    (!__is_real(root_uid, new) &&
	     (effective ||
	      __cap_grew(permitted, ambient, new))))
		bprm->cap_elevated = 1;

	return 0;
}

↓

static void handle_privileged_root(struct linux_binprm *bprm, bool has_fcap,
				   bool *effective, kuid_t root_uid)
{
	const struct cred *old = current_cred();
	struct cred *new = bprm->cred;

	if (!root_privileged())
		return;
	/*
	 * If the legacy file capability is set, then don't set privs
	 * for a setuid root binary run by a non-root user.  Do set it
	 * for a root user just to cause least surprise to an admin.
	 */
	if (has_fcap && __is_suid(root_uid, new)) {
		warn_setuid_and_fcaps_mixed(bprm->filename);
		return;
	}
	/*
	 * To support inheritance of root-permissions and suid-root
	 * executables under compatibility mode, we override the
	 * capability sets for the file.
	 */
	/* (2.1) 如果当前进程的euid或者uid等于root用户0
			则给进程赋值所有capability (cap_bset成员的值包含了capability全集,所有bit都为1)
	 */
	if (__is_eff(root_uid, new) || __is_real(root_uid, new)) {
		/* pP' = (cap_bset & ~0) | (pI & ~0) */
		new->cap_permitted = cap_combine(old->cap_bset,
						 old->cap_inheritable);
	}
	/*
	 * If only the real uid is 0, we do not set the effective bit.
	 */
	if (__is_eff(root_uid, new))
		*effective = true;
}

在权限判断时,如果UGO权限匹配拒绝,继续尝试进行capability的匹配:

inode_permission() → __inode_permission() → do_inode_permission() → generic_permission():

int generic_permission(struct inode *inode, int mask)
{
	int ret;

	/*
	 * Do the basic permission checks.
	 */
	/* (1) 首先进行UGO权限匹配,如果失败则尝试进行capability能力匹配 */
	ret = acl_permission_check(inode, mask);
	if (ret != -EACCES)
		return ret;

	if (S_ISDIR(inode->i_mode)) {
		/* DACs are overridable for directories */
		if (!(mask & MAY_WRITE))
			/* (2.1) 尝试进行CAP_DAC_READ_SEARCH能力匹配 */
			if (capable_wrt_inode_uidgid(inode,
						     CAP_DAC_READ_SEARCH))
				return 0;
		/* (2.2) 尝试进行CAP_DAC_OVERRIDE能力匹配 */
		if (capable_wrt_inode_uidgid(inode, CAP_DAC_OVERRIDE))
			return 0;
		return -EACCES;
	}

	/*
	 * Searching includes executable on directories, else just read.
	 */
	mask &= MAY_READ | MAY_WRITE | MAY_EXEC;
	if (mask == MAY_READ)
		/* (2.3) 尝试进行CAP_DAC_READ_SEARCH能力匹配 */
		if (capable_wrt_inode_uidgid(inode, CAP_DAC_READ_SEARCH))
			return 0;
	/*
	 * Read/write DACs are always overridable.
	 * Executable DACs are overridable when there is
	 * at least one exec bit set.
	 */
	if (!(mask & MAY_EXEC) || (inode->i_mode & S_IXUGO))
		/* (2.4) 尝试进行CAP_DAC_OVERRIDE能力匹配 */
		if (capable_wrt_inode_uidgid(inode, CAP_DAC_OVERRIDE))
			return 0;

	return -EACCES;
}

4.4 selinux规则

综合上述的规则来说,在UGO使用的过程中,大家越来越发现了UGO的弊端就是权限划分的粒度过粗。为了解决这个问题,ACL和Capability从不同的角度尝试解决这个问题:

  • ACL尝试扩充UGO的用户组。把3组扩充成自定义多组。
  • Capability尝试扩充RWX行为。把3组行为扩充成多组行为。

在这之后,selinux综合了ACL和Capability的扩展思路,推出了一套完成的扩充用户组和行为的细粒度权限管理方案。

后续我们再来详细分析selinux的实现。

5. 提权漏洞及防护

5.1 内核漏洞提权

利用内核的漏洞,直接修改运行进程的euid的值,用来获取root权限。例如:CVE-2017-16995、CVE-2018-1000001、CVE-2016-5195。

这种漏洞的防护分为几步:

  • step 1、在程序execve()执行时记录进程的uid/suid/euid/fsuid初始值。在LSM钩子security_bprm_committed_creds()上记录:
do_execve() → do_execveat_common() → exec_binprm() → load_elf_binary() → install_exec_creds() → security_bprm_committed_creds()
  • step 2、在更改uid的合法途径,这几个系统调用(setreuid()/setuid()/setresuid()/setfsuid())中判断uid有没有被非法修改,合法则记录改动。在LSM钩子security_task_fix_setuid()上记录:
setreuid()/setuid()/setresuid()/setfsuid() → prepare_creds() → security_prepare_creds() // 可以在这个pre钩子点,判断当前uid和上次记录的合法修改uid有没有变动。
setreuid()/setuid()/setresuid()/setfsuid() → security_task_fix_setuid()
  • step 3、在各个关键路径上,比较当前的uid值和初始值,如果合法途径不可能做到,则为非法提权:
有两条简单的规则来进行判断:
1、如果execve()初始的uid或者euid的值为0则表明进程有root权限,则当前uid/suid/euid/fsuid为任意值都是合法的,因为root用户有这种能力。
2、如果execve()初始的uid或者euid的值不为0则表明进程没有root权限,则当前的uid/suid/euid/fsuid值只能为初始值uid或euid的其中一种。因为合法途径setreuid()/setuid()/setresuid()/setfsuid()只能在有限范围内改动。

5.2 sudo漏洞提权

CVE-2019-14287,对于sudoer中配置的非root权限用户,使用"sudo #-1"漏洞获得了root权限。

该漏洞的防护,可以在setresuid()系统调用中做pre钩子,如果普通用户传入-1参数则进行拦截。

参考资料:

1、Linux 用户身份与进程权限
2、Linux 特殊权限 SUID,SGID,SBIT
3、linux内核open过程的权限管理
4、Linux Capabilities 简介
5、linux权限检查机制
6、inode权限检查
7、linux内核setuid分析
8、setuid seteuid setreuid 三个函数讲解
9、Linux Capabilities 入门:让普通进程获得 root 的洪荒之力
10、UID, EUID, SUID, FSUID

猜你喜欢

转载自blog.csdn.net/pwl999/article/details/110878563