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集。以下是正确的步骤和代码:
正确的操作流程(子进程中)
- 以root身份,将
CAP_NET_RAW添加到进程的Permitted和Inheritable集(不需要设置Effective或Ambient)。 - 调用
setuid()切换到普通用户(此时Effective和Ambient被清空,但Permitted/Inheritable集保留CAP_NET_RAW)。 - 切换用户后,将
CAP_NET_RAW从Permitted集移动到Effective集(非root进程允许将Permitted中的能力激活到Effective)。 - 直接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




