每个进程都有一套标识用户ID(UID)和组(GID)。这些标识ID也可以称作进程凭证。

1. 实际用户ID和实际组ID

进程所属的用户和组称为实际用户ID和实际组ID。登录Shell从 /etc/password文件中读取到的用户id和组id设置为实际ID和实际组ID。创建新进程时,将从父进程中继承这些ID

2.有效用户ID和有效组ID

当进程执行系统调用时,通过有效用户ID和有效组ID确定能做什么操作(访问权限)。

特权级进程(privileged progress): 有效用户ID为0(root用户的id)的进程拥有超级用户的所有权限。某些系统调用只能由 root用户执行。

默认情况下,有效用户ID、组ID等于实际用户ID、组ID,可以通过两种方式改变。

  • 执行系统调用
  • 执行设置SUID位的进程时,有效用户ID会设置为可执行文件的所属用户ID。

3. Set-User-ID和Set-Group-ID程序(SUID、SGID)

SUID程序会将进程的有效用户ID设置为可执行文件的所属用户ID,获得所属用户ID的权限。SGID程序对进程有效组ID做类似的操作。

例如:普通用户执行设置了 SUID 的 passwd 程序时,会临时获得 root 权限(因为 passwd 的所有者是 root),从而修改 /etc/shadow 文件(只有 root 可写)。

3.1 SUID、SGID权限位表示

SUID、SGID权限位分别位于用户、组执行权限位(x)上。

  • 若文件没用执行权限,则显示为S(大写)
  • 若文件用执行权限,则显示为s(小写)

可通过命令设置SUID和SGID位

chmod u+s file
chmod g+s file

通过命令取消SUID和SGID位

chmod u-s file
chmod g-s file

3.2 SUID、SGID应用

如果一个可执行文件的属主为root,并且设置了SUID权限位,当运行程序时,进程会取得超级用户权限。(需要慎用SUID-root权限,可能存在安全隐患)

也可以利用SUID、SGID机制,将进程有效ID设置为root以外的其他用户。

例如:需要访问一个受保护的文件,创建一个具有该文件访问权限的用户(组)ID,然后再创建一个SUID(SGID)程序,程序执行时,会将进程有效ID设置为保护文件的专用ID,这样无需root用户权限也能访问该文件。

3.3 Linux系统中经常使用的SUID程序

  • passwd 用于修改用户密码
  • mount和umount 用于加载和卸载文件系统
  • su 允许用户使用另一个用户身份执行shell

3.4 linux系统中的SGID程序

  • wall 用于向tty组下的所有终端写入一条消息

4. 保存set-user-ID和保存set-group-ID(保存SUID和保存SGID)

有效用户ID、组ID的备份,用于恢复权限。

  • 若可执行文件的SUID(SGID)权限位开启,将进程的有效用户ID(组)ID设置为可执行文件的属主。若未设置,则进程的有效用户(组)ID保持不变
  • 保存SUID和保存SGID的默认值为对应的有效ID

例如进程的初始ID为real=1000,effective=1000,save=1000,当执行了root用户(uid=0)拥有的SUID程序后,进程的用户ID将发生变化:real=1000,effective=0,save=0。

某些系统调用允许将SUID进程的有效用户ID在实际用户ID和保存SUID之间切换。如此,便能做到提升权限做某些操作,然后恢复权限。

5. 文件系统用户ID和组ID

linux系统特有的用户ID和组ID,用于文件系统权限检查。

通常情况下,文件系统用户ID和组ID的值等于有效用户ID和组ID,并跟随后者改变。只有使用Linux系统特有的两个系统调用(setfsuidsetfsgid)时,文件系统用户ID和组ID的值才有与对应的有效ID不同。

5.1 Linux为何提供文件系统ID

由于历史原因。文件系统ID始见于Linux1.2版本。在该版本内核中,如果进程A 的有效用户ID等于进程B的实际用户ID或有效用户ID,那么A就可以向B发送信号。

例如:Linux NFS服务器在处理客户请求时,需要以适当权限访问文件,但不能修改自身有效ID。

如果NFS将自身有效ID设置为客户进程的有效ID例如(1000),则该进程拥有该UID的权限。如果攻击者发送恶意信号(如 SIGKILLSIGSEGV)给该进程,可能导致服务崩溃或权限被滥用。

5.1.1 Linux的信号机制

linux的进程可以通过信号与进程通信,甚至强制终止进程。非特权进程只能发送 SIGTERM等信号给自己的进程,而无权限发送信号给其他用户的进程。

5.1.2 文件系统ID解决避免直接修改有效用户ID

NFS服务器可通过 setfsuid() 系统调用设置FSUID为客户端的UID,从而以该身份访问文件,保持自身进程的有效用户ID不变。

