c - 在保留 CAP_SYS_NICE 的同时删除根 UID

标签 c linux pthreads scheduler linux-capabilities

我正在尝试编写一个守护进程,它将使用 setuid 位以 root 身份启动,然后快速恢复到运行该进程的用户。然而,守护进程需要保留将新线程设置为“实时”优先级的能力。我用来设置优先级的代码如下(一旦创建就在线程中运行):

struct sched_param sched_param;
memset(&sched_param, 0, sizeof(sched_param));
sched_param.sched_priority = 90;

if(-1 == sched_setscheduler(0, SCHED_FIFO, &sched_param)) {
  // If we get here, we have an error, for example "Operation not permitted"
}

但是,我遇到问题的部分是设置 uid,同时保留对 sched_setscheduler 进行上述调用的能力.

我有一些代码在我的应用程序的主线程中运行接近启动:
if (getgid() != getegid() || getuid() != geteuid()) {
  cap_value_t cap_values[] = {CAP_SYS_NICE};
  cap_t caps;
  caps = cap_get_proc();
  cap_set_flag(caps, CAP_PERMITTED, 1, cap_values, CAP_SET);
  cap_set_proc(caps);
  prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0);
  cap_free(caps);
  setegid(getgid());
  seteuid(getuid());
}

问题是,在运行此代码后,我在调用 sched_setscheduler 时得到“不允许操作”。正如上面评论中所提到的。我究竟做错了什么?

最佳答案

编辑以描述原始失败的原因:

Linux 中有三组功能:可继承、允许和有效。 Inheritable 定义了在 exec() 中允许哪些功能。 .允许定义进程允许哪些能力。有效定义了当前有效的功能。

将进程的所有者或组从 root 更改为非 root 时,始终清除有效的能力集。

默认情况下,允许的功能集也被清除,但调用 prctl(PR_SET_KEEPCAPS, 1L)在身份更改之前告诉内核保持允许的集合完整。

在进程将身份改回非特权用户后,CAP_SYS_NICE必须添加到有效集合中。 (它也必须在允许的集中设置,所以如果你清除你的能力集,记得也要设置它。如果你只是修改当前的能力集,那么你知道它已经设置了,因为你继承了它。)

