Skip to content

元数据卡

  • 前置知识:Vol 3 计算机系统(进程、内存、系统调用)、Linux 基本操作
  • 预计时间:55 分钟
  • 核心难度:进阶
  • 完成标志:能解释 DAC 与 MAC 的区别,能用 seccomp 限制进程系统调用,理解 Linux Capabilities 的权限切割思路

你的进度

你加固了驿站应用层的咒语防护。但所有防护咒语都运行在法师塔的操作法阵之上。如果邪术师攻破了驿站应用进程的权限,操作法阵没有足够的内置防御符文,他就能直接访问整座塔的魔力流、符文库和其他进程。

你想起 Vol 3 中见过的那张图:操作系统是管理所有硬件资源的守护者。如果守护者本身没有安全设计,所有上层保护都是浮沙筑塔。 你的任务

理解操作系统安全的核心机制。从最基本的 Unix 权限模型(DAC),到强制访问控制(MAC)、能力(Capabilities)和系统调用过滤(seccomp)。这些机制共同构成隔离和最小权限的基础设施。

本章分层

  • 必读:DAC(Unix 权限模型)、Linux Capabilities、seccomp
  • 选读:SELinux / AppArmor 的策略编写
  • 进阶:LSM(Linux Security Module)框架、命名空间与容器隔离

破局 · 溯源

问题:要塞的巡逻报告系统跑在一台服务器上。如果攻击者通过 SQL 注入或文件上传漏洞拿到了 shell,他能做什么?

答案是:取决于 web 应用进程以什么身份运行。

如果它运行在 root 下(很多早期配置和开发环境就是这样),攻击者获得了对整台机器的完全控制。如果它运行在 www-data 用户下,攻击者能读取该用户可以读的文件,写入该用户可以写的地方。

这就是最基础的自主访问控制(DAC, Discretionary Access Control)

第一层:Unix 权限模型(DAC)

每个文件有三组权限:

-rw-r--r--    1 www-data www-data   2048 Jun 24 10:00 patrol_reports.db

解读:

  • 第一个字符 -:普通文件(d 目录,l 链接)
  • rw-:所有者(www-data)有读写权限
  • r--:所属组(www-data 组)有读权限
  • r--:其他人只有读权限

权限位解释:

权限r (4)w (2)x (1)
文件读取内容修改内容执行(脚本/二进制)
目录列出文件列表创建/删除文件进入目录(cd)

DAC 的命名很准确:自主(Discretionary)——文件的所有者可以自主决定谁能访问他们的文件。所有者可以把文件权限设为 777 让所有人都能写。

问题是 DCA 的语义太粗糙了。要么有权限,要么没有。你不能说"这个进程只能绑定 80 端口,不能做其他 root 操作"。

第二层:Linux Capabilities

传统 Unix 中,root 用户(UID 0)拥有一切权限。如果你想让一个程序绑定 80 端口(<1024 的端口只有 root 能绑),你必须给它完全的 root 权限——而这远超过它需要的。

Capabilities 把 root 权限切分成细颗粒度的"能力":

Capability含义使用场景
CAP_NET_BIND_SERVICE绑定 <1024 端口Web 服务器
CAP_SYS_TIME修改系统时钟NTP 服务
CAP_DAC_OVERRIDE绕过文件权限检查备份工具
CAP_NET_RAW使用 RAW Socketping / traceroute
CAP_SYS_ADMIN系统管理操作大多敏感操作(危险)
CAP_KILL向任何进程发送信号
CAP_SETUID设置用户 IDlogin / sudo

给 nginx 只分配它需要的权限:

bash
# 不用让 nginx 以 root 运行
# 给它 CAP_NET_BIND_SERVICE 就够了

# 运行时设置(systemd 中)
# service 文件中:
# AmbientCapabilities=CAP_NET_BIND_SERVICE

# 用 setcap 给二进制文件设置
sudo setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx

# 查看已设置的 capabilities
getcap /usr/sbin/nginx
# 输出:/usr/sbin/nginx = cap_net_bind_service+ep

# 查看进程的 capabilities
cat /proc/<pid>/status | grep Cap

查看某个进程实际拥有的 capabilities:

bash
# 用 capsh 工具
capsh --decode=$(grep CapEff /proc/1/status | awk '{print $2}')

+ep 的含义:

  • e(Effective):当前生效
  • p(Permitted):允许使用的最大集合
  • i(Inheritable):子进程可以继承

