LinuxUNIX系统编程手册——(九)进程凭证

每个进程都有一套用数字表示的用户 ID(UID)和组 ID(GID)。有时,也将这些 ID 称之为进程凭证。具体如下所示。

  • 实际用户 ID(real user ID)和实际组 ID(real group ID)。
  • 有效用户 ID(effective user ID)和有效组 ID(effective group ID)。
  • 保存的 set-user-ID(saved set-user-ID)和保存的 set-group-ID(saved set-group-ID)。
  • 文件系统用户 ID(file-system user ID)和文件系统组 ID(file-system group ID)(Linux专有)。
  • 辅助组 ID。

9.1 实际用户 ID 和实际组 ID

实际用户 ID 和实际组 ID 确定了进程所属的用户和组。作为登录过程的步骤之一,登录shell 从/etc/passwd 文件中读取相应用户密码记录的第三字段和第四字段,置为其实际用户 ID和实际组 ID。当创建新进程(比如,shell 执行一程序)时,将从其父进程中继承这些 ID。

9.2 有效用户 ID 和有效组 ID

在大多数 UNIX 实现中,当进程尝试执行各种操作(即系统调用)时,将结合有效用户 ID、有效组 ID,连同辅助组 ID 一起来确定授予进程的权限。

有效用户 ID 为 0(root 的用户 ID)的进程拥有超级用户的所有权限。这样的进程又称为特权级进程(privileged process)。而某些系统调用只能由特权级进程执行。

通常,有效用户 ID 及组 ID 与其相应的实际 ID 相等,但有两种方法能够致使二者不同。其一是使用 9.7 节中所讨论的系统调用,其二是执行 set-user-ID 和 set-group-ID 程序。

9.3 Set-User-ID 和 Set-Group-ID 程序

set-user-ID 程序会将进程的有效用户 ID 置为可执行文件的用户 ID(属主),从而获得常规情况下并不具有的权限。set-group-ID 程序对进程有效组 ID 实现类似任务。

与其他文件一样,可执行文件的用户 ID 和组 ID 决定了该文件的所有权。另外,可执行文件还拥有两个特别的权限位 set-user-ID 位和 set-group-ID 位。(实际上,任何文件都是如此,但此处只关注可执行文件的这两个权限位。)可使用 chmod 命令来设置这些权限位。非特权用户能够对其拥有的文件进行设置,而特权级用户(CAP_FOWNER)能够对任何文件进行设置。

【注】除此之外还有SBIT 特殊权限位,可确保用户只能删除自己的文件,而不能删除其他用户的文件。当目录被设置 SBIT 特殊权限位后,文件的其他用户权限部分的 x 执行权限就会被替换成 t 或者 T — 原本有 x 执行权限则会写成 t,原本没有 x 执行权限则会被写成 T。

使用 chmod 命令设置特殊权限的参数如表所示。

参数作用
u+s设置 SUID 权限
u-s取消 SUID 权限
g+s设置 SGID 权限
g-s取消 SGID 权限
o+t设置 SBIT 权限
o-t取消 SBIT 权限
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ ls -l test
-rwxr-xr-x 1 vainx vainx 15960 Jul 19 11:52 test
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ chmod u+s test
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ chmod g+s test
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ ls -l test
-rwsr-sr-x 1 vainx vainx 15960 Jul 19 11:52 test

当使用ls –l 命令查看文件权限时,如果为程序设置了 set-user-ID 权限位和 set-group-ID 权限位,那么通常用来表示文件可执行权限的 x 标识会被 s 标识所替换。

【注】SUID、SGID 与 SBIT 也有对应的数字表示法,分别为 4、2、1。也就是说 777 还不是最大权限,最大权限应该是 7777,其中第 1 个数字代表的是特殊权限位。

