Linux 中的凭据

作者:David Howells <dhowells@redhat.com>

概述

当一个对象对另一个对象执行操作时,Linux 执行的安全检查包含以下几个部分

  1. 对象。

    对象是系统中可被用户空间程序直接操作的事物。 Linux 有各种可操作的对象,包括

    • 任务

    • 文件/inode

    • 套接字

    • 消息队列

    • 共享内存段

    • 信号量

    • 密钥

    作为所有这些对象的描述的一部分,有一组凭据。该集合的内容取决于对象的类型。

  2. 对象所有权。

    在大多数对象的凭据中,会有一个子集指示该对象的所有权。这用于资源核算和限制(例如,磁盘配额和任务 rlimit)。

    例如,在标准的 UNIX 文件系统中,这将由 inode 上标记的 UID 定义。

  3. 客观上下文。

    同样,在这些对象的凭据中,会有一个子集指示该对象的“客观上下文”。这可能与 (2) 中的集合相同也可能不同 - 例如,在标准 UNIX 文件中,这是由 inode 上标记的 UID 和 GID 定义的。

    客观上下文用作当对象被操作时执行的安全计算的一部分。

  4. 主体。

    主体是对另一个对象执行操作的对象。

    系统中的大多数对象都是非活动的:它们不会在系统中对其他对象执行操作。进程/任务是明显的例外:它们会执行操作;它们会访问和操作事物。

    在某些情况下,任务以外的对象也可能是主体。例如,打开的文件可能会使用调用 fcntl(F_SETOWN) 的任务提供的 UID 和 EUID 向任务发送 SIGIO。在这种情况下,文件结构也将具有主观上下文。

  5. 主观上下文。

    主体对其凭据有额外的解释。其凭据的子集构成“主观上下文”。当主体执行操作时,主观上下文将用作安全计算的一部分。

    例如,Linux 任务在操作文件时具有 FSUID、FSGID 和补充组列表 - 这与通常形成任务客观上下文的实际 UID 和 GID 完全不同。

  6. 操作。

    Linux 有许多主体可以对对象执行的操作。可用的操作集取决于主体和对象的性质。

    操作包括读取、写入、创建和删除文件;派生或发送信号以及跟踪任务。

  7. 规则、访问控制列表和安全计算。

    当主体对对象执行操作时,将进行安全计算。这涉及到获取主观上下文、客观上下文和操作,并搜索一个或多个规则集,以查看是否在给定这些上下文的情况下授予或拒绝主体以期望的方式对对象执行操作的权限。

    规则主要有两个来源

    1. 自主访问控制 (DAC)

      有时,对象会在其描述中包含规则集。这是一个“访问控制列表”或“ACL”。一个 Linux 文件可能提供多个 ACL。

      例如,传统的 UNIX 文件包含一个权限掩码,它是一个缩写的 ACL,具有三个固定的主体类(“用户”、“组”和“其他”),每个主体类都可能被授予某些特权(“读取”、“写入”和“执行” - 无论对于相关对象映射到什么)。但是,UNIX 文件权限不允许任意指定主体,因此用途有限。

      Linux 文件可能还具有 POSIX ACL。这是一个规则列表,其中向任意主体授予各种权限。

    2. 强制访问控制 (MAC)

      作为一个整体,系统可能有一组或多组规则,这些规则将应用于所有主体和对象,无论其来源如何。SELinux 和 Smack 就是这方面的示例。

      在 SELinux 和 Smack 的情况下,每个对象在其凭据中都获得一个标签。当请求操作时,它们会获取主体标签、对象标签和操作,并查找一条规则,该规则表明已授予或拒绝此操作。

凭据的类型

