// 安全研究 · 2025-08-30

Linux SUID 机制完全指南:原理、实践与安全

Linux SUID 机制完全指南:原理、实践与安全

引言

在 Linux 权限体系中,SUID(Set User ID)是最具特色的设计之一。它允许普通用户以文件所有者的身份执行特定程序,从而在最小权限原则下完成原本需要更高权限的任务。本文将深入剖析 SUID 的工作机制、典型应用、潜在风险及最佳实践。


一、SUID 机制概述

1.1 什么是 SUID

SUID 是一种特殊的文件权限位。当可执行文件设置了 SUID 位后,任何用户执行该程序时,进程的有效用户 ID(EUID)会变为文件所有者的 UID,而非执行者的 UID。

flowchart LR
    subgraph Without_SUID[无 SUID 位]
        UserA[用户 alice<br/>UID=1001] --> Exec1[执行 /bin/cat]
        Exec1 --> Process1[进程 EUID=1001<br/>alice 的身份]
    end
    
    subgraph With_SUID[有 SUID 位]
        UserB[用户 bob<br/>UID=1002] --> Exec2[执行 /usr/bin/passwd]
        Exec2 --> Process2[进程 EUID=0<br/>root 的身份]
        Process2 --> Shadow["/etc/shadow<br/>属主 root:shadow"]
    end
    
    With_SUID -.->|SUID 使能| Shadow

1.2 SUID 的视觉表示

# 查看 SUID 文件
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 Dec 15 10:00 /usr/bin/passwd
#   ^ 
#   s 出现在所有者执行位,表示 SUID

# 数字表示法中的 SUID
chmod 4755 script    # 4 表示 SUID
# 4 对应二进制 100,即 SUID 位

1.3 权限位的完整解析

符号 含义 数字 说明
-rws------ SUID 已设置,原所有者有执行权限 4xxx 正常 SUID
-rwS------ SUID 已设置,原所有者无执行权限 4xxx 无效 SUID(大写 S)
-rwxr-xr-x 无 SUID 0xxx 普通文件

大写 S 的含义: 当文件所有者本身没有执行权限(x)时,设置 SUID 位会显示为大写 S,表示 SUID 位已设置但实际无效——因为进程需要执行权限才能启动,没有执行权限的 SUID 没有意义。


二、进程凭证与 SUID 机制

2.1 进程的用户 ID 体系

每个 Linux 进程维护三组 UID:

flowchart TB
    subgraph Process_UIDs[进程的三种 UID]
        RUID[Real UID - RUID<br/>真实用户 ID]
        EUID[Effective UID - EUID<br/>有效用户 ID]
        SUID[Saved UID - SUID<br/>保存的用户 ID]
    end
    
    subgraph RUID_Desc[RUID 说明]
        R1["执行进程的用户的 UID"]
        R2["通常等于 EUID"]
        R3["用于审计和信号发送"]
    end
    
    subgraph EUID_Desc[EUID 说明]
        E1["决定进程的权限"]
        E2["普通程序 = RUID"]
        E3["SUID 程序 = 文件所有者的 UID"]
    end
    
    subgraph SUID_Desc[Saved UID 说明]
        S1["保存 EUID 的副本"]
        S2["允许权限降级后恢复"]
        S3["seteuid() 系统调用使用"]
    end
    
    Process_UIDs --> RUID
    Process_UIDs --> EUID
    Process_UIDs --> SUID
    RUID --> RUID_Desc
    EUID --> EUID_Desc
    SUID --> SUID_Desc

2.2 SUID 对进程凭证的影响

// 以 /usr/bin/passwd 为例
// 文件:属主 root:root,权限 -rwsr-xr-x

// 普通用户 alice (UID=1001) 执行 passwd 时:

// exec() 之前的凭证(父进程 bash)
RUID = 1001
EUID = 1001

// exec() 加载 passwd 时,内核检测到 SUID 位
// 内核设置:
RUID = 1001      // 不变,仍是 alice
EUID = 0         // 变为文件所有者 root 的 UID
SUID = 0         // 保存为 root

