大部分 Linux 用户敲过上万次 sudo,但几乎没人能讲清楚这 10 个字符背后内核到底做了什么。本文把整条链路讲透——setuid、动态链接器擦环境变量、PAM 验证密码、susu - 区别、5 分钟免验证的来由,以及 2026 年 sudo-rs 在解决什么样的安全问题。

简短版:当你敲下 sudo 时,内核先做权限提升(eUID 0),动态链接器清掉一长串危险环境变量,PAM 用 /etc/shadow 验证你的密码,sudo 读 sudoers 看允许你跑什么,最后一次 execve 以目标用户身份跑命令。密码提示、15 分钟免验证、审计日志——所有这些都是这五步的”包装层”。

setuid 机制——/usr/bin/sudo 怎么变 root 的

/usr/bin/sudo/bin/su 的 mode 是 4755,owner 是 root:root4 是 set-user-ID bit(S_ISUID)。内核的 ELF loader(fs/binfmt_elf.c)加载一个 setuid 二进制时,调用 bprm_fill_uid(),设置:

  • Real UID(rUID): 保持调用者(你的普通用户)
  • Effective UID(eUID): 设为文件 owner 的 UID(root = 0)
  • Saved-set-UID: 也设为 root

普通二进制的 eUID 等于 rUID。setuid root 二进制的 eUID 是 0,但 rUID 还是你。内核把这个状态记到 auxv 里——AT_SECURE 被设为非零(man ld.so(8) 原文:“AT_SECURE may have a nonzero value … if the process’s real and effective user IDs differ, typically … a set-user-ID program”)。C runtime 看到这个标志后,所有 getenv() 调用都走 secure_getenv(),该函数检查 AT_SECURE,对动态链接器认为危险的所有环境变量返回 NULL。

动态链接器为 AT_SECURE 进程 scrub 掉的变量列表,是 POSIX、glibc、Unix 历史安全模型收敛出的同一份:

Bash
LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, GCONV_PATH, GETCONF_DIR,
HOSTALIASES, LOCALDOMAIN, NIS_PATH, NLSPATH, RESOLV_HOST_CONF,
RES_OPTIONS, TMPDIR, TZDIR

所以你 LD_PRELOAD=/tmp/evil.so sudo whatever,动态链接器忽略这个变量,sudo 跑在真实 libc 之上,你的 evil shim 永远不加载。这是 sudo第一层安全——内核强制,不是 sudo 自己的策略选择。

为什么必须 root-owned? mode 4755 只授予文件 owner 的 UID,而文件必须 owned by 你想变成的那个 user。sudosu 都 owned by root 因为它们要变成 root。如果 sudo owned by nobody,内核就只会把 eUID 设为 65534,没用。

为什么脚本不能 setuid? 脚本的 execve 通过 shebang 行重新进入解释器。内核不能信任 shebang,因为脚本文件是被另一个进程解释的,而内核无法强制解释器本身也是 setuid。现代 Linux 完全拒绝在解释脚本上 honor S_ISUID。chmod 4755 some-script.sh 在所有现代 Linux 上静默什么也不做。只有 native ELF 才获得权限提升。

审计日志里有什么? auditd 记录 type=USER_AUTHtype=USER_CMD 事件,exe="/usr/bin/sudo"exe="/bin/su"acct=<target>res=successres=failed。PAM 操作(op=PAM:authenticationop=PAM:session_openop=PAM:session_close)出现在外层 audit 记录的 msg='...' 字段里。你的安全团队 grep “谁跑了 sudo 什么时候” 就是在解析这个。

PAM——真正的认证步骤

sudo 拿到 eUID 0 之后,在 execve 目标命令之前,调用 PAM(Pluggable Authentication Modules)。PAM 是可加载模块框架——sudo 不知道怎么直接读 /etc/shadow,它问 PAM,PAM 加载一串模块做实际工作。

配置在 /etc/pam.d/sudo/etc/pam.d/su。Debian/Ubuntu 上典型的 /etc/pam.d/sudo 看起来像:

Bash
auth       include      common-auth
account    include      common-account
session    include      common-session-noninteractive

common-auth 才是真正列模块的地方。Debian/Ubuntu 典型栈:

Bash
auth        sufficient   pam_rootok.so
auth        [success=1 default=ignore] pam_uid.so
auth        requisite     pam_nologin.so
auth        @include      common-auth
auth        optional      pam_mail.so
account     include      common-account
password    include      common-password
session     required      pam_limits.so
session     required      pam_unix.so
session     optional      pam_systemd.so

从上到下读:pam_rootok.so 在调用者已经是 root 时跳过密码。pam_uid.so(如果有)验证调用者 UID 在白名单里。pam_nologin.so/etc/nologin 存在时拒绝登录。然后 common-auth include 进来,里面通常是 pam_unix.so/etc/shadow 验证密码。控制标志含义:

  • sufficient — 立即成功并跳过本栈其余模块
  • requisite — 立即失败并终止 auth
  • required — 失败则整栈失败,但继续跑剩余模块
  • optional — 除非这是唯一返回结果的模块,否则忽略

auth 成功后,PAM 转到 account 栈(账户是否允许登录、是否过期),然后 session 栈(应用资源限制、注册到 logind、记录 session 开始/结束)。sudo 的 session 栈以 root 身份在 target execve 之前跑,所以 pam_systemd.so 把子进程作为 transient scope 注册到 logind,pam_unix.sosession opened / session closed 日志行,pam_limits.so 应用 /etc/security/limits.conf 里的 ulimit。

/etc/pam.d/su 类似但通常多一行 pam_wheel.so,限制谁能 su 到 root(只允许 wheel 组成员)。Debian/Ubuntu 上惯例是 sudo 组(GID 27),对应的检查活在 /etc/pam.d/sudo 里。

整个 wheel 传统从 BSD 来(4.3BSD 1986),这个词比 Unix 还早——来自 1960 年代 TENEX/TOPS-20 的 “big wheel” group bit。1980 年代进 Unix,活到今天主要因为 PAM 还在用 pam_wheel.so

su vs su -——为什么有那个横杠

这是 su 家族最被低估的区别:

  • su target_user —— 非登录 shell。内核以 target_user 身份 exec /bin/bash不传 argv[0] = "-bash"。所以 /bin/bash~/.bashrc(或用户的 rc 文件),但不/etc/profile~/.bash_profile~/.bash_login。PATH、HOME、USER、LOGNAME、SHELL、MAIL 都保持调用者的值。最危险后果:调用者的 PATH 仍生效——调用者 PATH 里有人植的 binary 会以 target user 身份运行。

  • su - target_user(或 su -l target_user)—— 登录 shell。argv[0] 设为 -bash,内核的 PAM session 栈从 /etc/environmentpam_env,shell 读 /etc/profile~/.bash_profile~/.bash_login,把 PATH/UMASK/等重置为 target user 的默认值。pam_env 这一步是核心安全点——它显式 source /etc/environment 并应用 /etc/security/pam_env.conf 里的 env_keep / env_block 规则。

通用安全建议:在你不能完全信任的系统上用 su -,永远不要裸 su。在单人 dev 机上无所谓,在多用户生产环境上有所谓。

sudo vs su——配置文件

su/etc/shadow 认证(跟 login 一样)。sudo 既用 /etc/shadow(或 PAM 模块)认证,/etc/sudoers(只能 visudo 编辑)或 /etc/sudoers.d/ 里的 drop-in 文件。sudoers 行的格式:

Bash
user  HOST=(RUNAS_USER:RUNAS_GROUP)  COMMANDS

root ALL=(ALL:ALL) ALL 意思是”在任何 host 上,root 可以以任何 user/group 跑任何命令”。%sudo ALL=(ALL:ALL) ALL 意思是”sudo 组的任何 user 可以以任何 user 跑任何命令”(带默认 15 分钟免验证)。生产 sudoers 通常更紧——限定 user 能跑哪些命令、危险命令要重新认证、脚本自动化用 NOPASSWD: 标签。

15 分钟免验证Defaults timestamp_timeout 控制。man 5 sudoers“The default is 15.” 时间戳存在 BSD 系系统的 /var/db/sudo/ts/<user> 或 Linux 的 /var/run/sudo/ts/<user>(很小的二进制文件,不是数据库)。sudo -v 不跑命令、只重新验证时间戳;sudo -k 杀死时间戳,下一次 sudo 会重新提示密码。

env_reset 默认开。意思是 sudo 为目标命令构造一个最小环境:

Bash
TERM, PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,
HOME, MAIL, SHELL, LOGNAME, USER, USERNAME

调用者环境只有 env_keep 列出时才存活。这就是为什么 sudo FOO=BAR some_commandFOO 不在 env_keep 里时 FOO 会丢失。

LectureDefaults lecture=once|first|always;默认 once)就是”We trust you have received the usual lecture about the importance of password hygiene”那段你按空格跳过的字。每个 user 第一次跑 sudo 时显示一次。

sudoedit 路径:sudoedit /etc/hosts,sudo 用 root 打开一个临时副本,drop privileges 到调用者,跑你的 $EDITOR 编辑临时副本,存盘时原子性地 rename 临时文件回原位置。这样你的 editor 用非特权身份跑,但写入用 root 权限。CIS、DISA-STIG 这类 sudo 加固指南要求用 sudoedit 而不是 sudo $EDITOR,原因就在这。

run0doaspolkit——现代替代品

三个项目的存在是因为 setuid-root 工具二十年来一直是 CVE 工厂:

run0systemd v256(2024-06) 一起发布。不是 setuid——是 systemd-run 的 symlink。跑 run0 some_command 时,systemd-run 让 PID 1 把目标 spawn 为 transient unit。鉴权走 polkit,进程拿到一个新的 pseudo-TTY,终端可选择性染红。Lennart Poettering 的设计理念:“an execution context for privileged code that is half under the control of unprivileged code … is just not how security engineering should be done in 2024 anymore.”——把全公司的权限提升放在一个你不能逐行审计的 setuid binary 后面是 2024 年不该再有的设计。

doas 2015 年源于 OpenBSD(Ted Unangst)。~300 行 C,单一配置文件 /etc/doas.conf,语法 permit nopass user as root cmd /path/to/program。OpenBSD 的 doas 在 base system 里。Linux 移植版有——Alpine 和 Void 用 opendoas,其他发行版有用户维护的包。doas 比 sudo 简单太多——没有 lecture、没有 timestamp_timeout(默认每次重新提示)、没有花哨的日志基础设施——但也没有 sudoers 那么丰富的语义。

