我们在使用类UNIX系统时,经常会涉及到各种ID,比如,文件属性相关的用户ID、组ID,进程运行时相关的6个ID:实际ID、实际组ID、有效ID、有效组ID、保存的用户设置ID、保存的设置组ID。
实际使用过程中,我们经常搞混各个ID基本概念和使用方式,所以,本文用于记录相关内容,方便查阅和使用。
Unix文件相关属性
“一切皆文件”是Unix的基本哲学,Unix系统的所资源都可以用文件来表示。具体到每个文件,其都会有相关的文件属性,本文要说的文件的用户ID、组ID,文件的访问权限就包括在文件的属性中。
文件属性操作相关命令
通过stat命令查看文件的所有属性:
lhl@ubuntu18:~/develops/linux$ stat test 文件:test 大小:0 块:0 IO 块:4096 普通空文件 设备:801h/2049d Inode:5769793 硬链接:1 权限:(0644/-rw-r--r--) Uid:( 1000/ lhl) Gid:( 1000/ lhl) 最近访问:2020-02-12 12:54:38.006718573 +0800 最近更改:2020-02-12 12:54:38.006718573 +0800 最近改动:2020-02-12 12:54:38.006718573 +0800 创建时间:-
可以看到,文件的权限、用户ID、组ID。
通过id命令查看某个用户的相关信息,默认为当前用户。
lhl@ubuntu18:~/develops/linux$ id root uid=0(root) gid=0(root) 组=0(root)
通过chown命令修改文件所属的用户和组。
hl@ubuntu18:~/develops/linux$ chown --help 用法:chown [选项]... [所有者][:[组]] 文件... -R选项:用于递归修改各级目录下的文件所属用户和用户组。
通过chmod修改文件相关权限。
lhl@ubuntu18:~/develops/linux$ chmod --help 用法:chmod [选项]... 模式[,模式]... 文件... 其中,模式来自于'[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+'.
通过usermod修改用户相关的信息。
比如,将用户lhl添加到docker用户组中。 lhl@ubuntu18:~/develops/docker/docs$ sudo usermod -aG docker lhl [sudo] lhl 的密码: lhl@ubuntu18:~/develops/docker/docs$ id lhl uid=1000(lhl) gid=1000(lhl) 组=1000(lhl),127(docker)
进程相关ID
与一个进程相关的ID有6个或更多,表示如下:
下面分别解析一下各种ID:
- 首先,必须明确上述个ID是依赖于进程存在的,每当谈到这些ID时,都是相对于进程来说的。
- 实际用户ID和实际组ID标识我是谁。那么,在Unix系统中,这个“谁”是如何来的呢?答案就是,我们登陆系统时,肯定会指定登陆用户,那么这个登陆用户就是这里的“我“。用户登陆成功后,在整个登陆会话期间,这个“我”不会改变。
- 有效用户ID和有效组ID决定了进程在访问文件时可以获得权限。
- 保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本。
设置用户ID和设置组ID
通常情况下,进程的有效用户ID等于实际用户ID,进程的有效组ID等于实际组ID。每个文件都有自己的所有者和组所有者,可以通过stat命令查看。
由上文可知,进程依赖于有效用户ID和有效组ID来提供对于文件的访问权限的能力。
Unix文件系统提供一种这样的能力,那就是,通过在可执行文件的模式字st_mode中设置一个特殊的标志,即"设置用户ID"来实现进程运行时的有效用户ID的切换,即由实际用户ID转换为可执行文件的所有者ID。对于有效用户组,在st_mode中同样有“设置用户组ID”标志位。
举个例子,Unix的系统passwd命令,用户可以使用该命令修改指定用户的密码,该可执行程序文件的权限如下所示:
lhl@ubuntu18:/usr/bin$ stat passwd 文件:passwd 大小:59640 块:120 IO 块:4096 普通文件 设备:801h/2049d Inode:3408705 硬链接:1 权限:(4755/-rwsr-xr-x) Uid:( 0/ root) Gid:( 0/ root)
注意,passwd所有者权限中的那个s,表示启用了“设置用户ID”,而且passwd的所有者ID和所有者组ID都为0(root)。
passwd最终修改/etc/passwd和/etc/shadow两个文件来达到用户密码修改目的,/etc/passwd和/ect/shadow两个文件的用户ID和用户组ID都是root。普通用户是没有权限需改这个两个文件的,但是,通过给passwd增加“设置用户ID“标志之后,就可以实现任何普通用户修改/etc/passwd和/etc/shadow文件的目的。
做一个实验,如果将passwd的“设置用户ID”标志位去掉,那么普通用户也就失去了密码的修改能力了。
去掉passwd的“设置用户ID”标志位。
lhl@ubuntu18:/usr/bin$ sudo chmod u-s passwd [sudo] lhl 的密码: lhl@ubuntu18:/usr/bin$ stat passwd 文件:passwd 大小:59640 块:120 IO 块:4096 普通文件 设备:801h/2049d Inode:3408705 硬链接:1 权限:(0755/-rwxr-xr-x) Uid:( 0/ root) Gid:( 0/ root)
通过stat passwd可以看到“设置用户ID”标志位已经去掉了。
尝试修改用户密码。
lhl@ubuntu18:/usr/bin$ passwd lhl 更改 lhl 的密码。 (当前)UNIX 密码: 输入新的 UNIX 密码: 重新输入新的 UNIX 密码: passwd:认证令牌操作错误 passwd:密码未更改
可以看到普通用户lhl,失去了修改其用户密码的能力。
注意,由于给可执行文件增加”设置用户ID“或”设置用户组ID”权限之后,普通用户获得了额外的权限,所以,在使用这项能力时,要特别的谨慎,否则极有可能会造成系统安全问题。
文件访问权限
上面提到过,任何文件都有访问权限,这些对于文件的访问权限保存在文件的模式字st_mode中,Unix系统为三类用户:文件用户,文件用户组,其他用户,分别提供三种权限:可读、可写、可执行,所以共9种权限。
对于文件权限的理解,应该注意一下几个方面:
- 目录文件:Unix系统把目录当做一种文件,称为目录文件。目录文件的三种权限的含义,经常引起误解,正确的含义如下:
- 读权限:读取当前目录下所有文件名的权限,所谓的当前目录就是指的具有相应权限的目录。
- 写权限:修改当前目录下所有文件名的权限,所以在一个目录下创建一个新的文件,或者删除一个文件时,需要对该文件所在的目录具有写权限和执行权限。
- 执行权限:搜索当前目录下所有文件名的权限,所谓搜索,即 进入目录的权限。对于这个权限,需要注意的PATH环境变量中命令目录,如果引用了一个没有可执行权限的目录,那么shell就不会在该目录下搜索到想要的命令。
- 对一个文件的读权限决定了我们能够打开该文件进行读操作,这与open函数的O_RSONLY和O_RDWR标志相关。
- 对一个文件的写权限决定了我们能够打开该文件进行写操作,这与open函数的O_WRONLY和O_RDWR标志相关。
- 函数族exec执行任何一个普通文件时,必须要有对于该文件的可执行权限。
进程操作文件权限
进程每次打开、创建、删除文件时,内核都会匹配文件访问权限。下图对展示了进程foo访问文件file1时,内核所做的访问权限检验过程。
可以看到,由于进程的有效用户ID和有效用户组ID为101,而文件的用户ID和用户组ID为100,所以,进程匹配到的文件访问权限为r-x,而进程foo调用open系统调用打开文件file1时,需要的访问权限是O_RDWR,所以,进程打开文件失败,失败原因是权限不足。
更改用户ID和组ID
Unix系统中,进程的权限依赖于文件本身的权限、进程的有效用户ID、有效组ID以及内核中进程关于文件权限的验证系统。实际情况下,进程可能为了获得对于某些资源的访问权限,需要提权;同样,为了系统安全,进程也会放弃对于某些敏感资源的访问权限,所以,需要降权。一般情况下,在设计应用程序时,我们应该本着“最小特权”的原则设计我们的应用程序,即,应用程序应该只具有完成自身任务所需的最小特权。
那么,Unix如何实现“提权”和“降权”呢?主要依赖于两个函数和一套规则,先说两个函数。
设置函数
#include <unistd.h> int setuid(uid_t uid); int setguid(uid_t gid); 两个函数,执行成功返回0,出错返回-1,可以通过perror查看具体的错误信息。
规则
- 若进程具有root权限,则setuid函数将实际用户ID、有效用户ID,以及保存的设置用户ID设置为uid;
- 若进程没有root权限,但uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid,不改变实际用户ID和保存的设置用户ID;
- 若上面两个条件都不满足,则将errno设置为EPERM,并返回-1。
注意事项
- 只有root用户可以修改进程的实际用户ID;
- 仅当进程文件设置了设置用户ID标志位时,exec函数才会设置有效用户ID,并且将有效用户ID设置为文件的用户ID。否则,exec不会改变有效用户ID,而是将其维持原值。任何时候,都可以调用setuid,将有效用户ID设置为实际用户ID和保存的设置用户ID。自然,有效用户ID不能任意设置值;
修改进程的实际用户ID;
- 保存的设置用户ID是由exec复制有效用户ID而来的。若设置了进程文件的设置用户ID位,则在执行exec时,其会将文件的用户ID设置为保存的设置用户ID,然后保存这个副本。