// 进程现在以 root 权限运行,可以访问 /etc/shadow
// 但可以通过 setuid(1001) 主动降级权限

2.3 运行时查看进程凭证

# 查看进程的 UID 信息
cat /proc/self/status | grep -E "Uid|Gid"
# Uid:    1000    1000    1000    1000
#          ^       ^       ^       ^
#          RUID    EUID    SUID    FSUID

# 使用 ps 查看
ps -eo pid,uid,euid,ruid,suid,comm | grep passwd

字段说明:

位置 字段 含义
1 RUID 真实用户 ID
2 EUID 有效用户 ID
3 SUID 保存的用户 ID
4 FSUID 文件系统用户 ID

三、SUID 的工作原理

3.1 内核视角的 SUID

flowchart TD
    Start["用户执行 execve()"] --> Load["加载可执行文件"]
    Load --> Check{"检查文件的<br/>SUID 位?"}
    
    Check -->|未设置| Normal["新进程的 EUID = RUID"]
    Check -->|已设置| SUIDSet["新进程的 EUID = 文件所有者 UID"]
    
    SUIDSet --> Save["SUID = EUID"]
    Normal --> Run["进程运行"]
    Save --> Run
    
    Run --> Access{"执行需要特权的操作"}
    Access -->|需要| KernelCheck["内核检查 EUID"]
    KernelCheck -->|"EUID=0"| Permit["允许操作"]
    KernelCheck -->|"EUID≠0"| Deny["拒绝操作"]

3.2 SUID 程序的标准模式

一个编写良好的 SUID 程序通常遵循以下模式:

#include <unistd.h>
#include <stdio.h>

int main() {
    // 步骤1:获取原始用户
    uid_t original_uid = getuid();  // RUID = 执行者
    
    // 步骤2:以 root 权限执行必要操作
    // 例如:打开 /etc/shadow
    
    // 步骤3:立即降级权限
    if (setuid(original_uid) != 0) {
        perror("setuid failed");
        return 1;
    }
    
    // 步骤4:以普通用户权限完成剩余工作
    // 后续操作不再具有特权
    
    return 0;
}

3.3 /usr/bin/passwd 的实际案例

// 简化的 passwd 实现逻辑
int main(int argc, char *argv[]) {
    uid_t ruid = getuid();  // 保存原始用户
    
    // 以 root 身份打开 shadow 文件(仅此步骤需要特权)
    FILE *shadow = fopen("/etc/shadow", "r");
    if (!shadow) {
        perror("fopen");
        return 1;
    }
    
    // 立即丢弃 root 权限
    setuid(ruid);
    
    // 以普通用户身份处理密码修改逻辑
    // ... 正常流程 ...
    
    return 0;
}

四、查找系统中的 SUID 文件

4.1 标准查找命令

# 查找所有 SUID 文件
find / -perm -4000 -type f 2>/dev/null

# 查找 SUID 和 SGID 文件
find / -perm -6000 -type f 2>/dev/null

# 使用更精确的语法
find / -perm /4000 -type f -ls 2>/dev/null

# 仅查找特定目录
find /usr/bin -perm -4000 -type f -ls

4.2 典型系统上的 SUID 程序

# 常见 SUID 文件列表
ls -l /usr/bin/passwd          # 修改密码
ls -l /usr/bin/sudo            # 以其他用户身份执行命令
ls -l /usr/bin/su              # 切换用户
ls -l /usr/bin/pkexec          # PolicyKit 执行代理
ls -l /usr/bin/gpasswd         # 管理 /etc/group
ls -l /usr/bin/chfn            # 修改 finger 信息
ls -l /usr/bin/chsh            # 修改登录 Shell
ls -l /usr/bin/mount           # 挂载文件系统(某些系统)
ls -l /usr/bin/umount          # 卸载文件系统(某些系统)
ls -l /bin/ping                # 需要原始套接字权限
ls -l /bin/su                  # 切换用户

