• Linux 安全 - SUID机制


    一、简介

    1.1 简介

    最初 UNIX 为文件分配了九个允许位,对应三类用户(同主、同组、其他),三种操作(读、写、执行)。后来,UNIX 又增加了三个允许位:set-user-bit(又称 set-user-id 或 setuid)、set-group-bit(又称 set-group-id 或 setgid)、set-other-bit(又称 sticky bit)。看起来似乎是为文件新增加了一种set 操作,但实际上不是这样,这三个允许位与操作许可无关。先说 setuid。进程调用 execve 执行了一个允许位 setuid 为 1 的文件后,进程的 euid,还有Linux 特有的 fsuid,被改变为所执行文件的属主 id。效果就是进程执行文件不仅将文件的内容读入进程的代码区内存和数据区内存,还将文件属性中的部分数据读入进程凭证。于是,进程可以操作一些以前不能操作的客体。

    引入 setuid 更深层的目的是特权提升。

    UNIX 的设计是简单的,一些任务只能由特权用户来做,而特权用户只有一个,就是用户id 为 0 的 root。如果普通用户需要做一些特权操作,那么正规的做法是请求管理员代劳,比如用户需要加载一块新硬盘。但是有的特权操作使用频繁,又不会对系统带来危害,比如passwd命令,普通用户可以在不具备修改密码文件权限的情况下,通过执行该命令来更改自己的密码。
    它实现起来是这样的:
    (1)passwd 二进制文件的属主是 root。
    (2)passwd 二进制文件的 setuid 位被置位。
    (3)passwd 二进制文件的允许位设置为所有人都可以执行。

    1.2 文件权限位

    UNIX 在诞生之初就采用了一种相对简单的方式来管理操作许可,这种方式被 Linux 继承,以文件为例:
    (1)在每个文件的属性中存储文件所属的用户(属主)和所属的用户组(属组)。
    (2)根据用户的属主和属组将所有用户分为三类:同主用户、同组用户、其他用户。
    (3)在每个文件的属性中为三类用户分别存储访问许可。

    (1)

    $ ls -l text.txt 
    -rw-rw-r-- 1 yl yl 0 Sep 28 16:25 text.txt
    
    • 1
    • 2

    其中第一个字段-rw-rw-r–,我们可以把它分为四部分看:

    -rw-rw-r--
    
    • 1
    1-     :第一个字符表示文件的类型
    (2)rw-   :第 2-4 个字符表示文件所有者权限位
    (3)rw-   :5-7 个字符表示所属组权限位
    (4)r--	   :8 -10 个字符表示其他用户权限位
    
    • 1
    • 2
    • 3
    • 4

    每个文件和目录都有一组权限位,用于控制对其的访问和操作权限。文件权限由九个位组成,分为三组,每组三个位,分别代表文件所有者、所属组和其他用户的权限。

    这三组权限位分别是:
    (1)文件所有者权限位:这组权限位控制文件所有者对文件的权限。它们的顺序是读取(r,位值为4)、写入(w,位值为2)和执行(x,位值为1)。文件所有者权限位的符号表示为:例如,rw-表示文件所有者具有读取和写入权限,但没有执行权限。

    (2)所属组权限位:这组权限位控制与文件属于相同组的其他用户对文件的权限。它们的顺序和符号表示与文件所有者权限位相同。

    (3)其他用户权限位:这组权限位控制所有其他用户对文件的权限,即除了文件所有者和所属组之外的用户。同样,它们的顺序和符号表示与文件所有者权限位相同。

    内核代码采用一个 bit 来表示一个操作许可,对于文件就需要 9 个 bit 来表示文件的操作许可:同主读、同主写、同主执行、同组读、同组写、同组执行、其他读、其他写、其他执行。这些表示操作许可的比特位合在一起就成为权限位。

    在文件权限中,每个用户类型可以具有读取、写入和执行的权限组合。对于目录而言,读取权限允许查看目录内容,写入权限允许创建、删除和重命名目录中的文件,执行权限允许进入目录并访问其内容。

    (2)
    特殊权限:权限格式中可以存在三个特殊权限字符:
    “s”(setuid、setgid):当出现在所有者或所属组的执行权限中时,表示设置了 setuid 或 setgid 权限。如果相应的读取或写入权限未设置,则用 “S” 表示。
    “t”(粘着位):当出现在目录的其他用户执行权限中时,表示粘着位。通常用于限制只有文件所有者能删除目录中的文件。

    当文件设置了SUID权限时,文件的权限位会显示为包含"S"的特殊形式。具体来说,文件所有者权限位中的执行权限位 “x” 将被替换为 “s”。这表示当执行该文件时,它将以文件所有者的权限运行。

    以下是SUID权限位的不同组合及其含义:
    -rwS------:文件所有者具有读写权限,并且设置了SUID权限。其他用户没有任何权限。
    -rwSr-xr-x:文件所有者具有读写权限,并且设置了SUID权限。所属组和其他用户具有读取和执行权限。
    -rwsr-xr-x:文件所有者具有读写执行权限,并且设置了SUID权限。所属组和其他用户具有读取和执行权限。

    要设置SUID权限,可以使用chmod命令与数字或符号表示法。例如,chmod 4755 file将文件的权限设置为-rwsr-xr-x,其中SUID权限被设置为文件所有者的执行权限。

    二、SUID简介

    在Linux中,SUID(Set User ID)是一种权限机制,用于赋予程序在执行时临时获取文件所有者的权限。当一个可执行文件具有SUID权限时,无论是哪个用户执行该文件,该程序都会以文件所有者的权限运行,而不是执行者自身的权限。

    SUID机制允许普通用户执行特定的程序或命令,以便执行特权操作,例如修改系统配置或访问受限资源。一个典型的例子是/usr/bin/passwd命令,它具有SUID权限,因此普通用户可以在不具备修改密码文件权限的情况下,通过执行该命令来更改自己的密码。

    类似的还有 sudo、su命令等,比如在普通用户下执行sudo就可以执行root权限可以执行的操作。
    在这里插入图片描述
    SUID权限位:SUID由文件权限位中用户部分的"s"表示。如果一个文件被设置了SUID位,会表现在所有者(文件的属主)的权限的可执行位上(x)。

    所有者的权限的可执行位(x)上定义了一个补充的s位,如果文件设置了SUID,那么它在执行的时候,会把进程的权限(euid)设置成文件属主的uid。
    当设置了SUID时,还需要所有者的执行权限。当s这个标志出现在文件所有者的x权限上时,则就被称为Set UID。

    可以使用chmod命令的数字符号(“4”)或符号符号(“u+s”)来设置SUID位。

    #define S_ISUID  0004000
    
    • 1

    执行上下文:当用户执行一个启用了SUID的程序时,该程序以文件所有者的有效用户ID(EUID)运行。这暂时提升了运行程序的用户的权限级别到文件所有者的级别。

    SUID权限仅对二进制程序(binary program)有效,不能用在脚本上(script)。

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

    因此当我们以普通用户在终端上执行 passwd,sudo 等命令时,比如普通用户的uid是1000,那么终端上的bash进程是普通用户的uid1000,当执行 passwd,sudo 等命令时, passwd,sudo 等二进制文件设置了SUID位,这么bash进程的euid以文件所有者的有效用户ID(EUID)运行,而 passwd,sudo 等二进制文件是由root用户管理的,其文件属主的用户名是 root 用户,因此passwd,sudo 等二进制文件的 EUID 等于 root 用户的uid = 0,因此bash进程的euid以文件所有者的有效用户ID = 0运行,这暂时提升了bash程序的用户的权限级别到文件所有者的级别。这样就达到了提升用户权限的作用了。

    在这里插入图片描述

    三、源码解析

    接下来分析内核源码SUID机制的实现:
    在终端bash进程执行exec系统调用时:

    execve
    	-->do_execve
    		-->do_execveat_common
    			 /*
    			 * sys_execve() executes a new program.
    			 */
    			-->__do_execve_file
    				-->prepare_bprm_creds
    				-->prepare_binprm
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.1 prepare_bprm_creds

    /*
     * This structure is used to hold the arguments that are used when loading binaries.
     */
    struct linux_binprm {
    	struct cred *cred;	/* new credentials */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    /*
     * Prepare credentials and lock ->cred_guard_mutex.
     * install_exec_creds() commits the new creds and drops the lock.
     * Or, if exec fails before, free_bprm() should release ->cred and
     * and unlock.
     */
    static int prepare_bprm_creds(struct linux_binprm *bprm)
    {
    	if (mutex_lock_interruptible(&current->signal->cred_guard_mutex))
    		return -ERESTARTNOINTR;
    
    	bprm->cred = prepare_exec_creds();
    	if (likely(bprm->cred))
    		return 0;
    
    	mutex_unlock(&current->signal->cred_guard_mutex);
    	return -ENOMEM;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    prepare_bprm_creds函数用于准备执行新的二进制程序所需的凭证(credentials),确保在执行新程序时具备正确的身份验证和权限信息。

    prepare_bprm_creds
    	-->prepare_exec_creds
    		-->prepare_creds
    
    • 1
    • 2
    • 3
    /**
     * prepare_creds - Prepare a new set of credentials for modification
     *
     * Prepare a new set of task credentials for modification.  A task's creds
     * shouldn't generally be modified directly, therefore this function is used to
     * prepare a new copy, which the caller then modifies and then commits by
     * calling commit_creds().
     *
     * Preparation involves making a copy of the objective creds for modification.
     *
     * Returns a pointer to the new creds-to-be if successful, NULL otherwise.
     *
     * Call commit_creds() or abort_creds() to clean up.
     */
    struct cred *prepare_creds(void)
    {
    	struct task_struct *task = current;
    	const struct cred *old;
    	struct cred *new;
    
    	validate_process_creds();
    
    	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
    	if (!new)
    		return NULL;
    
    	kdebug("prepare_creds() alloc %p", new);
    
    	old = task->cred;
    	memcpy(new, old, sizeof(struct cred));
    
    	new->non_rcu = 0;
    	atomic_set(&new->usage, 1);
    	set_cred_subscribers(new, 0);
    	get_group_info(new->group_info);
    	get_uid(new->user);
    	get_user_ns(new->user_ns);
    
    #ifdef CONFIG_KEYS
    	key_get(new->session_keyring);
    	key_get(new->process_keyring);
    	key_get(new->thread_keyring);
    	key_get(new->request_key_auth);
    #endif
    
    #ifdef CONFIG_SECURITY
    	new->security = NULL;
    #endif
    
    	if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
    		goto error;
    	validate_creds(new);
    	return new;
    
    error:
    	abort_creds(new);
    	return NULL;
    }
    EXPORT_SYMBOL(prepare_creds);
    
    • 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
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    为对任务的凭证进行修改准备一个新的副本。在一般情况下,任务的凭证不应直接被修改,而是通过先准备一个新的副本,然后由调用者对副本进行修改,最后通过调用 commit_creds() 来提交修改。

    函数的主要步骤如下:
    首先,它验证当前任务的凭证是否有效。

    然后,它使用 kmem_cache_alloc 分配内存来创建一个新的 struct cred 对象,并将其赋值给 new。

    接着,它通过调用 memcpy 将当前任务的凭证内容复制到新的凭证对象中。

    设置新的凭证对象的一些属性,如 non_rcu、usage、subscribers 等。

    获取相关的用户组信息、用户ID以及用户命名空间。

    如果系统启用了 CONFIG_KEYS,则增加对会话、进程、线程和请求密钥的引用计数。

    如果系统启用了 CONFIG_SECURITY,则将 security 字段设置为 NULL。

    最后,如果 security_prepare_creds() 失败,它将通过 abort_creds() 清理并释放新凭证对象的内存,并返回 NULL。否则,它将验证新凭证对象的有效性,并返回指向新凭证的指针。

    这段代码用于创建一个新的任务凭证的副本,以备修改和提交。它确保了在修改任务凭证时能够进行适当的拷贝和处理,并提供了必要的清理机制以防出现错误。

    3.2 prepare_binprm

    /*
     * Fill the binprm structure from the inode.
     * Check permissions, then read the first BINPRM_BUF_SIZE bytes
     *
     * This may be called multiple times for binary chains (scripts for example).
     */
    int prepare_binprm(struct linux_binprm *bprm)
    {
    	int retval;
    	loff_t pos = 0;
    
    	bprm_fill_uid(bprm);
    
    	/* fill in binprm security blob */
    	retval = security_bprm_set_creds(bprm);
    	if (retval)
    		return retval;
    	bprm->called_set_creds = 1;
    
    	memset(bprm->buf, 0, BINPRM_BUF_SIZE);
    	return kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos);
    }
    
    EXPORT_SYMBOL(prepare_binprm);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    prepare_binprm函数作用是填充 struct linux_binprm 结构,该结构用于执行二进制程序。调用 bprm_fill_uid() 函数填充 bprm 结构的用户ID信息。

    3.2.1 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();
    
    	if (!mnt_may_suid(bprm->file->f_path.mnt))
    		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 */
    	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;
    
    	if (mode & S_ISUID) {
    		bprm->per_clear |= PER_CLEAR_ON_SETID;
    		bprm->cred->euid = uid;
    	}
    
    	if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
    		bprm->per_clear |= PER_CLEAR_ON_SETID;
    		bprm->cred->egid = gid;
    	}
    }
    
    • 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
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    bprm_fill_uid函数根据文件的权限和设置,填充 struct linux_binprm 结构的用户ID信息。

    具体步骤如下:
    (1)将 bprm->cred->euid 和 bprm->cred->egid 设置为当前进程的有效用户ID和有效组ID。
    (2)获取文件的inode,即获取passwd,sudo 等可执行二进制文件的inode,并读取其权限模式。如果文件的权限模式中没有设置 setuid/setgid 位,则函数直接返回。

    /*
     * Keep mostly read-only and often accessed (especially for
     * the RCU path lookup and 'stat' data) fields at the beginning
     * of the 'struct inode'
     */
    struct inode {
    	umode_t			i_mode;
    	......
    	kuid_t			i_uid;
    	kgid_t			i_gid;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    /*
     * This structure is used to hold the arguments that are used when loading binaries.
     */
    struct linux_binprm {
    	struct file * file;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    struct file {
    	struct path		f_path;
    }
    
    • 1
    • 2
    • 3
    struct path {
    	struct dentry *dentry;
    } __randomize_layout;
    
    • 1
    • 2
    • 3
    struct dentry {
    	struct inode *d_inode;		/* Where the name belongs to - NULL is  negative */
    }
    
    • 1
    • 2
    • 3

    (3)在获取文件的inode之前,对其进行加锁。在持有锁的情况下,原子地重新加载文件的权限模式、用户ID和组ID。解锁inode。
    (4)如果文件的权限模式中设置了 setuid 位,则将 bprm->cred->euid 设置为文件的用户ID,并将 bprm->per_clear 的 PER_CLEAR_ON_SETID 标志设置为1。
    (5)如果文件的权限模式中设置了 setgid 位且组执行位也同时设置了,则将 bprm->cred->egid 设置为文件的组ID,并将 bprm->per_clear 的 PER_CLEAR_ON_SETID 标志设置为1。

    这段代码用于根据文件的权限模式和设置,将适当的用户ID和组ID填充到 struct linux_binprm 结构中。它会检查文件的 setuid 和 setgid 位,并根据情况设置相关的用户ID和组ID。

    3.2.2 security_bprm_set_creds

    static inline int security_bprm_set_creds(struct linux_binprm *bprm)
    {
    	return cap_bprm_set_creds(bprm);
    }
    
    • 1
    • 2
    • 3
    • 4
    /**
     * cap_bprm_set_creds - Set up the proposed credentials for execve().
     * @bprm: The execution parameters, including the proposed creds
     *
     * Set up the proposed credentials for a new execution context being
     * constructed by execve().  The proposed creds in @bprm->cred is altered,
     * which won't take effect immediately.  Returns 0 if successful, -ve on error.
     */
    int cap_bprm_set_creds(struct linux_binprm *bprm)
    {
    	//把bash普通进程的suid和sgid替换为passwd,sudo 等可执行二进制文件的uid,gid
    	new->suid = new->fsuid = new->euid;
    	new->sgid = new->fsgid = new->egid;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    新凭证中的用户和组ID设置为有效用户和组ID。

    3.3 install_exec_creds

    /*
     * sys_execve() executes a new program.
     */
     __do_execve_file
     	-->exec_binprm
     		-->search_binary_handler
     		{
    			struct linux_binfmt *fmt;
    			fmt->load_binary(bprm);
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对于加载 elf 二进制可执行文件来说:

    static struct linux_binfmt elf_format = {
    	.load_binary	= load_elf_binary,
    };
    
    • 1
    • 2
    • 3
    /*
     * These are the functions used to load ELF style executables and shared
     * libraries.  There is no binary dependent code anywhere else.
     */
    
    static int load_elf_binary(struct linux_binprm *bprm)
    {
    	install_exec_creds(bprm);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    /*
     * install the new credentials for this executable
     */
    void install_exec_creds(struct linux_binprm *bprm)
    {
    	security_bprm_committing_creds(bprm);
    
    	commit_creds(bprm->cred);
    	bprm->cred = NULL;
    
    	/*
    	 * Disable monitoring for regular users
    	 * when executing setuid binaries. Must
    	 * wait until new credentials are committed
    	 * by commit_creds() above
    	 */
    	if (get_dumpable(current->mm) != SUID_DUMP_USER)
    		perf_event_exit_task(current);
    	/*
    	 * cred_guard_mutex must be held at least to this point to prevent
    	 * ptrace_attach() from altering our determination of the task's
    	 * credentials; any time after this it may be unlocked.
    	 */
    	security_bprm_committed_creds(bprm);
    	mutex_unlock(&current->signal->cred_guard_mutex);
    }
    EXPORT_SYMBOL(install_exec_creds);
    
    • 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

    commit_creds替换当前进程的凭证来安装新的执行凭证。

    总结

    除了以用户和用户组控制权限,还有以下凭证机制:

    Capabilities
    Secure management flags (securebits)
    Keys and keyrings
    LSM
    AF_KEY
    
    • 1
    • 2
    • 3
    • 4
    • 5

    详细请参考:Linux 安全 - Credentials

    参考资料

    Linux 5.4.18

    Linux DAC 权限管理详解
    Linux 内核安全模块深入剖析

  • 相关阅读:
    差点跳起来了! 全靠这份“Java 核心知识笔记”我成功拿到美团 offer
    使用Docker本地安装部署Drawio绘图工具并实现公网访问
    确保网络的安全技术介绍
    现金储备超400亿的小鹏,进入中途蓄力时刻
    万界星空科技智能管理系统低代码平台
    CentOS停更在即,国内厂商该如何应对?KeyarchOS X2Keyarch 迁移体验
    如何改变胆小怕事的性格?
    Python——比 Seaborn 更好的相关性热力图:Biokit Corrplot
    TikTok快速起号技巧(下篇)
    MIPI CSI-2笔记(12) -- Low Level Protocol(数据加扰,扰码,Data Scrambling)
  • 原文地址:https://blog.csdn.net/weixin_45030965/article/details/133385501