当运行 set-user-ID 程序(即通过调用 exec()将 set-user-ID 程序载入进程的内存中)时,内核会将进程的有效用户 ID 设置为可执行文件的用户 ID。set-group-ID 程序对进程有效组 ID 的操作与之类似。通过这种方法修改进程的有效用户 ID 或者组 ID,能够使进程(换言之,执行该程序的用户)获得常规情况下所不具有的权限。例如,如果一个可执行文件的属主为 root(超级用户),且为此程序设置了 set-user-ID 权限位,那么当运行该程序时,进程会取得超级用户权限。

也可以利用程序的 set-user-ID 和 set-group-ID 机制,将进程的有效 ID 修改为 root 之外的其他用户。例如,为提供对一个受保护文件(或其他系统资源)的访问, 采用如下方案就绰绰有余:创建一个具有对该文件访问权限的专用用户(组)ID,然后再创建一个 set-user-ID(set-group-ID)程序,将进程有效用户(组)ID 变更为这个专用 ID。这样,无需拥有超级用户的所有权限,程序就能访问该文件。

有时会使用术语 set-user-ID-root 来表示 root 用户所拥有的 set-user-ID 程序,以示与由其他用户所拥有的 set-user-ID 程序有所区别,后者仅为进程提供其属主所具有的权限。

Linux 系统中经常使用的 set-user-ID 程序包括:passwd(1),用于更改用户密码;mount(8)和 umount(8),用于加载和卸载文件系统;su(1),允许用户以另一用户的身份运行 shell。set-group-ID 程序的例子之一为 wall(1),用来向 tty 组下辖的所有终端(通常情况下,所有终端都属于该组)写入一条消息。

如test程序需要以 root 用户身份运行,欲使该程序可为任一用户执行,必须将其设置为 set-user-ID-root程序,如下所示:

root@DESKTOP-0DN0PNJ:/home/vainx/wsl-code# chown root test
root@DESKTOP-0DN0PNJ:/home/vainx/wsl-code# chgrp root test
root@DESKTOP-0DN0PNJ:/home/vainx/wsl-code# ls -l test
-rwxr-xr-x 1 root root 15960 Jul 19 11:52 test
root@DESKTOP-0DN0PNJ:/home/vainx/wsl-code# chmod u+s test
root@DESKTOP-0DN0PNJ:/home/vainx/wsl-code# ls -l test
-rwsr-xr-x 1 root root 15960 Jul 19 11:52 test
root@DESKTOP-0DN0PNJ:/home/vainx/wsl-code# su vainx
vainx@DESKTOP-0DN0PNJ:~/wsl-code$ ./test
Hello World!
vainx@DESKTOP-0DN0PNJ:~/wsl-code$

set-user-ID/set-group-ID 技术集实用性与强大的功能于一身,但一旦设计欠佳也可能造成安全隐患。

9.4 保存 set-user-ID 和保存 set-group-ID

设计保存 set-user-ID(saved set-user-ID)和保存 set-group-ID (saved set-group-ID),意在与set-user-ID 和 set-group-ID 程序结合使用。当执行程序时,将会(依次)发生如下事件(在诸多事件之中)。

  • 若可执行文件的 set-user-ID (set-group-ID)权限位已开启,则将进程的有效用户(组)ID置为可执行文件的属主。若未设置 set-user-ID (set-group-ID)权限位,则进程的有效用户(组)ID 将保持不变。
  • 保存 set-user-ID 和保存 set-group-ID 的值由对应的有效 ID 复制而来。无论正在执行的文件是否设置了 set-user-ID 或 set-group-ID 权限位,这一复制都将进行。

举例说明上述操作的效果,假设某进程的实际用户 ID、有效用户 ID 和保存 set-user-ID 均为 1000,当其执行了 root 用户(用户 ID 为 0)拥有的 set-user-ID 程序后,进程的用户 ID 将发生如下变化:

real=1000 effective=0 saved=0

有不少系统调用,允许将 set-user-ID 程序的有效用户 ID 在实际用户 ID 和保存 set-user-ID之间切换。针对 set-group-ID 程序对其进程有效组 ID 的修改,也有与之相类似的系统调用来支持。如此一来,对于与执行文件用户(组)ID 相关的任何权限,程序能够随时 “收放自如”。(换言之,程序可以游走于两种状态之间:具备获取特权的潜力和以特权进行实际操作。)

