BusyBox 1.38.0 权限模型分析:从 Linux UID 机制到 check_suid() 实现
关于本文
本文基于 BusyBox 1.38.0 源码、实际实验环境以及 Alpine Linux 发行版行为进行分析。
写作过程中使用 AI 工具辅助发现可能遗漏的问题、审查逻辑链条以及优化表达,但最终结论均以源码验证和实验结果为准,并已由作者逐项复核,若存在错误欢迎指出。
背景
UID 是 Linux 内核为每个进程维护的用户身份标识。一个进程同时持有三份 UID:
- ruid(真实 UID):原始的登录用户身份
- euid(有效 UID):权限检查时实际使用的身份
- suid(保存的 UID):用于临时降权后恢复特权
setuid 位(chmod u+s)允许普通用户以文件属主的 euid 执行程序——这是 Linux 中最经典的提权机制。
BusyBox 是一个将所有常用 Unix 工具打包进一个二进制文件的工具箱。在 Alpine Linux 等发行版中,/bin/ls、/bin/id、/bin/cat 等实际上都是指向 /bin/busybox 的符号链接。每个这样的命令称为一个 applet——BusyBox 通过 argv[0] 识别要执行哪个 applet,并在其 main() 之前运行统一的 check_suid() 进行特权管理。
本文要回答的是一个具体的安全问题:从 SUID Bash 拿到 euid=0 的 shell 后,BusyBox 的每个 applet 到底以什么身份运行?
0. 实验起点:SUID Bash + BusyBox
busybox 项目源码下载:https://busybox.net/downloads/busybox-1.38.0.tar.bz2
0.1 现象
实验环境为 Alpine Linux。Alpine 以 BusyBox 作为核心系统工具集,/bin/sh、/bin/ls、/bin/id、/bin/cat 等绝大多数基础命令均为指向 /bin/busybox 的符号链接。因此任何在 Alpine 中执行的常用命令,最底层几乎都经过 BusyBox 的 check_suid() 特权管理。
# 准备一个 setuid bash(bash 是额外安装的,不属于 BusyBox)
cp /bin/bash /var/tmp/bash
chmod 4755 /var/tmp/bash
以普通用户 demo(uid=1000)运行它,加上 -p 保持特权:
demo@localhost:/var/tmp$ ./bash -p
bash-5.3# # ← prompt 为 #,说明 euid=0
./bash的 setuid 位使exec后的进程处于ruid=1000, euid=0, suid=0。bash 的-p选项禁止 bash 自行setuid(getuid())降权。
此时执行 BusyBox 的 id applet:
bash-5.3# id
uid=1000(demo) gid=1000(demo) groups=1000(demo)
BusyBox 1.38.0 的 coreutils/id.c 第 27 行明确声明:
//applet:IF_ID(APPLET_NOEXEC(id, id, BB_DIR_USR_BIN, BB_SUID_DROP, id))
因此 check_suid() 在进入 id_main() 之前已执行 setuid(ruid),将 euid 降为 1000。整个调用链如下——掉特权的是 id 这个 applet 自己,不是 bash -p 那个 shell:
id_main() 内部调用 getuid() 和 geteuid() 后,发现 euid == ruid,因此不额外打印 euid 行。所以虽然 shell 提示符是 #(进程原始 euid 为 0),但 id 看到的已经是降权后的结果。
bash -p 进程 (ruid=1000, euid=0, suid=0)
│
│ exec /usr/bin/id → /bin/busybox
▼
busybox 入口
│
▼
check_suid()
│ ruid=1000 ≠ 0
│ APPLET_SUID(id) == BB_SUID_DROP
▼
setuid(1000) ← euid 降为 1000, suid 降为 1000
│
▼
id_main()
│ getuid()=1000, geteuid()=1000
│ euid == ruid → 不打印 euid 行
▼
uid=1000(demo) gid=1000(demo) groups=1000(demo)
⚠ 实验陷阱:在 BusyBox 环境中,
cat、id、ls等命令本身就是 BusyBox applet。使用这些工具观察 UID 状态时,你观察到的是 applet 自身被check_suid()处理后的身份,而非调用者(如bash -p)的原始 UID。要验证 shell 的真正权限,必须用独立编译的测试程序直接调用getuid()/geteuid()。
用一个小程序绕过 BusyBox 层,直接打印 getuid() 和 geteuid(),可以确认真实状态:
bash-5.3# cat b.c
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
printf("before: r=%d e=%d\n",
getuid(),
geteuid());
int ret = setuid(0);
printf("ret=%d errno=%d\n",
ret,
errno);
printf("after : r=%d e=%d\n",
getuid(),
geteuid());
execl("/bin/sh","sh",NULL);
}
bash-5.3# ./b
before: r=1000 e=0
ret=0 errno=0
after : r=0 e=0
/var/tmp #
./b 直接输出 r=1000 e=0,确认 bash -p 后的进程处于 ruid=1000, euid=0, suid=0。setuid(0) 调用成功(ret=0),之后 r=0 e=0——拥有 CAP_SETUID 的进程调用 setuid() 会将 ruid、euid、suid 三个字段全部设为指定值。
对比 BusyBox
id的输出与./b的输出:同一个 bash -p 中,id只显示uid=1000(因为check_suid()已经降权),而./b直接输出r=1000 e=0(绕过了 BusyBox)。这正是本实验的核心教训——在 BusyBox 环境中验证 shell 权限,必须用独立程序而非 BusyBox 工具。
另一个实验:在 bash -p 中运行仅含 setuid(0) 的 ./a,然后调用 BusyBox 的 id 观察效果:
bash-5.3# ./a
/var/tmp # id
uid=0(root) gid=1000(demo) egid=0(root) groups=1000(demo)
/var/tmp # which id
/usr/bin/id
/var/tmp # ls -alh /usr/bin/id
lrwxrwxrwx 1 root root 12 Jan 27 08:17 /usr/bin/id -> /bin/busybox
此时 id 输出 uid=0(root),ruid 已经变成 0。两次 id 调用的都是同一个 BusyBox applet,输出却完全不同。
0.2 拆解:./a 里的 setuid(0) 做了什么
执行 ./a 前: ruid=1000, euid=0, suid=0
setuid(0): 当前进程 euid=0,具备 CAP_SETUID。
Linux 将此次视为特权 UID 切换,
setuid() 会同时修改 ruid、euid、suid 三者。
执行 ./a 后: ruid=0, euid=0, suid=0 ← 全部三个 UID 被同步为 0
之后进程中 ruid == 0。BusyBox 的 check_suid() 看到 ruid == 0,直接 return,跳过所有检查。因此再次执行 busybox id 时,整个进程已是完整 root 身份,输出 uid=0(root)。
0.3 三个问题
./bash -p之后 euid=0,为什么id输出却是uid=1000?./a一个setuid(0),为什么能把 ruid 从 1000 变成 0?- 如果拿到一个 euid=0 的 shell,BusyBox 的每一个 applet 到底以什么身份运行?
答案在 Linux 的 UID 模型和 BusyBox 的 check_suid() 机制里。
1. Linux 身份模型
1.1 进程的四种 UID
每个 Linux 进程维护以下 UID 属性:
| 字段 | 含义 | 用途 |
|---|---|---|
| ruid (real UID) | 真实用户 ID,原始登录用户 | getuid() 获取 |
| euid (effective UID) | 有效用户 ID,传统 DAC 权限检查依据 | geteuid() 获取 |
| suid (saved set-user-ID) | 保存的设置用户 ID,用于特权恢复 | getresuid() 获取第三字段 |
| fsuid (filesystem UID) | 文件系统访问 UID(Linux 特有,通常与 euid 同步) | setfsuid() |
核心原则:传统 DAC 权限检查(文件读写、信号发送等)依据 euid,而非 ruid。 现代 Linux 还同时依赖 Capability、LSM 和用户命名空间,详见 1.4 节。
1.2 setuid 位的作用
当可执行文件的 setuid 位(chmod u+s)置位且文件属主为 root 时,exec 后:
ruid = 调用者原有 ruid (例如 1000)
euid = 文件属主 (0, root)
suid = 0 (root)
即 euid 和 suid 都被设为文件属主的值,ruid 保持为原用户。
这正是实验中 ./bash -p 之后的状态:ruid=1000, euid=0, suid=0。
1.3 特权恢复与丢失
假设一个 setuid root 进程启动后状态为 ruid=1000, euid=0, suid=0:
场景一:seteuid(1000) — 临时降权
seteuid(1000); // 只改 euid
// 结果: ruid=1000, euid=1000, suid=0
seteuid(0); // 仍可恢复!因为 suid 还是 0
// 结果: ruid=1000, euid=0, suid=0
场景二:setuid(1000) — 永久降权(核心区别)
setuid(1000); // 在 setuid-root 程序降权场景下,setuid(ruid) 会同时清除 euid 与 suid
// 结果: ruid=1000, euid=1000, suid=1000 ← suid 也变了!
seteuid(0); // 失败!suid 已经是 1000,无法恢复
setuid(0); // 同样失败
这正是 BusyBox
BB_SUID_DROP做的事情,也是"BusyBox applet 看起来像普通用户"的根本原因。
场景三:setresuid(-1, 1000, 0) — 精细控制
setresuid(-1, 1000, 0); // 不变更 ruid,euid=1000, suid=0
// 结果: ruid=1000, euid=1000, suid=0 ← suid 仍为 0
seteuid(0); // 仍可恢复
场景四:setresuid(-1, 1000, 1000) — 彻底封锁
setresuid(-1, 1000, 1000); // euid=1000, suid=1000
// 结果: ruid=1000, euid=1000, suid=1000
seteuid(0); // 失败!特权永久丢失
关键理解:对于不具备
CAP_SETUID的进程,setuid()无法任意切换身份——参数必须是 ruid、euid、suid 三者中已有的值。在最常见的 setuid 程序降权场景(euid=0, suid=0, ruid≠0)下,调用setuid(ruid)会同时将 euid 和 suid 设为 ruid,表现为"彻底清理"特权。seteuid()只改 euid,不改 suid,所以 suid 中保存的 0 允许后续seteuid(0)恢复。这正是 BusyBoxcheck_suid()和/etc/busybox.conf机制的核心安全考量。此处省略 Linux 内核关于CAP_SETUID、用户命名空间和历史兼容语义的细节,仅讨论 BusyBox 场景下最常见的行为。
1.4 Linux Capability 与 UID
本文聚焦于 BusyBox 的传统 setuid 权限模型,讨论 ruid/euid/suid 的变化过程。但需注意,现代 Linux 中大量特权操作已从"全有或全无的 root"拆分为细粒度的 Capability(能力集):
# 仅授予 ping 所需的最小能力,而非整个 root
setcap cap_net_raw+ep /bin/ping
此时进程的 ruid 与 euid 均为普通用户,但仍可创建 raw socket。
因此"权限检查只看 euid"这一论断仅在传统 setuid 模型下成立。实际内核的权限判断同时依赖:
- euid(传统 DAC 检查)
- Capability 集合(
CAP_NET_RAW、CAP_SYS_ADMIN等) - 用户命名空间(容器场景下的 UID 映射)
BusyBox 的 check_suid() 自身仅围绕 UID 切换设计,不涉及 Capability 管理(libbb/capability.c 仅提供了 CAP_SETUID 等名称常量),因此本文的讨论范围限于 ruid/euid/suid 维度。
2. check_suid() 执行流程
2.1 流程图
关键:
check_suid()的第一个判断是ruid == 0(real uid),而非 euid。这意味着只有真正以 root 登录(或 ruid 已被改为 0)的场景才会跳过所有检查。euid=0但ruid≠0(如 setuid 场景)仍需继续执行后续逻辑。
exec busybox applet
│
▼
run_applet_no_and_exit()
(appletlib.c:963)
│
▼
┌─── check_suid(applet_no) ───┐
│ (appletlib.c:555) │
│ │
ruid == 0 ? ───yes──→ return (skip all checks)
│ ⚠ 注意:判断的是 real uid,不是 effective uid
no
(euid=0 但 ruid≠0 仍会走到这里)
│
/etc/busybox.conf
可读且有匹配?
│
┌───────┼───────┐
│ yes │ no
▼ ▼
[Config 分支] [Fallback 分支]
│ │
│ APPLET_SUID(applet_no)
│ │
│ ┌────────┼────────┐
│ ▼ ▼ ▼
│ REQUIRE DROP MAYBE
│ │ │ │
│ geteuid() setgid(rgid) 保持
│ != 0 ? +setuid(ruid) 不变
│ 报错退出 特权丢失
│ applet applet
│ 以 root 以 ruid
│ 身份运行 身份运行
│
▼
检查 mode 权限位:
UID/GID 匹配?
│
┌─┼─┐
no│ yes
▼ ▼
拒绝 setresuid(-1, uid, uid)
setresgid(-1, gid, gid)
(同时设 euid 和 suid,
阻止后续提权)
2.2 三种模式详解 (libbb/appletlib.c:555-650)
| 模式 | 条件 | 行为 | suid 状态 | 能否后续提权 |
|---|---|---|---|---|
BB_SUID_REQUIRE |
geteuid() == 0(即维持 setuid 状态或由 root 运行) |
不做变更,保持 euid=0 | ruid=1000, euid=0, suid=0 | 是 |
BB_SUID_DROP |
无条件 | setgid(rgid) + setuid(ruid) |
ruid=1000, euid=1000, suid=1000 | 否(当前进程永久失权;父进程不受影响) |
| Config 模式 | 配置匹配,setresuid(-1, uid, uid) |
同时改 euid 和 suid | 取决于配置 | 取决于 suid 值 |
BB_SUID_MAYBE |
无条件 | 不干预 | 保持原始 | 取决于原始 |
2.3 关键代码片段
// libbb/appletlib.c:620-642 (精简)
static void check_suid(int applet_no)
{
gid_t rgid;
if (ruid == 0) // root 运行,跳过一切检查
return;
rgid = getgid();
// === /etc/busybox.conf 配置模式 ===
// ... 匹配配置条目 ...
// setresuid(-1, uid, uid);
// setresgid(-1, rgid, rgid);
// === Fallback: 硬编码模式 ===
if (APPLET_SUID(applet_no) == BB_SUID_REQUIRE) {
if (geteuid())
bb_simple_error_msg_and_die("must be suid to work properly");
} else if (APPLET_SUID(applet_no) == BB_SUID_DROP) {
setgid(rgid); // 放弃组特权
setuid(ruid); // 放弃用户特权 (suid 也被设为 ruid!)
}
}
3. 运行时特权管理工具
3.1 环境清理 sanitize_env_if_suid() (libbb/login.c:138-168)
// 禁用的危险环境变量列表 (libbb/login.c:138-151)
static const char forbid[] ALIGN1 =
"ENV" "\0"
"BASH_ENV" "\0"
"HOME" "\0"
"IFS" "\0"
"SHELL" "\0"
"LD_LIBRARY_PATH" "\0"
"LD_PRELOAD" "\0"
"LD_TRACE_LOADED_OBJECTS" "\0"
"LD_BIND_NOW" "\0"
"LD_AOUT_LIBRARY_PATH" "\0"
"LD_AOUT_PRELOAD" "\0"
"LD_NOWARN" "\0"
"LD_KEEPDIR" "\0";
// 入口条件: getuid() != geteuid() 时才生效
int FAST_FUNC sanitize_env_if_suid(void)
{
if (getuid() == geteuid())
return 0; // 不是 setuid 场景,放行
// 逐一 unset 危险变量
p = forbid;
do {
unsetenv(p);
p += strlen(p) + 1;
} while (*p);
putenv((char*)bb_PATH_root_path); // 设置安全 PATH
return 1; // 确实检测到了 setuid 场景
}
3.2 文件访问特权切换 xopen_as_uid_gid() (libbb/xfuncs_printf.c:187-200)
// 临时切换到指定 UID 打开文件后立即恢复
int FAST_FUNC xopen_as_uid_gid(const char *pathname, int flags, uid_t u, gid_t g)
{
uid_t old_euid = geteuid();
gid_t old_egid = getegid();
xsetegid(g);
xseteuid(u);
fd = xopen(pathname, flags);
xseteuid(old_euid); // 恢复
xsetegid(old_egid); // 恢复
return fd;
}
3.3 完整身份切换 change_identity() (libbb/change_identity.c:33-58)
void FAST_FUNC change_identity(const struct passwd *pw)
{
res = initgroups(pw->pw_name, pw->pw_gid); // 设置附加组
// ...
xsetgid(pw->pw_gid); // 先设 GID
xsetuid(pw->pw_uid); // 再设 UID (顺序重要: 先 gid 后 uid)
}
3.4 封装函数 (libbb/xfuncs_printf.c:415-432)
void xsetuid(uid_t uid) { if (setuid(uid)) bb_simple_perror_msg_and_die("setuid"); }
void xseteuid(uid_t euid) { if (seteuid(euid)) bb_simple_perror_msg_and_die("seteuid"); }
void xsetgid(gid_t gid) { if (setgid(gid)) bb_simple_perror_msg_and_die("setgid"); }
void xsetegid(gid_t egid) { if (setegid(egid)) bb_simple_perror_msg_and_die("setegid"); }
4. 各 Applet 的 SUID 策略与业务权限检查
4.1 编译期 SUID 分类
每个 applet 在注册时声明一个编译期策略,由 check_suid() 统一执行。以下为 BusyBox 1.38.0 默认配置下的分类:
| Applet | 模式 | 含义 |
|---|---|---|
| passwd | BB_SUID_REQUIRE |
必须以 setuid root 运行,否则拒绝 |
| su | BB_SUID_REQUIRE |
同上 |
| login | BB_SUID_REQUIRE |
同上 |
| vlock | BB_SUID_REQUIRE |
同上 |
| crontab | BB_SUID_REQUIRE |
同上 |
| wall | BB_SUID_REQUIRE |
同上 |
| crond | BB_SUID_DROP |
进入 main() 前主动降权为 ruid |
| adduser | BB_SUID_DROP |
同上 |
| deluser | BB_SUID_DROP |
同上 |
| addgroup | BB_SUID_DROP |
同上 |
| chpasswd | BB_SUID_DROP |
同上 |
| httpd | BB_SUID_DROP |
同上 |
| tcpsvd/udpsvd | BB_SUID_DROP |
同上 |
| id | BB_SUID_DROP |
同上 |
| ash | BB_SUID_DROP |
同上 |
| vi | BB_SUID_DROP |
同上 |
| test | BB_SUID_DROP |
同上 |
| cat | BB_SUID_DROP |
同上 |
| ls | BB_SUID_DROP |
同上 |
| traceroute | BB_SUID_MAYBE |
不主动降权;是否保留特权取决于编译选项与 capability 设置 |
| ping | BB_SUID_MAYBE |
同上(含 ping6) |
| findfs | BB_SUID_MAYBE |
同上 |
| mount | BB_SUID_MAYBE/DROP |
DESKTOP 版本为 MAYBE,否则 DROP |
4.2 运行时业务权限检查
在 check_suid() 处理之后,各 applet 的 main() 内部还会进行独立的业务级权限判断:
| Applet | SUID 策略 | 业务权限检查 | 说明 |
|---|---|---|---|
| passwd | REQUIRE | getuid() 非 root 时仅允许改自己密码;写入 /etc/passwd 前 xsetuid(0) 恢复 root |
保持 euid=0 → 检查 ruid → 恢复 root → 写入 |
| su | REQUIRE | getuid() 非 root 必须认证密码;getuid() 非 root 不能绕过受限 shell;change_identity() 切换身份 |
保持 euid=0 → 认证 → 切换 |
| login | REQUIRE | 无额外 UID 检查;change_identity() 切换身份 |
保持 euid=0 → 认证 → 切换 |
| vlock | REQUIRE | xgetpwuid(getuid()) 以真实用户锁定 |
保持 euid=0,以 ruid 锁定 |
| crontab | REQUIRE | sanitize_env_if_suid() 清理环境;getuid() 确定操作哪个用户的 crontab;-u/-c 拒绝非 root |
保持 euid=0 → 清理环境 → 以 ruid 确定操作对象 |
| wall | REQUIRE | xopen_as_uid_gid(argv, ..., getuid(), getgid()) 以真实用户身份读文件参数 |
保持 euid=0 → 以 ruid 打开用户文件 |
| traceroute | MAYBE | getuid() != 0 拒绝;创建 raw socket 后 xsetuid(getuid()) 释放 |
保持原始 euid → 权限检查 → 降权。实际权限需求取决于内核配置与编译选项(FEATURE_TRACEROUTE_USE_ICMP 等),并非一定需要 raw socket |
| crond | DROP | change_identity() 在 vfork 子进程中以对应用户执行任务 |
降权 → fork → 以目标用户执行 |
| adduser | DROP | geteuid() != 0 拒绝非 root |
降权后自查也是 ruid,实际需 root 直接运行 |
| deluser | DROP | geteuid() != 0 拒绝非 root |
同上 |
| addgroup | DROP | geteuid() != 0 拒绝非 root |
同上 |
| chpasswd | DROP | geteuid() != 0 拒绝非 root |
同上 |
| httpd | DROP | -u USER[:GROUP] 在 bind 后 setgroups() + xsetgid() + xsetuid() |
降权 → bind → 再次降权 |
| tcpsvd/udpsvd | DROP | root 且无 -u 拒绝;有 -u 则 xsetgid()+xsetuid() |
降权 → bind → 降权到指定用户 |
| ash | DROP | 非 Linux: getuid()==geteuid() && getgid()==getegid() 时才读 $ENV |
check_suid 已降权,但仍保留此检查以防御非 Linux 平台 |
| vi | DROP | .exrc 属主 == getuid() 且非组/他人可写才加载 |
check_suid 已降权,防御更深 |
| test | DROP | -r/-w/-x 用 geteuid() 判断;-O 用 geteuid() 判断属主 |
降权后以 ruid 身份进行权限测试 |
注意:
BB_SUID_REQUIRE的 applet(crontab、wall 等)在check_suid()中不做降权,保持 euid=0 进入main()。它们内部通过getuid()获取真实用户身份,通过sanitize_env_if_suid()和xopen_as_uid_gid()实现"以 root 运行,以 ruid 决策"的模式。这与BB_SUID_DROP的"先降权再运行"是两种不同的安全策略。
BB_SUID_DROP的 applet(如 adduser)通过geteuid() != 0自查,但这发生在check_suid()已执行setuid(ruid)之后。所以这些 applet 实际上要求的是以 root 身份直接运行(而非通过 setuid 位),因为 setuid 来的特权在进入 main() 之前就被丢弃了。
5. 回看实验:完整状态转换表
现在可以用第 0 节的实验数据,结合 2-4 节的分析,做一次完整的状态追踪。
| 场景 | ruid | euid | suid | BusyBox DROP 类 | BusyBox REQUIRE 类 | traceroute (MAYBE) |
|---|---|---|---|---|---|---|
| 直接以普通用户执行 | 1000 | 1000 | 1000 | 以 1000 运行 | 报错退出 | 以 1000 运行 |
./bash -p 后进入 |
1000 | 0 | 0 | setuid(1000) 降权 |
保持 euid=0 | 保持 euid=0 |
| 以 setuid busybox 执行 | 1000 | 0 | 0 | setuid(1000) 降权 |
保持 euid=0 | 保持 euid=0 |
./a (setuid(0)) 后 |
0 | 0 | 0 | ruid==0 → 跳过 → root | 跳过 → root | 跳过 → root |
| root 直接执行 | 0 | 0 | 0 | ruid==0 → 跳过 → root | 跳过 → root | 跳过 → root |
关键洞察:
check_suid()的入口判断是if (ruid == 0) return;——依据的是 real uid,而非 effective uid。这意味着即使 euid=0,只要 ruid≠0,检查继续执行。这是全文最重要的发现,很多人会默认认为"euid=0 即 root 即跳过所有检查",而 BusyBox 并非如此。
6. 编译配置对权限模型的影响
6.1 三个关键配置项
在 BusyBox 的 Config.in 中定义:
| 配置项 | 默认值 | 含义 |
|---|---|---|
FEATURE_SUID |
y | 是否启用 suid 机制,禁用后整个 check_suid() 变为空操作 |
FEATURE_SUID_CONFIG |
y | 是否支持 /etc/busybox.conf 运行时配置 |
FEATURE_PREFER_APPLETS |
n | 是否优先使用 BusyBox 内置 applet(影响 shell/find/xargs) |
6.2 FEATURE_SUID=n 的影响
当发行版编译 BusyBox 时关闭此选项:
// appletlib.c 中的实际效果
// 变量声明: 不存在
IF_FEATURE_SUID(static uid_t ruid;) → (nothing)
// check_suid(): 变为空宏
#define check_suid(x) ((void)0)
// applet_suid[] 表: 不生成
// APPLET_SUID() 宏: 不定义
后果:
- 所有 applet 不做任何特权管理
- 如果 busybox 二进制是 setuid root,则每个 applet 都以 root 运行——这是巨大的安全漏洞(Config.in 帮助文本明确警告:"think cp /some/file /etc/passwd")
- 如果 busybox 二进制不是 setuid,则所有 applet 以普通用户运行——需要 root 的 applet 直接失败
6.3 FEATURE_SUID_CONFIG=n 的影响
// parse_config_file(): 退化为只保存 ruid
static inline void parse_config_file(void)
{
IF_FEATURE_SUID(ruid = getuid();)
}
// check_suid() 中: config 相关代码块被 #if 移除
// setresuid/setresgid 的精细控制逻辑不存在
// 只能使用硬编码的 BB_SUID_REQUIRE / BB_SUID_DROP
后果:
- 无法通过配置文件细粒度控制每个 applet 的权限
suid_config_t结构体、ingroup()函数、get_trimmed_slice()均不存在- 只支持二元的"保留 root / 放弃 root"模式,不支持"以指定用户身份运行"
6.4 FEATURE_PREFER_APPLETS=n 的影响
// BB_EXECVP / BB_EXECLP 退化为普通 libc 调用
#define BB_EXECVP(prog,cmd) execvp(prog,cmd)
#define BB_EXECLP(prog,cmd,...) execlp(prog,cmd,__VA_ARGS__)
// NOFORK / NOEXEC 优化全部关闭
#define APPLET_IS_NOFORK(i) 0
#define APPLET_IS_NOEXEC(i) 0
// spawn_and_wait() 回退到 fork+exec
// run_nofork_applet() / run_noexec_applet_and_exit() 全部编译剔除
后果:
- Shell 中的 busybox 内置命令需要真正 fork+exec(而非直接函数调用)
- 不影响权限模型,但影响性能和行为(进程名显示、/proc/self/exe 重执行等)
- 在 chroot 环境中(无 /proc)不会有
/proc/self/exe相关的兼容问题
6.5 Alpine Linux 的编译策略
Alpine 使用 BusyBox 作为核心系统工具集,其编译配置直接影响容器安全性。以下为常见配置趋势,具体应以目标版本的构建参数为准(可通过 zcat /proc/config.gz 或 busybox --help 查看实际 CONFIG_*):
- FEATURE_SUID 通常开启:提供基础的 suid 保护,避免 setuid busybox 导致所有 applet 获得 root
- FEATURE_SUID_CONFIG 可选:在需要精细化控制的场景中启用
- FEATURE_PREFER_APPLETS:不同 Alpine 版本配置不一,某些版本会关闭以减少对
/proc/self/exe的依赖 - bbsuid 分离:Alpine 系列发行版通常会将需要 setuid 的 applet 从普通 busybox 中分离,由
/bin/bbsuid(历史路径还包括/usr/libexec/bbsuid等)或类似机制承载。bbsuid本身仍是同一份 BusyBox 源码编译产物,并非独立实现的 su/login/passwd。该设计使绝大多数 applet 根本不会通过 setuid busybox 入口执行,因此在 Alpine 中check_suid()更多承担纵深防御角色,而非第一道防线。
以上为一般规律,具体配置应以目标发行版的
busybox版本及实际编译参数为准。不同 Alpine 版本间可能存在差异。
7. 设计总结
BusyBox 的权限管理分为三个层次:
- 编译时:每个 applet 通过
BB_SUID_REQUIRE/BB_SUID_DROP/BB_SUID_MAYBE声明自身对特权的需求 - 启动时(
check_suid):根据声明统一完成预处理——要求特权则验证、不需要特权则放弃、配置文件则精细控制 - 运行时:各 applet 自行调用
sanitize_env_if_suid()(环境清理)、xopen_as_uid_gid()(临时切换)、change_identity()(完整切换)、xsetuid(getuid())(特权释放)、xsetuid(0)(特权恢复)等函数
整体设计在安全性与功能完整性之间取得了平衡,特权最小化原则贯穿始终。
8. 安全意义
8.1 为什么 BusyBox 必须做特权分离
BusyBox 将数十个系统工具集中到单一二进制文件中。如果没有 check_suid():
┌─ /bin/busybox (setuid root) ─────────────────────┐
│ │
│ 任何一个用户都能通过任意 applet 获得 root: │
│ │
│ $ busybox cp /dev/null /etc/passwd ← root 写入│
│ $ busybox cat /etc/shadow ← root 读取│
│ $ busybox mv /bin/busybox /tmp ← root 操作│
│ │
└────────────────────────────────────────────────────┘
因此 check_suid() 的三层模型(REQUIRE / DROP / MAYBE)是 BusyBox 安全设计的基石:
- REQUIRE:必须使用 root 的程序保留特权(passwd、su、login)
- DROP:不需要特权的程序主动丢弃(cp、mv、cat、ls 等数百个 applet)
- MAYBE:不主动降权,是否保留特权取决于编译选项、内核 capability 及发行版配置(traceroute、ping、findfs 等)
8.2 从攻击者视角看
获得 euid=0 的 shell(如通过 SUID bash -p)
│
├── busybox cat → check_suid → DROP → setuid(ruid) → 降为普通用户
├── busybox ls → check_suid → DROP → setuid(ruid) → 降为普通用户
├── busybox id → check_suid → DROP → setuid(ruid) → 降为普通用户
├── busybox ash → check_suid → DROP → setuid(ruid) → 无法获得 root shell
├── busybox vi → check_suid → DROP → setuid(ruid) → 降为普通用户
├── traceroute → check_suid → MAYBE → 不主动降权
├── busybox passwd → check_suid → REQUIRE → 保持 euid=0 → root 身份
└── ./a(setuid(0)) → ruid=0 → check_suid 全部跳过 → 完整 root
攻击者在 euid=0 环境下,绝大多数 applet 会在 main() 之前主动放弃特权。真正保持 root 的只有:
- REQUIRE 类(6 个):passwd、su、login、vlock、crontab、wall
- MAYBE 类(数量依赖编译配置):常见包括 traceroute、ping、ping6、arping、ether-wake、findfs 等
其他全部 applet 都是 DROP,包括 ash、vi、cat、ls、id 等——无法直接利用。
8.3 Alpine 的纵深防御
Alpine 在此基础上增加了两层额外防护:
-
bbsuid 分离:将 BusyBox 编译为两份二进制——
/bin/busybox:无 setuid 位,承载所有 DROP 类 applet(绝大多数)/bin/bbsuid:带 setuid 位,仅承载 REQUIRE 类 applet(passwd、su、login 等)
这样
/bin/busybox本身不存在 euid=0 的攻击面,check_suid()和 DROP 分类更多是防御"万一 busybox 意外获得 setuid 位"的纵深措施。 -
SUID 分类收敛:BusyBox 1.38.0 中绝大多数 applet 被归类为 DROP,MAYBE 仅有 traceroute、ping、findfs 等少数几个。这意味着即使通过 SUID bash 获得 euid=0,可利用的 BusyBox 攻击面也局限在 REQUIRE(6 个)和 MAYBE(约 5 个)两类 applet 内。
附录:grep 定位命令速查
本附录可作为独立参考卡使用。建议用
grep -rn <pattern> --include='*.c' --include='*.h' .在 busybox 源码根目录下执行。
A.1 核心 UID 函数调用定位
# 定位所有 getuid / geteuid / getresuid 调用点
grep -rn '\bgetuid\b\|\bgeteuid\b\|\bgetresuid\b' --include='*.c' --include='*.h' .
# 定位所有 setuid / seteuid / setreuid / setresuid 调用点
grep -rn '\bsetuid\b\|\bseteuid\b\|\bsetreuid\b\|\bsetresuid\b' --include='*.c' --include='*.h' .
# 精确定位 setuid(0) 提权操作(恢复 root)
grep -rn '\bsetuid(0)\|\bxsetuid(0)\b' --include='*.c' --include='*.h' .
# 精确定位 setuid(getuid()) 降权操作(释放特权)
grep -rn 'setuid(getuid())' --include='*.c' --include='*.h' .
A.2 check_suid 及 Suid 模式定位
# 定位 check_suid 函数定义及所有调用点
grep -rn 'check_suid' --include='*.c' --include='*.h' .
# 定位三种 Suid 模式的使用
grep -rn 'BB_SUID_REQUIRE\|BB_SUID_DROP\|BB_SUID_MAYBE' --include='*.c' --include='*.h' .
# 定位 applet_suid 表生成逻辑
grep -n 'applet_suid\|APPLET_SUID' applets/applet_tables.c include/busybox.h
A.3 特权切换封装函数定位
# 定位 xsetuid / xseteuid / xsetgid / xsetegid 定义及调用
grep -rn '\bxsetuid\b\|\bxseteuid\b\|\bxsetgid\b\|\bxsetegid\b' --include='*.c' --include='*.h' .
# 定位 change_identity 调用链
grep -rn 'change_identity' --include='*.c' --include='*.h' .
# 定位 xopen_as_uid_gid 使用点
grep -rn 'xopen_as_uid_gid' --include='*.c' --include='*.h' .
# 定位 sanitize_env_if_suid 调用点
grep -rn 'sanitize_env_if_suid' --include='*.c' --include='*.h' .
A.4 权限检查定位(含 ruid/euid 比较)
# 定位 real uid == effective uid 比较(判断是否为 setuid 运行)
grep -rn 'getuid()\s*==\s*geteuid()\|getuid()\s*!=\s*geteuid()' --include='*.c' .
# 定位 getpwuid/getgrgid 与 UID 关联的调用
grep -rn 'getpwuid(getuid())\|xgetpwuid(getuid())' --include='*.c' .
# 定位非 root 拒绝的防护检查
grep -rn 'geteuid()\|\bgetuid()\s*!=\s*0\|you must be root\|perm_denied_are_you_root\|must be suid' --include='*.c' .
# 定位所有 you_must_be_root 消息的触发条件
grep -rn 'bb_msg_you_must_be_root' --include='*.c' .
A.5 /etc/busybox.conf 配置解析定位
# 定位 parse_config_file 函数
grep -rn 'parse_config_file\|busybox.conf' --include='*.c' --include='*.h' .
# 定位 setresuid 调用(含 saved uid 设置)
grep -rn 'setresuid' --include='*.c' --include='*.h' .
A.6 编译配置影响定位
# 定位 FEATURE_SUID 条件编译块
grep -rn 'FEATURE_SUID\|IF_FEATURE_SUID\|ENABLE_FEATURE_SUID' --include='*.c' --include='*.h' libbb/ applets/
# 定位 FEATURE_SUID_CONFIG 条件编译块
grep -rn 'FEATURE_SUID_CONFIG' --include='*.c' --include='*.h' libbb/
# 定位 FEATURE_PREFER_APPLETS 影响范围
grep -rn 'FEATURE_PREFER_APPLETS\|PREFER_APPLETS' --include='*.c' --include='*.h' libbb/
# 查看 Config.in 中的功能说明
grep -A 40 'config FEATURE_SUID$' Config.in
A.7 按文件精确跳转
# 核心特权管理文件 - 查看与 UID 相关的关键行
grep -n 'ruid\|check_suid\|parse_config\|APPLET_SUID\|BB_SUID' libbb/appletlib.c
# 环境清理 - 查看禁用变量列表
grep -n 'forbid\|sanitize_env' libbb/login.c
# 网络类 - 查看特权释放点
grep -n 'setuid\|setgid\|getuid\|geteuid\|xsetuid\|xsetgid' networking/traceroute.c networking/httpd.c networking/tcpudp.c networking/arping.c networking/ether-wake.c
# 用户管理类 - 查看身份切换点
grep -n 'setuid\|change_identity\|getuid\|myuid\|cur_uid' loginutils/passwd.c loginutils/su.c loginutils/login.c
# Shell 安全 - ash 中的 suid 检查
grep -n 'getuid\|geteuid\|setuid\|suid' shell/ash.c
A.8 快速统计
# 统计各文件中 UID 相关 API 调用次数
grep -rc '\bgetuid\|geteuid\|setuid\|seteuid\|setreuid\|setresuid\|getresuid\b' --include='*.c' . | sort -t: -k2 -rn | head -20
# 统计每种 Suid 模式的使用频次
grep -r 'BB_SUID_REQUIRE' --include='*.c' . | wc -l # REQUIRE 数量
grep -r 'BB_SUID_DROP' --include='*.c' . | wc -l # DROP 数量
grep -r 'BB_SUID_MAYBE' --include='*.c' . | wc -l # MAYBE 数量