放弃 root 用户身份,同时保留 CAP_SYS_NICE 权限

10

我正在尝试编写一个守护进程,它将使用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时,我会收到“操作不允许”的错误。我做错了什么吗?


不要使用 seteuid(geteuid());,而是使用显式的 seteuid(0); 并在代码中始终使用 seteuid(),除了对 setuid(0); 的第一次调用。 - user529758
@H2CO3,你的评论有几个我不理解的地方。首先,我没有使用代码“seteuid(geteuid())”(你的代码中多了一个'e')。其次,为什么要调用“setuid(0)”?我正试图取消“root”的身份。也许你可以发表一个答案来详细说明你的建议。 - brooks94
是的,那是个笔误,抱歉。所以,我的意思是在放弃root权限之后,你可以通过setuid(0);重新获得root权限,以避免与权限不足相关的错误...这不就是你想要解决的问题吗?还是我漏掉了什么? - user529758
2个回答

27

编辑以描述原始故障的原因:

Linux 中有三组功能:可继承、可允许和有效。可继承定义了哪些功能在 exec() 期间保持被允许。可允许定义了一个进程可以使用哪些功能。有效定义了当前生效的功能。

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

默认情况下,也会清除可允许的能力集,但在标识更改之前调用 prctl(PR_SET_KEEPCAPS, 1L) 告诉内核保持可允许集合不变。

在进程将身份更改回未经特权的用户后,必须将 CAP_SYS_NICE 添加到有效集中。(它也必须在可允许的集合中设置,因此如果您清除了您的能力集,请记得也设置它。如果您只修改当前的能力集,则已经设置了它,因为您继承了它。)

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

  1. Save real user ID, real group ID, and supplemental group IDs:

     #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 */
    
  2. Filter out unnecessary and privileged supplementary groups (be paranoid!)

     n = 0;
     while (n < gids)
         if (gid[n] == 0 || gid[n] == group)
             gid[n] = gid[--gids];
         else
             n++;
    

    Because you cannot "clear" the supplementary group IDs (that just requests the current number), make sure the list is never empty. You can always add the real group ID to the supplementary list to make it non-empty.

     if (gids < 1) {
         gid[0] = group;
         gids = 1;
     }
    
  3. Switch real and effective user IDs to root

     if (setresuid(0, 0, 0)) /* error */
    
  4. Set the CAP_SYS_NICE capability in the CAP_PERMITTED set. I prefer to clear the entire set, and only keep the four capabilities that are required for this approach to work (and later on, drop all but 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 */
    
  5. Tell the kernel you wish to retain the capabilities over the change from root to the unprivileged user; by default, the capabilities are cleared to zero when changing from root to non-root identity

     if (prctl(PR_SET_KEEPCAPS, 1L)) /* error */
    
  6. Set real, effective, and saved group IDs to the initially saved group ID

     if (setresgid(group, group, group)) /* error */
    
  7. Set supplemental group IDs

     if (setgroups(gids, gid)) /* error */
    
  8. Set real, effective and saved user IDs to the initially saved user ID

     if (setresuid(user, user, user)) /* error */
    

    At this point you effectively drop root privileges (without the ability to gain them back anymore), except for the CAP_SYS_NICE capability. Due to the transition from root to non-root user, the capability is never effective; the kernel will always clear the effective capability set on such a transition.

  9. Set the CAP_SYS_NICE capability in the CAP_PERMITTED and CAP_EFFECTIVE set

     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 */
    

    Note that the latter two cap_set_flag() operations clear the three capabilities no longer needed, so that only the first one, CAP_SYS_NICE remains.

    At this point the capabilities' descriptor is no longer needed, so it's a good idea to free it.

     if (cap_free(capabilities)) /* error */
    
  10. Tell the kernel you don't wish to retain the capability over any further changes from root (again, just paranoia)

     if (prctl(PR_SET_KEEPCAPS, 0L)) /* error */
    

在使用GCC-4.6.3、libc6-2.15.0ubuntu10.3和linux-3.5.0-18内核的Xubuntu 12.04.1 LTS上,x86-64可行。在安装了libcap-dev软件包后,这个过程可以运行。

编辑以添加:

您可以简化该过程,只需依靠有效用户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,用于锁定内存)。

谢谢,我想我明白了。为什么需要第三步?可执行文件是否已设置为setuid并由root拥有?此外,我认为您在几个地方使用了“CAP_SET_NICE”,而实际上您的意思是“CAP_SYS_NICE”? - brooks94
@brooks94,谢谢;问题已解决。从技术上讲,使用root用户的有效用户ID足以满足需求,但完全切换到root用户可以降低在进程具有提升权限时任何恶意行为(如基于信号或者/proc/PID/访问)成功的可能性。因此,请将其视为一种纯粹的防御性和稍微有点偏执的细节。 - Nominal Animal
另外,在实际实现过程中我发现了另外两个错误:第一步中getgroups()的参数顺序被颠倒了;我认为第九步需要调用cap_set_flag(capabilities, CAP_PERMITTED, 1, capability, CAP_SET),否则在调用cap_set_proc()时会出现“操作不允许”的错误。 - brooks94
根据手册页面,cap_clear会初始化由cap_p标识的工作存储中的能力状态,以便清除所有能力标志。因此,在cap_clear之后,cap_set_flag(..., CAP_CLEAR)调用似乎是多余的。libcap的源代码证实了这一点。 - Lekensteyn
@NominalAnimal 从您的描述来看,CAP_CLEAR似乎是必需的,但实际上并不是,并且可能会让读者(包括我)感到困惑,而不是增加深度防御。我在源代码中还发现cap_get_proc实际上是cap_init + capget。正如手册页面(和代码)所提到的那样,cap_get_proc + cap_clearcap_init相同。至于CAP_SETPCAP,您能否在您的答案中说明为什么需要或不需要添加它? - Lekensteyn
显示剩余11条评论

1
我有一些代码在我的应用程序的主线程中接近启动时运行:
您必须在每个想要使用它们的线程中获取这些功能,或者使用CAP_INHERITABLE集。
来自capabilities(7):
Linux将传统上与超级用户相关联的特权分成不同的单元,称为功能,可以独立地启用和禁用。功能是每个线程的属性。

那么,我就没有选择了,我的守护进程需要在其生命周期内保留根权限? - brooks94
我不这么认为。尝试将“CAP_PERMITTED”更改为“CAP_INHERITABLE”。 - Brian Cain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接