4.3 不同发行版的差异

发行版 SUID 程序特点
Debian/Ubuntu mount/umount 通常无 SUID,使用 fuse 或 udisks
RHEL/CentOS mount/umount 保留 SUID
Alpine Linux 极简 SUID 集合,仅 busybox 相关程序
Arch Linux 传统 SUID 程序保留,部分改用 Capabilities

五、SUID 与 Linux Capabilities 的关系

5.1 为什么需要 Capabilities

SUID 机制过于“粗暴”——它将整个 root 权限赋予进程,而进程可能只需要其中一项特权。

flowchart LR
    subgraph SUID_Model[SUID 模型]
        S_Prog[SUID 程序<br/>/bin/ping]
        S_Exec[执行后进程]
        S_Root[EUID=0<br/>完整 root 权限]
        S_Need[实际只需要<br/>CAP_NET_RAW]
    end
    
    subgraph Cap_Model[Capabilities 模型]
        C_Prog[普通文件<br/>/bin/ping]
        C_Exec[执行后进程]
        C_Cap[仅有 CAP_NET_RAW]
        C_Other[其他 root 能力<br/>全部被剥夺]
    end
    
    S_Prog --> S_Exec --> S_Root
    S_Root -.->|过度授权| S_Need
    
    C_Prog --> C_Exec --> C_Cap
    C_Cap -.->|最小权限| C_Other

5.2 用 Capabilities 替代 SUID

# 查看文件的能力集
getcap /bin/ping
# /bin/ping = cap_net_raw+ep

# 传统 SUID 方式(不安全)
ls -l /bin/ping
# -rwsr-xr-x 1 root root

# 更安全的能力方式(新发行版)
sudo setcap cap_net_raw+ep /bin/ping
sudo chmod u-s /bin/ping  # 移除 SUID

能力位的含义:

后缀 含义
+ep Effective + Permitted(有效位和允许位)
+ei Effective + Inheritable
+p Permitted only

5.3 检查进程的能力

# 查看进程的能力
cat /proc/self/status | grep Cap
# CapInh: 0000000000000000
# CapPrm: 0000000000000000
# CapEff: 0000000000000000
# CapBnd: 000001ffffffffff
# CapAmb: 0000000000000000

# 使用 capsh 解码
capsh --decode=000001ffffffffff

# 使用 getpcaps 查看进程能力
getpcaps $$

六、SUID 程序的安全风险

6.1 风险分类

mindmap
  root((SUID 安全风险))
    
    环境变量污染
      PATH 劫持
      LD_PRELOAD 注入
      IFS 变量篡改
      SHELL 变量利用
    
    内存破坏
      缓冲区溢出
      格式化字符串
      整数溢出
      Use-After-Free
    
    逻辑漏洞
      不安全的环境传递
      不彻底的权限降级
      竞争条件 TOCTOU
      符号链接跟随
    
    配置问题
      不应 SUID 的程序
      脚本语言 SUID
      不安全的所有者
      错误的文件系统挂载

6.2 经典漏洞示例

环境变量污染 - PATH 劫持:

// 有漏洞的 SUID 程序
#include <stdlib.h>
int main() {
    // 危险:使用 system() 且依赖 PATH
    system("ls -la /tmp");
    return 0;
}
# 攻击方法
echo "/bin/sh" > /tmp/ls
chmod +x /tmp/ls
export PATH=/tmp:$PATH
./vulnerable_suid_program
# 获得 root shell

LD_PRELOAD 注入:

# 编写恶意共享库
cat > evil.c << EOF
#include <unistd.h>
#include <sys/types.h>
uid_t getuid() { return 0; }
EOF

gcc -shared -fPIC evil.c -o evil.so

# 注入到 SUID 程序(现代内核会阻止)
export LD_PRELOAD=./evil.so
/path/to/suid_program

现代防御机制:

