英语

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 有许多可用的操作,主题可以对对象执行。 可用操作集取决于主题和对象的性质。

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

  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 文件)。 这些(主要)定义了该对象的客观上下文,但在某些情况下,任务略有不同。

    • 有效用户 ID、已保存用户 ID 和 FS 用户 ID

    • 有效组 ID、已保存组 ID 和 FS 组 ID

    • 补充组

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

  2. 功能。

    • 允许的功能集

    • 可继承的功能集

    • 有效的功能集

    • 功能边界集

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

    允许的功能是进程可以通过 capset() 将自身授予其有效集或允许集的 cap。 这个可继承的集合也可能受到限制。

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

    可继承功能是可能在 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 执行特权提升位 (SUID/SGID);

  • 文件功能执行特权提升位。

这些将与任务的主观安全上下文进行比较,并允许或禁止某些操作。 在 execve() 的情况下,特权提升位会发挥作用,并且可能允许生成的进程具有额外的特权,这基于可执行文件上的注释。

任务凭据

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

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

  1. 可以更改其引用计数;

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

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

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

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

  6. 可以更改它指向的任何密钥环的内容(密钥环的全部意义在于一组共享凭据,可由具有适当访问权限的任何人修改)。

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

任务只能更改其 _自身_ 的凭据; 不再允许任务更改另一个任务的凭据。 这意味着不再允许 capset() 系统调用接受当前进程的 PID 以外的任何 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);

    这会在实时凭据集上获取引用,并返回指向该凭据集的指针。

打开文件凭据

打开新文件时,会获得对打开任务的凭据的引用,并将其作为 f_cred 附加到文件结构中,以代替 f_uidf_gid。 曾经访问 file->f_uidfile->f_gid 的代码现在应访问 file->f_cred->fsuidfile->f_cred->fsgid

可以安全地访问 f_cred 而无需使用 RCU 或锁定,因为该指针在文件结构的生命周期内不会更改,并且所指向的 cred 结构的内容也不会更改,但上述列出的例外情况除外(请参见任务凭据部分)。

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

覆盖 VFS 对凭据的使用

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

  • sys_faccessat().

  • do_coredump().

  • nfs4recover.c。