这是我建议您应该遵循的程序:

  • 保存真实用户 ID、真实组 ID 和补充组 ID:
     #define  _GNU_SOURCE
     #define  _BSD_SOURCE
     #include <unistd.h>
     #include <sys/types.h>
     #include <sys/capability.h>
     #include <sys/prctl.h>
     #include <grp.h>
    
     uid_t   user = getuid();
     gid_t   group = getgid();
     gid_t  *gid;
     int     gids, n;
    
     gids = getgroups(0, NULL);
     if (gids < 0) /* error */
    
     gid = malloc((gids + 1) * sizeof *gid);
     if (!gid) /* error */
    
     gids = getgroups(gids, gid);
     if (gids < 0) /* error */
    
  • 过滤掉不必要的和特权的补充组(偏执!)
     n = 0;
     while (n < gids)
         if (gid[n] == 0 || gid[n] == group)
             gid[n] = gid[--gids];
         else
             n++;
    

    由于您无法“清除”补充组 ID(仅请求当前编号),因此请确保该列表永远不会为空。您始终可以将真实的组 ID 添加到补充列表中,使其不为空。
     if (gids < 1) {
         gid[0] = group;
         gids = 1;
     }
    
  • 将真实有效的用户ID切换为root
     if (setresuid(0, 0, 0)) /* error */
    
  • 设置 CAP_SYS_NICE能力在 CAP_PERMITTED放。
    我更喜欢清除整个集合,只保留此方法工作所需的四个功能(稍后,除 CAP_SYS_NICE 外,删除所有功能):
     cap_value_t capability[4] = { CAP_SYS_NICE, CAP_SETUID, CAP_SETGID, CAP_SETPCAP };
     cap_t       capabilities;
    
     capabilities = cap_get_proc();
     if (cap_clear(capabilities)) /* error */
     if (cap_set_flag(capabilities, CAP_EFFECTIVE, 4, capability, CAP_SET)) /* error */
     if (cap_set_flag(capabilities, CAP_PERMITTED, 4, capability, CAP_SET)) /* error */
     if (cap_set_proc(capabilities)) /* error */
    
  • 告诉内核您希望保留从 root 到非特权用户的更改的功能;默认情况下,当从 root 更改为非 root 身份时,这些功能会被清除为零
     if (prctl(PR_SET_KEEPCAPS, 1L)) /* error */
    
  • 将真实、有效、保存的组ID设置为初始保存的组ID
     if (setresgid(group, group, group)) /* error */
    
  • 设置补充组 ID
     if (setgroups(gids, gid)) /* error */
    
  • 将真实、有效、保存的用户ID设置为初始保存的用户ID
     if (setresuid(user, user, user)) /* error */
    

    在这一点上,您有效地放弃了 root 权限(无法再重新获得它们),除了 CAP_SYS_NICE能力。由于从root用户过渡到非root用户,该能力永远不会生效;内核将始终清除在此类转换中设置的有效功能。
  • 设置 CAP_SYS_NICE能力在 CAP_PERMITTEDCAP_EFFECTIVE
     if (cap_clear(capabilities)) /* error */
     if (cap_set_flag(capabilities, CAP_PERMITTED, 1, capability, CAP_SET))  /* error */
     if (cap_set_flag(capabilities, CAP_EFFECTIVE, 1, capability, CAP_SET))  /* error */
     if (cap_set_flag(capabilities, CAP_PERMITTED, 3, capability + 1, CAP_CLEAR))  /* error */
     if (cap_set_flag(capabilities, CAP_EFFECTIVE, 3, capability + 1, CAP_CLEAR))  /* error */
    
     if (cap_set_proc(capabilities)) /* error */
    

    注意后两个cap_set_flag()操作清除不再需要的三个能力,这样只有第一个,CAP_SYS_NICE遗迹。

    此时不再需要功能的描述符,因此最好将其释放。
     if (cap_free(capabilities)) /* error */
    
  • 告诉内核您不希望保留对 root 的任何进一步更改的功能(再次,只是偏执)
     if (prctl(PR_SET_KEEPCAPS, 0L)) /* error */
    

  • 在安装 libcap-dev 后,这适用于在 Xubuntu 12.04.1 LTS 上使用 GCC-4.6.3、libc6-2.15.0ubuntu10.3 和 linux-3.5.0-18 内核的 x86-64包裹。

    编辑添加:

    您可以通过仅依赖有效用户 ID 为 root 来简化该过程,因为可执行文件为 setuid root。在这种情况下,您也不必担心补充组,因为 setuid root 只影响有效用户 ID 而不会影响其他任何事情。回到原来的真实用户,技术上你只需要一个setresuid()在过程结束时调用(如果可执行文件也被标记为 setgid root,则调用 setresgid()),将保存的和有效的用户(和组)ID 设置为真实用户。

    但是,重新获得原始用户身份的情况很少见,获得指定用户身份的情况很常见,这里的程序最初是为后者设计的。您将使用 initgroups()为指定用户获取正确的补充组,依此类推。在这种情况下,小心处理真实、有效和保存的用户和组 ID 以及补充组 ID 很重要,否则进程将从执行该进程的用户那里继承补充组。

    这里的过程是偏执的,但是当您处理安全敏感问题时,偏执并不是一件坏事。对于恢复到真实用户的情况,可以简化。

    于 2013-03-17 编辑以显示一个简单的测试程序。这假定它已安装 setuid root,但它会删除所有权限和功能(CAP_SYS_NICE 除外,这是在正常规则之上的调度程序操作所必需的)。我减少了我更喜欢做的“多余”操作,希望其他人觉得这更容易阅读。
    #define  _GNU_SOURCE
    #define  _BSD_SOURCE
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/capability.h>
    #include <sys/prctl.h>
    #include <grp.h>
    #include <errno.h>
    
    #include <string.h>
    #include <sched.h>
    #include <stdio.h>
    
    
    void test_priority(const char *const name, const int policy)
    {
        const pid_t         me = getpid();
        struct sched_param  param;
    
        param.sched_priority = sched_get_priority_max(policy);
        printf("sched_get_priority_max(%s) = %d\n", name, param.sched_priority);
        if (sched_setscheduler(me, policy, &param) == -1)
            printf("sched_setscheduler(getpid(), %s, { %d }): %s.\n", name, param.sched_priority, strerror(errno));
        else
            printf("sched_setscheduler(getpid(), %s, { %d }): Ok.\n", name, param.sched_priority);
    
        param.sched_priority = sched_get_priority_min(policy);
        printf("sched_get_priority_min(%s) = %d\n", name, param.sched_priority);
        if (sched_setscheduler(me, policy, &param) == -1)
            printf("sched_setscheduler(getpid(), %s, { %d }): %s.\n", name, param.sched_priority, strerror(errno));
        else
            printf("sched_setscheduler(getpid(), %s, { %d }): Ok.\n", name, param.sched_priority);
    
    }
    
    
    int main(void)
    {
        uid_t       user;
        cap_value_t root_caps[2] = { CAP_SYS_NICE, CAP_SETUID };
        cap_value_t user_caps[1] = { CAP_SYS_NICE };
        cap_t       capabilities;
    
        /* Get real user ID. */
        user = getuid();
    
        /* Get full root privileges. Normally being effectively root
         * (see man 7 credentials, User and Group Identifiers, for explanation
         *  for effective versus real identity) is enough, but some security
         * modules restrict actions by processes that are only effectively root.
         * To make sure we don't hit those problems, we switch to root fully. */
        if (setresuid(0, 0, 0)) {
            fprintf(stderr, "Cannot switch to root: %s.\n", strerror(errno));
            return 1;
        }
    
        /* Create an empty set of capabilities. */
        capabilities = cap_init();
    
        /* Capabilities have three subsets:
         *      INHERITABLE:    Capabilities permitted after an execv()
         *      EFFECTIVE:      Currently effective capabilities
         *      PERMITTED:      Limiting set for the two above.
         * See man 7 capabilities for details, Thread Capability Sets.
         *
         * We need the following capabilities:
         *      CAP_SYS_NICE    For nice(2), setpriority(2),
         *                      sched_setscheduler(2), sched_setparam(2),
         *                      sched_setaffinity(2), etc.
         *      CAP_SETUID      For setuid(), setresuid()
         * in the last two subsets. We do not need to retain any capabilities
         * over an exec().
        */
        if (cap_set_flag(capabilities, CAP_PERMITTED, sizeof root_caps / sizeof root_caps[0], root_caps, CAP_SET) ||
            cap_set_flag(capabilities, CAP_EFFECTIVE, sizeof root_caps / sizeof root_caps[0], root_caps, CAP_SET)) {
            fprintf(stderr, "Cannot manipulate capability data structure as root: %s.\n", strerror(errno));
            return 1;
        }
    
        /* Above, we just manipulated the data structure describing the flags,
         * not the capabilities themselves. So, set those capabilities now. */
        if (cap_set_proc(capabilities)) {
            fprintf(stderr, "Cannot set capabilities as root: %s.\n", strerror(errno));
            return 1;
        }
    
        /* We wish to retain the capabilities across the identity change,
         * so we need to tell the kernel. */
        if (prctl(PR_SET_KEEPCAPS, 1L)) {
            fprintf(stderr, "Cannot keep capabilities after dropping privileges: %s.\n", strerror(errno));
            return 1;
        }
    
        /* Drop extra privileges (aside from capabilities) by switching
         * to the original real user. */
        if (setresuid(user, user, user)) {
            fprintf(stderr, "Cannot drop root privileges: %s.\n", strerror(errno));
            return 1;
        }
    
        /* We can still switch to a different user due to having the CAP_SETUID
         * capability. Let's clear the capability set, except for the CAP_SYS_NICE
         * in the permitted and effective sets. */
        if (cap_clear(capabilities)) {
            fprintf(stderr, "Cannot clear capability data structure: %s.\n", strerror(errno));
            return 1;
        }
        if (cap_set_flag(capabilities, CAP_PERMITTED, sizeof user_caps / sizeof user_caps[0], user_caps, CAP_SET) ||
            cap_set_flag(capabilities, CAP_EFFECTIVE, sizeof user_caps / sizeof user_caps[0], user_caps, CAP_SET)) {
            fprintf(stderr, "Cannot manipulate capability data structure as user: %s.\n", strerror(errno));
            return 1;
        }
    
        /* Apply modified capabilities. */
        if (cap_set_proc(capabilities)) {
            fprintf(stderr, "Cannot set capabilities as user: %s.\n", strerror(errno));
            return 1;
        }
    
        /*
         * Now we have just the normal user privileges,
         * plus user_caps.
        */
    
        test_priority("SCHED_OTHER", SCHED_OTHER);
        test_priority("SCHED_BATCH", SCHED_BATCH);
        test_priority("SCHED_IDLE", SCHED_IDLE);
        test_priority("SCHED_FIFO", SCHED_FIFO);
        test_priority("SCHED_RR", SCHED_RR);
    
        return 0;
    }
    

    请注意,如果您知道二进制文件仅在相对较新的 Linux 内核上运行,则可以依赖文件功能。那么,您的 main()不需要任何身份或能力操作——您可以删除 main() 中的所有内容除了 test_priority()函数——你只需给出你的二进制文件,比如 ./testprio , CAP_SYS_NICE 优先级:
    sudo setcap 'cap_sys_nice=pe' ./testprio
    

    您可以运行 getcap查看执行二进制文件时授予哪些优先级:
    getcap ./testprio
    

    哪个应该显示
    ./testprio = cap_sys_nice+ep
    

    到目前为止,文件功能似乎很少使用。在我自己的系统上,gnome-keyring-daemon是唯一一个具有文件功能(CAP_IPC_LOCK,用于锁定内存)。

    关于c - 在保留 CAP_SYS_NICE 的同时删除根 UID,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13183327/

    相关文章:

    c - 对漏洞利用示例感到困惑?

    c++ - 使用 C++ 或 C 处理图像

    c - 对 gcc pthreads C 中函数的 undefined reference

    linux - 我在哪里可以找到 request_irq 函数中的 IRQ 号?

    linux - 在 expect 脚本中执行 shell 命令时无法读取此类变量

    c++ - 如何以编程方式从工作线程获取父线程和兄弟线程的 CPU 利用率?

    c - 在 C 中重新连接线程

    c - 追踪 cuda 内核寄存器的使用

    android - 需要帮助改进 OpenCV 2.4.0 中人脸检测的执行时间

    windows - 线程 ID 与线程句柄