setuid系统调用及示例

我们来深入学习 setuid 系统调用

1. 函数介绍

在 Linux 系统中,每个进程都运行在一个特定的用户 (User) 上下文中。这个用户上下文决定了进程拥有哪些权限,比如能否读写某个文件、能否绑定到特权端口(端口号小于 1024)等。

每个进程通常有三类相关的用户 ID:

  • 真实用户 ID (Real User ID – RUID): 登录系统时分配给用户的 ID。它标识了“你是谁”。
  • 有效用户 ID (Effective User ID – EUID): 内核用来进行权限检查时使用的 ID。它决定了“你能做什么”。这是最重要的一个。
  • 保存的设置用户 ID (Saved Set-User-ID – SUID): 用于在有效用户 ID 和真实用户 ID 之间来回切换的一个“备份”ID。

setuid (Set User ID) 系统调用的主要作用是设置调用进程的有效用户 ID (EUID)。根据调用者的权限和当前 ID 状态,它也可能同时修改保存的设置用户 ID (SUID)。

简单来说,setuid 让你的程序可以“以某个用户的身份”去执行操作,从而获得或限制与该用户相关的权限。

一个非常常见的场景是:一个需要监听 80 端口(特权端口)的 Web 服务器程序。它通常以 root 用户(UID 0)启动,以便能绑定到 80 端口。但一旦绑定成功,为了安全起见,它会使用 setuid 将自己的有效用户 ID 切换到一个权限较低的普通用户(如 www-data),这样即使程序出现漏洞被攻击,攻击者也无法获得 root 权限。

2. 函数原型

#include <unistd.h>    // 包含系统调用声明
#include <sys/types.h> // 包含 uid_t 类型定义

int setuid(uid_t uid);

3. 功能

设置调用进程的有效用户 ID (Effective UID)。根据调用者的权限(是否为 root)和目标 uid,行为会有所不同。

4. 参数

  • uid:
    • uid_t 类型。
    • 指定要设置的新的有效用户 ID。

5. 返回值

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 行为规则

setuid 的具体行为取决于调用进程的权限:

  1. 如果调用者是特权用户 (超级用户, root, EUID == 0):
    • 可以将有效用户 ID (euid) 设置为任意有效的用户 ID。
    • 同时,真实用户 ID (ruid) 和保存的设置用户 ID (suid) 也会被设置为相同的 uid 值
    • 这是特权用户的强大能力。
  2. 如果调用者是普通用户 (EUID != 0):
    • uid 参数必须是调用进程的真实用户 ID (ruid) 或 保存的设置用户 ID (suid) 之一。
    • 只能将有效用户 ID (euid) 设置为 ruid 或 suid
    • 真实用户 ID (ruid) 和 保存的设置用户 ID (suid) 不会被修改
    • 这是为了防止普通用户随意获取其他用户的权限。

7. 错误码 (errno)

  • EINVALuid 参数无效(虽然在 Linux 中通常不会返回此错误)。
  • EPERM: 调用者没有权限执行此操作。对于普通用户,这意味着 uid 既不是 ruid 也不是 suid

8. 相似函数或关联函数

  • setgid: 设置组 ID,与 setuid 功能类似,但针对的是组而非用户。
  • seteuid: 专门用于设置有效用户 ID,行为比 setuid 更受限(普通用户只能设置为 ruid 或 suid)。
  • setreuid: 同时设置真实用户 ID 和 有效用户 ID。
  • setresuid: 同时设置 真实有效 和 保存的设置 用户 ID,提供了最精细的控制。
  • getuid: 获取调用进程的真实用户 ID。
  • geteuid: 获取调用进程的有效用户 ID。
  • chmod / chown: 与文件权限和所有权相关的系统调用,其行为也受 setuid 影响。

9. 示例代码

下面的示例演示了 setuid 在不同权限下的行为,以及如何检查用户 ID。

#define _GNU_SOURCE // 启用 GNU 扩展以使用 getresuid
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

void print_current_uids(const char* context) {
    uid_t ruid, euid, suid;
    printf("[%s] Current UIDs - Real: %d, Effective: %d, Saved: %d\n",
           context, getuid(), geteuid(), (getresuid(&ruid, &euid, &suid) == 0) ? suid : -1);
}

