*本文仅供娱乐,请勿参考
作为一个 “万物皆文件” 的系统,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 someDirbut notls -l someDir. With execute but not read permission, you canls -l someDir/filebut notls someDirorls -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
...
Leave a Reply