Linux 中的权限系统

*本文仅供娱乐,请勿参考

作为一个 “万物皆文件” 的系统,Linux 中的身份和权限很大程度上都是由文件定义,为控制文件访问而服务的。

身份的定义

用户和组

Linux 中身份的定义分为两个层级:用户(user)和组(group)。除了拥有名称之外,用户由 User ID 定义,组由 Group ID 定义。

  • 用户作为权限分割的单元不一定真的对应一个坐在计算机前面的 “用户”,它可以对应真人,也可以对应系统中的服务。
  • 组由用户组成,用户归属于一个主要组(primary group),与此同时可以归属多个组。

详见【演示1】。

一个组到用户的映射在所有人可读的 /etc/group 文件中。一个包含所有用户的列表在所有人可读的 /etc/passwd 文件中:

root:x:0:0:Super User:/root:/bin/bash
bin:x:1:1:bin:/bin:/usr/sbin/nologin
daemon:x:2:2:daemon:/sbin:/usr/sbin/nologin
...
alice:x:1000:1000::/home/alice:/bin/bash

冒号分隔开的分别是:用户名、密码的占位符、UID、Primary Group ID、注释、用户 Home 文件夹路径;login shell。即:

username:password:UID:GID:GECOS:home_directory:shell

用户的鉴权方式

在 “密码的占位符” 字段成为占位符之前,它曾经被用来存储密码的 Hash。这样一来,任何一个用户都可以使用彩虹表撞出用户密码,非常不安全。因此,对用户的鉴权信息要转而存储在 /etc/shadow 文件中,这个文件存储了用户 ID、盐、密码的加盐哈希等等信息,以便系统对用户鉴权。一种格式为:

<name>:<auth>:<days>

其中 name 为用户名,days 为关于过去/将来密码更改的倒数日,auth 为鉴权方式,一般而言内容为:

  • *, ! 特殊字符,代表不允许使用密码鉴权,只能使用其它鉴权方式
  • $id$salt$hash,代表算法、盐、密码的加盐哈希值

进程的身份

除非绕过操作系统(如使用物理手段改变存储介质中的内容),我们作为一个用户操作热的计算机,其实都是指使进程为我们操作计算机。行动的主体是进程,而不是字面意义上的用户。因此,身份最终必然关联到进程上。

了解了这一点后继续:随便举一个更复杂的例子,如果把进程比作汽车,身份(GID, UID)比作真人身份证号的 ID,与汽车关联的身份并不唯一:比如正在驾驶车辆的驾驶人的身份,车辆主人(owner)的身份,车辆留置权人(lienholder)的身份,这些身份未必同一。

同理,POSIX 标准定义了进程表中的三个字段,代表进程三种不同意义下的身份。以 UID 为例(GID 同理),先暂且从 User identifier – Wikipedia 机械地复制粘贴一下(要深入了解,需要了解它和文件权限位之间的交互):

  • euid: Effective UID,普遍在与文件系统交互,访问和创建文件时使用(除非另外设置了 Filesystem UID)
  • ruid: Real UID,代表运行程序来发起进程者的身份,影响进程间信号发送
  • suid: Saved UID,需要暂时降低 euid 的特权级时,利用 suid 暂存以及恢复特权级

这些身份可以在 ps 命令的格式化输出中输出:ps -o pid,uid,euid,ruid,suid,gid,egid,rgid,sgid,comm。我们也可以在 proc 文件系统中查看进程对应的文件获得这些信息:cat /proc/392/status

文件的归属和权限位

首先确认你已了解:文件夹是一种特殊的文件,是一个名字到文件的查找表;而每个文件可以由它的 inode 索引。因此,文件的归属和权限位,正是存储在 inode 中。我们可以这样查询(对于文件夹,可以使用 ls -ld):

$ ls -l /etc/passwd
-rw-r--r-- 1 root root 676 Apr 17 03:30 /etc/passwd

这里从左到右列出的是:

  • 文件的权限位
  • inode 的引用计数(即硬链接数,见【演示2】)
  • 拥有文件的用户
  • 拥有文件的组(未必是拥有文件的用户的 Primary Group,见【演示3】)
  • etc

权限位

关于权限位,第一位代表文件类型,后面的三个三元组代表:拥有文件的用户、拥有文件的组、所有人 的权限,分为 rwx 三位。

进程访问文件的鉴权过程为:首先检查进程的 effective UID 是否是拥有文件的 UID,是则使用第一组 rwx。然后检查进程的 effective GID 是否是拥有文件的组,是则使用第二组 rwx。否则使用第三组 rwx。

文件夹的权限位

对于一般文件而言,rwx 代表读、写、执行文件本身的权限;对于文件夹这张 “查找表” (通过 “查找表” 这个实体来思考权限更容易)而言,rwx 代表阅读这张查找表、修改这张查找表、透过这张查找表的权限。

具体而言:

  • r – 读取权限,即列举查找表中所有的名字
  • x – 透过(search/traverse)权限,即依据给定名字查找 inode 的权限
  • 可以参考这篇博文的解释:要执行 cat /home/user/foo,你需要有 /, /home, /home/user 这三个文件(夹)的透过权限,但不需要它们的读权限。
  • 直接粘贴前述博文的举例:

With read but not execute, you can do ls someDir but not ls -l someDir.  With execute but not read permission, you can ls -l someDir/file but not ls someDir or ls -l someDir

举了 r,x 的例子,也可以举 w 的例子:

  • 如果不允许依据文件名查找 inode,则对 Directory Table 的任何修改都无法进行下去。因此,实际中 w 位必须配合 x 位才能工作。
  • 在 r-x 权限的情况下,一般而言,文件夹允许你在其中创建文件(包括软硬链接)、删除文件(可能导致 inode 即被引用的文件本身被永久删除)、重命名文件 – 注意上述过程完全不在乎文件本身的权限位!