5.1.3 SUSv3信号发送权限规则

  • 特权级进程可以向任何进程发送信号
  • 以root用户和组运行的init进程是特例,只能接收安装了处理器函数的信号,这是为了防止init进程被误杀。
  • 如果发送的实际或有效用户ID与接收进程的实际用户ID和保存SUID匹配,非特权进程也可以向另一个进程发送信号。

由于linux内核2.0开始遵循了SUSv3信号发送规则,文件系统ID特性已经没有必要,为了兼容保留下来。

6. 辅助组ID

辅助组ID是用户所属主组以外的其他组的标识ID。添加多个辅助组,用户可以扩展权限范围,访问多个组的资源。

7. 进程凭证系统调用

7.1 获取实际、有效和保存设置标识

#include <unistd.h>

//获取实际用户id
uid_t getuid(void);

//获取有效用户id
uid_t geteuid(void);

//获取实际组id
gid_t getgid(void);

//获取有效组id
gid_t getegid(void);

7.2 修改有效ID

7.2.1 setuid和setgid

setuid系统调用可以通过uid参数修改调用进程的有效用户id,也可能修改实际用户ID和保存SUID。系统调用 setgid对应组ID的修改。

#include <unsitd.h>
//修改有效用户id
int setuid(uid_t uid);

//修改有效组id
int setgid(gid_t gid);

setuid系统调用规则:

  • 对于非特权进程调用而言,只能修改进程的有效用户ID,并且只能修改成相应的实际用户ID或保存SUID。(如果违反该约定则会引发 EPERM错误)只有在执行SUID程序时,该系统调用才起作用,因为普通程序执行时,进程的实际、有效、和SUID都相同。在不同的UNIX实现中,setuid或setgid系统调用的语义可能不用 ,存在可移植性问题
  • 特权进程调用时,实际用户ID、有效用户ID和保存SUID都被修改为uid参数的值。修改为非特权ID后无法修改回0(root用户)。

7.2.2 seteuid和setegid

#include<unistd.h>

//修改有效用户id
int seteuid(uid_t euid);

//修改有效组id
int setegid(gid_t egid);

该系统调用遵循以下规则:

  • 非特权进程能够将有效ID修改为相应的实际ID或者保存设置ID,但是没有可移植性问题。
  • 特权级进程能够将有效ID设置成任意值。如果设置成非特权用户,可以恢复特权。
//获取当前有效UID
euid = geteuid();

//删除特权
if(seteuid(getuid()) == -1)
    errExit("set error");
//恢复特权
if(seteuid(euid) == -1)
    errExit("set error");

7.2.3 setreuid和setregid

setreuid系统调用能够修改实际和有效用户ID。setregid对于组有类似功能

#include <unistd.h>

//修改实际和有效用户ID
int setreuid(uid_t ruid, uid_t euid);

//修改实际和有效组ID
int setregid(gid_t rgid, gid_t egid);

如果不想修改其中一个ID,可以将其设置为-1。

系统调用规则:

  1. 非特权进程只能将实际用户ID修改为当前实际用户ID或有效用户ID,只能将有效用户ID修改为当前实际用户ID、有效用户ID、或者保存SUID。
  2. 特权进程能够将两个值修改为任意值。
  3. 无论是否特权进程,只要满足a)ruid不为-1,b)对有效用户ID所设置的值不同与系统调用之前的实际用户ID。这两个条件之一,就能将保存SUID修改为新的有效用户ID

规则3为SUID程序提供了一个永久放弃特权的方法

setreuid(getuid(),getuid());

SUID-root进程如果需要修改用户ID和组ID,应该先调用 setregid 再调用 setreuid,避免先调用 setreuid失去特权后将无法修改组ID。

7.3 getresuid和getresgid

Linux提供两个非标准系统调用,可以或者实际、有效、保存ID

#define _GNU_SOURCE

//获取当前实际用户ID、有效用户ID 和保存SUID
int getresuid(uid_t *ruid,uid_t *euid,uid_t *suid);

//获取当前实际组ID、有效组ID 和保存SGID
int getresgid(gid_t *rgid,gid_t *egid,gid_t *sgid);

7.4 setresuid和setresgid

setresuid允许修改实际用户ID、有效用户ID 和保存SUID

#define _GNU_SOURCE
#include <unistd.h>

//修改实际用户ID、有效用户ID 和保存SUID
int setresuid(uid_t ruid,uid_t euid,uid_t suid);

//修改当前实际组ID、有效组ID 和保存SGID
int setresgid(gid_t rgid,gid_t egid,gid_t sgid);

若不想修改ID,可以将ID值设置为-1。