第三层:强制访问控制(MAC)

DAC 的问题是:root 用户可以忽略一切权限规则。root 能读任何文件,杀任何进程。

MAC(Mandatory Access Control)彻底改变了模型:

  • 系统管理员定义全局安全策略
  • 即使是 root 用户也不能违反策略
  • 每个主体(进程)和客体(文件、端口、设备)都有安全标签

SELinux

SELinux(Security-Enhanced Linux)是 NSA 开发的 MAC 实现(2000 年开源,2003 年合入 Linux 主线)。

SELinux 的安全上下文:

bash
# 查看进程的安全上下文
ps -Z
# 系统输出: system_u:system_r:httpd_t:s0

# 查看文件的安全上下文
ls -Z /var/www/html/
# 输出: system_u:object_r:httpd_sys_content_t:s0

格式:user:role:type:sensitivity

SELinux 的核心是类型强制(Type Enforcement)。如果 httpd_t 类型的进程只能读 httpd_sys_content_t 类型的文件,那么即使 web 服务器被攻破(以 httpd_t 运行),攻击者也写不了 /etc/shadow(类型是 shadow_t)。

bash
# 查看 SELinux 策略允许哪些交互
sesearch --allow --source httpd_t --target shadow_t

# 如果策略没允许,操作会被 SELinux 拦截(记录在 audit.log)
# ausearch -m avc -ts recent

AppArmor

AppArmor 是 MAC 的另一种实现(比 SELinux 更注重路径策略而不是安全上下文):

# /etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>

