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 三章至此完成——模型、引擎、现实拼图。