CVE-2026-31431 Copy.Fail 在容器逃逸上的想法

Created At: Apr 30, 2026

今天有一个奇怪的洞 Copy Fail (CVE-2026-31431)。看了一圈都在讲主机上 732 字节脚本拿 root 的事,官方 Part 2 才会写容器逃逸。这篇就先聊聊我自己的想法——这玩意儿在容器里到底怎么打。

TL;DR

能逃,但是需要一些特殊的权限和配置。

背景

先讲三个概念。

Page Cache

Linux 不会每次读文件都跑磁盘。进程读文件,内核把内容缓存在内存里,后续读直接先找缓存,找不到才是缺页 + reload。

这里有个背景知识 page cache 以 inode 为粒度。不管同一个文件被多少进程打开,内核里只有一份 Page Cache。

splice

splice() 是 Linux 的 Zero Copy 机制,通过 pipe 在两个 fd 之间搬数据,不走用户态,内核里直接传页面引用:

文件 ──splice──→ pipe ──splice──→ socket

pipe 持有的就是源文件 page cache 页面的引用。

AF_ALG

AF_ALG 是内核暴露给用户态的密码学接口。一个普通用户开个 socket,bind 到 AEAD,就能直接调内核的加解密。

漏洞根因

这块照着 Theori 的 writeup 复述一遍,他们写得已经很清楚了。

先看 in-place 操作

用户通过 splice 把文件喂进 AF_ALG socket 后,socket 的输入 scatterlist 直接持有 page cache 的引用。

AEAD 解密的输入格式是 AAD || ciphertext || tag。2017 年 algif_aead 加了个 in-place 优化:解密时,AAD 和 ciphertext 通过 memcpy 拷到用户的 RX buffer,但 tag 那段是用 sg_chain() 直接挂到输出 scatterlist 末尾的,也就是说 page cache 的页面被链到了"可写"的目的 scatterlist 上。

Input SGL:     AAD  ||  CT  ||  Tag
                |       |        ^
                | copy  |        | sg_chain (还指向 page cache 页面)
                v       v        |
Output SGL:    AAD  ||  CT  -----+

然后 req->src = req->dst,都指向这个组合链。

然后是 authencesn 的 scratch write

authencesn 是 IPsec 用的 AEAD wrapper,处理 64 位 ESN(扩展序列号)。ESN 高 32 位 seqno_hi 和低 32 位 seqno_lo 在 wire 上和 HMAC 输入的位置不一样,所以解密时需要重排。

crypto_authenc_esn_decrypt() 里:

scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);                       // 读 AAD 0-7 字节
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);                       // 把 dst[4..7] 覆盖成 seqno_hi
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // 在 tag 之后写 seqno_lo

第三行,在 assoclen + cryptlen 写 4 字节,越界到 tag 那段了。

两个独立的"合理"决策叠在一起

  • 2011:authencesn 加进内核,scratch write 设计就是这样,当时 AAD 在独立的 scatterlist 里,xfrm 内部用,对其他人没影响
  • 2015:AF_ALG 支持 AEAD,但当时是 out-of-place,page cache 页面只在 src 里(只读),scratch write 落在 dst(用户 buffer),没事
  • 2017:algif_aead 改成 in-place,page cache 页面被 sg_chain 挂到了可写的目的 scatterlist 上

到这里,authencesn 那个写出合法输出区的 scratch write,正好就落到了 page cache 页面上。

攻击者控制三件事:

  • **写哪个文件: **任何当前用户能读的文件
  • **写哪个偏移: **通过 splice 起始偏移、长度、assoclen 三个参数算出来
  • **写什么值: **就是 sendmsg 里 AAD 的 4-7 字节

那么这样就可以每次4字节,想写什么写什么了。而且页面不会被标 dirty,在文件不修改的基础上,下次有人读这个文件,内核就会优先从 page cache 里拿,所以会读到被改的版本。

Exploit

PoC 目标默认是 /usr/bin/su:

  1. AF_ALG socket bind 到 authencesn(hmac(sha256),cbc(aes)),setkey、accept 拿操作 fd
  2. 每个 4 字节 chunk:sendmsg(AAD 里塞要写的 4 字节, MSG_MORE),然后 splice(目标文件 → pipe → AF_ALG socket),参数算好让 dst[assoclen + cryptlen] 落在 su 里要改的偏移
  3. recv() 触发解密,authencesn 把 seqno_lo 写进 page cache,HMAC 失败 recvmsg 报错,但那 4 字节留下了
  4. 全部写完,execve("/usr/bin/su"),内核从 page cache 加载被注入 shellcode 的版本,SUID 生效

容器场景下的 Page Cache 污染

容器里个人感觉关键在 OverlayFS 和文件共享。

OverlayFS 和 Page Cache

容器的文件系统是 OverlayFS:

容器视图
├─ upper layer (可写,容器跑起来后改的东西)
└─ lower layer (只读,从镜像来的)
     └─ 实际 inode → Page Cache
          共享! 同一镜像 = 同一 inode = 同一份 page cache

容器读 lower layer 的文件,page cache 缓存的是底层的 inode 的内容。多个容器跑同一镜像,共享同一份 page cache。

image.png

那思路就很直接:

  • 找节点上有哪些镜像,相同 layer 在 overlayfs 下其实是同一个 inode
  • 改底层layer可能被触发的二进制、so 文件
  • 其他容器一旦碰到对应的 page cache,就执行 payload
  • 突破隔离

场景 1:打 DaemonSet

集群里 DaemonSet 满天飞。复制一份 DaemonSet 用的镜像起个 pod,攻击者在自己 Container A 里跑 exploit,污染的 page cache 被同节点的 DaemonSet Container B、C 同时使用。

很多 DaemonSet 会周期性跑任务或者拉起子进程,改一下它频繁调用的二进制就行——监控、日志守护进程里这种调用一抓一大把。下次执行就触发 payload。

可以直接用execsnoop,看下 ppid,和执行的什么内容,那么可以当场搞事。

而且 DaemonSet 普遍权限较高,或者挂了宿主目录,剩下的就是常规集群渗透。

场景 2:hostPath 也跑不掉?

AI GPU 业务里,宿主机的 nvidia 驱动一般都 bind mount 进容器。

容器里和宿主的 inode 是同一个,按这次漏洞,容器里有人用 splice 把 nvidia-smi 这种 bin 或者 .so 改了,宿主一旦用到这些东西就直接 RCE。

image.png

image.png

场景 3:PaaS

再看 PaaS 这种多租户场景。每个用户启动的镜像都一样,攻击者摸清楚厂商的镜像启动会调哪些东西,改 page cache,后面其他人起的容器全都可以带这你的 payload。

横向移动

把这些能力组合起来:

低权限 Pod (没特殊 capability)
├─ 1. 污染 lower layer 的文件,比如 /usr/bin/python3
│     注入:启动时反弹 shell
├─ 2. 等同节点其他 Pod 调 python3
│     触发点:health check、cron、应用启动
│     (大部分 Python 服务几秒钟就会触发一次)
└─ 3. 在目标 Pod 上下文里拿到执行权
       能用的东西:
       ├─ ServiceAccount token → K8s API 横向
       ├─ 挂的 Secret / ConfigMap
       ├─ 网络策略允许的内网访问
       └─ 更高的 Linux capability

总结

OverlayFS 这条思路只是抛砖引玉,证明这个 CVE 在容器场景下的实际影响。官方的 Part 2 还没出,但是他们既然提到"From Pod to Host",那就感觉应该会有更优雅的姿势,等出了对照看。

Tags: