元数据卡
- 前置知识: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 Socket | ping / traceroute |
CAP_SYS_ADMIN | 系统管理操作 | 大多敏感操作(危险) |
CAP_KILL | 向任何进程发送信号 | |
CAP_SETUID | 设置用户 ID | login / sudo |
给 nginx 只分配它需要的权限:
# 不用让 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:
# 用 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 的安全上下文:
# 查看进程的安全上下文
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)。
# 查看 SELinux 策略允许哪些交互
sesearch --allow --source httpd_t --target shadow_t
# 如果策略没允许,操作会被 SELinux 拦截(记录在 audit.log)
# ausearch -m avc -ts recentAppArmor
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 的学习模式:
# 先在学习模式下运行,记录所有访问
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.nginxSELinux vs AppArmor 选择:
| 维度 | SELinux | AppArmor |
|---|---|---|
| 粒度 | 安全上下文(类型) | 路径 + 权限 |
| 易用性 | 较复杂 | 更直观 |
| 分发 | 默认在 RHEL/CentOS/Fedora | 默认在 Ubuntu/Debian/openSUSE |
| 策略模型 | 全局类型强制 | 每个程序单独策略 |
| 学习曲线 | 陡峭 | 适中 |
第四层:seccomp(系统调用过滤)
再往下走一层——系统调用(syscall)是进程和内核的接口。每个进程通过 syscall 请求内核服务(读写文件、创建连接、分配内存)。
如果一个 Web 服务器只需要 read、write、accept 等几十个系统调用,为什么它要拥有调用 open、execve、socket 的能力?
seccomp(Secure Computing Mode) 允许进程限制自己能调用的系统调用。
// 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 多个不必要的系统调用:
# 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-appPython 中通过 prctl 或调用 libseccomp:
# 用 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 的二进制文件都是潜在的攻击面(如sudo、passwd)。检查并清理不必要的 SUID:
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 在等我们。下一章,我们走进网络安全的领域。