You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Linux进程继承Capability失败问题求助:CAP_NET_RAW权限无法跨exec传递

问题排查与解决方案

看起来你遇到的问题主要来自两个关键错误:操作顺序导致Ambient能力被清空,以及shell脚本解释器默认会丢弃继承的能力。让我们一步步拆解并解决:

1. 核心问题:setuid()会清空Ambient能力集

当你从root(UID=0)切换到普通用户时,Linux内核会自动清空进程的Effective能力集Ambient能力集——这是安全机制的一部分。你的代码顺序是:设置能力 → 配置Ambient → setuid() → exec,这就导致setuid()之后Ambient集已经是空的,exec自然无法继承。

而且,切换到普通用户后,你也没有权限再重新设置Ambient集(修改Ambient需要root权限或CAP_SETPCAP能力)。

2. 次要问题:Shell脚本解释器会丢弃能力

即使你修复了顺序,直接exec shell脚本也会有问题:默认情况下,bash等解释器在启动时会主动丢弃所有继承的能力(除非显式配置)。这就解释了为什么capsh --print显示没有有效能力——bash已经把它们清空了。


修正后的解决方案

我们不需要依赖Ambient能力集,而是利用Permitted集可以在切换用户后保留的特性,在切换用户后手动将需要的能力加入Effective集。以下是正确的步骤和代码:

正确的操作流程(子进程中)

  1. 以root身份,将CAP_NET_RAW添加到进程的PermittedInheritable集(不需要设置Effective或Ambient)。
  2. 调用setuid()切换到普通用户(此时Effective和Ambient被清空,但Permitted/Inheritable集保留CAP_NET_RAW)。
  3. 切换用户后,将CAP_NET_RAW从Permitted集移动到Effective集(非root进程允许将Permitted中的能力激活到Effective)。
  4. 直接exec目标二进制程序(避免通过shell脚本,否则解释器会丢弃能力)。

修正后的代码示例

#include <stdio.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <vector>

int main() {
    // 假设我们要切换到的普通用户UID/GID是1000,根据实际情况修改
    const uid_t target_uid = 1000;
    const gid_t target_gid = 1000;

    // Step 1: 以root身份设置Permitted和Inheritable集
    cap_t caps = cap_get_proc();
    if (!caps) {
        printf("cap_get_proc failed: %s\n", strerror(errno));
        return 1;
    }

    std::vector<cap_value_t> cap_list = {CAP_NET_RAW};
    // 设置Permitted集
    if (cap_set_flag(caps, CAP_PERMITTED, cap_list.size(), cap_list.data(), CAP_SET) < 0) {
        printf("cap_set_flag(PERMITTED) failed: %s\n", strerror(errno));
        cap_free(caps);
        return 1;
    }
    // 设置Inheritable集(可选,但为了兼容性保留)
    if (cap_set_flag(caps, CAP_INHERITABLE, cap_list.size(), cap_list.data(), CAP_SET) < 0) {
        printf("cap_set_flag(INHERITABLE) failed: %s\n", strerror(errno));
        cap_free(caps);
        return 1;
    }

    if (cap_set_proc(caps) < 0) {
        printf("cap_set_proc failed: %s\n", strerror(errno));
        cap_free(caps);
        return 1;
    }
    cap_free(caps);

    // Step 2: 切换到目标用户(先切换gid,再切换uid)
    if (setgid(target_gid) < 0) {
        printf("setgid failed: %s\n", strerror(errno));
        return 1;
    }
    if (setuid(target_uid) < 0) {
        printf("setuid failed: %s\n", strerror(errno));
        return 1;
    }

    // Step 3: 切换用户后,将CAP_NET_RAW激活到Effective集
    caps = cap_get_proc();
    if (!caps) {
        printf("cap_get_proc (post-setuid) failed: %s\n", strerror(errno));
        return 1;
    }

    if (cap_set_flag(caps, CAP_EFFECTIVE, cap_list.size(), cap_list.data(), CAP_SET) < 0) {
        printf("cap_set_flag(EFFECTIVE) failed: %s\n", strerror(errno));
        cap_free(caps);
        return 1;
    }

    if (cap_set_proc(caps) < 0) {
        printf("cap_set_proc (post-setuid) failed: %s\n", strerror(errno));
        cap_free(caps);
        return 1;
    }
    cap_free(caps);

    // Step 4: 直接exec目标程序(替换为你的程序路径)
    printf("Executing target program...\n");
    execl("/path/to/your/target/program", "target-program", NULL);
    // 如果execl返回,说明出错
    printf("execl failed: %s\n", strerror(errno));
    return 1;
}

处理Shell脚本的情况

如果必须通过shell脚本调用目标程序,你需要让解释器保留能力。可以用capsh代替bash作为解释器,或者在脚本中显式重新激活能力:

示例脚本(run-target.sh):

#!/usr/bin/env capsh
# 保留继承的能力,然后调用目标程序
capsh --keep=1 --user=$(whoami) --addamb=CAP_NET_RAW -- /path/to/your/target/program

不过更可靠的方式是直接exec目标二进制,避免中间脚本的干扰。


验证方法

你可以在目标程序中添加代码,打印当前能力集来验证:

#include <sys/capability.h>
#include <stdio.h>

void print_capabilities() {
    cap_t caps = cap_get_proc();
    char* cap_str = cap_to_text(caps, NULL);
    printf("Current capabilities: %s\n", cap_str);
    cap_free(cap_str);
    cap_free(caps);
}

调用这个函数后,你应该能看到cap_net_raw出现在Effective集(e标记)中。

内容的提问来源于stack exchange,提问作者Steve Summit

火山引擎 最新活动