9.5 文件系统用户 ID 和组 ID

在 Linux 系统中,要进行诸如打开文件、改变文件属主、修改文件权限之类的文件系统操作时,决定其操作权限的是文件系统用户 ID 和组 ID(结合辅助组 ID),而非有效用户 ID 和组ID。

通常,文件系统用户 ID 和组 ID 的值等同于相应的有效用户 ID 和组 ID(因而一般也等同于相应的实际用户 ID 和组 ID)。此外,只要有效用户或组 ID 发生了变化,无论是通过系统调用,还是通过执行 set-user-ID 或者 set-group-ID 程序,则相应的文件系统 ID 也将随之改变为同一值。

9.6 辅助组 ID

辅助组 ID 用于标识进程所属的若干附加的组。新进程从其父进程处继承这些 ID,登录shell 从系统组文件中获取其辅助的组 ID。如前所述,将这些 ID 与有效 ID 以及文件系统 ID相结合,就能决定对文件、System V IPC 对象和其他系统资源的访问权限。

9.7 获取和修改进程凭证

可以利用 Linux 系统特有的 proc/PID/status 文件,通过对其中 Uid、Gid 和 Groups各行信息的检查,来获取任何进程的凭证,这与下面即将介绍的系统调用有异曲同工之妙。Uid 和 Gid 各行,按实际、有效、保存设置和文件系统 ID 的顺序来展示相应标识符。

在下列章节中所论及的特权级进程,其定义是基于传统意义上的,即进程的有效用户 ID为 0。然而,Linux 将超级用户权限划分成多种各不相同的能力(capability)。在讨论修改用户 ID 和组 ID 的所有系统调用时,将涉及其中的两种。

  • CAP_SETUID 能力允许进程任意修改其用户 ID。
  • CAP_SETGID 能力允许进程任意修改其组 ID。

9.7.1 获取和修改实际、有效和保存设置标识

获取实际和有效 ID

系统调用 getuid()和 getgid()分别返回调用进程的实际用户 ID 和组 ID。而系统调用geteuid()和 getegid()则对进程的有效 ID 实现类似功能。对这些系统函数的调用总会成功。

uid_t getuid(void);				/* 返回进程的实际用户ID */
uid_t geteuid(void);			/* 返回进程的有效用户ID */
gid_t getgid(void);				/* 返回进程的实际组ID */
gid_t getegid(void);			/* 返回进程的实际组ID */

修改有效 ID

setuid()系统调用以给定的 uid 参数值来修改调用进程的有效用户 ID,也可能修改实际用户 ID 和保存 set-user-ID。系统调用 setgid()则对相应组 ID 实现了类似功能。

int setuid(uid_t uid);			/* 成功返回0,出错返回-1 */
int setgid(gid_t gid);			/* 成功返回0,出错返回-1 */

进程使用 setuid()和 setgid()系统调用能对其凭证做哪些修改呢?其规则取决于进程是否拥有特权(即有效用户 ID 为 0)。适用于 setuid()系统调用的规则如下。

  • 当非特权进程调用 setuid()时,仅能修改进程的有效用户 ID。而且,仅能将有效用户 ID修改成相应的实际用户 ID 或保存 set-user-ID。(企图违反此约束将引发 EPERM 错误。)这意味着,对于非特权用户而言,仅当执行 set-user-ID 程序时,setuid()系统调用才起作用,因为在执行普通程序时,进程的实际用户 ID、有效用户 ID 和保存 set-user-ID 三者之值均相等。
  • 当特权进程以一个非0 参数调用 setuid()时,其实际用户ID、有效用户ID 和保存set-user-ID均被置为 uid 参数所指定的值。这一操作是单向的,一旦特权进程以此方式修改了其 ID,那么所有特权都将丢失,且之后也不能再使用 setuid()调用将有效用户 ID 重置为 0。如果不希望发生这种情况,请使用稍后介绍的 seteuid()或者 setreuid()系统调用来替代 setuid()。

