Linux内核分析之安全模块-01
This language version is unavailable; showing the other language.
53.1 SELinux 策略模型 —— TE、RBAC、MCS
SELinux 的策略模型由三层组成:类型强制(TE)是核心——"源类型对目标类型的类是否有权限";RBAC(角色)约束"哪些角色能进哪些域";MCS/M LS(多级/类别安全)做敏感性分级(容器用 MCS 隔离)。本节用一次实际判定解剖三层。
53.1.1 安全上下文的四段格式
安全上下文 (security context) 的格式:
user:role:type:level
──── ──── ──── ─────
u r t s0:c0.c5 (ML/MCS 级别+类别)
三个真实样例:
unconfined_u:unconfined_r:unconfined_t:s0 ← 无约束管理员
system_u:system_r:httpd_t:s0:c0,c2 ← Web 服务进程
system_u:object_r:shadow_t:s0 ← /etc/shadow 文件
主体 (进程): 上下文存在 cred 的 LSM blob (52.3.3 节 sid)
客体 (文件): 上下文存在 inode 的 xattr
security.selinux = "system_u:object_r:shadow_t:s0"
(33.1.2 节 inode 的 LSM 分租区存解析后的 sid)
type 是判定的主角:TE 规则只看"源 type → 目标 type + class + 权限",user/role 是管理约束(谁能进哪个域),level 是 MLS/MCS 层。
53.1.2 TE 规则:allow 与 neverallow
策略语言的核心规则 (refpolicy 语法):
# 允许规则: 源域 对 目标类型 的类 授予权限
allow httpd_t httpd_log_t:file { read append open getattr };
# 源域 目标类型 类 权限集
# 默认拒绝: 没有显式 allow 的一律拒绝
# (与 DAC 的"默认放行"哲学完全相反!)
# neverallow: 编译期断言 (比 allow 优先级更高的
# 禁令 — 策略编译器检查, 内核不参与)
neverallow unconfined_t shadow_t:file write;
类 (class) 与权限是内核定义的:
file: { read write append getattr setattr
open unlink rename execute ... }
dir: { search read write add_name remove_name ... }
socket/tcp_socket/process: 各自权限集
(约 60 个类, 每类数个~数十个权限 — 内核与
用户态策略头文件共享编号)
转换规则 (域迁移/文件打标的"自动导航"):
type_transition httpd_t httpd_log_t:file httpd_log_t;
# httpd 域进程在 httpd_log_t 目录建文件 →
# 新文件自动打 httpd_log_t (而非继承目录标)
domain_auto_login / type_change: 同思想的变体
53.1.3 RBAC 与 MCS:两层约束
RBAC (角色):
role httpd_r types httpd_t; ← httpd_r 角色只能进 httpd_t 域
allow httpd_r user_r; ← 角色切换授权
(用户态登录/切换时的"能变什么身份"约束)
MCS (Multi-Category Security, 容器在用):
每容器一对级别: s0:c1,c2 (两个数字类别)
规则: dom 类型间的 MLS 约束 — 读要求目标级别
≤ 主体, 写要求相等 (Bell-LaPadula 简化)
libvirt-lxc/docker -l 选项: 每容器随机 c 对
→ 同类型 (container_t) 的容器互不可读
(TE 相同 + MCS 不同 = 轻量隔离, 配额式的
"标签不同互不可见")
三层协同的全景:
用户态登录 → RBAC 决定可进的角色/域
进程运行 → TE 决定"每一步操作可否"
跨级交互 → MCS/MLS 加一道级别闸
策略规模:Fedora/RHEL 的系统策略编译后数 MB、数十万条 allow——策略即"系统全部行为的白名单数据库",这也是 SELinux 部署复杂度的根源(任何新服务都要策略作者为它建模)。
小结
SELinux 策略三层各司其职:TE(类型对类型+类+权限的 allow/neverallow,默认全拒)是判定核心,RBAC 约束域进入,MCS/MLS 以级别类别做轻量隔离(容器 c1/c2 对);安全上下文四段格式(u:r:t:level)在主体存 cred blob、客体存 inode xattr。模型的"默认拒绝+显式白名单"与 DAC 相反,是其安全强度与部署难度的共同来源。下一节看内核如何高速执行这套策略。
53.2 SELinux 内核实现 —— AVC 与策略数据库
内核不解析策略语言——用户态编译器把策略编译为二进制库,security_load_policy 装入内核的安全服务器(ss/ 子系统);运行期的每次判定先查 AVC(Access Vector Cache),未命中才进安全服务器查库并填缓存。本节解剖判定路径、缓存结构与策略装载。
53.2.1 判定路径:avc_has_perm
// security/selinux/avc.c(AVC 主体, 节选锚点)
static int avc_xperms_populate(struct avc_node *node, ...) /* :350 */
static void avc_node_delete(struct avc_node *node) /* :437 */
// security/selinux/include/security.h:94
struct selinux_state { ... /* 全局态: enforcing/策略载入/检查回调 */ };
// security/selinux/ss/services.c
/* security_compute_av: 策略库查询引擎 (未命中时调用) */
一次 security_has_perm 的内部:
avc_has_perm(ssid, tsid, tclass, requested):
[1] 构造键 (source sid, target sid, class)
[2] AVC 哈希表查找 (avc_node, RCU 读):
命中 → 检查 requested ⊆ allowed?
全部允许 → 放行 (0)
部分拒绝 → denied 集合 → -EACCES + 审计
[3] 未命中 → security_compute_av (ss/services.c):
遍历策略库的 avtab (allow 规则哈希表)
合并全部匹配规则的权限位 → 填新 avc_node
(neverallow 已在用户态编译期保证不冲突)
[4] permissive 态: 拒绝只记审计, 返回放行
(53.1 节调试模式)
AVC 规模: 默认 512 桶哈希, 节点 ~10 万级
命中率: 稳态 >99% — 判定成本 ≈ 一次哈希+比较
(这是 SELinux 能上生产的关键工程参数)
53.2.2 策略装载与 selinuxfs
策略的装载链 (系统启动期):
用户态 (semanage/semodule 编译好的 policy.bin)
→ mount selinuxfs (/sys/fs/selinux, 37 章家族)
→ write(policy.bin) 到 /sys/fs/selinux/load
→ selinuxfs.c 的 load 钩子
→ security_load_policy:
校验签名/版本 → 解析 avtab/类型表
→ 原子替换 ss->policydb (RCU)
→ AVC 全清 (旧缓存基于旧策略!)
→ selnl_notify_policyload (48.2.3 节
netlink 广播版本号 → 用户态缓存失效)
/sys/fs/selinux/enforce 强制/宽容切换 (写)
/sys/fs/selinux/context 读/改上下文 (安全服务解析)
(37.2 节"一人一值"的 sysfs 纪律在此同样适用)
重载的原子性:策略库替换是 RCU 指针切换+AVC 清空——判定中的旧查询用旧库完成,新查询用新库;"先装库后清缓存"的顺序保证无中间态。selinux_state(security.h:94)是全局开关集合(enforcing/loading/initialized)。
53.2.3 sid 的解析与缓存
上下文字符串 ↔ sid 的双向表:
security_context_to_sid("u:r:httpd_t:s0")
→ 字符串哈希表查 sid (数字, 内核内部只用 sid)
sid_to_security_context(sid) → 字符串 (审计用)
sid 存放位置:
cred blob → 主体 (52.3.3 节)
inode blob → 客体 (打开时从 xattr 读并缓存;
无 xattr 的 fs → fsuse 转换规则
或 genfscon 兜底, 53.3 节)
新建文件: security_inode_create 钩子 →
type_transition 规则 (53.1.2 节) 决定新 sid
→ 内核直接写 xattr (fs 支持时)
小结
SELinux 内核实现的三层分工:AVC(哈希缓存,命中率>99%——判定≈一次哈希)→ 安全服务器(avtab 策略库查询,未命中才走)→ selinuxfs(策略装载/状态切换接口);策略重载是"RCU 换库+清 AVC+netlink 广播"的原子序列,sid 的字符串↔数字双向表让热路径只碰整数。它把"数十万条规则的判定"压到纳秒级缓存查询——MAC 可用性的工程答案。下一节看标签从哪来、进程如何换域。
53.3 SELinux 文件打标与进程域迁移
策略模型(53.1)与判定引擎(53.2)之间还差两块现实拼图:文件的标签从哪来(xattr/转换规则/genfs 兜底)与进程如何进入自己的域(exec 时的域自动转换)。这两件事决定"系统里每个对象是否被正确建模"——SELinux 部署难度的实体。
53.3.1 文件打标:xattr 与三级兜底
文件标签的获取优先级 (打开文件时的解析):
[1] xattr security.selinux 存在 → 直接用
(ext4/xfs 等: 格式化后 setfiles/restorecon 批量打)
[2] fs_use 转换规则 (对无 xattr 的伪 fs):
fs_use_trans tmpfs system_u:object_r:tmp_t
fs_use_task pipefs ... (管道/socket 标签)
(26 章 FIFO / 28 章 IPC 的标签来源)
[3] genfscon 兜底 (网络 fs/陌生 fs):
genfscon nfs / system_u:object_r:nfs_t
(整挂载点一个标签 — 粗但可用)
打标工具链 (用户态):
semanage fcontext -a -t httpd_log_t "/var/log/httpd(/.*)?"
restorecon -Rv /var/log/httpd ← 按 policy 重打
ls -Z / matchpathcon 查看/预演
新建文件的标签 (53.2.3 节 security_inode_create):
type_transition 规则 (53.1.2) 或继承父目录标签
→ 内核写 xattr (fs 支持 security.* 时)
53.3.2 域迁移:exec 时的自动换域
域迁移 (domain transition) 的四个前提:
init (init_t) 启动 httpd 二进制
→ security_bprm_check / bprm_creds_for_exec
(52.1 节钩子) 检查四条规则全部满足:
[1] type_transition init_t httpd_exec_t:process httpd_t;
(执行 httpd_exec_t 类型文件 → 迁 httpd_t 域)
[2] allow init_t httpd_exec_t:file execute;
(有权执行该文件)
[3] allow httpd_t httpd_exec_t:file entrypoint;
(目标域声明"可从该文件进入")
[4] allow init_t httpd_t:process transition;
(源域授权迁移)
任何一条缺失 → 不迁移, 进程保持原域 (或拒绝执行)
exec 后: cred blob 的 sid 换成 httpd_t (52.3 节
prepare/commit), 后续判定全部按新域
setexeccon (libselinux): 应用显式指定子进程域
(systemd 单元的 SELinuxContext= 的机制)
setcon: 运行中直改 (需策略授权 — 少用)
域迁移 vs setuid 的对照:setuid 让进程"变成 root"(能力暴涨、无状态约束);域迁移让进程"变成 httpd_t"(只获得该域的策略白名单)——最小权限是规则推导的结果而非身份授予。守护进程链(systemd→httpd→CGI 脚本→数据库)每跳都有明确的迁移规则,攻击者拿下 httpd 也只活在 httpd_t 的世界里。
53.3.3 容器与 MCS 的实战拼装
容器场景的标签拼装 (libvirt-lxc/docker -l):
容器进程: system_u:system_r:container_t:s0:c1,c2
容器文件: system_u:object_r:container_file_t:s0:c1,c2
(container_t 是所有容器共用类型 —
靠 MCS 类别对区分: c1,c2 vs c3,c4 互不可见)
挂载卷打标: -v /data:/data 时自动打
container_file_t:s0:<容器类别> (卷只属于该容器)
排查: docker 事件拒绝 →
ausearch -m avc | grep s0:c1,c2
(类别不匹配 = 卷被另一容器打过标 — 常见错)
小结
打标与迁移补齐 SELinux 的现实面:文件标签按"xattr → fs_use → genfscon"三级解析,新文件由 type_transition 或继承自动打标,工具链 semanage/restorecon 批量维护;进程域迁移要求四条规则齐备(transition/execute/entrypoint/transition 授权),exec 时经 bprm 钩子自动换域——最小权限由策略推导而非身份授予;容器场景以共用 container_t + MCS 类别对实现轻量隔离。SELinux 三章至此完成——模型、引擎、现实拼图。