gVisor 如何隔离真实漏洞:以 CVE-2020-14386 容器逃逸漏洞为例的纵深防御剖析

📅 发布时间:2026/9/13 22:41:15
gVisor 如何隔离真实漏洞:以 CVE-2020-14386 容器逃逸漏洞为例的纵深防御剖析
gVisor 如何隔离真实漏洞以 CVE-2020-14386 容器逃逸漏洞为例的纵深防御剖析【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor导读本文以 2020 年 9 月公布的 Linux 内核容器逃逸漏洞 CVE-2020-14386 为案例系统剖析 gVisor容器应用内核如何通过默认保护secure-by-default与隔离内核Isolated Kernel两大设计即使自身内核Sentry存在同类缺陷也无法让攻击者逃逸容器。读完本文你将掌握PACKET_RX_RING环形缓冲区的攻击原理、gVisor 对 raw socket 的--net-raw默认关闭策略、Sentry 与宿主机之间多重隔离层cgroup、空 namespace、只读 rootfs、非特权用户、seccomp 过滤器的具体实现与源码位置并理解 Kubernetes 场景下Pod 即隔离单元的安全边界含义。背景一次将安全设计与网络攻防串起来的漏洞gVisor 在 architecture_guide 中将自己定位为容器的应用内核每个沙箱内部运行一个名为Sentry的用户态内核负责为容器内的进程实现系统调用、文件系统与网络栈。此前关于 gVisor 安全设计的两篇文章security design principles 与网络上下文中的应用阐述了其核心安全理念而 2020 年 9 月公开的 CVE-2020-14386 恰好把这两个主题串联在一起——它是一起真实的 Linux 内核漏洞gVisor 对该特定问题并不存在漏洞但它为深入理解 gVisor 的安全机制提供了绝佳的案例。需要说明的是gVisor 并不声称对漏洞免疫但它采取多重步骤来最小化漏洞影响并在发现漏洞时进行修复与缓解这正是本文要展开的内容。漏洞剖析攻击者如何逃出容器共享环形缓冲区高性能收发包机制Linux 上收发网络字节的方式有很多种其中性能最高的方式之一是使用环形缓冲区ring buffer——一块由应用程序与内核共享的内存区域。通过调用 setsockopt(2) 传入PACKET_RX_RING接收与PACKET_TX_RING发送即可创建这类环形缓冲区。此外还有一个PACKET_RESERVE选项它要求内核在环形缓冲区中每个数据包之前预留一段空间供应用程序存放控制结构等自定义数据。溢出根因一个 16 位字段的加法溢出漏洞位于启用PACKET_RX_RING时的收包代码中。当数据包到达时内核需要根据每个包之前预留的字节数计算数据包的拷贝位置。当预留量很大时内核发生了错误的计算导致最多 10 字节的越界写入out-of-bounds write且写入内容完全由攻击者控制。原始漏洞代码位于 Linux 内核的tpacket_rcv函数如下static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { // ... if (sk-sk_type SOCK_DGRAM) { macoff netoff TPACKET_ALIGN(po-tp_hdrlen) 16 po-tp_reserve; } else { unsigned int maclen skb_network_offset(skb); // tp_reserve is unsigned int, netoff is unsigned short. // Addition can overflow netoff netoff TPACKET_ALIGN(po-tp_hdrlen (maclen 16 ? 16 : maclen)) po-tp_reserve; if (po-has_vnet_hdr) { netoff sizeof(struct virtio_net_hdr); do_vnet true; } // Attacker controls netoff and can make macoff be smaller // than sizeof(struct virtio_net_hdr) macoff netoff - maclen; } // ... // macoff - sizeof(struct virtio_net_hdr) can be negative, // resulting in a pointer before h.raw if (do_vnet virtio_net_hdr_from_skb(skb, h.raw macoff - sizeof(struct virtio_net_hdr), vio_le(), true, 0)) { // ...关键点在于tp_reserve是unsigned int32 位而netoff是unsigned short16 位加法运算会发生整数溢出使macoff被算成一个小于sizeof(struct virtio_net_hdr)的值最终virtio_net_hdr_from_skb写入的指针落在共享区域之前造成越界写。攻击者通过loopback 接口发送精心构造的数据包并使用精心挑选的PACKET_RESERVE大小配合PACKET_RX_RING接收即可轻易控制写入的数据——loopback 全程在沙箱内部数据包无需经过宿主机网络栈。为什么默认容器环境可以被利用创建上述 socket 需要CAP_NET_RAW能力。然而为了支持ping、tcpdump这类常见调试工具Docker 容器包括为 Kubernetes 创建的容器默认赋予CAP_NET_RAW因此容器内的攻击者通常具备触发该漏洞、提权并逃逸容器的前提条件。这也正是该漏洞影响面大的原因。第一道防线默认保护Secure by DefaultgVisor 支持 raw socket但默认禁用gVisor没有实现PACKET_RX_RING但确实支持 raw socket——而 raw socket 正是PACKET_RX_RING的前提。raw socket 在沙箱环境中是一个颇具争议的功能一方面它允许ping等关键工具进行深度定制另一方面它允许数据包不经任何校验直接写入网络。允许不受信任的应用程序向网络写入精心构造的数据包从历史来看是漏洞的常见来源。在 gVisor 首次实现 raw socket 时经过多轮讨论后做出决定默认禁用 raw socket即使应用程序被授予CAP_NET_RAW也一样。要启用 raw socket管理员在配置 runtime 时必须显式设置--net-raw标志同时应用程序仍需具备CAP_NET_RAW能力。代价是部分工具开箱即用可能无法工作但这正是 secure-by-default 原则的体现更不安全的配置必须被显式选择。源码级证据从 runsc 配置到 Sentry 能力剥离这一策略在仓库中有清晰的实现链路配置项定义位于 runsc/config/config.goEnableRaw bool \flag:net-raw注释明确写道启用 raw socket禁用时通过从能力列表中剥离CAP_NET_RAW 来实现。命令行标志在 runsc/config/flags.go 注册flagSet.Bool(net-raw, false, enable raw sockets. When false, raw sockets are disabled by removing CAP_NET_RAW from containers (...). Raw sockets allow malicious containers to craft packets and potentially attack the network.)——默认值false。在沙箱启动阶段runsc/boot/loader.go 调用specutils.Capabilities(conf.EnableRaw, spec.Process.Capabilities)处理能力集runsc/boot/loader.go 中还有conf.EnableRaw !specutils.HasCapabilities(capability.CAP_NET_RAW)的校验逻辑。在 Sentry 内部pkg/sentry/socket/netstack/provider.go 检查创建 raw socket 时是否具备CAP_NET_RAW否则记录Should the container config enable CAP_NET_RAW?而 provider.go 对默认禁用状态的提示是A process tried to create a raw socket, which is disabled by default. Should the runtime config enable --net-raw?——两条日志分别对应能力缺失与运行时配置未开启两种失败原因便于排障。顺带一提仓库还提供了另一个相关开关allow-packet-socket-writerunsc/config/flags.go默认false禁止对AF_PACKETsocket 的写操作开启后不受信任的工作负载可能因能够构造任意数据包而攻击网络——它进一步收窄了 Sentry 对宿主网络栈暴露的面。由于实现不同本漏洞天然不生效由于 CVE-2020-14386 源于特定 Linux 内核版本对 packet ring 的溢出实现gVisor 的 raw socket 实现并不受影响。但更重要的是即使 gVisor 自身存在类似漏洞容器默认情况下也不允许利用它——因为默认配置根本不会让 raw socket 进入可用状态。Kubernetes 侧的对齐做法准入控制器剥离 CAP_NET_RAW在 Kubernetes 中同样可以通过配置**准入控制器admission controller**来定制请求实现与该约束等效的策略。例如 GKE 为 gVisor 实现了一个准入控制器从 gVisor Pod 中移除CAP_NET_RAW除非 Pod spec 中显式设置了该能力。这展示了平台层策略 运行时层默认值的双重收紧。第二道防线隔离内核Isolated KernelSentry 是独立于宿主内核的应用内核gVisor 拥有自己的应用内核 Sentry它与宿主机内核截然不同。与人们对一个内核的预期一致gVisor 具备内存管理子系统、虚拟文件系统、完整的网络栈。宿主机网络仅被用作传输通道负责把数据包送入和送出沙箱而漏洞利用中使用的 loopback 接口则完全停留在沙箱内部永远不会触及宿主机。即使 Sentry 中招仍有双重因素阻止容器逃逸即使 Sentry 对该攻击存在漏洞仍有两个因素阻止容器逃逸漏洞被限制在 Sentry 内部攻击者最多只能攻陷应用内核本身而 Sentry 被一组受限的 seccomp 过滤器约束下文详述Sentry 是 Go 编写的 API 独立实现Go 的边界检查很可能阻止访问共享区域越界。文档中列举了拥有类似共享区域的先例内核 AIO 环形缓冲区 pkg/sentry/kernel/aio.go 与内核代码覆盖率追踪机制 KCOV pkg/sentry/kernel/kcov.go后者通过memmap.Mappable与ConfigureMMap管理共享映射相关实现见 kcov.go。gVisor 对这些共享内存区域类接口都做了严谨的边界校验这是与有缺陷的 C 语言实现本质不同的地方。Kubernetes 场景Pod 是隔离单元这里需要进一步解释 Kubernetes 下的安全边界。gVisor 以Pod 为隔离单元一个 Pod 可以运行多个容器。换言之每个 Pod 是一个 gVisor 实例每个容器是运行在 gVisor 内部的一组进程容器之间通过 Sentry 内部的 namespace类似普通 Pod 内容器间隔离相互隔离。因此如果 gVisor 自身存在漏洞权限提升最多允许 Pod 内的一个容器逃逸到同一 Pod 的其他容器但容器依然无法逃出 Pod——即无法突破到宿主机或其他 Pod。这正是沙箱边界等于 Pod 边界的架构含义也是 gVisor 隔离模型与普通容器共享宿主内核的本质区别。纵深防御Defense in Depth攻破一层远不够原则两层保护需要不同的突破手段gVisor 遵循 Google 常用的安全原则系统应当有两层保护且这两层需要不同的妥协方式才能被攻破。具体做法是假设 Sentry第一层防线会被攻破、不可信任然后围绕它部署大量安全与隔离特性确保只向宿主机内核暴露最小功能集。五重宿主隔离机制沙箱在宿主机上的运行姿态每一条都对应可验证的启动路径见 runsc/boot/loader.go 的沙箱创建流程cgroup 限制沙箱运行在 cgroup 内可限制与节流宿主机资源使用空 namespace沙箱加入空的 namespace包括 user 与 mount namespace进一步与宿主机隔离只读 rootfs将进程根目录切换为只读目录其中只包含/proc别无他物非特权用户以无特权用户/组nobody执行并剥离全部 capabilitiesseccomp 过滤器最关键严格限制 gVisor 可访问的宿主机系统调用面。seccomp 过滤器的具体约束seccomp 过滤器不仅限制可调用的系统调用还校验这些调用的参数是否落在预期集合内。像execve(2)、open(2)、socket(2)这类危险系统调用被直接禁止因此攻击者无法在宿主机上执行二进制或获取新资源。完整的允许系统调用集合位于 runsc/boot/filter/其具体规则由 filter.go 生成子目录 config/ 中还有预编译的 seccomp 配置。允许的宿主机系统调用面远小于Sentry 为应用程序实现的系统调用面——应用程序面对的是一整套 Linux API而 Sentry 面向宿主机只有一小撮经过严格筛选的调用。效果评估被攻破的 Sentry 比普通容器更受限如果 gVisor 存在允许攻击者在 Sentry 内执行代码的漏洞攻击者在宿主机上的权限依然极其有限。事实上一个被攻破的 Sentry 比一个未受攻击的普通容器受到的限制更多。针对 CVE-2020-14386 这个具体攻击它会被不止一个安全层拦截非特权用户、无能力no capability、seccomp 过滤器——三层叠加。仍要警惕的残余攻击面尽管攻击面已大幅缩减被允许的系统调用中仍存在出现漏洞的可能因此保持攻击面小、谨慎选择允许的系统调用至关重要。另一个潜在攻击向量是Sentry 内持有的资源例如打开的文件描述符日志文件、平台文件如/dev/kvm、允许与 Sentry 外部通信的 RPC 端点以及把沙箱连接到网络的Netstack 端点。其中 Netstack 端点尤其值得关注因为它提供对网络的直接访问它是一个AF_PACKETsocket允许向网络写入任意 L2 数据包。正常场景下Netstack 组装发往网络的数据包容器只能控制 payload但若 Sentry 被攻破攻击者就能向网络构造数据包——这在很多方面类似于任何人向互联网发送随机数据包但这是宿主机内核暴露面大于预期的一个地方也是 gVisor 团队持续关注收窄的残余风险。结论多层保护的实战价值安全常常伴随着难以取舍的权衡——例如默认禁用 raw socket 的决定。但这些权衡在实践中被证明是值得的。CVE-2020-14386 提供了一个绝佳视角展示多层保护如何有效抵御此类攻击第一层gVisor 实现上的差异 默认禁用 raw socket让漏洞根本无从触发第二层Sentry 与宿主机内核隔离 Go 语言边界检查即使 Sentry 中招也难以越界第三层cgroup / 空 namespace / 只读 rootfs / 非特权用户 / seccomp 五重宿主隔离即使代码执行也被锁死在极小的权限内。gVisor 无法保证容器逃逸永远不会发生但会尽一切努力让它尽可能难以发生。若想实际体验 gVisor 的安全隔离可按照 g3doc/user_guide/quick_start 或 g3doc/user_guide/install.md 中的步骤安装运行 runsc runtime并在 runtime 配置中观察--net-raw默认关闭带来的行为差异。注沙箱发出的数据包最终仍会由宿主机处理宿主机需要将其路由到本地容器或通过 NIC 发出。数据包沿途会经过众多交换机、路由器、代理、服务器等这些设备各自可能存在漏洞——这也是 gVisor 无法替宿主机网络基础设施兜底的原因。【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考