# 内核模块自动加载:绕过命名空间权限限制 ## 问题 在用户命名空间中,即使映射了 root (UID 0) 并拥有完整 capabilities,也无法直接加载内核模块: ```bash $ unshare -U --map-root-user modprobe geneve modprobe: ERROR: could not insert 'geneve': Operation not permitted ``` `strace` 确认 `finit_module` 返回 `EPERM`: ``` finit_module(3, "", MODULE_INIT_COMPRESSED_FILE) = -1 EPERM (Operation not permitted) ``` **根因**:内核 `finit_module` 使用 `capable(CAP_SYS_MODULE)` 做权限检查,`capable()` 以 **初始用户命名空间** (`&init_user_ns`) 为基准。用户命名空间中的 capability 只在该命名空间内有效,不被 `capable()` 认可。 ## 原理:request_module() 窃取 init_ns 权限 内核存在两条权限检查路径: | 检查函数 | 基准命名空间 | 用户命名空间中的结果 | |---------|-------------|-------------------| | `capable(CAP_*)` | `init_user_ns` | **拒绝** | | `ns_capable(net->user_ns, CAP_*)` | 当前命名空间 | **通过**(如有该 cap) | `request_module()` 是内核的模块自动加载机制。当内核代码发现某个功能(协议族、网络设备类型、XFRM 类型等)未注册时,调用 `request_module()` 触发 modprobe。 **关键**:`request_module()` 内部通过 `call_usermodehelper` 在 **初始用户命名空间** 中执行 `/sbin/modprobe`,此 modprobe 进程拥有 `init_user_ns` 的 `CAP_SYS_MODULE`,因此 `finit_module` 成功。 ``` 用户命名空间 (有 CAP_NET_ADMIN) │ ├─ ip link add type geneve ← ns_capable() 通过 │ └─ rtnetlink.c: request_module("rtnl-link-geneve") │ └─ call_usermodehelper ← 在 init_user_ns 中运行 │ └─ modprobe rtnl-link-geneve │ └─ finit_module() ← CAP_SYS_MODULE in init_ns → 成功! │ ├─ 直接 modprobe geneve ← finit_module 在当前 ns │ └─ capable(CAP_SYS_MODULE) ← 检查 init_ns → 失败! │ └─ 结果: 直接 modprobe = EPERM, 触发 autoload = 成功 ``` --- ## 案例 1:geneve.ko — 完整演示 ### 触发机制 | 项 | 值 | |---|-----| | 目标模块 | `geneve.ko` | | 依赖模块 | `udp_tunnel.ko`, `ip6_udp_tunnel.ko` | | 触发路径 | `ip link add type geneve` → `rtnetlink.c:4041` → `request_module("rtnl-link-geneve")` | | 别名定义 | `drivers/net/geneve.c:MODULE_ALIAS_RTNL_LINK("geneve")` | | 权限检查 | `rtnetlink.c:3928`: `netlink_ns_capable(skb, net->user_ns, CAP_NET_ADMIN)` ← `ns_capable`,非 `capable` | ### 源码分析 **权限检查点 (rtnetlink.c:3928)**: ```c // 使用 ns_capable 检查当前用户命名空间的 CAP_NET_ADMIN if (!netlink_ns_capable(skb, net->user_ns, CAP_NET_ADMIN)) return -EPERM; ``` **自动加载触发 (rtnetlink.c:4041)**: ```c if (kind != RTNL_KIND_GET && !ops) { request_module("rtnl-link-%s", kind); // 触发 modprobe ops = rtnl_link_ops_get(kind); } ``` **模块别名声明 (drivers/net/geneve.c)**: ```c MODULE_ALIAS_RTNL_LINK("geneve"); ``` ### 运行结果 ``` 步骤 1: 直接 modprobe(失败) ────────────────────────────────────────────────────────────── $ unshare -U --map-root-user strace -f -e finit_module modprobe geneve 2>&1 finit_module(3, "", MODULE_INIT_COMPRESSED_FILE) = -1 EPERM modprobe: ERROR: could not insert 'geneve': Operation not permitted $ lsmod | grep geneve [空] 步骤 2: ip link add 触发自动加载(成功) ────────────────────────────────────────────────────────────── $ unshare -U -n --map-root-user ip link add geneve0 \ type geneve remote 10.0.0.1 id 100 $ lsmod | grep -E '^geneve|udp_tunnel' geneve 53248 0 ip6_udp_tunnel 16384 2 geneve,rxrpc udp_tunnel 40960 2 geneve,rxrpc ``` **结果**:`geneve.ko` 及其依赖 `udp_tunnel.ko`、`ip6_udp_tunnel.ko` 全部加载成功。这些模块是通过 `call_usermodehelper` 在 init_user_ns 中由 modprobe 加载的,绕过了用户命名空间的 `EPERM` 限制。 --- ## 案例 2:tls.ko — 通过依赖链加载 ### 问题:tls.ko 的直接触发路径被 `capable()` 阻断 `tls.ko` 的直接自动加载入口在 TCP ULP 层: **tcp_ulp.c:42**: ```c if (!ulp && capable(CAP_NET_ADMIN)) { // ← capable(), 不是 ns_capable()! rcu_read_unlock(); request_module("tcp-ulp-%s", name); // request_module("tcp-ulp-tls") rcu_read_lock(); ulp = tcp_ulp_find(name); } ``` 这里使用的是 `capable(CAP_NET_ADMIN)`(检查 init_user_ns),所以即使用户命名空间有 `CAP_NET_ADMIN`,`setsockopt(TCP_ULP, "tls")` 也不会触发自动加载。 ### 绕过:bonding 依赖链 但 `tls.ko` 可以通过**模块依赖**间接加载。`modules.dep` 中定义: ``` kernel/drivers/net/bonding/bonding.ko.xz: kernel/net/tls/tls.ko.xz ``` 即 **bonding → tls**。当 bonding 被加载时,modprobe 先加载其依赖 tls。 ### 触发 bonding 自动加载 bonding 的自动加载入口使用 `ns_capable`: **rtnetlink.c:3928**: ```c if (!netlink_ns_capable(skb, net->user_ns, CAP_NET_ADMIN)) return -EPERM; ``` 用户命名空间中的 `CAP_NET_ADMIN` 可以通过此检查。 ### 别名定义 **drivers/net/bonding/bonding_main.c**: ```c MODULE_ALIAS_RTNL_LINK("bond"); ``` ### 完整调用链 ``` 用户命名空间 │ ├─ setsockopt(TCP_ULP, "tls") ← capable(CAP_NET_ADMIN) = 否 → 阻断 │ └─ ip link add type bond ← ns_capable(net->user_ns, CAP_NET_ADMIN) = 是 → 通过 └─ rtnetlink.c: request_module("rtnl-link-bond") └─ modprobe rtnl-link-bond ← call_usermodehelper in init_user_ns ├─ modprobe tls ← 依赖先加载 │ └─ finit_module → init_ns CAP_SYS_MODULE → 成功 └─ modprobe bonding └─ finit_module → 成功 ``` ### 运行结果 ``` 步骤 1: 命名空间内直接 modprobe tls(失败) ────────────────────────────────────────────────────────────── $ unshare -U --map-root-user modprobe tls modprobe: ERROR: could not insert 'tls': Operation not permitted $ lsmod | grep tls [空 — 未加载] 步骤 2: setsockopt(TCP_ULP, "tls") 不会触发自动加载(capable 阻断) ────────────────────────────────────────────────────────────── $ unshare -U --map-root-user python3 -c " import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.IPPROTO_TCP, 31, b'tls') # TCP_ULP=31 " setsockopt 失败: [Errno 2] No such file or directory $ lsmod | grep tls [空 — tls 仍未加载,request_module 被 capable(CAP_NET_ADMIN) 阻断] 步骤 3: ip link add type bond 触发自动加载(成功) ────────────────────────────────────────────────────────────── $ unshare -U -n --map-root-user ip link add bond0 type bond $ lsmod | grep -E '^bonding|^tls' bonding 303104 0 tls 176128 1 bonding $ modinfo bonding | grep depends depends: tls $ grep 'bonding.ko' /lib/modules/7.0.9-205.fc44.x86_64/modules.dep kernel/drivers/net/bonding/bonding.ko.xz: kernel/net/tls/tls.ko.xz ``` `tls.ko` 通过 bonding → tls 依赖链被间接加载。用户从未直接触发 tls 加载,也未通过 `capable(CAP_NET_ADMIN)` 检查。 **三种路径对比**: | 路径 | 命令 | 权限检查 | 结果 | |------|------|---------|------| | 直接 modprobe | `modprobe tls` | `capable(CAP_SYS_MODULE)` → init_ns | **失败 (EPERM)** | | 直接 autoload | `setsockopt(TCP_ULP, "tls")` | `capable(CAP_NET_ADMIN)` → init_ns | **失败 (request_module 被阻断)** | | 依赖链加载 | `ip link add type bond` | `ns_capable(CAP_NET_ADMIN)` → 当前 ns | **成功 (tls 随 bonding 依赖加载)** | --- ## 通用化:N × M 自动加载矩阵 攻击面不只是单一模块,而是所有可以被自动加载的模块 × 所有依赖链。 ### 触发侧(使用 `ns_capable` 的入口) | 自动加载别名 | 触发方式 | 权限 | 示例模块 | |------------|---------|------|---------| | `net-pf-%d` | `socket(family, ...)` | **无** | rxrpc, kcm, smc, mctp | | `rtnl-link-%s` | `ip link add type X` | `ns_capable` CAP_NET_ADMIN | geneve, vxlan, bond, bridge, macsec | | `xfrm-type-%d-%d` | `ip xfrm state add ... proto esp` | `ns_capable` CAP_NET_ADMIN | esp4, esp6 | | `tcp-ulp-%s` | `setsockopt(TCP_ULP, name)` | `capable` → **阻断** | — | ### 被加载侧(通过依赖链间接加载的模块) | 依赖模块 | 通过谁加载 | 触发命令 | |---------|-----------|---------| | `tls.ko` | `bonding.ko` | `ip link add type bond` | | `rxrpc.ko` | `socket(AF_RXRPC, ...)` (直接) | Python `socket.socket(33, 2, 2)` | | `krb5.ko` | `rxrpc.ko` (依赖) | 同上,自动连带 | | `udp_tunnel.ko` | `geneve.ko` / `rxrpc.ko` | 多种方式 | ### 对安全审计的影响 1. **`lsmod` 不可信**:未加载的模块不等于不可达。审计时必须检查所有可自动加载的模块 2. **依赖链扩大攻击面**:触发模块 A 会连带加载 B、C、D 3. **权限校验不对称**:`capable()` vs `ns_capable()` 的差异是绕过的根源 4. **`request_module` 各类入口不下 30 个**:涉及 socket、rtnetlink、xfrm、netfilter、ethtool 等多个子系统 ## 总结 这本质上是内核里**两类"加载模块"入口权限模型不一致的表现**:显式 `init_module/finit_module` syscall 严格限定到全局 root,而各子系统基于 alias(`rtnl-link-%s`、`net-pf-%d`、`crypto-%s` 等)的自动加载走的是完全不同的、以内核身份执行、且仅由触发点自身逻辑把关的路径。历史上这类 autoload 入口(尤其是 netns/userns 隔离出现后)多次被作为提权/信息泄露的研究点,可以查一下 `request_module` 在 `net/`、`crypto/`、`fs/` 下各调用点是否都有对应的"是否允许非 init netns/userns 触发"的额外限制(有些子系统后来专门加了 `if (!net_eq(net, &init_net)) return -ENODEV;` 之类的收紧),rtnetlink 的 link-kind autoload 目前依然是开放的。