在学习 Linux 系统权限相关的主题时,我们首先关注的基本都是文件的 ugo 权限。ugo 权限信息是文件的属性,它指明了用户与文件之间的关系。但是真正操作文件的却是进程,也就是说用户所拥有的文件访问权限是通过进程来体现的。本文主要介绍进程的权限,并通过示例解释用户身份与进程权限之间的关系。说明:本文的演示环境为 Ubuntu 16.04。
基本概念
用户
对于支持多任务的 Linux 系统来说,用户就是获取资源的凭证。
权限
权限用来控制用户对计算机资源(CPU、内存、文件等)的访问,一般会分为认证和授权两步。比如用户先经过认证机制(authentication)登录系统,然后由授权系统(authorization)对用户的操作进行授权。
进程
进程是任何支持多道程序设计的操作系统中的基本概念。通常把进程定义为程序执行时的一个实例。因此,如果有 10 个用户同时运行 vi,就会有 10 个独立的进程(尽管它们共享同一份可执行代码)。
实际上,是进程在帮助我们完成各种任务。进程就是用户访问计算机资源的代理,用户执行的操作其实是带有用户身份信息的进程执行的操作。
进程权限
既然是进程在为用户执行具体的操作,那么当用户要访问系统的资源时就必须给进程赋予权限。也就是说进程必须携带发起这个进程的用户的身份信息才能够进行合法的操作。
从登陆过程观察进程携带的用户身份信息
在 Linux 系统启动后,init 系统会 fork 出子进程执行 /sbin/getty 程序等待用户登录。当用户进行登录操作时,该子进程通过 exec 函数开始执行 /bin/login 程序(此时该进程已经变成了 login 进程)。由 login 进程验证我们的用户名和密码并查询 /etc/passwd 和 /etc/shadow 确定其合法性。如果是合法的用户,该进程再次通过 exec 函数执行用户的默认 shell 程序,此时的 login 进程就变成了 shell 进程(笔者机器上是 bash 进程)。并且该 shell 进程的有效身份被设置成为该用户的身份,之后 fork 此 shell 进程的子进程都会继承该有效身份。我们可以通过下图来理解用户从 tty 登录系统的过程(此图来自互联网):
上图描述了 init 进程、getty 进程、login 进程和 shell 进程的交互。
简单点说就是:用户登录后, shell 进程的有效用户就是该用户。下面我们来了解下进程的用户信息。
进程的 real user id、effective user id 和 saved set user id
通过 cat /proc/<PID>status 命令,我们可以查看到进程所属的用户和组相关的信息:
通过 man proc 可以查询到第一行的四个数字分别是 real user id, effective user id, saved set user id 和 filesystem UID,第二行则是对应的组 ID。这里我们只介绍第一行中的前三个 ID,即 real user id, effective user id 和 saved set user id。
real user id
real user id 是执行进程者的 user id,一般情况下就是用户登录时的 user id。子进程的 real user id 从父进继承。通常这个是不更改的,也不需要更改。比如我以用户 nick 登录 Linux 系统,我接下来运行的所有命令的进程的 real user id 都是 nick 的 user id。
effective user id
如果要判断一个进程是否对某个文件有操作权限,验证的是进程的 effective user id,而不是 real user id。
通常我们是不建议直接使用 root 用户进行操作的,但是在很多情况下,程序可能需要特殊的权限。比如 passwd 程序需要 root 权限才能够为普通用户修改密码,一些 services 程序的操作也经常需要特殊的权限。这里我们以 passwd 程序为例,为二进制可执行文件 /usr/bin/passwd 设置 set-user-id bit=ON,这个可执行文件被用 exec 启动之后的进程的 effective user id 就是这个可执行文件的 owner id,而并非父进程的 real user id。如果 set-user-id bit=OFF 的时候,这个被 exec 起来的进程的 effective user id 应该是等于进程的 user id 的。所以,effective user id 存在的意义在于,它可能和 real user id 不同。
saved set user id
saved set user id 相当于是一个 buffer,在 exec 函数启动之后,它会拷贝 effective user id 位的信息覆盖自己。对于非 root 用户来说,可以在未来使用 setuid() 函数将 effective user id 设置成为 real user id 或 saved set user id 中的任何一个。但是不允许非 root 用户用 setuid() 函数把 effective user id 设置成为任何第三个 user id。
对于 root 用户来说,调用 setuid() 的时候,将会设置所有的这三个 user id。
从总体上来看,进程中 real user id, effective user id 和 saved set user id 的设计是为了让 unprivilege user 可以获得两种不同的权限。同时我们也可以得出下面的结论:
Linux 系统通过进程的有效用户 ID(effective user id) 和有效用户组 ID(effective group id) 来决定进程对系统资源的访问权限。
其实我们通过 ps aux 查看的结果中,第一列显示的就是进程的 effective user:
Shell 中外部命令的执行方式
在 shell 中执行的命令分为内部命令和外部命令两种。
内部命令:内建的,相当于 shell 的子函数
外部命令:在文件系统的某个路径下的一个可执行文件
外部命令的执行过程如下:
Shell 通过 fork() 函数建立一个新的子进程,新的子进程为当前 shell 进程的一个副本。
在新的进程里,从 PATH 变量所列出的目录中寻找指定的命令程序。当命令名称包含有斜杠(/)符号时,将略过路径查找步骤。
在新的进程里,通过 exec 系列函数,以所找到的新程序替换 shell 程序并执行。
子进程退出后,最初的 shell 会接着从终端读取并执行下一条命令。
我们通过下面的例子来理解在 shell 中执行外部命令的过程,例子很简单就是通过 cat 命令查看一个文本文件 test.log:
$ cat test.log
我们先来检查一下当前用户以及相关文件的权限:
当前用户 nick 的 real user id 为 1000,/bin/cat 文件的所有者为 root,但是所有人都有执行权限,test.log 文件的所有者为 nick。我们结合下图来介绍 cat test.log 命令的执行过程:
当我们在 shell 中执行一个外部程序的时候,默认情况下进程的 effective user ID 等于 real user ID,进程的 effective group ID 等于 real group ID(接下来的介绍中省略 group ID)。当我们以用户 nick 登录系统,并在 bash 中键入 cat test.log 命令并回车后。Bash 先通过 fork() 建立一个新的子进程,这个新的子进程是当前 bash 进程的一个副本。新的进程在 PATH 变量指定的路径中搜索 cat 程序,找到 /bin/cat 程序后检查其权限。/bin/cat 程序的所有者为 root,但是其他人具有读和执行的权限,所以新进程可以通过 exec 函数用 cat 程序的代码段替换当前进程中的代码段(把 /bin/cat 程序加载到了内存中,此时的进程已经变成了 cat 进程,cat 进程会从 _start 函数开始执行)。由于 cat 进程是由用户 nick 启动的,所以 cat 进程的 effective user ID 是 1000(nick)。同时 cat 进程的 effective user ID 和 test.log 文件的 owner ID 相同(都是 1000),所以 cat 进程拥有对此文件的 rw- 权限,那么顺理成章地就可以读写 test.log 文件的内容了。
下面我们演示一个通过设置特殊权限 set uid ID 改变进程 effective user ID 的例子。
创建文件 root.log,权限为 640,此时只有 root 有权限读写该文件的内容,用户 nick 连读取该文件的权限都没有:
然后通过设置特殊权限 set uid ID 让运行 cat 程序的进程具有 root 权限:
$ sudo chmod 4755 /bin/cat
现在可以了!因为运行 cat 程序的进程的 effective user ID 变成了 root。记得要把 /bin/cat 的权限改回去呀:
$ sudo chmod 755 /bin/cat
Shell 脚本的执行方式
在 shell 中执行脚本的方式和执行外部命令的方式差不多,比如我们要执行下面的脚本:
$ /bin/bash ./test.sh
这时同样会 fork 出一个子进程。只不过脚本与程序相比没有代码段,也没有 _start 函数,此时 exec 函数就会执行另外一套机制。比如我们在 test.sh 文件的第一行通过 #!/bin/bash 指定了一个解释器,那么解释器程序的代码段会用来替换当前进程的代码段,并且从解释器的 _start 函数开始执行,而这个文本文件被当作命令行参数传给解释器。所以上面的命令执行过程为:Bash 进程 fork/exec 一个子 bash 进程用于执行脚本,子 bash 进程继承父进程的环境变量、用户信息等内容,父进程等待子 bash 进程终止。
总结
文件上的权限信息和用户的信息都是静态的,而进程是动态的,它把自身携带的用户信息和将要进行的操作结合起来,从而实现权限管理。至此我们也基本上搞明白了 Linux 权限系统的工作原理。