本文中,继续上周关于用户命名空间的讨论。特别的,我们看一下更多有关与用户命名空间、capabilities 的交互及用户命名空间与其它类型的命名空间的结合。本文是命名空间系列的最后一篇。
每个进程都会关联特定用户命名空间。一个通过不带 CLONE_NEWUSER 标志的 fork() 或 clone() 创建的进程与父进程位于同一用户命名空间。进程如果在目标命名空间中有 CAP_SYS_ADMIN capability,则可借助 setns() 改变自己的用户-命名空间关系;那样,它进入目标命名空间时会获得所有 capabilities。
另一方面,clone(CLONE_NEWUSER) 可创建一个新用户命名空间,并将创建的子进程放到该用户命名空间。该调用也会在两个命名空间之间建立一个亲缘关系:每个用户命名空间(最初的命名空间除外)都有一个父亲,即调用 clone(CLONE_NEWUSER) 创建该用户命名空间的进程的用户命名空间。unshare() 不同,它会将调用者放到一个新的用户命名空间,该用户命名空间的父亲是调用者的前一个用户命名空间。待会会看到,用户命名空间之间的亲缘关系很重要,因为这定义了一个进程在新的子命名空间的 capabilities。
每个进程都有三组相关的 capabilities:允许的,有效的和可继承的。capabilities(7) 手册有详细的介绍。本文主要介绍有效的 capability,它决定了进程能否执行特权操作。
用户命名空间改变了解释(有效的)capabilities 的方式。首先,在特定用户命名空间中有一个 capability,允许进程操作由该命名空间管理的资源。当我们讨论用户命名空间与其他类型命名空间的交互时,将进一步讨论这一点。此外,进程是否具有特定用户命名空间中的 capabilities 取决于它是否是命名空间的成员以及用户命名空间之间是否有亲缘关系。规则如下:
可通过一个小程序 userns_setns_test.c 来证明第三条规则。该程序采用一个命令行参数:一个 /proc/PID/ns/user 文件(标识用户命名空间)的路径名。它在新用户命名空间中创建一个子进程,然后父(与启动 userns_setns_test 程序的 shell 在同一用户命名空间)进程和子进程都会试图通过 setns() 加入该命名空间;如上所述,setns() 要求调用者在目标命名空间中拥有 CAP_SYS_ADMIN capability。
为了证明,使用该程序和前面文章中的 userns_child_exec.c 。首先,使用该程序启动一个 shell(创建一个被命名为 ksh 的进程),该 shell 将运行于新用户命名空间中。
$ id -u
1000
$ readlink /proc/$$/ns/user # Obtain ID for initial namespace
user:[4026531837]
$ ./userns_child_exec -U -M '0 1000 1' -G '0 1000 1' ksh
ksh$ echo $$ # Obtain PID of shell
528
ksh$ readlink /proc/$$/ns/user # This shell is in a new namespace
user:[4026532318]
现在,切换到运行于最初的命名空间终端,并执行测试程序:
$ readlink /proc/$$/ns/user # Verify that we are in parent namespace
user:[4026531837]
$ ./userns_setns_test /proc/528/ns/use
parent: readlink("/proc/self/ns/user") ==> user:[4026531837]
parent: setns() succeeded
child: readlink("/proc/self/ns/user") ==> user:[4026532319]
child: setns() failed: Operation not permitted
下图展示来进程之间(黑色箭头)和命名空间之间(蓝色箭头)的亲缘关系:
在每个 shell 中看一下 readlink 命令的输出,可以看到当最初用户命名空间(4026531837)(在前面的文章中所提,这些数字是 /proc/PID/ns 中的链接的 i-node 号)中的 userns_setns_test 程序运行时,父进程被创建了。这样,根据前面三条规则,因为父进程与创建新用户命名空间(4026532318)的进程有着相同的有效用户 ID(1000),所以在该用户命名空间中拥有所有的 capabilities,包括 CAP_SYS_ADMIN;因此,父进程中的 setns() 成功了。
另一方面,被 userns_setns_test 创建的子进程位于不同的命名空间(4026532319)--- 运行 ksh 进程的命名空间的同级命名空间。这样,就违反了上述第二条规则,因为该命名空间不是 4026532318 命名空间的祖先。因此,该子进程在那个命名空间中没有 CAP_SYS_ADMIN capability,setns() 也会失败。
创建用户命名空间以外的命名空间需要 CAP_SYS_ADMIN capability。另一方面,创建一个用户命名空间不需要任何 capabilities(自 Linux 3.8),并且命名空间中的第一个进程会获得所有 capabilities(新用户命名空间中)。这意味着该进程可以通过再次调用 clone() 来创建任何其它类型的命名空间。
然而,两步过程并非必需。也可以通过在同一个使用 CLONE_NEWUSER 的 clone() (或 unshare())中附加 CLONE_NEW* 标志来创建新的用户命名空间。这种情况下,内核先对 CLONE_NEWUSER 标志执行操作,创建一个新用户命名空间,其内的将被创建的子进程都会拥有全部 capabilities。然后内核再对剩余的 CLONE_NEW* 标志执行操作,创建相应的新命名空间,并让子进程成为所有命名空间中的成员。
因此,非特权进程可通过如下形式的调用,创建一个同时为新用户命名空间和新 UTS 命名空间成员的子进程:
clone(child_func, stackp, CLONE_NEWUSER | CLONE_NEWUTS, arg);
可使用 userns_child_exec 执行与上面相同的 clone() 调用,并在子进程中启动一个 shell。如下命令指定创建一个新 UTS 命名空间(-u),和一个新用户命名空间(-U),新用户命名空间内的用户和组 ID 1000 都会映射到 0:
$ uname -n # Display hostname for later reference
antero
$ ./userns_child_exec -u -U -M '0 1000 1' -G '0 1000 1' bash
正如所料,shell 进程拥有一整套允许的和有效的 capabilities:
$ id -u # Show effective user and group ID of shell
0
$ id -g
0
$ cat /proc/$$/status | egrep 'Cap(Inh|Prm|Eff)'
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff
在上述输出中,16 进制值 1fffffffff 表示一个 capability 集,其中,所有的 37 个当前可用的 Linux 功能都已启用。
现在,我们可以继续修改主机名 --- 被 UTS 命名空间隔离的全局资源之一 --- 通过使用标准的 hostname 命令;该操作需要 CAP_SYS_ADMIN capability。我们将主机名设置为一个新值,然后使用 uname 命令检查该值:
$ hostname bizarro # Update hostname in this UTS namespace
$ uname -n # Verify the change
bizarro
切换到在初始 UTS 命名空间中运行的另一个终端窗口,然后检查该 UTS 命名空间中的主机名:
$ uname -n # Hostname in original UTS namespace is unchanged
antero
从上面的输出中,我们可以看到子 UTS 命名空间中主机名的更改在父 UTS 命名空间中不可见。
尽管内核将所有 capabilities 都授予用户命名空间中的初始进程,但这并不意味着该进程在更大范围系统中具有超级用户权限(但是,这可能意味着非特权用户现在可以访问以前只能由 root 用户访问的内核代码,正如关于 tmpfs 挂载点漏洞的邮件所述。)当通过 clone() 或 unshare() 创建新的 IPC、挂载、网络、PID 或 UTS 命名空间时,内核会根据新命名空间记录创建者的用户命名空间。每当进程操作被命名空间控制的全局资源时,都会根据该进程在内核关联的命名空间的用户命名空间中的 capabilities 进行权限检查。
假设通过 clone(CLONE_NEWUSER) 创建一个新用户命名空间。子进程将在该新用户命名空间中拥有全部的 capabilities,这意味着,可以创建其它类型的命名空间,也可以定义自己的用户和组 ID 到该命名空间中的 ID 的映射。(本系列的前面几篇文章中,我们看到仅仅在父用户命名空间中的特权进程可以创建除了创建该命名空间的进程的有效用户和组 ID 以外的 ID 的映射,因此没有安全漏洞。)
另一方面,子进程不能挂载文件系统。子进程仍然在最初的挂载空间,为了在该命名空间挂载一个文件系统,它需要与该挂载空间关联的用户命名空间中的 capabilities(即,需要最初用户命名空间中的 capabilities)。类似的情况适用于 IPC,网络,PID 和 UTS 名空间。
此外,子进程将无法执行特权操作,这些操作需要不受命名空间(当前)控制的 capabilities。因此,子进程无法执行诸如提高其硬件资源限制、设置系统时间、设置进程优先级、加载内核模块之类的操作。这些操作都需要用户命名空间层次结构之外的功能;实际上,这些操作要求调用方具有初始用户命名空间中的 capabilities。
通过隔离 capabilities 对命名空间的影响,用户命名空间可安全地允许未经授权的用户访问以前仅限于 root 用户的功能。这反过来又为新用户空间中的应用创造了有趣的可能性。例如,非特权用户可以在没有 root 权限的情况下运行 Linux 容器,可以在不使用 set-user-id-root 的情况下构建 Chrome-样式的沙盒,可以在不使用动态链接的情况下实现 fakeroot-类型的应用程序,还可以实现基于 chroot() 的应用程序以隔离进程。除了内核错误,应用通过使用用户命名空间来访问内核的特权功能比基于 set-user-ID-root 更安全:通过使用用户命名空间,应用程序即使受到损害,它也没有特权在更大范围的系统造成破坏。
原文:https://lwn.net/Articles/540087/
公众号:Geek乐园
博客:https://blog.csdn.net/u012319493/article/details/102872596
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。