• Linux 安全 - DAC机制


    一、安全简介

    计算机系统应对安全挑战的办法大致有四种:隔离、控制、混淆、监视。
    (1)隔离:计算机系统安全的设计者在系统的各个层级都发明了不同的技术来实现隔离,隔离的结果常常被称作“沙箱”。隔离是对外的,阻断内部和外部的交互。
    (2)控制:则是对内的,在计算机世界是通过系统代码内在的逻辑和安全策略来维护信息流动和信息改变。如用户 A 可不可以读取文件 a,用户 B 能不能改变文件 b 的内容,等等。
    (3)混淆:加密。可以接触到数据却无法还原信息。加密的本质是通过计算来混淆数据,让攻击者在不知道密钥的情况下很难将加密后的数据还原为原始数据。
    (4)监视:计算机系统中的日志和审计就是在做监视工作。

    为了更有效地利用计算机,计算机操作系统步入分时多用户时代。比如UNIX ,UNIX 是诞生于 20 世纪 70 年代的分时多任务多用户操作系统。当时的场景是许多用户同时登录到一台主机,运行多个各自的进程。许多人登录到一台主机,张三是个程序员,李四是个文档管理员,王五是系统管理员。随之出现了基于角色的访问控制(Role-based Acess Control,RBAC),让用户分属于不同的角色,再基于角色赋予访问权限。

    当 PC 时代来临,计算机设备专属于某个人,系统中的所谓用户也背离了原有的含义。随便打开 Linux 系统上的/etc/passwd 文件,看看里面还有几个是真正的用户?因此,在PC 中使用基于角色的访问控制就有些力不从心了。接下来诞生了另一个访问控制模型——类型增强(Type Enforcement,TE)。模型中控制的对象不再是人,或角色,而是进程。进程属于不同的类型,不同类型有不同的访问权限。

    二、DAC

    访问的三要素是主体、操作和客体。主体是代表用户执行任务的进程。客体有很多种,包
    括文件、目录、管道、设备、IPC(进程间通信)、socket(套接字)、key(密钥)等。操作其实就是系统调用。

    对照访问的三个要素,有了主体标记(请参考第三节)和客体标记(请参考第四节),只要有一种方法规定哪个主体可以对哪个客体进行何种操作就可以做到访问控制了。

    访问控制(Access Control):是对访问进行控制。比如,允许进程 A 读文件 a,不允许进程 B 读文件 b。要实现访问控制,需要两个东西,一个是标记,标记主体和客体,这样才有控制的对象;另一个是策略,允许某主体对某客体做什么。

    自主访问控制(Discretionary Access Control):
    (1)UGO
    (2)ACL
    (3)Capabilities

    2.1 UNIX 的自主访问控制

    UNIX 的自主访问控制的设计是简单而有效的。它分为两个部分,第一部分可以概括为进程操作文件。操作分三种:读、写、执行。在进程操作文件时,内核会检查进程有没有对文件的相应操作许可。第二部分可以概括为:拥有特权的进程可以做任何事情,内核不限制。特权机制实际上包含了两类行为,一类是超越第一部分的操作许可控制,比如 root 用户可以读或写任何文件。另一类是无法纳入上述“进程操作文件”模型之内的行为,比如重启动系统。

    Unix系统的UGO(User、Group、Other)权限管理方式在文件和目录上设置权限位,用来控制用户或用户组对文件或目录的访问。Linux继承了Unix的UGO权限管理方式。

    对于这种DAC模型:
    主体:一般指进程。
    客体:一般指文件。
    行为(操作):读取权限(r)、写入权限(w)和执行权限(x)。
    主体 操作 客体时查询的规则(UGO规则):把主体分为User、Group、Other三种类型,每种类型拥有自己的RWX mask。

    每个文件和目录都有一个所有者和所属组,以及一组权限位。权限位包括读取权限(r)、写入权限(w)和执行权限(x)。权限位可以分为三个类别:所有者权限(User)、所属组权限(Group)和其他用户权限(Others)。这些权限位决定了哪些用户或用户组可以对文件执行特定的操作。

    例如,如果一个文件的权限位设置为-rw-r–r–,表示文件的所有者具有读写权限,所属组用户具有读权限,其他用户也具有读权限,但没有写权限。

    DAC是一种简单而直观的访问控制机制,但它具有一定的局限性。例如,DAC不能提供细粒度的访问控制,无法限制特定用户对特定文件的访问。此外,它无法提供强制性的安全策略,因此需要其他访问控制机制来提供更高级别的安全性。

    为了解决这些限制,Linux还引入了其他访问控制机制,如访问控制列表(ACLs)、Capabilities、Linux 安全模块LSM等。

    2.2 Linux 的自主访问控制

    Linux继承了Unix的UGO权限管理方式。

    在自主访问控制上,Linux 对 UNIX 的扩展主要有两处,一是提供了访问控制列表(Access Control List),使得能够规定某一个用户或某一个组的操作许可;二是对特权操作细化(Capabilities),将原有属于根用户的特权细化为互不相关的三十几个能力。

    三、进程凭证

    没有标记就谈不上区分,没有区分就无从实施控制,接下来介绍进程的标记。

    UNIX 是诞生于 20 世纪 70 年代的分时多任务多用户操作系统。当时的场景是许多用户同时登录到一台主机,运行多个各自的进程。因此,很自然的,UNIX 系统中进程的标记是基于用户的。在人类的世界中,人的标记是名字。相比字符串而言,计算机更擅长处理数字,UNIX使用一个整数来标记运行进程的用户,这个整数被称作 user id,简写为 uid。人通常被分组,比如这几个人做研发工作,被分到研发组,那几个人做销售工作,被分到销售组。UNIX 用另一个整数来标记用户组,这个整数被称作 group id,简写为 gid。uid 和 gid 是包括 Linux 在内的所有类 UNIX 操作系统的自主访问控制的基础。

    3.1 简介

    下面看一下 uid 和 gid 是如何记录在内核的进程控制结构之中的:

    struct task_struct {
    	/* Process credentials: */
    
    	/* Tracer's credentials at attach: */
    	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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Linux 内核没有用户这个数据结构。内核中只会识别UID,至于用户名是通过在/etc/passwd文件中查找对应关系得到的。

    这里我们只关系 real_cred 和 cred 这两个进程凭证。进程的凭证中存储有和访问控制相关的成员。

    在内核代码注释中将 real_cred 称为客体(objective)凭证,将 cred 称为主体(subjective)凭证。

    task->real_cred为任务的客观上下文,这些部分在其他任务试图对当前任务产生影响时使用。它们描述了当其他任务作用于当前任务时所涉及的信息。

    task->cred为任务的主观上下文,这些部分在任务作用于其他对象时使用,例如文件、任务、密钥或其他对象。

    进程一般都是主体,在某些场景下又是客体。典型的场景是进程间发信号,进程 A 向进程 B 发送信号,进程 A 是主体,进程 B 就是客体。在大多数情况下,主体凭证和客体凭证的值是相同的,但在某些情况下内核代码会修改当前进程的主体凭证,以获得某种访问权限,待执行完任务后再将主体凭证改回原值,比如SUID机制,请参考:Linux 安全 - SUID机制

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

    凭证的数据结构:

    /*
     * 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.
     */
    struct cred {
    	......
    	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	*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
    	......
    } __randomize_layout;
    
    • 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

    安全上下文(security context)是Linux内核中用于实施访问控制和授权策略的重要概念。它用于确定任务(进程)对系统资源的访问权限,并管理任务与其他实体之间的交互。

    task->real_cred (Objective Context):客观上下文描述了当其他任务或实体试图对当前任务产生影响时,系统需要考虑的任务属性和细节。这些属性包括任务的标识符、权限、安全策略和相关的安全模块信息。当其他任务或实体需要与当前任务进行交互时,客观上下文用于确定是否允许该交互以及采取何种操作。
    简单来说:task->real_cred为任务的客观上下文,这些部分在其他任务试图对当前任务产生影响时使用。它们描述了当其他任务作用于当前任务时所涉及的信息。

    task->cred(Subjective Contex):主观上下文描述了当前任务在与其他对象进行交互时所具有的属性和行为方式。这些对象可以是文件、其他任务、密钥或其他实体。主观上下文指定了任务在执行特定操作时的权限和访问控制策略。通过主观上下文,系统可以根据任务的特定需求和角色来限制或允许其对对象的操作。
    简单点来说:task->cred为任务的主观上下文,这些细节在任务作用于其他对象时使用,例如文件、任务、密钥或其他对象。

    需要注意的是,该结构的某些成员同时属于两个类别,例如LSM安全指针。

    LSM安全指针:在安全上下文结构中,LSM(Linux Security Modules)安全指针是一个重要的组成部分。LSM是Linux内核中的一个可插拔模块化框架,用于实施安全策略和访问控制。LSM安全指针在安全上下文中既属于客观上下文又属于主观上下文,因为它在任务受到其他任务影响时起作用,同时也在任务与其他对象进行交互时发挥作用。

    一个任务有两个LSM安全指针:
    每个任务在内核中都有两个安全指针。task->real_cred指向任务的客观上下文,定义了任务的实际详细信息。当其他任务或实体试图对当前任务产生影响时,会使用客观上下文中的相关部分。task->cred指向任务的主观上下文,定义了任务在与其他对象进行交互时的权限和行为方式。通常情况下,task->cred指针指向与task->real_cred相同的上下文,但可以在需要时临时更改为指向其他安全上下文。

    通过安全上下文的使用,内核可以根据任务的特定属性、角色和上下文来限制或允许其对系统资源的访问。安全上下文在操作系统安全性和访问控制的实施中起着关键作用,确保任务只能执行其被授权的操作,并保护系统免受未经授权的访问和恶意行为。不同的安全模块可以通过修改安全上下文来实现自定义的安全策略和访问控制机制,从而增强系统的安全性和灵活性。

    进程凭证中不止有 id 相关的成员,还有能力集相关的成员、密钥串相关的成员和强制访问控制相关的成员。

    3.2 uid/gid

    struct cred {
    	......
    	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 */
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以看到进程凭证结构体struct cred包含了多个 uid/gid。

    (1)uid
    这是最早出现的 user id。有时也被称为 real uid,实际的 uid,简写为(r)uid,代表启动进程的原始用户身份。这个 uid 在资源统计和资源分配中使用,比如限制某用户拥有的进程数量。
    (2)euid
    euid(effective uid)即有效 uid。操作系统用于确定进程权限和访问权限的用户 ID。它表示进程在访问系统资源时所扮演的用户身份。
    在内核做特权判断时使用它。它的引入和提升权限有关。此外,内核在做 ipc(进程间通信)和 key(密钥)的访问控制时也使用 euid。
    权限判断时看的就是euid。初始状态时,uid和euid相同,做一些权限切换时euid可能改变不等于uid了。
    (3)suid
    suid 是“saved set user id”。suid 存储了有效用户 ID,以便稍后恢复。euid 和特权有关,当 euid 为 0 时,进程就具有了超级用户的权限,拥有了全部特权,在系统中没有做不了的事情。这有些危险。我们需要锋利的刀,但不用的时候希望把刀放入刀鞘。为了让进程不要总是具有全部特权,总能为所欲为,系统的设计者引入了suid,用于暂存 euid 的值。euid 为 0 时做需要特权的操作,执行完操作,将 0 赋予suid,euid 恢复为非 0 值,做普通的不需要特权的操作,需要特权时再将 suid 的值传给 euid。
    (4)fsuid
    fsuid 是“file system user id”。这个 uid 是 Linux 系统独有的。它用于在文件系统相关的访问控制中判断操作许可。

    3.3 系统调用

    进程的控制结构和进程凭证都是内核中的数据结构,相应的数据对象都是被内核掌控的。用户态进程只能通过内核提供的接口来查看和修改进程凭证。Linux 内核提供了数个系统调用来查看和修改进程的凭证中的uid和gid。这部分系统调用都要求进程只能修改自己的uid和gid,不可以修改别的进程的。
    (1)

    NAME
           setuid - set user identity
    
    SYNOPSIS
           #include 
           #include 
    
           int setuid(uid_t uid);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    DESCRIPTION
           setuid() sets the effective user ID of the calling process.  If the effective UID of the caller is root, the real UID and saved set-user-ID are also set.
    
    • 1
    • 2

    setuid() 函数用于设置调用进程的有效用户 ID(euid)。如果调用者的有效用户 ID 是 root,那么实际用户 ID(uid) 和保存的设置用户 ID(suid) 也会被设置。

    (2)

    NAME
           seteuid, setegid - set effective user or group ID
    
    SYNOPSIS
           #include 
           #include 
    
           int seteuid(uid_t euid);
           int setegid(gid_t egid);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    DESCRIPTION
           seteuid()  sets  the effective user ID of the calling process.  Unprivileged user processes may only set the effective user ID to the real user ID, the effective user ID
           or the saved set-user-ID.
    
           Precisely the same holds for setegid() with "group" instead of "user".
    
    • 1
    • 2
    • 3
    • 4
    • 5

    seteuid() 函数用于设置调用进程的有效用户 ID。非特权用户进程只能将有效用户 ID 设置为实际用户 ID、有效用户 ID 或保存的设置用户 ID。

    在 Linux 系统中,非特权用户进程的有效用户 ID 受到限制,只能设置为它自身的实际用户 ID、有效用户 ID 或保存的设置用户 ID。这是为了确保进程无法将有效用户 ID 设置为其他特权用户的用户 ID,从而防止滥用权限。

    这种限制是为了维护系统的安全性,防止非特权用户进程滥用特权。只有特权用户(例如 root 用户)才能更改为任意有效用户 ID。

    (3)

    NAME
           setreuid, setregid - set real and/or effective user or group ID
    
    SYNOPSIS
           #include 
           #include 
    
           int setreuid(uid_t ruid, uid_t euid);
           int setregid(gid_t rgid, gid_t egid);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    DESCRIPTION
           setreuid() sets real and effective user IDs of the calling process
    
    • 1
    • 2

    setreuid() 函数用于设置调用进程的实际用户 ID(real user ID)和有效用户 ID(effective user ID)。

    当给定的实际用户 ID 或有效用户 ID 的值为 -1 时,系统将保持该 ID 不变。

    非特权进程只能将有效用户 ID 设置为实际用户 ID、有效用户 ID 或保存的设置用户 ID。

    非特权用户只能将实际用户 ID 设置为实际用户 ID 或有效用户 ID。

    如果设置了实际用户 ID,或者将有效用户 ID 设置为与先前的实际用户 ID 不相等的值,保存的设置用户 ID 将被设置为新的有效用户 ID。

    通过调用setreuid()函数,进程可以更改其实际用户 ID 和有效用户 ID。这对于需要在进程中切换用户身份的场景非常有用。

    (4)

    NAME
           setresuid, setresgid - set real, effective and saved user or group ID
    
    SYNOPSIS
           #define _GNU_SOURCE         /* See feature_test_macros(7) */
           #include 
    
           int setresuid(uid_t ruid, uid_t euid, uid_t suid);
           int setresgid(gid_t rgid, gid_t egid, gid_t sgid);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    DESCRIPTION
           setresuid() sets the real user ID, the effective user ID, and the saved set-user-ID of the calling process.
    
    • 1
    • 2

    setresuid() 函数用于设置调用进程的实际用户 ID(real user ID)、有效用户 ID(effective user ID)和保存的设置用户 ID(saved set-user-ID)。

    非特权用户进程可以将实际用户 ID、有效用户 ID 和保存的设置用户 ID 分别设置为以下值之一:当前实际用户 ID、当前有效用户 ID 或当前保存的设置用户 ID。

    特权进程(在 Linux 上,拥有 CAP_SETUID 能力的进程)可以将实际用户 ID、有效用户 ID 和保存的设置用户 ID 设置为任意值。

    如果其中一个参数等于 -1,则对应的值不会改变。

    无论对实际用户 ID、有效用户 ID 和保存的设置用户 ID 进行了何种更改,文件系统的用户 ID 总是设置为与(可能是新的)有效用户 ID 相同的值。

    通过调用 setresuid() 函数,进程可以同时更改实际用户 ID、有效用户 ID 和保存的设置用户 ID。这对于需要在进程中切换用户身份的场景非常有用。

    (5)

    NAME
           setfsuid - set user identity used for file system checks
    
    SYNOPSIS
           #include  /* glibc uses  */
    
           int setfsuid(uid_t fsuid);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    DESCRIPTION
           The  system call setfsuid() sets the user ID that the Linux kernel uses to check for all accesses to the file system.  Normally, the value of fsuid will shadow the value
           of the effective user ID.  In fact, whenever the effective user ID is changed, fsuid will also be changed to the new value of the effective user ID.
    
    • 1
    • 2
    • 3

    setfsuid() 系统调用用于设置 Linux 内核用于检查对文件系统的所有访问的用户 ID。通常情况下,fsuid 的值会跟随有效用户 ID 的值。事实上,每当有效用户 ID 发生变化时,fsuid 也会被更改为新的有效用户 ID 的值。

    通过调用 setfsuid() 函数,可以显式地设置文件系统用户 ID(fsuid)。这个值将影响进程对文件系统的访问权限检查。如果 fsuid 不同于有效用户 ID,那么内核将使用 fsuid 来进行访问权限检查。

    设置 fsuid 的主要目的是允许进程在文件系统访问权限方面切换用户身份。这对于需要使用不同用户身份访问文件系统的场景非常有用,例如在特定用户权限下处理文件或资源。通过显式设置 fsuid,进程可以模拟以其他用户身份执行操作,而不必更改有效用户 ID。

    (6)
    在设置 user id 的系统调用中,内核代码遵守了以下原则:
    (1)具备 setuid 特权的进程可以把®uid、euid、suid、fsuid 设置为任意值。即如果进程有root权限,那么uid/suid/euid/fsuid可以设置任意值。
    (2)不具备 setuid 特权的进程只能将®uid、euid、suid 的值设置为现有的®uid、euid、或 suid
    的值。以 euid 为例,euid 的新值只能是现在的®uid 的值、现在的 euid 的值或现在的 suid
    的值。即如果进程没有root权限,那么设置新的uid只能是原uid/suid/euid/fsuid中的一个值,不能是任意值。
    (3)不具备 setuid 特权的进程只能将 fsuid 的值置为现有的®uid、euid、suid、fsuid 的值
    之一。

    setreuid()/setuid()/setresuid()系统调用可以降级权限,它没有升级的能力。

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

    四、客体标记

    4.1 简介

    UNIX 中进程有多个 uid 和多个 gid,但是文件只有一个 uid 和一个 gid,分别称为属主和属组。它们的含义是标记文件属于哪一个用户,属于哪一个用户组。

    在文件系统中一个文件不仅要包含文件的内容(数据),还要包含所谓的元数据(meta data)。
    元数据包括文件的存取访问方式、文件的创建日期、文件所在的设备、属主和属组以及允许位等。

    在 UNIX 中,元数据存储在文件系统的 inode 中。有些文件系统,比如 ext4,在物理存储介质上存储 inode。有些非 UNIX/Linux 原生的文件系统没有 inode概念。Linux 内核会在内存中临时创建 inode。对于那些内存文件系统,比如 tmpfs,Linux 内核也是在内存中创建临时的 inode。

    下面看一下内核中 inode 的定义,这里我们只看成员文件所在的属主和属组:

    struct inode {
    	......
    	kuid_t			i_uid;
    	kgid_t			i_gid;
    	......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.2 系统调用

    文件属性是内核控制的数据,只能由内核来修改。内核开放了几个系统调用供用户态进程使用,用户态进程可以请求内核修改文件属性。涉及属主和属组的系统调用是:

    NAME
           chown, fchown, lchown - change ownership of a file
    
    SYNOPSIS
           #include 
    
           int chown(const char *path, uid_t owner, gid_t group);
           int fchown(int fd, uid_t owner, gid_t group);
           int lchown(const char *path, uid_t owner, gid_t group);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这些系统调用用于更改文件的所有者和组。它们的区别仅在于文件的指定方式:
    chown() 函数更改路径指定的文件的所有者,如果该文件是符号链接,则对其进行解引用。
    fchown() 函数更改由打开的文件描述符 fd 引用的文件的所有者。
    lchown() 函数类似于 chown(),但不对符号链接进行解引用。

    只有特权进程(Linux 上具有 CAP_CHOWN 能力的进程)才能更改文件的所有者。文件的所有者可以将文件的组更改为该所有者所属的任何组。特权进程(Linux 上具有 CAP_CHOWN 能力的进程)可以任意更改组。

    如果将所有者或组指定为 -1,则该 ID 不会更改。

    当非特权用户更改可执行文件的所有者或组时,S_ISUID 和 S_ISGID 模式位将被清除。POSIX 并未指定在 root 用户执行 chown() 时是否也应该清除这些模式位,Linux 的行为取决于内核版本。对于非组可执行文件(即 S_IXGRP 位未设置的文件),S_ISGID 位指示强制锁定,并且不会被 chown() 清除。

    五、UGO规则源码分析

    UGO规则请参考:2.1 UNIX 的自主访问控制

    客体文件inode->i_mode中存储了UGO 3组mask,每组mask由rwx三个行为组成。

    struct inode {
    	umode_t			i_mode;
    	......
    }
    
    • 1
    • 2
    • 3
    • 4

    有了UGO规则,主体进程在RWX客体文件时会做对应的权限规则检查。

    以open系统调用为例:

    SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
    {
    	if (force_o_largefile())
    		flags |= O_LARGEFILE;
    
    	return do_sys_open(AT_FDCWD, filename, flags, mode);
    }
    
    SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
    		umode_t, mode)
    {
    	if (force_o_largefile())
    		flags |= O_LARGEFILE;
    
    	return do_sys_open(dfd, filename, flags, mode);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    open系统调用会调用inode_permission函数进行权限检查,open一个文件时,几个权限校验的关键节点,即调用inode_permission函数进行权限检查:
    (1)第一步权限检查: 最开始对文件所在路径上每个目录项对应的inode进行执行权限检查。

    path_openat()
    	-->link_path_walk()
    		-->may_lookup()
    			-->inode_permission()
    
    • 1
    • 2
    • 3
    • 4

    (2)第二步权限检查:如果是新建文件,对文件所在目录项的inode做写和可执行权限检查。

    path_openat()
    	-->do_last()
    		-->lookup_open()
    			-->vfs_create()
    				-->may_create()
    					-->inode_permission()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    (3)真正打开文件之前,对文件所对应的inode做读写权限检查。

    path_openat()
    	-->do_last()
    		-->may_open()
    			-->inode_permission()
    
    • 1
    • 2
    • 3
    • 4

    具体请参考:linux内核open过程的权限管理

    /**
     * inode_permission - Check for access rights to a given inode
     * @inode: Inode to check permission on
     * @mask: Right to check for (%MAY_READ, %MAY_WRITE, %MAY_EXEC)
     *
     * Check for read/write/execute permissions on an inode.  We use fs[ug]id for
     * this, letting us set arbitrary permissions for filesystem access without
     * changing the "normal" UIDs which are used for other things.
     *
     * When checking for MAY_APPEND, MAY_WRITE must also be set in @mask.
     */
    int inode_permission(struct inode *inode, int mask)
    {
    	int retval;
    
    	retval = sb_permission(inode->i_sb, inode, mask);
    	if (retval)
    		return retval;
    
    	if (unlikely(mask & MAY_WRITE)) {
    		/*
    		 * Nobody gets write access to an immutable file.
    		 */
    		if (IS_IMMUTABLE(inode))
    			return -EPERM;
    
    		/*
    		 * Updating mtime will likely cause i_uid and i_gid to be
    		 * written back improperly if their true value is unknown
    		 * to the vfs.
    		 */
    		if (HAS_UNMAPPED_ID(inode))
    			return -EACCES;
    	}
    
    	retval = do_inode_permission(inode, mask);
    	if (retval)
    		return retval;
    
    	retval = devcgroup_inode_permission(inode, mask);
    	if (retval)
    		return retval;
    
    	return security_inode_permission(inode, mask);
    }
    EXPORT_SYMBOL(inode_permission);
    
    • 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

    从函数注释可以看到:

    Check for read/write/execute permissions on an inode.
    
    • 1

    UGO权限检查:

    inode_permission()
    	-->do_inode_permission()
    		-->generic_permission()
    
    • 1
    • 2
    • 3
    /**
     * generic_permission -  check for access rights on a Posix-like filesystem
     * @inode:	inode to check access rights for
     * @mask:	right to check for (%MAY_READ, %MAY_WRITE, %MAY_EXEC, ...)
     *
     * Used to check for read/write/execute permissions on a file.
     * We use "fsuid" for this, letting us set arbitrary permissions
     * for filesystem access without changing the "normal" uids which
     * are used for other things.
     *
     * generic_permission is rcu-walk aware. It returns -ECHILD in case an rcu-walk
     * request cannot be satisfied (eg. requires blocking or too much complexity).
     * It would then be called again in ref-walk mode.
     */
    int generic_permission(struct inode *inode, int mask)
    {
    	int ret;
    
    	/*
    	 * Do the basic permission checks.
    	 */
    	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))
    			if (capable_wrt_inode_uidgid(inode,
    						     CAP_DAC_READ_SEARCH))
    				return 0;
    		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)
    		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))
    		if (capable_wrt_inode_uidgid(inode, CAP_DAC_OVERRIDE))
    			return 0;
    
    	return -EACCES;
    }
    EXPORT_SYMBOL(generic_permission);
    
    • 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

    在acl_permission_check函数中做UGO权限检查:

    /*
     * This does the basic permission checking
     */
    static int acl_permission_check(struct inode *inode, int mask)
    {
    	//获取文件的inode->i_mode
    	unsigned int mode = inode->i_mode;
    
    	//查当前文件系统用户 ID(current_fsuid())是否等于 inode 的用户 ID(i_uid)。如果它们相等,表示用户与文件的所有者匹配,它将 mode 按位向右移动 6 位(获取所有者的权限)
    	if (likely(uid_eq(current_fsuid(), inode->i_uid)))
    		mode >>= 6;
    	else {
    		//如果当前用户不是所有者,函数会检查 inode 是否启用了 POSIX ACL,并且组权限是否已设置。
    		if (IS_POSIXACL(inode) && (mode & S_IRWXG)) {
    			//如果两个条件都满足,它会使用 check_acl() 函数执行额外的基于 ACL 的权限检查。
    			int error = check_acl(inode, mask);
    			if (error != -EAGAIN)
    				return error;
    		}
    
    		//如果当前用户不是所有者,并且未执行 ACL 检查或 ACL 检查结果为 -EAGAIN,函数会检查当前用户是否属于文件所属组。如果是这样,它会考虑组对请求访问的权限。它检查当前用户是否是 inode 所属组的成员(in_group_p(inode->i_gid))。如果是这样,它将 mode 按位向右移动 3 位(获取组的权限)。
    		if (in_group_p(inode->i_gid))
    			mode >>= 3;
    	}
    
    	//如果以上条件都未匹配成功,则为Other用户,取最低3bit规则(获取其他用户权限)
    
    	/*
    	 * If the DACs are ok we don't need any capability check.
    	 */
    	//将请求的访问掩码与相关权限(mode)进行比较,基于用户与文件的关系(所有者、组成员或其他用户)。如果允许请求的访问,它返回 0 表示成功。否则,返回 -EACCES 表示权限被拒绝。 
    	if ((mask & ~mode & (MAY_READ | MAY_WRITE | MAY_EXEC)) == 0)
    		return 0;
    	return -EACCES;
    }
    
    • 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

    (1)该函数通常在需要进行权限检查的文件系统操作中调用,比如打开文件、读写文件或执行文件等操作。
    inode 参数表示文件或目录的元数据,包括权限、所有权、时间戳和文件大小等属性。
    mask 参数是一个访问掩码,用于指定请求的权限,比如读取、写入或执行权限。
    函数根据 inode 的 mode 位执行权限检查。这些位表示所有者、组和其他用户的权限。

    (2)函数首先检查当前用户是否与文件的所有者匹配。如果匹配,它会考虑所有者对请求访问的权限。它检查当前文件系统用户 ID(current_fsuid())是否等于 inode 的用户 ID(i_uid)。如果它们相等,表示用户与文件的所有者匹配,它将 mode 按位向右移动 6 位(获取所有者的权限)

    (3)如果当前用户不是所有者,函数会检查 inode 是否启用了 POSIX ACL,并且组权限是否已设置。如果两个条件都满足,它会使用 check_acl() 函数执行额外的基于 ACL 的权限检查。
    如果 ACL 检查返回除了 -EAGAIN 之外的错误码,表示明确拒绝权限,函数会立即返回该错误,不再进行进一步处理。

    (4)如果当前用户不是所有者,并且未执行 ACL 检查或 ACL 检查结果为 -EAGAIN,函数会检查当前用户是否属于文件所属组。如果是这样,它会考虑组对请求访问的权限。它检查当前用户是否是 inode 所属组的成员(in_group_p(inode->i_gid))。如果是这样,它将 mode 按位向右移动 3 位(获取组的权限)。

    (5)如果以上条件都未匹配成功,则为Other用户,取最低3bit规则(获取其他用户权限)。

    (6)最后,函数将请求的访问掩码与相关权限(mode)进行比较,基于用户与文件的关系(所有者、组成员或其他用户)。如果允许请求的访问,它返回 0 表示成功。否则,返回 -EACCES 表示权限被拒绝。

    参考资料

    Linux 5.4.18

    Linux DAC 权限管理详解
    linux内核open过程的权限管理

    Linux 内核安全模块深入剖析

  • 相关阅读:
    什么是通配符SSL证书?它和多域名SSL证书的区别是什么?
    拦截浏览器从服务器后台加载的js 文件 替换为本地js文件 方便调试线上前端bug
    sqlyon连接mysql碰到的问题及mysql卸载须注意的地方
    BCC源码内容概览(1)
    【Java】中Maven依赖详解
    【Java】反射、注解、动态代理总结
    Java环境准备——JDK下载和安装、IDEA下载和安装
    数据结构与算法--图
    二叉搜索树的最近公共祖先
    如何分析精准分析出问题件
  • 原文地址:https://blog.csdn.net/weixin_45030965/article/details/133775356