# 内核检查 LD_PRELOAD 在 SUID 程序中的使用
# 当检测到程序有 SUID 位时,会忽略 LD_PRELOAD
# 以及以下环境变量:
# - LD_LIBRARY_PATH
# - LD_ORIGIN_PATH
# - LD_AUDIT
# - LD_PROFILE

6.3 竞争条件(TOCTOU)

// Time of Check to Time of Use 漏洞
if (access("/etc/important", W_OK) == 0) {
    // 检查通过后,在 open 之前
    // 攻击者可以将 /etc/important 改为符号链接到 /etc/shadow
    fd = open("/etc/important", O_WRONLY);
    write(fd, data, len);
}

6.4 防御机制

# 查看系统对 SUID 的额外限制
sysctl kernel.dmesg_restrict
sysctl fs.suid_dumpable

# 某些文件系统挂载禁用 SUID
mount -o nosuid /dev/sda1 /mnt

# /etc/fstab 中的 nosuid 选项
/dev/sda1 /home ext4 defaults,nosuid 0 2

七、创建安全的 SUID 程序

7.1 基本原则

flowchart TB
    subgraph Rules[SUID 程序安全准则]
        R1[1. 最小化特权代码]
        R2[2. 尽早降级权限]
        R3[3. 清理环境变量]
        R4[4. 使用绝对路径]
        R5[5. 禁用不安全函数]
        R6[6. 验证所有输入]
        R7[7. 不要执行外部程序]
        R8[8. 设置安全的 umask]
    end

7.2 安全实现模板

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    // 1. 保存原始用户
    uid_t original_uid = getuid();
    uid_t original_euid = geteuid();
    
    // 2. 清理危险的环境变量
    char *unsafe_vars[] = {
        "LD_PRELOAD", "LD_LIBRARY_PATH",
        "LD_ORIGIN_PATH", "LD_AUDIT",
        "LD_PROFILE", NULL
    };
    for (char **var = unsafe_vars; *var != NULL; var++) {
        unsetenv(*var);
    }
    
    // 3. 重置 PATH 为安全值
    setenv("PATH", "/bin:/usr/bin", 1);
    
    // 4. 设置安全的 umask
    umask(077);
    
    // 5. 执行必要的特权操作
    // ... 仅此处需要 root 权限 ...
    
    // 6. 立即降级权限
    if (setuid(original_uid) != 0) {
        fprintf(stderr, "Failed to drop privileges\n");
        return 1;
    }
    
    // 7. 验证降级成功
    if (seteuid(0) == 0) {
        fprintf(stderr, "Failed to properly drop privileges\n");
        return 1;
    }
    
    // 8. 以普通用户身份继续
    // ... 业务逻辑 ...
    
    return 0;
}

7.3 检查已创建的 SUID 文件

# 创建测试程序
gcc -o suid_test suid_test.c
sudo chown root:root suid_test
sudo chmod 4755 suid_test

# 验证 SUID 位
ls -l suid_test
# -rwsr-xr-x 1 root root

# 测试执行
./suid_test
id
# uid=1000(alice) euid=0(root) ...

八、SUID 的现代替代方案

8.1 方案对比

方案 原理 安全性 复杂度
SUID 整个进程获得文件所有者权限 较低 简单
Capabilities 仅获得特定能力位 中等 中等
Polkit 基于 D-Bus 的细粒度授权 较高 复杂
sudo 基于配置文件的命令授权 较高 中等
守护进程 特权进程通过 IPC 提供服务 较高 复杂

8.2 Capabilities 替代示例

# 传统方式
sudo chown root:root myprogram
sudo chmod 4755 myprogram

# 现代方式
sudo setcap cap_net_admin,cap_net_raw+ep myprogram
sudo chmod 755 myprogram

8.3 守护进程模式

// 特权守护进程
// 以 root 启动,监听 Unix 套接字
// 普通客户端通过套接字请求服务

// 守护进程端
int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(server_fd, 5);

while (1) {
    int client_fd = accept(server_fd, NULL, NULL);
    // 读取客户请求
    // 以 root 权限执行操作
    // 返回结果
}
# 客户端连接
socat - UNIX-CONNECT:/var/run/privdaemon.sock