使用 setgid()系统调用修改组 ID 的规则与之相类似,仅需要把 setuid()替换为 setgid(),把用户替换为组。因之,规则 1 与前述完全一致,但在规则 2 中,由于对组 ID 的修改不会引起进程特权的丢失(拥有特权与否由有效用户 ID 决定),特权级程序可以使用 setgid()对组 ID进行任意修改。

进程能够使用 seteuid()来修改其有效用户 ID(改为参数 euid 所指定的值),还能使用setegid()来修改其有效组 ID(改为参数 egid 所指定的值)。

int seteuid(uid_t euid);			/* 成功返回0,出错返回-1 */
int setegid(gid_t egid);			/* 成功返回0,出错返回-1 */

进程使用 seteuid()和 setegid()来修改其有效 ID 时,会遵循以下规则。

  • 非特权级进程仅能将其有效 ID 修改为相应的实际 ID 或者保存设置 ID。(换言之,对非特权级进程而言,除去前面讨论的BSD可移植性问题,seteuid()和setegid()分别等效于setuid()和 setgid()。)
  • 特权级进程能够将其有效 ID 修改为任意值。若特权进程使用 seteuid()将其有效用户 ID 修改为非 0 值,那么此进程将不再具有特权(但可以根据规则 1 来恢复特权)。

对于需要对特权“收放自如”的 set-user-ID 和 set-group-ID 程序,更推荐使用 seteuid(),示例如下:

euid = geteuid();				/* 保存原始的有效用户ID(与保存SUID相同) */

if(seteuid(getuid()) == -1) 	/* 放弃特权 */
    errExit("seteuid");
if(seteuid(euid) == -1)			/* 再次得到特权 */
    errExit("seteuid");

修改实际 ID 和有效 ID

setreuid()系统调用允许调用进程独立修改其实际和有效用户 ID。setregid()系统调用对实际和有效组 ID 实现了类似功能。

int setreuid(uid_t ruid, uid_t euid);		/* 成功返回0,出错返回-1 */
int setregid(gid_t rgid, gid_t egid);		/* 成功返回0,出错返回-1 */

这两个系统调用的第一个参数都是新的实际 ID,第二个参数都是新的有效 ID。若只想修改其中的一个 ID,可以将另外一个参数指定为−1。

使用 setreuid()和 setregid()来作出变更也要遵循一定的规则。下面将从 setreuid()的视角来描述这些规则,除非另有说明,setregid()函数的规则也与
之类似。

  • 非特权进程只能将其实际用户 ID 设置为当前实际用户 ID 值(即保持不变)或有效用户ID 值,且只能将有效用户 ID 设置为当前实际用户 ID、有效用户 ID(即无变化)或保存set-user-ID。
  • 特权级进程能够设置其实际用户 ID 和有效用户 ID 为任意值。
  • 不管进程拥有特权与否,只要如下条件之一成立,就能将保存 set-user-ID 设置成(新的)有效用户 ID。
    • ruid 不为-1(即设置实际用户 ID,即便是置为当前值)。
    • 对有效用户 ID 所设置的值不同于系统调用之前的实际用户 ID。

获取实际、有效和保存设置 ID

在大多数 UNIX 实现中,进程不能直接获取(或修改)其保存 set-user-ID 和保存set-group-ID 的值。然而,Linux 提供了两个(非标准的)系统调用来实现此项功能:getresuid()和 getresgid()。

int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);			/* 成功返回0,出错返回-1 */
int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid);			/* 成功返回0,出错返回-1 */

getresuid()系统调用将调用进程的当前实际用户 ID、有效用户 ID 和保存 set-user-ID 值返回至给定 3 个参数所指定的位置。getresgid()系统调用针对相应的组 ID 实现了类似功能。