/usr/sbin/nginx {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  /usr/sbin/nginx mr,
  /var/log/nginx/*.log w,
  /etc/nginx/** r,
  /var/www/html/** r,
  /run/nginx.pid w,
  
  # Deny these
  deny /etc/shadow r,
  deny /bin/bash r,
}

AppArmor 的学习模式:

bash
# 先在学习模式下运行,记录所有访问
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx

# 查看日志,生成合理策略
sudo tail -f /var/log/syslog | grep nginx

# 确认策略覆盖了所有合法操作后,切换到强制模式
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx

SELinux vs AppArmor 选择:

维度SELinuxAppArmor
粒度安全上下文(类型)路径 + 权限
易用性较复杂更直观
分发默认在 RHEL/CentOS/Fedora默认在 Ubuntu/Debian/openSUSE
策略模型全局类型强制每个程序单独策略
学习曲线陡峭适中

第四层:seccomp(系统调用过滤)

再往下走一层——系统调用(syscall)是进程和内核的接口。每个进程通过 syscall 请求内核服务(读写文件、创建连接、分配内存)。

如果一个 Web 服务器只需要 readwriteaccept 等几十个系统调用,为什么它要拥有调用 openexecvesocket 的能力?

seccomp(Secure Computing Mode) 允许进程限制自己能调用的系统调用。

c
// C 语言:用 seccomp 限制系统调用
// 编译:gcc -o sandbox sandbox.c -lseccomp

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

int main() {
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 默认杀死
    if (!ctx) return 1;

    // 允许最基本的系统调用
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
    // 明确禁止……
    // open、execve、socket、clone 都会被默认杀死

    seccomp_load(ctx);
    seccomp_release(ctx);

    // 从现在开始,只能使用上面列出的系统调用
    printf("Hello from sandbox!\n");

    // 尝试打开文件——进程会被内核杀死
    // FILE *f = fopen("/etc/passwd", "r"); // KILLED

    return 0;
}

在 Docker 和 Chrome 沙箱中,seccomp 是核心隔离机制。Docker 的默认 seccomp 配置禁止了 40 多个不必要的系统调用:

bash
# Docker 的默认 seccomp 配置
cat /etc/docker/seccomp/default.json | python3 -m json.tool | head -30

# 自定义 seccomp 配置运行容器
docker run --security-opt seccomp=my-custom-profile.json my-app

Python 中通过 prctl 或调用 libseccomp:

python
# 用 python-seccomp 库(pip install python-seccomp)
import seccomp
import sys

def setup_sandbox():
    filter = seccomp.SyscallFilter(seccomp.KILL)
    
    filter.add_rule(seccomp.ALLOW, "read")
    filter.add_rule(seccomp.ALLOW, "write")
    filter.add_rule(seccomp.ALLOW, "exit_group")
    filter.add_rule(seccomp.ALLOW, "futex")
    filter.add_rule(seccomp.ALLOW, "mmap")
    filter.add_rule(seccomp.ALLOW, "munmap")
    filter.add_rule(seccomp.ALLOW, "brk")
    filter.add_rule(seccomp.ALLOW, "clock_gettime")
    # ... 更多允许的调用
    
    filter.load()  # 设置后不可撤销

综合防线:纵深防御

一个安全的系统不在单层设防,而是多层叠在一起:

┌─────────────────────────────────────┐
│          应用层(认证、授权、编码)     │
├─────────────────────────────────────┤
│       seccomp(系统调用白名单)        │
├─────────────────────────────────────┤
│  Linux Capabilities(最小权限切割)    │
├─────────────────────────────────────┤
│    MAC(SELinux / AppArmor 策略)     │
├─────────────────────────────────────┤
│    DAC(Unix 用户/组/权限)            │
├─────────────────────────────────────┤
│        硬件 / 虚拟化隔离               │
└─────────────────────────────────────┘

即使攻击者攻破了 web 应用(第一层),seccomp 可能阻止他 execve(执行 shell),Capabilities 可能阻止他修改系统时间,SELinux 可能阻止他读 /etc/shadow,DAC 让他只能写给自己的目录。

每一层都是城墙的一道门。攻破一道不等于攻破全部。


常见陷阱

  • 进程以 root 运行然后降权。 很多程序用 root 启动、绑定端口,然后降权到普通用户。这可以,但要确保降权彻底:setuid + setgid + setgroups + 丢掉 capabilities。很多程序降权不彻底(只 setuid 没丢 capabilities)。
  • 只依赖 DAC。 大多数 Linux 发行版默认只有 DAC。SELinux/AppArmor 需要额外配置。如果你运行的是生产服务器,配置 MAC 是值得的。
  • 给容器完整的 --privileged 权限。 这几乎等于给了容器宿主 root 权限。明确指定 --cap-add 清单更安全。
  • 使用不完整的 seccomp 配置。 有些系统调用的危险性不直观(如 userfaultfd 可以绕过某些内存限制)。建议从 Docker 的默认 seccomp profile 开始修改。
  • 禁用 SELinux。 "不知道这玩意怎么配,先 setenforce 0"——这是最常见但最糟糕的做法。SELinux 在权限检查失败时的错误信息可以告诉你缺了什么权限,用 audit2allow 可以生成修复策略。
  • UNIX 的 SUID 二进制文件。 chmod u+s 让二进制文件以所有者身份运行。每个 SUID root 的二进制文件都是潜在的攻击面(如 sudopasswd)。检查并清理不必要的 SUID:
bash
find / -perm -4000 -type f 2>/dev/null

通关挑战

  • 热身:在 Linux 系统上运行 cat /proc/<pid>/status | grep Cap 解释输出。查看 CapEff 对应的权限含义。找一个不重要的进程,思考它有没有多余的权限。
  • 挑战:用 seccomp 写一个 Python 脚本,这个脚本可以计算 PI 到 10000 位,但不能读取任何文件或建立网络连接。注意你的脚本在 setup_sandbox 之前需要什么系统调用来加载 Python 解释器。
  • 观察:运行 docker run --rm alpine ping 8.8.8.8,然后运行 docker run --rm --cap-drop=NET_RAW alpine ping 8.8.8.8,观察两者的差异。用 strace 跟踪两者的系统调用差异。
  • 排障:你的 web 应用在 Ubuntu 18.04 上运行正常,迁移到 RHEL 9 后,Nginx 返回 403,但文件和权限看起来都正确。调试这个问题(考虑 SELinux 上下文迁移)。

旅人笔记

  • DAC(Unix 权限)是最基础的防线,但粒度太粗,对 root 没有约束
  • Linux Capabilities 把 root 权限切成小粒度(如 CAP_NET_BIND_SERVICE
  • MAC(SELinux/AppArmor)即使 root 也不能绕过策略——全局强制
  • seccomp 在系统调用层面做白名单过滤,是最内层的防御
  • 纵深防御:每一层都不能完全信任下一层或上一层

下一站预告

操作系统层的隔离让你能限制单个进程的能力。但进程之间怎么通信?数据怎么跨网络传输?网络攻击不只是应用层的 XSS 和 SQLi——防火墙、VPN、IDS/IPS 在等我们。下一章,我们走进网络安全的领域。

Built with VitePress | Software Systems Atlas