权限位变种

事情到这里变得相当庞杂琐碎历史遗留了,但还是容我在这里解释辩经,来回应进程的三个身份问题。在常规的 3×3=9 个权限位之外的 3 个独立、额外的属性(位):SUID(Set UID)、SGID(Set GID)、Sticky。这三个权限位被逐一编码到每一组常规权限位的 x 位中,让它们事实上承载 2 比特的信息量:

  • 当 SUID=1,第一组的 x(有执行权限)显示为 s-(无执行权限)显示为 S
  • 当 SGID=1,第二组的 x 显示为 s- 显示为 S
  • 当 sticky=1,第三组的 x 显示为 t- 显示为 T

SUID 和 SGID

当 SUID=1,任何有权限执行文件的用户在执行文件时,产生的进程的 Effective UID 变成文件拥有者的 UID,这样进程的 Effective UID 就和 Real UID 不同了(见【演示4】)。可以参考仍然是这篇博文在 “SUID on a file” 一节举出的例子,翻译如下:

   -rwx--x--x    1 root    bin         4515 Aug 14 13:08 view
   -rw-------    1 root    bin          218 Aug 14 13:08 memo.txt
  • alice(并非 root)没有权限访问 memo.txt。但这里有一个 view 程序,包含 read() 系统调用读取传来的文件名对应的文件。
  • alice 执行 “view memo.txt” 命令时,view 进程的 Effective UID 被设置成了 root,所以该进程能够读取 memo.txt 的内容

另一个 SUID 的应用是 /usr/bin/sudo,它的权限位是 -rwsr-xr-x,这样非 root 用户在执行 sudo 命令时 Effective UID 就会提升到 root。

SGID 同理,把 UID 替换成 GID 即可。

Sticky bit

Sticky bit(黏滞位)曾经被用来让可执行文件滞留在内存中,来让下一次用户能更快地执行。

它的另一个用处是应用在文件夹上,让仅文件拥有者、文件夹拥有者或者 root 能删除文件(如前面对文件夹权限位的描述,从文件夹中删除一个文件不需要检验文件本身的权限)。它的典型应用是在 /tmp 文件夹中,防止一个用户删除另一个用户的文件:

$ ls -ld /tmp/
drwxrwxrwt 12 root root ...

实验演示

【演示1】关于身份

让我们以 root 身份登录,设置用户环境,允许 alice sudo:

# adduser alice
# passwd alice
# usermod -aG users alice
# sudo visudo # alice ALL=(ALL:ALL) ALL

然后退出,重新以 alice 用户的身份登录:

$ whoami
alice
$ id
uid=1000(alice) gid=1000(alice) groups=1000(alice),100(users)
$ groups alice
alice : alice users

【演示2】硬链接数

$ touch hello.txt
$ ls -l hello.txt
-rw-r--r-- 1 alice alice 0 Apr 17 05:55 hello.txt
$ ln -s hello.txt symlink
$ ls -l hello.txt
-rw-r--r-- 1 alice alice 0 Apr 17 05:55 hello.txt
$ ls -l link
lrwxrwxrwx 1 alice alice 9 Apr 17 05:55 link -> hello.txt
$ ln hello.txt hardlink
$ ls -l hello.txt
-rw-r--r-- 2 alice alice 0 Apr 17 05:55 hello.txt
$ ls -l hardlink
-rw-r--r-- 2 alice alice 0 Apr 17 05:55 hardlink

【演示3】拥有者组并非拥有者用户的组

这里给出两种方法创建这样一个文件。

比较显然的方法是先创建,再 chown:

$ sudo chown :clock hello.txt
$ ls -l hello.txt
-rw-r--r-- 2 alice clock ...

(alice 并不在 clock 组中)

还有一个办法就是利用 SUID 位:

$ cat <<'EOF' >create.cpp
> #include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <cstdlib>

int main(){
  int fd = open("artifact", O_CREAT | O_RDWR);
  if (fd == -1) {
    perror("Error creating file");
    exit(-1);
  }
  const char* buf = "hello";
  write(fd,buf,6);
  close(fd);
}
> EOF
$ g++ create.cpp -o create
$ sudo chown root:root create
$ sudo chmod 4777 create
$ ls -l create
-rwsrwxrwx 1 root root ...
$ ./create
$ ls -l artifact
---S--s--- 1 root alice 5 Apr 17 01:21 artifact

(root 并不在 alice 这个 user private group 中)

注意,在实际的系统中,出于诸多考虑,这个 setuid 的 trick 很可能仅对于二进制文件(如上面的 create)生效,而不对脚本生效。

这里还可以注意到,chown 默认会清除 setuid bit:(继续上面)

$ sudo chown root:root create
$ ls -l create
-rwxrwxrwx 1 root root ...

【演示4】SUID 设置进程的 Effective UID

与演示3 类似,可以让程序持续一段时间(如 sleep)以便观察。创建 sleep.cpp

#include <iostream>
#include <thread>
#include <chrono>
int main() {
   std::cout << "Waiting for 30 seconds...\n";
   std::this_thread::sleep_for(std::chrono::seconds(30));
   std::cout << "Done waiting!\n";
   return 0;
}

然后编译之,验证:

$ g++ sleep.cpp -o sleep
$ sudo chown root:root sleep
$ sudo chmod 4777 sleep
$ ./sleep >/dev/null &
[1] 6103
$ cat /proc/6103/status
...
Uid:    1000    0       0       0
Gid:    1000    1000    1000    1000
...

Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *