0 引言
本文主要总结linux权限模型相关的原理,譬如
- ruid,euid,rgid,egid的含义
- setuid,seteuid等系统调用的作用及使用讲解
- 如何获得linux相应进程的权限
等各种问题。
1 unix权限模型
文件,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等也即是上述权限位的不同组合。
2 Unix权限模型工作原理
Unix权限模型的工作需要涉及 共享对象(譬如上述main文件)以及访问该对象的进程。
首先,每个用户有一个记录(record), 该record记录在/etc/passwd 文件中,譬如我的虚拟机的用户qls,因此可得出如下结果
grep qls /etc/passwd
qls:x:1000:1000:qls,,,:/home/qls:/bin/bash
由上述可知,每个record由7列构成,其相应的内容如下
username:<passwd>: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是如何检查某个进程是否有权限操作某个资源呢?
- 检查以及确认资源(文件)的所有权和组所有权
- 检查进程目前具有哪个访问类别(譬如,U|G|O)
- 针对相应的访问类别,相应的权限位是否设置(譬如 w设置为1,则允许写文件)
问题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
当确定了进程的访问类别后,便可根据其权限位进行相应的权限判断。
3 RUID,RGID,EUID以及EGID讲解
为了简化,此处引入一个进程证书,其主要由{RUID,RGID,EUID,EGID}标识,当然,其也可以包含更多的属性,譬如PPID,PID等。
一个进程的UID主要由两个整数构成
- RUID(真实用户ID)
- EUID (有效用户ID)
一个进程的GID主要由两个整数构成
- RGID (真实用户组ID)
- EGID (有效用户组ID)
所谓真实用户ID(RUID), 也即进程所属的用户的ID,所谓真实用户组ID(RGID),也即进程所属的用户的组id,其具体值为/etc/passwad文件中相应record中相应的UID和GID的值。
有效用户ID是针对OS而言的,操作系统根据有效用户ID和有效用户组ID来判断相应进程的运行权限,具体来说
- 当对进程执行相应的权限检查时,OS是根据EUID/EGID来判断,而不是根据RUID/RGID
- EUID=0时,操作系统认为进程具有根权限
默认场景下:EUID = RUID, EGID=RGID
为了进一步加深对RUID,EUID等的理解,此处需要从一个问题出发也即
一个非特权用户如何更改其密码?
3.1 setuid二进制执行文件
为了回答上述问题,此处单开一小节。
在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。
4 系统调用(setuid,setgid,getuid,getgid等)
本部分为本文总结的最后一部分。该部分主要介绍如下几个系统调用
#include <unistd.h>
#include <sys/types.h>
uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
上述四个系统调用返回进程的标识{UID,EUID,GID,EGID}, 下面以一个示例代码来讲解上述系统调用
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
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是如何工作的呢?关于该问题,相应的答案本文不再赘述。
4.1 设置进程证书
本部分主要讲解如下几个系统调用
#include <sys/types.h>
#include <unistd.h>
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);
需要说明的是
- seteuid/setuid系统调用在root权限下可以将EUID和RUID设置为参数euid和uid
- 一个非root用户只能将EUID设置为其RUID
4.2 放弃特权
本部分主要以一个场景讲解,具体如下
如果业务中需要某个程序首先在特权状态下操作,譬如读取某些root用户的文件,然后需要切换到非特权状态。
为了达到此目的,可以利用setuid root binary相关的操作。
具体来说,你可以利用setuid(getuid())来达到相应的目的。
具体的示例代码本文后续补上。
5 总结
本文具体讲解了linux权限模型相关的概念以及EUID和RUID等具体的使用。通过本文可以加深对相应的概念的理解。