上一篇我们使用了 stat 函数取得了 test.txt 文件的相关属性,这些属性都保存在一个叫 struct stat 的结构体中:
struct stat {
dev_t st_dev; /* 包含这个文件的设备 ID */
ino_t st_ino; /* inode 编号 */
mode_t st_mode; /* 访问权限 */
nlink_t st_nlink; /* 硬链接数量 */
uid_t st_uid; /* 用户ID */
gid_t st_gid; /* 组ID */
dev_t st_rdev; /* 设备ID */
off_t st_size; /* 文件占用的字节数 */
blksize_t st_blksize; /* 文件系统块大小 */
blkcnt_t st_blocks; /* 文件占用了几个512字节 */
time_t st_atime; /* 最后访问时间 */
time_t st_mtime; /* 最后更改时间 */
time_t st_ctime; /* 最后状态更改时间 */
};
本篇我们只介绍 st_mode 字段。在上一篇中,我们得到的 st_mode 字段的10进制值是 33204. 记住这个值,待会我们要分析。
1 一堆 ID
- 实际用户 ID、实际组 ID
- 有效用户 ID、有效组 ID
- 设置用户 ID、设置组 ID
1、实际用户ID(uid)和实际用户组ID(gid):标识我是谁。也就是登录用户的uid和gid,比如我的Linux以allen登录,在Linux运行的所有的命令的实际用户ID都是allen的uid,实际用户组ID都是allen的gid(可以用id命令查看)。
2、有效用户ID(euid)和有效用户组ID(egid):进程用来决定我们对资源的访问权限。一般情况下,有效用户ID等于实际用户ID,有效用户组ID等于实际用户组ID。当设置-用户-ID(SUID)位设置,则有效用户ID等于文件的所有者的uid,而不是实际用户ID;同样,如果设置了设置-用户组-ID(SGID)位,则有效用户组ID等于文件所有者的gid,而不是实际用户组ID。
例:
- 你在登录 linux 时输入的帐号,就是实际用户。比方说我登录的帐号是 allen,那么实际用户就是 allen.
- 当你以 sudo 执行命令时,比如 sudo rm text.txt,这时候 ,在执行这条命令的时候 ,实际用户是 allen,有效用户是 root。
- 假设当前实际用户是 allen,你执行的文件 a.out 的所有者是 david,这时候你在执行这条命令的时候 ,实际用户是 allen,有效用户也是 allen,如果 a.out 这个文件设置用户 ID 标志打开,那么执行 a.out 的时候,实际用户是 allen, 有效用户是 david.
2 st_mode 的结构
st_mode 主要包含了 3 部分信息:
- 15-12 位保存文件类型
- 11-9 位保存执行文件时设置的信息
- 8-0 位保存文件访问权限
图1 展示了 st_mode 各个位的结构。
图1 st_mode 结构(图中花括号里的数字都是 2 进制)
2.1 黏着位(sticky)
要删除一个文件,你不一定要有这个文件的写权限,但你一定要有这个文件的上级目录的写权限。也就是说,你即使没有一个文件的写权限,但你有这个文件的上级目录的写权限,你也可以把这个文件给删除,而如果没有一个目录的写权限,也就不能在这个目录下创建文件。
如何才能使一个目录既可以让任何用户写入文件,又不让用户删除这个目录下他人的文件,sticky就是能起到这个作用。sticky一般只用在目录上,用在文件上起不到什么作用。
在一个目录上设了sticky位后,(如/home,权限为1777)所有的用户都可以在这个目录下创建文件,但只能删除自己创建的文件(root除外),这就对所有用户能写的目录下的用户文件启到了保护的作用。
如果用户对目录有写权限,则可以删除其中的文件和子目录,即使该用户不是这些文件的所有者,而且也没有读或写许可。黏着位出现执行许可的位置上,用t表示,设置了该位后,其它用户就不可以删除不属于他的文件和目录。但是该目录下的目录不继承该权限,要再设置才可使用。
普通文件的sticky位会被linux内核忽略。
目录的sticky位表示这个目录里的文件只能被owner和root删除 。
/tmp常被我们用来存放临时文件,是所有用户。但是我们不希望别的用户随随便便的就删除了自己的文件,于是便有了黏着位,它的作用便是让用户只能删除属于自己的文件。
$ ls -dl /tmp
drwxrwxrwt 15 root root ......... // 注意 other 用户的权限位,x 变成了 t
那么原来的执行标志x到哪里去了呢? 系统是这样规定的, 假如本来在该位上有x, 则这些特别标志 (suid, sgid, sticky) 显示为小写字母 (s, s, t). 否则, 显示为大写字母 (S, S, T) 。
另外:
chmod 777 abc
chmod +t abc
等价于
chmod 1777 abc
3 实例分析
3.1 普通文件实例
上一篇我们得到的 st_mode 10进制值是 33204,转换成 8 进制后为 100664.
把它填到图1中后,是这样:
图2 33204 填到 st_mode 中
根据图2 我们可以得到如下信息:
- 1000: 这是一个常规文件
- 000: 执行时设置信息为空,黏着位为 0
- 110-110-100: 用户权限为
RW-
,组员权限为RW-
,其他人权限为R--
3.2 带设置-用户-ID标志的文件分析
我们要分析的文件是这样的:
-rwsr-xr-x 1 root root 7612 11月 28 14:58 append
-rw-r--r-- 1 root root 6 11月 28 14:52 test.txt
append 是一个可执行文件,它的作用是以追加的方式打开 test.txt,代码如下:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
int fd = open("test.txt", O_WRONLY | O_APPEND);
if (fd == -1) {
perror("open");
return -1;
}
printf("uid: %d\n", getuid());
printf("euid: %d\n", geteuid());
close(fd);
return 0;
}
append 文件生成方式如下:
$ gcc append.c -o append
$ sudo chown root:root append
$ sudo chmod u+s append
当前实际用户并不是 root,而是 allen.
一般来说,直接执行 ./append 会出现访问被禁止,因为 test.txt 文件的所有者并不是 allen. 然而,这里的会正常执行,结果显示如下:
uid:1000
euid:0
即实际用户 ID 是 allen, 有效用户 id 是 root. 为什么直接执行 ./append 后有效用户 id 不是 allen,而变成 了 root? 这里实际上是 append 文件的 suid 位被置 1 了。
通过 stat 函数,我们得到的 append 的 st_mode 值为 35309. 转换成 8 进制后为 104755. 将其填充到图 1 的结构中后是这样的:
图3 填充 st_mode
分析图3,可以知道:
- 1000:这是一个普通文件
- 100:suid 为 1
- 111-101-101:用户权限为 RWX
,组员权限为R-X
,其他人权限为R-X
所以这里的 append 文件在执行的时候,发现 suid 标志被打开,它会把有效用户id (euid) 设置成 append 文件的所有者(root)。
4 一些常用的宏
前面我们通过手工分析 st_mode 字段,实际上是很不方便的。实际写程序,你可以使用 st_mode & 掩码来得到 st_mode 中特定的部分。比如:
- st_mode & 0170000 : 得到文件类型
- st_mode & 0007000 : 得到执行文件时设置信息
- st_mode & 0000777 : 得到权限位
- st_mode & 00100: 判断所有者是否可执行
……
如果在程序中真这么写,估计很少有人愿意去看。实际上,可以使用 linux 预定义的一些宏来代替这些生硬的数字。这些宏定义在 sys/stat.h 头文件中。
#define S_IFMT 00170000
#define S_IFSOCK 0140000
#define S_IFLNK 0120000
#define S_IFREG 0100000
#define S_IFBLK 0060000
#define S_IFDIR 0040000
#define S_IFCHR 0020000
#define S_IFIFO 0010000
#define S_ISUID 0004000
#define S_ISGID 0002000
#define S_ISVTX 0001000
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR)
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK)
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO)
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)
#define S_IRWXU 00700
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100
#define S_IRWXG 00070
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010
#define S_IRWXO 00007
#define S_IROTH 00004
#define S_IWOTH 00002
#define S_IXOTH 00001
总结
本篇的内容有点繁而多,需要掌握的知识点如下:
- 实际用户 ID,实际用户组 ID
- 有效用户 ID,有效用户组 ID
- 设置用户 ID,设置用户组 ID
- st_mode 字段结构(文件类型-执行时文件设置信息-权限位)
- 黏着位
参考资料:
[1] 实际用户ID、有效用户ID、设置用户ID
[2] UID, EUID, SUID, FSUID
[3] 黏着位