int main() {
    uid_t original_ruid, original_euid, original_suid;
    uid_t target_uid;
    int result;

    printf("--- Demonstrating setuid ---\n");

    // 1. 获取并打印初始 UID
    if (getresuid(&original_ruid, &original_euid, &original_suid) != 0) {
        perror("getresuid");
        exit(EXIT_FAILURE);
    }
    print_current_uids("Start");

    // 2. 检查是否以 root 权限运行
    if (geteuid() == 0) {
        printf("\nRunning as ROOT (Privileged User)\n");
        // 作为 root,可以设置为任意有效的 UID
        // 这里我们尝试设置为 'nobody' 用户 (通常 UID 65534, 但请检查你的系统)
        target_uid = 65534;
        printf("Attempting to set UID to %d (usually 'nobody' user)...\n", target_uid);

        print_current_uids("Before setuid");
        result = setuid(target_uid);
        if (result == 0) {
            printf("setuid(%d) succeeded.\n", target_uid);
            print_current_uids("After setuid");
            printf("Note: All UIDs (Real, Effective, Saved) are now %d.\n", target_uid);
            printf("The process is now running with 'nobody' privileges.\n");
        } else {
            perror("setuid");
            printf("Failed to set UID to %d.\n", target_uid);
        }

    } else {
        printf("\nRunning as a REGULAR USER (UID: %d)\n", getuid());

        // 作为普通用户,只能设置为自己的 ruid 或 suid
        target_uid = original_ruid; // 选择设置为自己的真实 UID (这不会改变任何东西)
        printf("Attempting to set UID to my Real UID (%d)...\n", target_uid);
        print_current_uids("Before setuid");
        result = setuid(target_uid);
        if (result == 0) {
            printf("setuid(%d) succeeded (as expected).\n", target_uid);
            print_current_uids("After setuid");
        } else {
            perror("setuid");
        }

        // 尝试设置为一个无效的 UID (比如一个不存在的或不属于我的 UID)
        // 这通常会失败
        target_uid = 9999; // 假设这是一个无效的或不属于当前用户的 UID
        printf("\nAttempting to set UID to an invalid/different UID (%d)...\n", target_uid);
        result = setuid(target_uid);
        if (result == -1) {
            if (errno == EPERM) {
                printf("setuid(%d) failed with EPERM (Operation not permitted) - as expected for a regular user.\n", target_uid);
                printf("This is because %d is not my Real UID (%d) or Saved Set-UID (%d).\n",
                       target_uid, original_ruid, original_suid);
            } else {
                perror("setuid");
            }
            print_current_uids("After failed setuid");
        } else {
            printf("setuid(%d) unexpectedly succeeded.\n", target_uid);
            print_current_uids("After unexpected setuid");
        }
    }

    printf("\n--- Summary ---\n");
    printf("The setuid() function changes the Effective UID of the process.\n");
    printf("For root: It can change to any UID, and also changes Real and Saved UID.\n");
    printf("For regular users: It can only change Effective UID to Real or Saved UID.\n");
    printf("This is crucial for security, especially in programs like setuid binaries.\n");

    return 0;
}

10. 编译和运行

# 假设代码保存在 setuid_example.c 中
gcc -o setuid_example setuid_example.c

# 1. 作为普通用户运行
./setuid_example

# 2. 作为 root 用户运行 (需要 sudo 权限)
# 注意:切换到 root 权限执行程序有风险,请小心!
sudo ./setuid_example

11. 预期输出 (作为普通用户运行)

--- Demonstrating setuid ---
[Start] Current UIDs - Real: 1000, Effective: 1000, Saved: 1000

Running as a REGULAR USER (UID: 1000)
Attempting to set UID to my Real UID (1000)...
[Before setuid] Current UIDs - Real: 1000, Effective: 1000, Saved: 1000
setuid(1000) succeeded (as expected).
[After setuid] Current UIDs - Real: 1000, Effective: 1000, Saved: 1000

Attempting to set UID to an invalid/different UID (9999)...
setuid(9999) failed with EPERM (Operation not permitted) - as expected for a regular user.
This is because 9999 is not my Real UID (1000) or Saved Set-UID (1000).
[After failed setuid] Current UIDs - Real: 1000, Effective: 1000, Saved: 1000

--- Summary ---
The setuid() function changes the Effective UID of the process.
For root: It can change to any UID, and also changes Real and Saved UID.
For regular users: It can only change Effective UID to Real or Saved UID.
This is crucial for security, especially in programs like setuid binaries.

12. 预期输出 (使用 sudo 以 root 权限运行)

--- Demonstrating setuid ---
[Start] Current UIDs - Real: 0, Effective: 0, Saved: 0

Running as ROOT (Privileged User)
Attempting to set UID to 65534 (usually 'nobody' user)...
[Before setuid] Current UIDs - Real: 0, Effective: 0, Saved: 0
setuid(65534) succeeded.
[After setuid] Current UIDs - Real: 65534, Effective: 65534, Saved: 65534
Note: All UIDs (Real, Effective, Saved) are now 65534.
The process is now running with 'nobody' privileges.

13. 关于 Set-UID (SUID) 位的说明

虽然上面的示例是在运行时调用 setuid,但 setuid 系统调用的强大之处还体现在可执行文件的 SUID 位上。

当你将一个可执行文件的 SUID 位设置为 1 时(例如使用 chmod u+s myprogram),会发生以下情况:

  1. 无论哪个用户执行这个文件,该进程启动时的有效用户 ID (EUID) 都会被设置为该文件所有者的用户 ID
  2. 这使得普通用户可以运行一个具有文件所有者权限的程序。

例如

  1. Root 用户创建一个程序 read_etc_shadow,其功能是读取 /etc/shadow 文件(普通用户无权读取)。
  2. Root 将此程序的所有者设为 root,并设置 SUID 位:sudo chown root:root read_etc_shadow && sudo chmod u+s read_etc_shadow
  3. 普通用户 alice 执行 ./read_etc_shadow
  4. 程序启动时,其 euid 是 0 (root),因此它可以成功读取 /etc/shadow

安全警告
SUID 程序是系统安全的关键点,因为它们允许普通用户临时获得更高的权限。编写 SUID 程序时必须极其小心,避免任何可能导致权限提升的安全漏洞。

14. 总结

setuid 是一个基础且重要的系统调用,用于管理进程的用户权限。理解它的行为规则(尤其是特权用户和普通用户的区别)对于编写安全的 Linux 程序至关重要。它常用于守护进程降权、SUID 程序以及需要特定用户权限的系统管理任务中。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注