临时小驻

求仁得仁,复无怨怼。

认识 Linux 进程的 UIDs GIDs

2018-10-25 20:35:00 +0800

我们知道,uid_t uid 是一个整数,用于标识用户。用户在被创建时便被分配了一个 uid。root 用户的 uid 是 0。

Linux 进程有一系列标识符,称为凭证(Credentials)。其中有四个 uid 和四个 gid。这里只介绍 uid 的用法,gid 的用法同理。

凭证的描述文档可参见 man 7 credentials

我们可以通过 /proc 看到进程的这些属性:

$ PID=1; cat /proc/$PID/task/$PID/status | grep "Uid"
Uid: 0 0 0 0

这四个值从左到右分别是:ruid、euid、suid、fsuid。

  • ruid (real uid) 这是进程所有者的 uid,默认是进程的创建者。
  • euid (effective uid) 是内核鉴权时使用的 uid,默认与 ruid 相同。
  • suid (saved set-uid) 这是一个允许进程切换使用的 uid。默认与 ruid 相同。作用将在下文详述。
  • fsuid (filesystem uid) 是内核针对访问文件系统的操作鉴权时使用的 uid,默认与 euid 相同。

根据这里的讨论,fsuid 可能是早期 Linux 引入的特性,而现在 euid fsuid 不一样的情况已经很少见了。

相关的 C 函数

getters

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

uid_t getuid();
uid_t geteuid();
uid_t setfsuid(uid_t fsuid);
int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);

void foo() {
    uid_t ruid = getuid();
    uid_t euid = geteuid();
    uid_t fsuid = setfsuid(-1);
    printf("%d %d %d\n", ruid, euid, fsuid);

    uid_t suid;
    getresuid(&ruid, &euid, &suid);
    printf("%d %d %d\n", ruid, euid, suid);
}

setters (只列举函数声明)

#include <sys/types.h>
#include <unistd.h>

int setuid(uid_t uid);
int seteuid(uid_t euid);
int setreuid(uid_t ruid, uid_t euid);
int setresuid(uid_t ruid, uid_t euid, uid_t suid);
uid_t setfsuid(uid_t fsuid);

setuid(uid) 会把 ruid euid suid fsuid 均修改为新的 uid。

seteuid(uid) 会把 euid fsuid 修改为新的 uid。

下面为了叙述方便,将拥有 CAP_SETUID 权能的用户(如 root)简称为“提权用户”,没有 CAP_SETUID 权能的用户简称为“普通用户”。

权能的描述文档可参见 man 7 capabilities

普通用户在调用这些 setter 函数时,只能从当前进程的 ruid euid suid 之中选择一个值使用。提权用户则可以在任意有效的 uid 中选择。

文件的 SUID 的应用

SUID 标志位是 Linux 文件的一种属性。

任何用户的进程在调用 exec 系列函数,载入了一个设置了 SUID (Set-UID) 标志的映像文件时,会将进程的 euid 和 suid 修改为该文件所有者的 uid(进程被创建时 ruid euid suid 继承父进程的值)。

进程提权

$ ls -ld $(which passwd)
-rwsr-xr-x 1 root root 54256 May 17  2017 /usr/bin/passwd
$ passwd
Changing password for user.
(current) UNIX password:

开启另一个终端,找到当前 passwd 的进程 ID,检查该进程的 UIDs:

$ PID=19531; cat /proc/$PID/task/$PID/status | grep "Uid"
Uid: 1000 0 0 0

可以发现,虽然 ruid 仍为当前用户,euid suid fsuid 已经设为了 0 (root)。也正是因为这个原因,passwd 程序才有权写入 /etc/shadow 文件,保存当前用户修改后的密码。

临时降权:进程的 suid 的应用

在运行设置了 SUID 标志的程序时,euid 和 suid 都被更改了,而 ruid 未被更改。

我们在程序运行过程中可能不必一直持有太高的权限,需要临时或永久地切换到其他用户身份。

如果我们使用 setuid(uid),那么当此时 euid 是 0 时,会将 ruid euid suid 都设为新的 uid(假设是普通用户)。然后我们就无法再用任何方法切换回 root 身份了,实现了永久降权。

int foo() {
	uid_t ruid, euid, suid;
	getresuid(&ruid, &euid, &suid);  // 1000 0 0
	seteuid(600);                    // 1000 600 0
	setuid(1000);                    // 1000 1000 0
	setuid(0);                       // 1000 0 0
}

假设我们使用 seteuid(uid),此时 euid 是 0,会将 euid 设为新的 uid(假设是普通用户),但是没有修改 suid。suid 仍为 0,之后可以重新将 euid 切换回 root 身份,实现了临时降权。

int foo() {
	uid_t ruid, euid, suid;
	getresuid(&ruid, &euid, &suid);  // 1000 0 0
	setuid(600);                     // 600 600 600
	setuid(0);                       // EPERM
}

进程凭据与 NPTL 线程实现

实际上,在 Linux 内核里,这些 UIDs 是记录在线程的数据结构上的。因为 POSIX 标准要求一个进程的所有线程拥有相同的凭据,所以 NPTL 线程实现(也就是 pthread)包装了相关的函数,为其增加了信号机制,使得在一个线程的凭据改变时,所有的线程都会得到通知,同时改变凭据。

NPTL 的描述文档可参见 man 7 nptl

原文链接 https://blog.xupu.name//p/2018-10-the-uids-of-linux-process/

如无特别指明,本站原创文章均采用 CC BY-NC-ND 4.0 许可,转载或引用请注明出处,更多许可信息请查阅这里