修改实际、有效和保存设置 ID

setresuid()系统调用允许调用进程独立修改其 3 个用户 ID 的值。每个用户 ID 的新值由系统调用的 3 个参数给定。setresgid()系统调用对相应的组 ID 实现了类似功能。若不想同时修改这些 ID,则需将无意修改的 ID 参数值指定为-1。

int setresuid(uid_t ruid, uid_t euid, uid_t suid);			/* 成功返回0,出错返回-1 */
int setresgid(gid_t rgid, gid_t egid, gid_t sgid);			/* 成功返回0,出错返回-1 */

关于 setresuid()可做何种修改的规则(setresgid()与之类似)如下所示。

  • 非特权进程能够将实际用户 ID、有效用户 ID 和保存 set-user-ID 中的任一 ID 设置为实际用户 ID、有效用户 ID 或保存 set-user-ID 之中的任一当前值。
  • 特权级进程能够对其实际用户 ID、有效用户 ID 和保存 set-user-ID 做任意设置。
  • 不管系统调用是否对其他 ID 做了任何改动,总是将文件系统用户 ID 设置为与有效用户ID(可能是新值)相同。

setresuid()和 setresgid()调用具有 0/1 效应,即对 ID 的修改请求要么全都成功,要么全部失败。

9.7.2 获取和修改文件系统 ID

前述所有修改进程有效用户 ID 或组 ID 的系统调用总是会修改相应的文件系统 ID。要想独立于有效 ID 而修改文件系统 ID,必须使用 Linux 特有的系统调用:setfsuid() 和 setfsgid()。

int setfsuid(uid_t fsuid);		/* 返回修改前文件系统用户ID */
int setfsgid(gid_t fsgid);		/* 返回修改前文件系统组ID */

同样,此类变更也存在一些规则。setfsgid()的规则类似于 setfsuid(),下面以 setfsuid()为例。

  • 非特权进程能够将文件系统用户 ID 设置为实际用户 ID、有效用户 ID、文件系统用户 ID(即保持不变)或保存 set-user-ID 的当前值。
  • 特权级进程能够将文件系统用户 ID 设置为任意值。

9.7.3 获取和修改辅助组 ID

getgroups()系统调用会将当前进程所属组的集合返回至由参数 grouplist 指向的数组中。

int getgroups(int gidsetsize, gid_t grouplist[]);	/* 成功回调用进程的辅助组ID,错误返回-1 */

调用程序必须负责为 grouplist 数组分配存储空间,并在 gidsetsize 参数中指定其长度。若调用成功,getgroups()会返回置于 grouplist 中的组 ID 数量。若进程属组的数量超出 gidsetsize,则 getgroups()将返回错误(错误号为 EINVAL)。为了避免发生这种情况,可将 grouplist 数组的大小调整为常量 NGROUPS_MAX+1 (考虑到可移植性,数组中可能包含了有效组 ID),该常量(定义于<limits.h>文件中)定义了进程属组的最大数量。因此,可声明 grouplist 如下:

gid_t grouplist[NGROUP_MAX + 1];

应用程序要在运行时获取 NGROUPS_MAX 的上限,还可使用如下方法。

  • 调用 sysconf(_SC_NGROUPS_MAX)。(11.2 节解释了 sysconf()的用法。)
  • 从 Linux 特有的/proc/sys/kernel/ngroups_max 只读文件中读取该限制。

除此之外,应用程序还能在调用 getgroups()时将 gidtsetsize 参数指定为 0。这样一来,grouplist 数组未作修改,但调用的返回值却给出了进程属组的数量。

特权级进程能够使用 setgroups()和 initgroups()来修改其辅助组 ID 集合。

int setgroup(size_t gidsetsize, const gid_t, gid_t *grouplist);		/* 成功返回0,出错返回-1 */
int initgroup(const char *user, gid_t group);						/* 成功返回0,出错返回-1 */