setresuid规则(setresgid类似):

  • 非特权进程能够将实际用户ID、有效用户ID和保存SUID中的任一ID修改为实际用户ID、有效用户ID和保存SUID中的任一当前值。
  • 特权进程能够修改实际用户ID、有效用户ID和保存SUID为任意值。
  • 不管系统调用是否对其他ID做了修改,总是将文件系统ID设置为有效用户ID的值。

这两个系统调用要么修改请求全部成功,要么全部失败,不会有一个失败别的成功的情况。

这些系统调用会带来可移植性问题

7.5 获取和修改文件系统ID

#include<sys/fsuid.h>

//设置或修改文件系统用户ID
int setfsuid(uid_t fsuid);

//设置或修改文件系统组ID
int setfsgid(gid_t fsgid);

这两个系统调用存在一些问题

  1. 没有相应的系统调用来获取当前文件系统ID
  2. 不会做错误检查。如果将文件系统ID设置为一个非法值,会被忽略掉,无论是否调用成功,都返回之前的文件系统ID。(这也是一种获取文件系统ID的方案)

这两个系统调用的规则:

  • 非特权进程可以将文件系统设置为实际ID、有效ID、文件系统ID、保存ID的当前值。
  • 特权用户能够修改为任意值。

若需要考虑程序可移植性,应该避免使用这两个系统调用。

7.6 获取和修改辅助组ID

7.6.1 getgroups

#include <unistd.h>

//获取辅助组ID
int getgroups(int gidsetsize,gid_t grouplist[]);

Linux中仅返回调用进程的辅助组ID,SUSv3规范允许UNIX实现返回结果中包含调用进程的有效组ID。

程序必须为 grouplist分配存储空间,并使用 gidsetsize参数指定数组长度。如果调用成功,会将组ID返回 grouplist中。如果返回数量超出 gidsetsize,则返回错误(EINVAL)。

为了避免调用出错,可以将 grouplist的大小设置为常量 NGROUPS_MAX+1,该常量定义在 <limits.h> 中。

如果需要在运行时获取 NGROUPS_MAX的上限值

  • 调用 scyscnof(_SC_NGROUPS_MAX)
  • 从Linux特有的 proc/sys/kernel/ngroups_max只读文件中读取。

应用程序还可以通过调用 getgroups时,将 gidsetsize参数设置为0,返回值中会返回进程属组的数量。

7.6.2 特权进程修改辅助组ID集合

特权进程可以通过 setgroupsinitgroups修改辅助组ID集合

#define _BSD_SOURCE
#include <grp.h>

//修改辅助组ID集合
int setgroups(size_t gidsetsize,const gid_t *grouplist);

//根据用户名和主组ID自动读取组文件,设置主组和辅助组
int initgroups(const char *user,gid_t group);

initgroups函数根据用户名 user参数和 group指定的组ID,从 /etc/groups文件中读取所有组(包括主组和辅助组),添加到当前进程的组列表中。

在登录过程中, login程序会调用 initgroups函数并传入两个参数用户名和主组ID(从 /etc/passwd中读取),然后 initgroups函数根据用户名和主组ID自动从组文件中查找用户所属的组(包括主组和辅助组),把这些组ID添加到进程的“组列表”中。

setgroups系统调用能够直接设置进程辅助组ID集合。

虽然未纳入SUSv3规范,但是这两个系统调用所有的UNIX实现都支持。

7.7 进程凭证系统调用小结

接口 作用 非特权进程 特权进程 可移植性
setuid
setgid
修改有效ID 将有效ID修改为当前实际ID
或保存设置ID
将实际ID、有效ID和保存设置ID
修改为任一值
SUSv3规范支持,
但是BSD都派生系统语义不同
seteuid
setegid
修改有效ID 将有效ID修改为当前实际ID
或保存设置ID
修改有效ID为任意值 SUSv3规范支持
setreuid(r,e)
setregid(r,e)
修改实际ID或有效ID 将实际ID修改为当前实际ID或有效ID,
将有效ID修改为当前实际ID、有效ID或保存设置ID
将实际ID和有效ID修改为任意值 SUSv3规范支持,但操作系统实现不同
setresuid(r,e,s)
setresgid(r,e,s)
修改实际ID、有效ID和保存设置ID 将实际用D、有效ID和保存设置ID中的任一ID
修改为实际ID、有效ID和保存设置ID中的任一当前值
将实际用D、有效ID和保存设置ID修改为任意值 有可移植性问题
sestfsuid
setfsgid
修改文件系统ID 将文件系统ID修改为实际ID、有效ID或者保存设置ID 修改文件系统ID为任意值 Linux系统特有
setgroups(n,l) 修改辅助组ID集合 无法调用 修改辅助组ID集合任意值 所有UNINX实现都支持

8. 参考资料

《Linux-UNIX系统编程手册》