跳转至

拓展阅读

文件系统的特殊权限位

有一个有趣的问题:众所周知(只要你看过了本章的主要内容),存储密码的 /etc/shadow 只有 root 用户可以查看和修改。但是,我作为一个普通用户,可以使用 passwd 修改自己的密码。而要修改密码,就必须修改 /etc/shadow,而我执行的 passwd 程序(应该)只拥有我的权限(否则用户权限就没有任何意义了)。难道 passwd 有什么魔法可以去动 /etc/shadow 吗?

是的。这样的「魔法」,是由文件系统特殊权限位 setuid 赋予的。有三个特殊权限位:setuid, setgid 和 sticky。

  • setuid: 以文件所属的用户的身份 (UID) 执行此程序。
  • setgid: 对文件来说,以文件所属的用户组的身份 (GID) 执行此程序;对目录来说,在这个目录下创建的文件的用户组都与此目录本身的用户组一致,而不是创建者的用户组。
  • sticky (restricted deletion flag): 目录中的所有文件只能由文件所有者(除 root 以外)删除或者移动。一个典型的例子是临时文件夹 /tmp,在此文件夹中你可以创建、修改、重命名、移动、删除自己的文件,但是动不了别人的文件。
setuid 和 sticky 位的历史

聪明的你可能已经注意到了:setuid 和 setgid 对可执行程序具有相同的语义,但 setuid 对目录没有作用。事实上大部分的 UNIX 系统和所有的 Linux 都从未对“设有 setuid 的目录”赋予过任何特别含义,但仍然有少部分系统可以配置为对目录中的 setuid 位采取和 setgid 位类似的语义,即此目录中新建的文件所有者与此目录的所有者一致,而不是为文件的创建者所有。这“少部分系统”中的一个例子是 FreeBSD,但仅当文件系统在挂载时配置了 suiddir 参数(FreeBSD)。

在很早的时候(1974 年 Unix 第五版),sticky bit 是为可执行文件设计的,它告诉操作系统在程序运行结束后将程序的代码段(text 段)保留在内存或交换区中,以便下一次更快地启动这个程序,因此这个位被称作 sticky(粘滞位)。由于现代计算机存储容量的增加和操作系统缓存功能的完善,已经没有系统再将 sticky bit 解释为这个最初的含义了。在 Linux 系统中,文件上的 sticky bit 从未有过任何功能,仅有目录的 sticky bit 功能如上所述。

我们可以看一下,/usr/bin/passwd 的文件权限设置:

$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 67992 Aug 29  2019 /usr/bin/passwd

可以看到,本来是执行权限位的地方变成了 s。这代表此文件有 setuid 特殊权限位。在你执行 passwd 的时候,它的实际权限和 root 一样,只是它知道,执行它的人是你(而非 root),所以只提供修改你自己的密码的功能。

同样,susudo 也有 setuid 权限位:

$ ls -l /usr/bin/su
-rwsr-xr-x 1 root root 67816 Jan  9 02:59 /usr/bin/su
$ ls -l /usr/bin/sudo
-rwsr-xr-x 1 root root 161448 Feb  1 01:07 /usr/bin/sudo

所以它们可以帮你切换用户、提升权限。

拥有 setuid 位、所有者为 root 的程序是非常危险的,因为稍不谨慎,它们的漏洞就会直接让普通的无权限用户获得 root 权限的大门,这在服务器上是极其致命的。sudo 曾经爆出一个安全漏洞(CVE-2019-18634),对于 1.8.31 之前的版本,sudo 在开启了 pwfeedback 选项(将输入的密码显示为 * 号,而非不显示)之后,有一个缓冲区溢出漏洞可以被利用来以 root 的身份执行任意命令;而在 2022 年初,另一个用于验证用户身份、提升权限的工具 pkexec 也被爆出安全漏洞(CVE-2021-4034):由于编写 C 语言程序时未考虑到 main() 函数参数 argc 可以为 0 的情况,攻击者可以编写程序引发其逻辑问题,从而提升自己的权限。

当然,Linux 在发展中也在尽可能减少 setuid 程序的使用。例如 ping 程序因为需要创建只能由 root 用户创建的原始 (raw) 的网络套接字 (socket),在以往也是一个 setuid 程序。但是随着 "Capabilities" 的概念引入 Linux 内核,ping 不再需要 setuid,只需要为它设置创建原始网络套接字的 capability 即可,提高了系统的安全性。

实际用户与有效用户

于是,我们就有了另一个问题:如果 passwd 在执行的时候的权限是 root 的话,那它是怎么知道是我(而不是 root)执行它的呢?

在 Linux 中,有两个系统调用可以获取当前进程归属的 UID:getuid()geteuid()。前者对应的是「实际用户」(Real user),是实际运行(拥有)这个进程的用户,后者对应的是「有效用户」(Effective user),对应进程拥有的权限。在运行 passwd 的时候,有效用户是 root,所以可以修改 /etc/shadow;而实际用户是你,所以它不会让你修改别人的密码。

对用户组来说,也有实际用户组 (GID) 和有效用户组 (EGID) 的区别。

「登录 Shell」(Login shell) 与「非登录 Shell」(Non-login shell)

在前文中我们提到,susu - 是有区别的。你也可能在学习 Linux 的时候会好奇:为什么我按下 Ctrl + Alt + F[1-7] 的时候出现的 TTY 会问我要用户名和密码,但是在桌面环境中点「终端」,不需要再输入用户名和密码,就可以操作。

这就涉及到「登录 Shell」和「非登录 Shell」的差别了。「登录 Shell」是属于你的当前会话操作中的第一个进程,一般是在你输入用户名和密码之后打开的 Shell。常见的场景有:

  • su - 之后的 Shell。
  • SSH 登录机器后的 Shell
  • Ctrl + Alt + F[1-7] 之后 TTY 中的 Shell

而「非登录 Shell」的常见场景:

  • su 打开的是「非登录 Shell」
  • 在桌面环境中打开的终端(模拟器),启动的也是「非登录 Shell」

一般地,「登录 Shell」会额外加载 profile 文件(文件名根据你使用的 Shell 的不同而有区别),且它的 argv[0][0] == '-'(相信你已经学过 C 语言了)。可以用以下方法验证:

$ echo $0  # 查看当前 Shell 的 argv[0] 的值
-bash
$ # 是 Login shell
$ sudo su # 进入 root
# echo $0
bash
# # 是 Non-login shell