setgroups()系统调用用 grouplist 数组所指定的集合来替换调用进程的辅助组 ID。参数gidsetsize 指定了置于参数 grouplist 数组中的组 ID 数量。

initgroups()函数将扫描/etc/groups 文件,为 user 创建属组列表,以此来初始化调用进程的辅助组 ID。另外,也会将参数 group 指定的组 ID 追加到进程辅助组 ID 的集合中。

initgroups()函数的主要用户是创建登录会话的程序 — 例如 login(1),在用户调用登录shell 之前,为进程设置各种属性。此类程序一般通过读取密码文件中用户记录的组属性来获取参数 group 的值。这稍微有点令人费解,因为密码文件中的组 ID 实际并非辅助组 ID,而是定义了登录shell 初始的实际组ID、有效组ID 和保存 set-group-ID。尽管如此,这却是 initgroups()函数的常用使用方式。

9.7.4 修改进程凭证的系统调用总结

在这里插入图片描述

9.7.5 示例:显示进程凭证

/***************************\proccred\idshow.c***************************/
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/fsuid.h>
#include <limits.h>
#include "ugid_functions.h"   /* userNameFromId() & groupNameFromId() */
#include "tlpi_hdr.h"

#define SG_SIZE (NGROUPS_MAX + 1)

int
main(int argc, char *argv[])
{
    uid_t ruid, euid, suid, fsuid;
    gid_t rgid, egid, sgid, fsgid;
    gid_t suppGroups[SG_SIZE];
    int numGroups, j;
    char *p;

    if (getresuid(&ruid, &euid, &suid) == -1)		/* 获取实际、有效和保存设置用户ID */ 
        errExit("getresuid");
    if (getresgid(&rgid, &egid, &sgid) == -1)		/* 获取实际、有效和保存设置组ID */ 
        errExit("getresgid");

    /* Attempts to change the file-system IDs are always ignored
       for unprivileged processes, but even so, the following
       calls return the current file-system IDs */

    fsuid = setfsuid(0);			/* 将文件系统用户ID设置为0,并记录之前的文件系统用户ID */
    fsgid = setfsgid(0);			/* 将文件系统组ID设置为0,并记录之前的文件系统组ID */

    printf("UID: ");				/* 输出各种UID */
    p = userNameFromId(ruid);
    printf("real=%s (%ld); ", (p == NULL) ? "???" : p, (long) ruid);
    p = userNameFromId(euid);
    printf("eff=%s (%ld); ", (p == NULL) ? "???" : p, (long) euid);
    p = userNameFromId(suid);
    printf("saved=%s (%ld); ", (p == NULL) ? "???" : p, (long) suid);
    p = userNameFromId(fsuid);
    printf("fs=%s (%ld); ", (p == NULL) ? "???" : p, (long) fsuid);
    printf("\n");

    printf("GID: ");				/* 输出各种GID */
    p = groupNameFromId(rgid);
    printf("real=%s (%ld); ", (p == NULL) ? "???" : p, (long) rgid);
    p = groupNameFromId(egid);
    printf("eff=%s (%ld); ", (p == NULL) ? "???" : p, (long) egid);
    p = groupNameFromId(sgid);
    printf("saved=%s (%ld); ", (p == NULL) ? "???" : p, (long) sgid);
    p = groupNameFromId(fsgid);
    printf("fs=%s (%ld); ", (p == NULL) ? "???" : p, (long) fsgid);
    printf("\n");

    numGroups = getgroups(SG_SIZE, suppGroups);			/* 获取当前进程所属组的数量和组集合 */
    if (numGroups == -1)
        errExit("getgroups");

    printf("Supplementary groups (%d): ", numGroups);	/* 输出当前进程所属组的数量和组集合 */
    for (j = 0; j < numGroups; j++) {
        p = groupNameFromId(suppGroups[j]);
        printf("%s (%ld) ", (p == NULL) ? "???" : p, (long) suppGroups[j]);
    }
    printf("\n");

    exit(EXIT_SUCCESS);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值