`polkit(前身 PolicyKit)是 D-Bus 鉴权守护进程,是 pkexec(GUI 应用)和 run0 的底层。规则在 /usr/share/polkit-1/actions/*.policy(XML)和 /etc/polkit-1/rules.d/*.rules(JavaScript,从 polkit 0.106 2012 年起)。polkit 是三者里最灵活的——能写依赖当前 session、时间、连接的智能卡的规则——但配置起来也最复杂。

sudo 的 CVE 历史(也是 sudo-rs 存在的原因)

sudo 的 CVE 记录就是 sudo-rs 存在的理由:

  • CVE-2021-3156 “Baron Samedit” —— 通过 sudoedit -s \ 触发参数 unescaping 的堆缓冲区溢出。任何本地用户,无需认证,root 提权。sudo 1.9.5p2 / 2021-01-26 修复。
  • CVE-2023-22809 —— sudoedit 错误处理 SUDO_EDITOR / VISUAL / EDITOR 环境变量,让有任意文件 sudoedit 权限的用户能编辑 /etc/shadow 或 sudoers。sudo 1.9.12p2 / 2023-01-19 修复。
  • CVE-2023-42465 —— 错误返回值上的 Rowhammer 位翻转;通过和 success 比而非 != error 修复。sudo 1.9.15p1 / 2023-12-21 修复。

sudo-rs 是 Trifecta Tech Foundation 出的内存安全 Rust 重写(最初 Prossimo 在 ISRG,2024-07 转到 TTF)。首次稳定版 2023-08-29Ubuntu 25.10(2025-10)把 sudo-rs 设为默认 sudo 实现Ubuntu 26.04 LTS 装的是 sudo-rs v0.2.13 + backports。sudo-rs 在 Debian 13、Fedora、Arch、NixOS、FreeBSD 也有包。重写不是为了快——是为了消灭产生 Baron Samedit 这类 CVE 的内存安全 bug 类型。

2026 年的 setuid 陷阱

内存安全重写也没法完全解决的三个问题:

  1. 非特权调用者传的 file descriptor 或 env handle 对提权后的 binary 仍然可见。 内核通过动态链接器 scrub LD_PRELOAD 等,但自定义 ELF dynamic tags 和 caller-provided argv 仍可能泄漏。
  2. LD_PRELOAD / LD_AUDIT 剥离只覆盖动态链接器知道的 env 变量。 直接调 getenv()(跳过 secure_getenv())的工具仍能读 caller 传的内容。这正是一些加固指南说”审计 setuid binary 的直接 getenv() 调用”的原因。
  3. 脚本在现代 Linux 上不能 setuid(已讲)。想要”root 脚本”的安全权限交付,正解是 sudoedit 或 polkit 规则,不是 setuid 脚本。

查审计日志

三个日志源,按有用度排序:

  • /var/log/audit/audit.log(启用 auditd 时)。ausearch -m USER_CMD,EXECVE -i(或 ausearch -k sudo_priv_cmd,如果你对 /usr/bin/sudo -p x 加了 audit rule)给你 per-command 历史,带 acct=<target>exe=/usr/bin/sudo、完整命令行、结果。
  • /var/log/auth.log(Debian/Ubuntu)或 /var/log/secure(RHEL/Fedora)。记录人类可读的 “user : TTY=pts/N ; PWD=… ; COMMAND=…” 行,accepted 和 rejected sudo 调用都有。
  • journalctl _COMM=sudo _COMM=su —— systemd-journald 的结构化视图,同 auth.log 事件。-f 实时跟踪,--since "1 hour ago" 限定时间。

TL;DR

sudosu 走同一条五步链:(1) 内核通过 S_ISUID bit 给 ELF binary 的 eUID 设为 0,(2) 动态链接器通过 AT_SECURE scrub 已知危险 env 变量,(3) PAM 用 /etc/shadow 验证密码,(4) binary 检查自己的策略文件(sudo 看 /etc/sudoers,su 没有策略文件——永远要 root 密码),(5) 第二次 execve 以目标 user 身份跑命令。5 分钟免验证、lecture、审计日志、env_reset——都是这五步上的包装层。CVE 历史证明这个设计是低层内存安全 bug 的持续来源——这就是为什么 Ubuntu 26.04 LTS 把 sudo-rs 设为默认。


参考资料(2026 年 7 月核对):man7.orgld.so(8)getenv(3)sudoers(5);Trifecta Tech Foundation sudo-rs 页面(Ubuntu 25.10 / 26.04 公告);sudo.ws 安全公告(CVE-2021-3156 / CVE-2023-22809 / CVE-2023-42465);LWN 2024-04 run0 报道;Linux-PAM FOSDEM 2026 演讲。具体值:timestamp_timeout=15(man 5 sudoers),sudo-rs 25.10 起在 Ubuntu 默认,run0 跟 systemd 256 一起发布。

最后修改: 2026年7月14日

作者

评论

发表评论

您的邮箱地址不会被公开。