关于一个神奇的 Linux 目录权限问题的探究

TL;DR

  • su 与单纯的 set{u,g}id 不一样:前者会通过 setgroups 系统调用来设置进程的 Supplementary Group1;而后者不会,只会修改进程的 {,s,e,fs}{u,g}id
  • Supplementary Group 在内核中体现为 struct cred 下的 group_info,本质上是一个长度不固定的 gid 数组(类似 C++ 中的 string_view)
  • 通过内核代码(init_credinit_groups)确认,内核不会为进程添加 Supplementary Group;且 /etc/group 中也没有对应的条目,但 root 用户进程的 Supplementary Group 依旧包含 root 组。具体原因暂不清楚。

故事背景

部门同事来咨询一个问题:执行 ltp 中的 dirtyc0w 用例2失败,错误码为 EACCESS。经初步了解,以 root 用户执行的 dirtyc0w 会拉起一个子进程,子进程先用 setuidsetgid 切换至名为 nobody 的用户,然后用 execlp 执行另一个名为 dirtyc0w_child 的程序。而报错发生在子进程执行 dirtyc0w_child 的时候。

上述问题的原因很快被定位:root 用户的 umask 默认为 0077,使得整个 ltp 目录下的文件/目录权限皆为 root:root:600/700,因此 nobody 用户无权进入 dirtyc0w_child 用例所处的目录,从而无权执行该文件。将用例路径上各级目录的权限改为 711,即可解决问题。

事情发展至此就本应结束了。然鹅,他们发现了一个奇怪的现象:将用例路径上某级目录的权限改为 701 后,又会报没有权限的错误。而理论上,nobody 作为 others,这个权限应该是足够的。

分析过程

先通过分析内核代码,初步估计是走下述调用路径走到了 generic_permission() 中进行权限检查:

 1SYSCALL_DEFINE3(execve, ...)
 2  do_execve
 3    do_execveat_common
 4      bprm_execve
 5        do_open_execat
 6          do_filp_open
 7            path_openat
 8              link_path_walk
 9                may_lookup
10                  inode_permission
11                    do_inode_permission
12                      generic_permission

随后通过 ftrace 追踪 do_execveat_common() 验证了上述判断基本正确,但随后具体的鉴权过程就较为模糊。因此重点观察 generic_permission() 及其下 acl_permission_check() 函数。先尝试在该函数中增加打印来定位最终失败的位置,但对该函数的调用非常频繁,在海量的无效日志下,内核无法启动,即使启动了也不好分析。因此为这些打印添加了一个全局开关,并采用插 ko 的方式创建一个与开关相连的用户态接口3,使得我们可以规避系统启动过程中的大量文件系统操作,待系统启动至平稳后再将开关打开使能打印。经过逐步加日志,我发现了一个神奇的现象:内核居然认为 nobody 和 root 是同组的!具体体现为 kgid = 0, in_group_p(kgid) == 1。而这也正是该问题的关键:目录的群组权限被设置为 0,而基于 nobody 与 root 同组的前提,内核自然会拒绝来自 nobody 的访问。

我一度笃定这肯定是内核的 bug,直到对 in_group_p() 进行一番抽丝剥茧后发现,居然是 nobody 的 Supplementary Group 包含 root 这个组……随后我又对 set{u,g}idfork 的代码检查了一番,最终确定:这个 Supplementary Group 是从 root 身份的父进程继承下来的(在用户态用 id 可确认这一点);另外,修改 Supplementary Group 的方式和动作都非常局限,而 set{u,g}id 压根儿不会去动它。

最后,关于是谁给 root 用户增加 Supplementary Group 这一点,我进行了一番探究,确定这不是内核的行为:内核为进程准备的初始凭证中没有任何 Supplementary Group,应该是用户态的行为。但究竟是哪个组件做的,就不得而知了。

相关代码

ftrace 操作

 1cd /sys/kernel/tracing
 2
 3echo > set_ftrace_filter
 4echo do_execveat_common > set_graph_function  # 设定要追踪的函数
 5echo function_graph > current_tracer
 6echo 1 > options/funcgraph-proc               # 打印进程命令行和 PID
 7echo 1 > options/funcgraph-tail               # 打印尾部注释
 8
 9echo nofuncgraph-irqs > trace_options  # 不追踪中断处理过程中的函数
10echo $BASH_PID > set_ftrace_pid        # 只追踪当前 bash 进程
11echo function-fork > trace_options     # 以及当前 bash 进程的子进程
12
13echo > trace  # 清除之前的记录
14cp trace ~/test.txt
15cat ~/test.txt

调试用的 ko

参考资料


  1. Supplementary Group 又称 Secondary Group,前者常见于代码注释,后者常见于网上资料和文档。 ↩︎

  2. 具体路径为 testcases/kernel/security/dirtyc0w。 ↩︎

  3. 关于这个 ko 可以参考本人的另一篇笔记 。 ↩︎


利用云服务器建立个人博客网站
进程切换与抢占