九、审计与监控

9.1 定期审计命令

# 查找所有 SUID 文件并生成报告
find / -perm -4000 -type f -exec ls -la {} \; 2>/dev/null > suid_report.txt

# 检查新增的 SUID 文件
find / -perm -4000 -type f -ctime -7 -ls 2>/dev/null

# 对比审计
# 昨天
find / -perm -4000 -type f -ls 2>/dev/null > suid_yesterday.txt
# 今天
find / -perm -4000 -type f -ls 2>/dev/null > suid_today.txt
diff suid_yesterday.txt suid_today.txt

9.2 自动化审计脚本

#!/bin/bash
# SUID 审计脚本

REPORT_FILE="/var/log/suid_audit_$(date +%Y%m%d).log"

echo "=== SUID File Audit Report ===" > $REPORT_FILE
echo "Date: $(date)" >> $REPORT_FILE
echo "" >> $REPORT_FILE

# 查找所有 SUID 文件
find / -type f -perm -4000 2>/dev/null | while read file; do
    # 检查文件的 MD5 哈希
    md5=$(md5sum "$file" 2>/dev/null | cut -d' ' -f1)
    perms=$(ls -l "$file" 2>/dev/null)
    owner=$(stat -c "%U:%G" "$file" 2>/dev/null)
    
    echo "File: $file" >> $REPORT_FILE
    echo "  Permissions: $perms" >> $REPORT_FILE
    echo "  Owner: $owner" >> $REPORT_FILE
    echo "  MD5: $md5" >> $REPORT_FILE
    echo "" >> $REPORT_FILE
done

echo "Report saved to $REPORT_FILE"

9.3 实时监控

# 使用 auditd 监控 SUID 相关操作
auditctl -w /usr/bin -p wa -k suid_change
auditctl -w /bin -p wa -k suid_change

# 查看审计日志
ausearch -k suid_change

十、常见问题与排查

10.1 SUID 不生效的原因

问题 原因 解决方案
显示大写 S 所有者没有执行权限 chmod u+x file
挂载点禁用 SUID nosuid 挂载选项 检查 mount | grep nosuid
脚本无法使用 SUID 解释器会忽略 SUID 改用二进制程序
容器内不生效 容器安全策略 检查容器的 --security-opt

10.2 调试 SUID 程序

# 查看进程的实际 UID
ps -eo pid,uid,euid,ruid,suid,comm | grep myprogram

# 使用 strace 跟踪系统调用
strace -u alice -e trace=open,setuid ./suid_program

# 使用 gdb 调试 SUID 程序(需要特殊处理)
sudo gdb ./suid_program
(gdb) set follow-fork-mode child
(gdb) run

10.3 禁用系统 SUID

# 全局禁用 SUID(危险,可能导致系统异常)
mount -o remount,nosuid /

# 仅禁用特定分区
# 在 /etc/fstab 中添加 nosuid 选项
UUID=xxx /home ext4 defaults,nosuid 0 2

总结

SUID 机制是 Linux 权限体系中的重要组件,它体现了最小权限原则在实际系统中的灵活应用:

核心价值:

  • 允许普通用户在受控范围内执行特权操作
  • 无需修改内核或添加复杂的权限框架
  • 数十年来稳定运行于各类 Unix/Linux 系统

安全要点:

  • SUID 程序必须编写严谨,遵循安全最佳实践
  • 应优先考虑 Capabilities 等更精细的替代方案
  • 定期审计系统中的 SUID 文件是安全基线

演进趋势:

  • 新程序优先使用 Capabilities 或 Polkit
  • 容器环境对 SUID 有额外限制
  • 现代 Linux 发行版逐步减少 SUID 程序数量

理解 SUID 机制不仅有助于使用 passwd、sudo 等日常工具,更是深入掌握 Linux 权限体系的关键一步。

原文 https://blog.csdn.net/2301_79518550/article/details/150791394