本文主要总结linux权限模型相关的原理,譬如
等各种问题。
文件,pipe,内存等这些对象都是以某种方式共享的,因此它们需要某种保护机制来保护它们不被滥用;这种机制称为Unix权限模型。
譬如,你有一个文件myfile,你不想让别人去写该文件,那么你便可以通过unix权限模型对文件进行相应的权限设置。
unix权限模型将资源的权限分为三组:
U(所有者, Owner), G(用户组, Group), O(其他, Other),而每组又由三个权限位构成,分别为r(读权限), w(写权限), x(可执行权限).
譬如,在我机器上运行如下命令
qls@qls-VirtualBox:~/cpp_learn$ ls -l main
-rwxrwxr-x 1 qls qls 155488 Jun 10 15:53 main
则结合上述可知,其具体可汇总如下表所示
访问类别 |
Owner(U) |
Group(G) |
Others(O) |
||||||
权限位 |
r |
w |
x |
r |
w |
x |
r |
w |
x |
例子(main) |
1 |
1 |
1 |
1 |
1 |
1 |
1 |
0 |
1 |
通过上表可知文件的所有者拥有可读,可写,可执行权限。而其他用户只拥有可读可执行权限。
在linux中,可通过chmod命令来更改文件的权限位,同时所谓的777,065等也即是上述权限位的不同组合。
Unix权限模型的工作需要涉及 共享对象(譬如上述main文件)以及访问该对象的进程。
首先,每个用户有一个记录(record), 该record记录在/etc/passwd 文件中,譬如我的虚拟机的用户qls,因此可得出如下结果
grep qls /etc/passwd
qls:x:1000:1000:qls,,,:/home/qls:/bin/bash
由上述可知,每个record由7列构成,其相应的内容如下
username::UID:GID:descriptive_name:home_dir:program
本文仅解释上述UID,GID以及program列的含义,
UID(也即用户标识符),GID(用户组标识符),譬如我在虚拟机的一个用户为qls,其UID为1000,GID为1000
program指的是用户登陆成功后所运行的程序,这列通常是shell,但也可以设置成任何其他的东西。
通过id命令可以查看用户的UID和GID,譬如在我的虚拟机运行id命令,可得如下结果
qls@qls-VirtualBox:~/cpp_learn$ id
uid=1000(qls) gid=1000(qls) groups=1000(qls),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare)
问题1: 一个进程的uid和gid是如何得来的呢?
进程的uid继承至登陆的shell进程,也即每个用户的进程的uid和gid为用户的uid和gid。
问题2: OS是如何检查某个进程是否有权限操作某个资源呢?
问题3:文件的所有权和组所有权如何获得?
文件的所有权和组所有权可以从文件的inode获取,两者均存放在inode数据结构中。在用户侧可通过stat系统调用获得inode的详细信息。
问题4:如何确定进程的访问类别?
在Unix权限模型中,访问类别主要由U(所有者,Owner), G(Group, 组),O(Other),一个进程的访问类别由OS通过如下方式确定
if process_UID == file_UID
then
access_category = U
else if process_GID == file_GID
then
access_category = G
else
access_category = O
fi
上述需要一些说明,一个进程会存在属于多个组,拥有多个GID, 所以只要有一个GID与文件的GID相等,那么进程的访问类别便为G
当确定了进程的访问类别后,便可根据其权限位进行相应的权限判断。
为了简化,此处引入一个进程证书,其主要由{RUID,RGID,EUID,EGID}标识,当然,其也可以包含更多的属性,譬如PPID,PID等。
一个进程的UID主要由两个整数构成
一个进程的GID主要由两个整数构成
所谓真实用户ID(RUID), 也即进程所属的用户的ID,所谓真实用户组ID(RGID),也即进程所属的用户的组id,其具体值为/etc/passwad文件中相应record中相应的UID和GID的值。
有效用户ID是针对OS而言的,操作系统根据有效用户ID和有效用户组ID来判断相应进程的运行权限,具体来说
默认场景下:EUID = RUID, EGID=RGID
为了进一步加深对RUID,EUID等的理解,此处需要从一个问题出发也即
一个非特权用户如何更改其密码?
为了回答上述问题,此处单开一小节。
在linux系统中,可知密码存在于/etc/passwd文件中,但为了安全性考虑,一般会存在/etc/shadow文件中。
qls@qls-VirtualBox:~/cpp_learn$ ls -l /etc/shadow
-rw-r----- 1 root shadow 1459 Sep 27 2021 /etc/shadow
由上述可知,/etc/shadow文件的真实用户为root,且只有root有可写可读权限。
在linux中,我们可以通过paswad命令来修改用户密码,但此刻出现了一个问题,
/etc/shadow只有root有读写权限,而普通用户没有读写权限,那么paswad为什么可以更改用户密码呢?
具体来看一下passwd的权限属性,在我的开发机如下所示
由上述可知,在U访问类别中,权限位的可执行位不是x而是一个s。
s权限位是一个特殊的标志,每当一个可执行文件的U类别的权限位出现s,那么这个可执行文件便称作setuid二进制文件。
每当一个setuid二进制文件运行时,其相应的进程的EUID便被设置为二进制文件的UID,针对passwd而言,进程的EUID被设置为0,也即使进程具备根权限。故其可以使普通用户更改相应的密码。
可以通过chown和chmod更改文件的的权限位并设置权限位为s。
本部分为本文总结的最后一部分。该部分主要介绍如下几个系统调用
#include
#include
uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
上述四个系统调用返回进程的标识{UID,EUID,GID,EGID}, 下面以一个示例代码来讲解上述系统调用
#include
#include
#include
int main() {
std::cout << "UID= " << getuid() << "\n"
<< "EUID= " << geteuid() << "\n"
<< "GID= " << getgid() << "\n"
<< "EGID= " << getegid() << "\n";
if (geteuid() == 0) {
std::cout << "running as root\n";
sleep(1);
}
return 0;
}
通过运行相应的二进制文件,获得如下结果
当通过sudo运行相应的二进制文件时,获得如下结果
那么sudo是如何工作的呢?关于该问题,相应的答案本文不再赘述。
本部分主要讲解如下几个系统调用
#include
#include
int setuid(uid_t uid);
int setgid(gid_t gid);
int seteuid(uid_t euid);
int setegid(gid_t egid);
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
需要说明的是
本部分主要以一个场景讲解,具体如下
如果业务中需要某个程序首先在特权状态下操作,譬如读取某些root用户的文件,然后需要切换到非特权状态。
为了达到此目的,可以利用setuid root binary相关的操作。
具体来说,你可以利用setuid(getuid())来达到相应的目的。
具体的示例代码本文后续补上。
本文具体讲解了linux权限模型相关的概念以及EUID和RUID等具体的使用。通过本文可以加深对相应的概念的理解。