Linux 内核支持以下类型的凭据

  1. 传统的 UNIX 凭据。

    • 实际用户 ID

    • 实际组 ID

    UID 和 GID 由大多数(如果不是全部)Linux 对象携带,即使在某些情况下必须发明(例如,从 Windows 派生的 FAT 或 CIFS 文件)。这些(大部分)定义了该对象的客观上下文,而任务在某些情况下略有不同。

    • 有效、保存和 FS 用户 ID

    • 有效、保存和 FS 组 ID

    • 补充组

    这些是仅由任务使用的其他凭据。通常,EUID/EGID/GROUPS 将用作主观上下文,而实际 UID/GID 将用作客观上下文。对于任务,应注意这并非总是如此。

  2. 功能。

    • 允许的功能集

    • 可继承的功能集

    • 有效的功能集

    • 功能边界集

    这些仅由任务携带。它们表明已零星地授予任务的卓越能力,而普通任务否则不会具有这种能力。这些通过对传统 UNIX 凭据的更改进行隐式操作,但也可以通过 capset() 系统调用直接操作。

    允许的功能是进程可能通过 capset() 将其授予其有效或允许集的功能。此可继承集也可能受到如此限制。

    有效功能是实际允许任务自身使用的功能。

    可继承功能是可能通过 execve() 传递的功能。

    边界集限制了可以通过 execve() 继承的功能,尤其是当执行将以 UID 0 身份执行的二进制文件时。

  3. 安全管理标志(securebits)。

    这些仅由任务携带。这些管理上述凭据在某些操作(例如 execve())中如何操作和继承。它们不会直接用作客观或主观凭据。

  4. 密钥和密钥环。

    这些仅由任务携带。它们携带并缓存不适合其他标准 UNIX 凭据的安全令牌。它们用于使诸如网络文件系统密钥之类的内容可用于进程执行的文件访问,而无需普通程序了解所涉及的安全细节。

    密钥环是一种特殊类型的密钥。它们携带其他密钥集,并且可以搜索所需的密钥。每个进程都可以订阅多个密钥环

    每个线程密钥 每个进程密钥环 每个会话密钥环

    当进程访问密钥时,如果该密钥尚不存在,通常会将其缓存在这些密钥环之一上,以供将来的访问查找。

    有关使用密钥的更多信息,请参见 Documentation/security/keys/*

  5. LSM

    Linux 安全模块允许对任务可能执行的操作施加额外的控制。当前,Linux 支持多种 LSM 选项。

    某些工作通过标记系统中的对象,然后应用一组规则(策略),这些规则说明具有一个标签的任务可以对具有另一个标签的对象执行哪些操作。

  6. AF_KEY

    这是一种用于网络堆栈的基于套接字的凭据管理方法 [RFC 2367]。本文档不讨论它,因为它不直接与任务和文件凭据交互;相反,它保留了系统级别的凭据。

打开文件时,会在创建的文件结构中记录打开任务的主观上下文的一部分。这允许使用该文件结构的操作使用这些凭据,而不是发出该操作的任务的主观上下文。一个示例是在网络文件系统上打开的文件,其中打开的文件的凭据应呈现给服务器,而无论谁实际对它执行读取或写入操作。

文件标记

磁盘上或通过网络获得的文件可能具有构成该文件的客观安全上下文的注释。根据文件系统的类型,这可能包括以下一项或多项

  • UNIX UID、GID、模式;

  • Windows 用户 ID;

  • 访问控制列表;

  • LSM 安全标签;

  • UNIX exec 特权提升位 (SUID/SGID);

  • 文件功能 exec 特权提升位。

这些与任务的主观安全上下文进行比较,并因此允许或不允许某些操作。在 execve() 的情况下,特权提升位开始起作用,并且可能允许根据可执行文件上的注释为生成的进程提供额外的特权。

任务凭据

在 Linux 中,任务的所有凭据都保存在 (uid, gid) 中,或者通过 (groups, keys, LSM 安全) 以“struct cred”类型的引用计数结构来保存。每个任务通过其 task_struct 中的一个名为“cred”的指针指向其凭据。

一旦准备并提交了一组凭据,就不能更改它,但以下例外情况除外

  1. 其引用计数可能会更改;

  2. 它指向的 group_info 结构的引用计数可能会更改;

  3. 它指向的安全数据的引用计数可能会更改;

  4. 它指向的任何密钥环的引用计数可能会更改;

  5. 它指向的任何密钥环都可能被撤销、过期或其安全属性发生更改;以及

  6. 它指向的任何密钥环的内容都可能被更改(密钥环的重点在于它是一组共享的凭据,任何具有适当访问权限的人都可以修改)。

要更改 cred 结构中的任何内容,必须遵守复制和替换原则。首先复制一份,然后更改副本,然后使用 RCU 更改任务指针,使其指向新的副本。有一些辅助函数可以帮助实现这一点(见下文)。

任务只能更改其_自身_的凭据;不再允许任务更改其他任务的凭据。这意味着 capset() 系统调用不再允许使用当前进程之外的任何 PID。此外,keyctl_instantiate()keyctl_negate() 函数不再允许附加到请求进程中的进程特定密钥环,因为实例化进程可能需要创建它们。

不可变凭据

一旦凭据集公开(例如,通过调用 commit_creds()),则必须将其视为不可变的,但有两个例外

  1. 引用计数可能会被更改。

  2. 虽然凭据集的密钥环订阅可能不会被更改,但订阅的密钥环的内容可能会被更改。

为了在编译时捕获意外的凭据更改,struct task_struct 具有指向其凭据集的 _const_ 指针,struct file 也是如此。此外,某些函数(例如 get_cred()put_cred())对 const 指针进行操作,因此无需进行强制类型转换,但需要暂时放弃 const 修饰符才能更改引用计数。

访问任务凭据

由于任务只能更改自己的凭据,因此当前进程可以读取或替换自己的凭据,而无需任何形式的锁定,这大大简化了操作。它可以直接调用

const struct cred *current_cred()

以获取指向其凭据结构的指针,并且它之后不必释放该指针。

有一些方便的包装器可以检索任务凭据的特定方面(在每种情况下,都会直接返回该值)

uid_t current_uid(void)         Current's real UID
gid_t current_gid(void)         Current's real GID
uid_t current_euid(void)        Current's effective UID
gid_t current_egid(void)        Current's effective GID
uid_t current_fsuid(void)       Current's file access UID
gid_t current_fsgid(void)       Current's file access GID
kernel_cap_t current_cap(void)  Current's effective capabilities
struct user_struct *current_user(void)  Current's user account

还有一些方便的包装器可以检索任务凭据的特定关联对

void current_uid_gid(uid_t *, gid_t *);
void current_euid_egid(uid_t *, gid_t *);
void current_fsuid_fsgid(uid_t *, gid_t *);

它们在从当前任务的凭据中检索后,通过其参数返回这些值对。

此外,还有一个函数可以获取对当前进程当前凭据集的引用

const struct cred *get_current_cred(void);

还有一些函数可以获取对实际上不在 struct cred 中的凭据之一的引用

struct user_struct *get_current_user(void);
struct group_info *get_current_groups(void);

分别获取对当前进程的用户记帐结构和补充组列表的引用。

一旦获得引用,必须使用相应的 put_cred()free_uid()put_group_info() 释放它。

访问其他任务的凭据

虽然任务可以在无需锁定的情况下访问自己的凭据,但对于想要访问其他任务凭据的任务而言,情况并非如此。它必须使用 RCU 读取锁和 rcu_dereference()

rcu_dereference()

const struct cred *__task_cred(struct task_struct *task);

包装。应在 RCU 读取锁内使用,如下例所示

void foo(struct task_struct *t, struct foo_data *f)
{
        const struct cred *tcred;
        ...
        rcu_read_lock();
        tcred = __task_cred(t);
        f->uid = tcred->uid;
        f->gid = tcred->gid;
        f->groups = get_group_info(tcred->groups);
        rcu_read_unlock();
        ...
}

如果需要长时间持有其他任务的凭据,并且可能需要在持有期间休眠,则调用方应使用以下方法获取对它们的引用:

const struct cred *get_task_cred(struct task_struct *task);

它内部完成了所有 RCU 操作。调用方在完成使用凭据后,必须对获得的凭据调用 put_cred()。

注意

__task_cred() 的结果不应直接传递给 get_cred(),因为它可能会与 commit_cred() 竞争。

有一些方便的函数可以访问另一个任务凭据的某些部分,从而对调用方隐藏 RCU 操作

uid_t task_uid(task)            Task's real UID
uid_t task_euid(task)           Task's effective UID

如果调用方此时正在持有 RCU 读取锁,则应改用

__task_cred(task)->uid
__task_cred(task)->euid

同样,如果需要访问任务凭据的多个方面,则应使用 RCU 读取锁,调用 __task_cred(),将结果存储在临时指针中,然后在释放锁之前,从该指针调用凭据方面。这可以防止多次调用可能代价高昂的 RCU 操作。

如果需要访问另一个任务凭据的某些其他单个方面,则可以使用

task_cred_xxx(task, member)

其中“member”是 cred 结构的非指针成员。例如

uid_t task_cred_xxx(task, suid);

将从任务中检索“struct cred::suid”,执行相应的 RCU 操作。这不能用于指针成员,因为它们指向的内容可能会在 RCU 读取锁被释放的那一刻消失。

更改凭据

如前所述,任务只能更改自己的凭据,而不能更改其他任务的凭据。这意味着它不需要使用任何锁定来更改自己的凭据。

要更改当前进程的凭据,函数应首先调用

struct cred *prepare_creds(void);

来准备一组新的凭据,此操作将锁定 current->cred_replace_mutex,然后分配并构造当前进程凭据的副本,如果成功,则返回时仍持有互斥锁。如果失败(内存不足),则返回 NULL。

互斥锁可以防止在对凭据的构造和更改进行安全检查时,ptrace() 更改进程的 ptrace 状态,因为 ptrace 状态可能会改变结果,尤其是在 execve() 的情况下。

应适当更改新的凭据集,并完成任何安全检查和挂钩。当前和建议的凭据集都可用于此目的,因为此时 current_cred() 仍会返回当前集。

在替换组列表时,必须在将其添加到凭据之前对新列表进行排序,因为会使用二分搜索来测试成员资格。实际上,这意味着应在 set_groups() 或 set_current_groups() 之前调用 groups_sort()。不能对共享的 struct group_list 调用 groups_sort(),因为即使数组已经排序,它也可能会作为排序过程的一部分置换元素。

当凭据集准备好后,应通过调用

int commit_creds(struct cred *new);

将其提交给当前进程。这将更改凭据和进程的各个方面,使 LSM 有机会执行类似的操作,然后它将使用 rcu_assign_pointer() 将新凭据实际提交给 current->cred,它将释放 current->cred_replace_mutex 以允许 ptrace() 进行,并且它将通知调度程序和其他人更改。

保证此函数返回 0,以便可以在 sys_setresuid() 等函数的末尾进行尾调用。

请注意,此函数会消耗调用方对新凭据的引用。调用方不应在之后对新凭据调用 put_cred()

此外,一旦在新凭据集上调用此函数,则_不能_再更改这些凭据。

如果在调用 prepare_creds() 后安全检查失败或发生其他错误,则应调用以下函数

void abort_creds(struct cred *new);

此操作将释放 prepare_creds() 获得的 current->cred_replace_mutex 上的锁,然后释放新凭据。

典型的凭据更改函数如下所示

int alter_suid(uid_t suid)
{
        struct cred *new;
        int ret;

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

        new->suid = suid;
        ret = security_alter_suid(new);
        if (ret < 0) {
                abort_creds(new);
                return ret;
        }

        return commit_creds(new);
}

管理凭据

有一些函数可以帮助管理凭据

  • void put_cred(const struct cred *cred);

    此操作将释放对给定凭据集的引用。如果引用计数达到零,则 RCU 系统将安排销毁凭据。

  • const struct cred *get_cred(const struct cred *cred);

    此操作获取对活动凭据集的引用,返回指向该凭据集的指针。

  • struct cred *get_new_cred(struct cred *cred);

    此操作获取对正在构建且因此仍然可变的凭据集的引用,返回指向该凭据集的指针。

打开文件凭据

当打开一个新文件时,会获取打开该任务的凭据的引用,并将其作为 f_cred 附加到文件结构中,以取代 f_uidf_gid。 过去访问 file->f_uidfile->f_gid 的代码现在应该访问 file->f_cred->fsuidfile->f_cred->fsgid

在不使用 RCU 或锁的情况下访问 f_cred 是安全的,因为指针在文件结构的生命周期内不会改变,所指向的 cred 结构的内容也不会改变,除非有上述列出的例外情况(请参阅“任务凭据”部分)。

为了避免“混淆的副手”特权提升攻击,后续对已打开文件执行操作期间的访问控制检查应该使用这些凭据,而不是“当前”的凭据,因为该文件可能已被传递给一个更具特权的进程。

覆盖 VFS 对凭据的使用

在某些情况下,需要覆盖 VFS 使用的凭据,这可以通过使用不同的凭据调用诸如 vfs_mkdir() 之类的函数来完成。 这在以下位置完成:

  • sys_faccessat().

  • do_coredump().

  • nfs4recover.c.