1.. SPDX-License-Identifier: GPL-2.0 2.. include:: ../disclaimer-zh_CN.rst 3 4:Original: Documentation/security/credentials.rst 5 6:翻译: 7 赵硕 Shuo Zhao <zhaoshuo@cqsoftware.com.cn> 8 9============= 10Linux中的凭据 11============= 12 13作者: David Howells <dhowells@redhat.com> 14 15.. contents:: :local: 16 17概述 18==== 19 20当一个对象对另一个对象进行操作时,Linux执行的安全检查包含几个部分: 21 22 1. 对象 23 24 对象是可以直接由用户空间程序操作的系统中的实体。Linux具有多种可操作 25 的对象,包括: 26 27 - 任务 28 - 文件/索引节点 29 - 套接字 30 - 消息队列 31 - 共享内存段 32 - 信号量 33 - 密钥 34 35 所有这些对象的描述的一部分是一组凭据。集合中的内容取决于对象的类型。 36 37 2. 对象所有权 38 39 大多数对象的凭据中会有一个子集用来表示该对象的所有权。 40 这用于资源核算和限制(如磁盘配额和任务资源限制)。 41 42 例如,在标准的UNIX文件系统中,这将由标记在索引节点上的UID定义。 43 44 3. 对象上下文 45 46 此外在这些对象的凭据中,将有一个子集表示对象的“对象上下文”。 47 这可能与(2)中相同,也可能不同 —— 例如,在标准的UNIX文件中, 48 这是由标记在索引节点上的UID和GID定义的。 49 50 对象上下文是进行安全计算的一部分,当对象被操作时会用到。 51 52 4. 主体 53 54 主体是正在对其他对象执行操作的对象。 55 56 系统中的大多数对象是不活动的:他们不会对系统中的其他对象起作用。 57 进程/任务是明显的例外:它们可以访问和操纵其他对象。 58 59 任务之外的其他对象在某些情况下也可以是主体。例如,打开的文件可以使用 60 名为 ``fcntl(F_SETOWN)`` 的任务给它的UID和EUID向一个任务发送SIGIO 61 信号。在这种情况下,文件结构也会有一个主体上下文。 62 63 5. 主体上下文 64 65 主体对其凭据有一个额外的解释。其凭据的一个子集形成了“主体上下文”。主体 66 上下文在主体执行操作时作为安全计算的一部分使用。 67 68 例如,Linux任务在操作文件时会有FSUID、FSGID和附加组列表 —— 这些凭据 69 与通常构成任务的对象上下文的真实UID和GID是相互独立的。 70 71 6. 操作 72 73 Linux提供许多操作,主体可以对对象执行这些操作。可用的操作集取决于主体 74 和对象的性质。 75 76 77 操作包括读取、写入、创建和删除文件,以及派生(forking)或发送 78 信号(signalling)和跟踪(tracing)任务等。 79 80 7. 规则,访问控制列表和安全计算 81 82 当主体对对象进行操作时,会进行安全计算。这涉及到使用主体上下文、对象 83 上下文和操作,并搜索一个或多个规则集,以确定在给定这些上下文的情况下, 84 主体是否被授予或拒绝以所需方式对对象进行操作的权限。 85 86 主要有两个规则来源: 87 88 a. 自主访问控制(DAC): 89 90 有时,对象的描述中会包含一组规则。这就是所谓的“访问控制列表”或‘ACL’。 91 一个Linux文件可以提供多个ACL。 92 93 例如,传统的UNIX文件包括一个权限掩码,它是一个简化的ACL,具有三个固定的 94 主体类别(“用户”、“组”和“其他”),每一个都可以被授予一定的特权(如“读取”、 95 “写入”和“执行” —— 无论这些映射对于对象意味着什么)。然而,UNIX文件权限不 96 允许任意指定主体,因此用途有限。 97 98 Linux文件还可以支持POSIX ACL。这是一个规则列表,为任意主体授予各种权限。 99 100 b. 强制访问控制(MAC): 101 102 整个系统可能有一个或多个规则集,适用于所有主体和对象,不考虑它们的来源。 103 SELinux和Smack就是这种情况的例子。 104 105 在SELinux和Smack的情况下,每个对象在其凭据中都被赋予一个标签。当请求执 106 行操作时,它们使用主体标签、对象标签和操作,寻找一个规则,该规则表示此操 107 作是授予还是拒绝的。 108 109 110凭据类型 111======== 112 113Linux内核支持以下类型的凭据: 114 115 1. 传统的UNIX凭据。 116 117 - 真实用户ID 118 - 真实组ID 119 120 UID和GID由大多数(如果不是全部)Linux对象携带,即使有时它们需要被虚构出 121 来(例如FAT或CIFS文件,这些文件来源于Windows)。这些(通常)定义了该对象 122 的对象上下文,但任务在某些情况下略有不同。 123 124 - 有效用户ID,保存用户ID和FS用户ID 125 - 有效组ID,保存组ID和FS组ID 126 - 补充组 127 128 这些是仅由任务使用的额外凭据。通常,一个EUID/EGID/GROUPS 被用作主体上下文, 129 而真实UID/GID 被用作对象上下文。对于任务,这并不总是正确的。 130 131 2. 能力 132 133 - 允许的能力集合 134 - 可继承的能力集合 135 - 有效的能力集合 136 - 能力边界集合 137 138 这些仅由任务携带,表示授予任务的超出普通任务权限的能力。这些可以通过传统 139 UNIX凭据的更改进行隐式操作,但也可以通过 ``capset()`` 系统调用直接操作。 140 141 允许的能力是指进程可以通过 ``capset()`` 将其添加到其有效或允许集合中的 142 那些能力。这个可继承的集合也可能受到这样的限制。 143 144 有效能力是任务本身实际可以使用的能力。 145 146 可继承能力是那些可以通过 ``execve()`` 传递的能力。 147 148 边界集限制了通过 ``execve()`` 继承的能力,特别是在以UID 0执行二进制文件时。 149 150 3. 安全管理标记(securebits) 151 152 它们用于控制上述凭据在特定操作如execve()中的操作和继承方式。它们并不直接 153 用作对象或主体凭据使用。 154 155 4. 密钥和密钥环 156 157 这些仅由任务携带。它们用于携带和缓存不适合放入其他标准UNIX凭据中的安全令牌。 158 它们用诸如使网络文件系统密钥在进程执行的文件访问时可用,而无需让普通程序了解 159 涉及的安全细节。 160 161 密钥环是一种特殊类型的密钥。它们携带一组其他密钥,并可以搜索来查找所需的密钥。 162 每个进程可以订阅多个密钥环: 163 164 每线程密钥 165 每进程密钥环 166 每会话密钥环 167 168 当进程访问一个密钥时,若尚不存在,则通常会将其缓存在一个密钥环中,以便将来的 169 访问时找到该密钥。 170 171 有关密钥的更多信息,请参见 ``Documentation/translations/zh_CN/security/keys/*`` 。 172 173 5. LSM 174 175 Linux安全模块允许在任务执行操作时施加额外的控制。目前,Linux支持几种LSM选项。 176 177 一些工作通过标记系统中的对象,并应用一组规则(策略)说明某个标签的任务可以对 178 另一标签的对象执行哪些操作。 179 180 6. AF_KEY 181 182 这是一种基于套接字网络协议栈中的凭据管理[RFC 2367]。本文档中没有讨论它,因为不 183 直接与任务和文件凭据进行交互,而是保留了系统级的凭据。 184 185 186当打开一个文件时,打开任务的主体上下文的一部分会记录在创建的文件结构中。 187这使得使用该文件结构的操作可以使用这些凭据,而不是发出操作的任务的主体上下文。 188一个例子是在网络文件系统上打开的文件,打开文件的凭据应该被呈现给服务器,而不管 189实际进行读取或写入操作的是谁。 190 191 192文件标记 193======== 194 195存储在磁盘上或通过网络获取的文件可能具有注释,构成该文件的对象安全上下文。 196根据文件系统的类型,这些注释可能包括以下一项或多项: 197 198 * UNIX UID, GID, mode; 199 * Windows user ID; 200 * Access control list; 201 * LSM security label; 202 * UNIX exec privilege escalation bits (SUID/SGID); 203 * File capabilities exec privilege escalation bits. 204 205将这些与任务的主体安全上下文进行比较,并根据比较结果允许或禁止执行某些操作。 206在execve()的情况下,特权提升位起作用,并且可能允许由可执行文件的注释决定的 207进程获得额外的特权。 208 209 210任务凭据 211======== 212 213在Linux中,一个任务的所有凭据都保存在一个引用计数结构体‘struct cred’中, 214通过(uid, gid)或(groups, keys, LSM security)进行访问。每个任务在其 215task_struct中通过一个名为‘cred’的指针指向其凭据。 216 217一旦一组凭据已经准备好并提交,除非以下几种情况,否则不能更改: 218 219 1. 其引用计数可以更改; 220 221 2. 它所指向的 group_info 结构体的引用计数可以更改; 222 223 3. 它所指向的安全数据的引用计数可以更改; 224 225 4. 它所指向的任何密钥环的引用计数可以更改; 226 227 5. 它所指向的任何密钥环可以被撤销、过期或其安全属性可以更改; 228 229 6. 它所指向的任何密钥环的内容可以更改(密钥环的整个目的就是作为一组共享凭据, 230 可由具有适当访问权限的任何人修改)。 231 232要更改cred结构体中的任何内容,必须遵循复制和替换的原则。首先进行复制,然后修 233改副本,最后使用RCU(读-复制-更新)将任务指针更改为指向新的副本。有一些封装可 234用于帮助执行这个过程(见下文)。 235 236一个任务只能修改自己的凭据;不再允许一个任务修改另一个任务的凭据。 237这意味着 ``capset()`` 系统调用不再允许使用除当前进程之外的任何PID。 238此外, ``keyctl_instantiate()`` 和 ``keyctl_negate()`` 函数也不再 239允许在请求进程中附加到特定于进程的密钥环,因为实例化进程可能需要创建它们。 240 241 242不可变凭据 243---------- 244 245一旦一组凭据已经被公开(例如通过调用 ``commit_creds()`` ),必须将其视为 246不可变的,除了两个例外情况: 247 248 1. 引用计数可以被修改。 249 250 2. 虽然无法更改一组凭据的密钥环订阅,但订阅的密钥环的内容可以被更改。 251 252为了在编译时捕获意外的凭据修改,struct task_struct具有_const_指针指向其凭据集, 253struct file也是如此。此外,某些函数如 ``get_cred()`` 和 ``put_cred()`` 在 254const指针上操作,因此不需要进行类型转换,但需要临时放弃const限定,以便能够修改 255引用计数。 256 257 258访问任务凭据 259------------ 260 261任务只能修改自己的凭据,允许当前进程可以读取或替换自己的凭据,无需任何形式锁定的 262情况下 —— 这极大简化了事情。它可以调用:: 263 264 const struct cred *current_cred() 265 266获取指向其凭据结构的指针,并且之后不必释放它。 267 268有一些方便的封装用于检索任务凭据的特定方面(在每种情况下都只返回值):: 269 270 uid_t current_uid(void) Current's real UID 271 gid_t current_gid(void) Current's real GID 272 uid_t current_euid(void) Current's effective UID 273 gid_t current_egid(void) Current's effective GID 274 uid_t current_fsuid(void) Current's file access UID 275 gid_t current_fsgid(void) Current's file access GID 276 kernel_cap_t current_cap(void) Current's effective capabilities 277 struct user_struct *current_user(void) Current's user account 278 279还有一些方便的封装,用于检索任务凭据的特定关联对:: 280 281 void current_uid_gid(uid_t *, gid_t *); 282 void current_euid_egid(uid_t *, gid_t *); 283 void current_fsuid_fsgid(uid_t *, gid_t *); 284 285在从当前任务的凭据中检索后,通过其参数返回这些值对。 286 287 288此外,还有一个函数用于获取当前进程的当前凭据集的引用:: 289 290 const struct cred *get_current_cred(void); 291 292以及用于获取对一个实际上不存在于struct cred中的凭据的引用的函数:: 293 294 struct user_struct *get_current_user(void); 295 struct group_info *get_current_groups(void); 296 297分别获得对当前进程的 user accounting structure 和补充组列表的引用。 298 299一旦获得引用,就必须使用 ``put_cred()``, ``free_uid()`` 或 300``put_group_info()`` 来适当释放它。 301 302 303访问其他任务的凭据 304------------------ 305 306虽然一个任务可以在不需要锁定的情况下访问自己的凭据,但想要访问另一个任务 307的凭据的任务并非如此。它必须使用RCU读锁和 ``rcu_dereference()``。 308 309``rcu_dereference()`` 是由:: 310 311 const struct cred *__task_cred(struct task_struct *task); 312 313这应该在RCU读锁中使用,如下例所示:: 314 315 void foo(struct task_struct *t, struct foo_data *f) 316 { 317 const struct cred *tcred; 318 ... 319 rcu_read_lock(); 320 tcred = __task_cred(t); 321 f->uid = tcred->uid; 322 f->gid = tcred->gid; 323 f->groups = get_group_info(tcred->groups); 324 rcu_read_unlock(); 325 ... 326 } 327 328如果需要长时间持有另一个任务的凭据,并且可能在此过程中休眠,则调用方 329应该使用以下函数来获取对这些凭据的引用:: 330 331 const struct cred *get_task_cred(struct task_struct *task); 332 333这个函数内部完成了所有的RCU操作。当使用完这些凭据时,调用方必须调用put_cred() 334函数释放它们。 335 336.. note:: 337 ``__task_cred()`` 的结果不应直接传递给 ``get_cred()`` , 338 因为这可能与 ``commit_cred()`` 发生竞争条件。 339 340还有一些方便的函数可以访问另一个任务凭据的特定部分,将RCU操作对调用方隐藏起来:: 341 342 uid_t task_uid(task) Task's real UID 343 uid_t task_euid(task) Task's effective UID 344 345如果调用方在此时已经持有RCU读锁,则应使用:: 346 347 __task_cred(task)->uid 348 __task_cred(task)->euid 349 350类似地,如果需要访问任务凭据的多个方面,应使用RCU读锁,调用 ``__task_cred()`` 351函数,将结果存储在临时指针中,然后从临时指针中调用凭据的各个方面,最后释放锁。 352这样可以防止多次调用昂贵的RCU操作。 353 354如果需要访问另一个任务凭据的其他单个方面,可以使用:: 355 356 task_cred_xxx(task, member) 357 358这里的‘member’是cred结构体的非指针成员。例如:: 359 360 uid_t task_cred_xxx(task, suid); 361 362将从任务中检索‘struct cred::suid’,并执行适当的RCU操作。对于指针成员, 363不能使用这种形式,因为它们指向的内容可能在释放RCU读锁的瞬间消失。 364 365 366修改凭据 367-------- 368 369如先前提到的,一个任务只能修改自己的凭据,不能修改其他任务的凭据。这意味 370着它不需要使用任何锁来修改自己的凭据。 371 372要修改当前进程的凭据,函数应首先调用:: 373 374 struct cred *prepare_creds(void); 375 376这将锁定current->cred_replace_mutex,然后分配并构建当前进程凭据的副本。 377如果成功,函数返回时仍然保持互斥锁。如果不成功(内存不足),则返回NULL。 378 379互斥锁防止 ``ptrace()`` 在进行凭据构建和更改的安全检查时更改进程的ptrace 380状态,因为ptrace状态可能会改变结果,特别是在 ``execve()`` 的情况下。 381 382新的凭据集应适当地进行修改,并进行任何安全检查和挂钩。在此时,当前和建议的 383凭据集都可用,因为current_cred()将返回当前的凭据集。 384 385在替换组列表时,必须在将其添加到凭据之前对新列表进行排序,因为使用二分查找 386测试成员资格。实际上,这意味着在set_groups()或set_current_groups()之 387前应调用groups_sort()。groups_sort()不能在共享的 ``struct group_list`` 388上调用,因为即使数组已经排序,它也可能作为排序过程的一部分对元素进行排列。 389 390当凭据集准备好时,应通过调用以下函数将其提交给当前进程:: 391 392 int commit_creds(struct cred *new); 393 394这将修改凭据和进程的各个方面,给LSM提供机会做同样的修改,然后使用 395``rcu_assign_pointer()`` 将新的凭据实际提交给 ``current->cred`` , 396释放 ``current->cred_replace_mutex`` 以允许 ``ptrace()`` 进行操 397作,并通知调度程序和其他组件有关更改的情况。 398 399该函数保证返回0,以便可以在诸如 ``sys_setresuid()`` 函数的末尾进行尾调用。 400 401请注意,该函数会消耗调用者对新凭据的引用。调用者在此之后不应调用 402``put_cred()`` 释放新凭据。 403 404此外,一旦新的凭据上调用了该函数,就不能进一步更改这些凭据。 405 406 407如果在调用 ``prepare_creds()`` 之后安全检查失败或发生其他错误, 408则应调用以下函数:: 409 410 void abort_creds(struct cred *new); 411 412这将释放 ``prepare_creds()`` 获取的 ``current->cred_replace_mutex`` 的锁, 413并释放新的凭据。 414 415一个典型的凭据修改函数看起来像这样:: 416 417 int alter_suid(uid_t suid) 418 { 419 struct cred *new; 420 int ret; 421 422 new = prepare_creds(); 423 if (!new) 424 return -ENOMEM; 425 426 new->suid = suid; 427 ret = security_alter_suid(new); 428 if (ret < 0) { 429 abort_creds(new); 430 return ret; 431 } 432 433 return commit_creds(new); 434 } 435 436 437管理凭据 438-------- 439 440有一些函数用来辅助凭据管理: 441 442 - ``void put_cred(const struct cred *cred);`` 443 444 这将释放对给定凭据集的引用。如果引用计数为零,凭据集将由 445 RCU系统安排进行销毁。 446 447 - ``const struct cred *get_cred(const struct cred *cred);`` 448 449 这将获取对活动凭据集的引用。返回指向凭据集的指针。 450 451 - ``struct cred *get_new_cred(struct cred *cred);`` 452 453 这将获取对当前正在构建且可变的凭据集的引用。返回指向凭据集的指针。 454 455打开文件凭据 456============ 457 458当打开新文件时,会获取对打开任务凭据的引用,并将其附加到文件结构体的 459``f_cred`` 字段中,替代原来的 ``f_uid`` 和 ``f_gid`` 。原来访问 460``file->f_uid`` 和 ``file->f_gid`` 的代码现在应访问 ``file->f_cred->fsuid`` 461和 ``file->f_cred->fsgid`` 。 462 463安全访问 ``f_cred`` 的情况下可以不使用RCU或加锁,因为指向凭据的指针 464以及指向的凭据结构的内容在文件结构的整个生命周期中保持不变,除非是 465上述列出的例外情况(参阅任务凭据部分)。 466 467为了避免“混淆代理”权限提升攻击,在打开的文件后续操作时,访问控制检查 468应该使用这些凭据,而不是使用“当前”的凭据,因为该文件可能已经被传递给 469一个更具特权的进程。 470 471覆盖VFS对凭据的使用 472=================== 473 474在某些情况下,需要覆盖VFS使用的凭据,可以通过使用不同的凭据集调用 475如 ``vfs_mkdir()`` 来实现。以下是一些进行此操作的位置: 476 477 * ``sys_faccessat()``. 478 * ``vfs_coredump()``. 479 * nfs4recover.c. 480