Contents
既然51fail已成事实,不如整理一下Dirty COW、Dirty Pipe、Copy Fail、RunC容器逃逸的知识吧。
Linux内核会维护内存中的Page Cache提升IO性能,读文件时直接从缓存返回、或是加载至缓存后返回,写文件时先标记Dirty Page内存,定期或满足特定条件时写回磁盘。
除了常规读写,Linux还可以通过mmap系统调用将文件或设备映射至内存中,MAP_SHARED模式会将所映射内存的修改写回文件,MAP_PRIVATE模式则不会写回文件。
当以MAP_PRIVATE模式映射只读文件时会经过Page Cache,对只读内存的写入行为会触发Page Fault Handler,通过Copy-on-Write复制出一个可写的新内存页。
同时,用户可以通过madvise系统调用向内核提供MADV_DONTNEED提示,内核会清理内存映射。
Dirty COW

攻击者可以只读权限打开/etc/passwd等目标文件,通过mmap(MAP_PRIVATE)模式映射至内存,随后高并发尝试 1.向目标文件的只读内存写Payload、2.调用madvise(MADV_DONTNEED)清理该内存映射。
任务1会促使Page Fault Handler执行Copy-on-Write,在完成新内存页分配之后、内容复制之前如果内核调度到了任务2,则新内存页会被清理丢弃,内核返回任务1重试。
内核此前COW过程经过了读写检查,发现是只读便不再额外标记,在后续重试时内核将缺少FOLL_WRITE标记的写行为放行,造成对只读内存的越权写入。任务1中调用的写操作,会在Page Cache中留下Dirty Page写回磁盘目标文件。
Patch

经过 4ceb5db ("Fix get_user_pages() race for write access") 和 f33ea7f ("fix get_user_pages bug") 的拉扯,最终在 19be0ea (mm: remove gup_flags FOLL_WRITE games from __get_user_pages()) 中通过增加 FOLL_COW 标记检查修复。
/proc是一个挂载Linux内核数据接口的伪文件系统,/proc/self这个特殊软链接会解析到当前进程/proc/<pid>目录。/proc/self/exe指向当前进程的可执行文件路径,/proc/self/fd指向当前进程的文件描述符目录。
runC是一个容器运行时环境,在Docker等工具中用于创建和处理运行容器相关的任务(例如docker run/exec)。

在正常流程中runC init会执行用户命令,但当容器内的程序(比如/bin/sh)被替换为#!/proc/self/exe时,runC init会创建一个文件上下文在容器内的runc进程,这个新runc进程会尝试在容器内加载libseccomp.so等动态链接库。
1 | $ ldd /usr/bin/runc |
动态链接库中可以构造__attribute__((constructor))函数在加载时执行,注入到新runc进程上下文中。
由于进程运行期间存在ETXTBSY锁无法覆盖runC文件,因此先通过open("/proc/self/exe", O_RDONLY)获取runC文件描述符并持续尝试覆写(在进程结束后仍有效),一旦进程结束即可成功向宿主机runc注入Payload,直到下一次runc运行时被执行。
Patch

起初runc通过完整克隆副本并设置F_SEAL_WRITE标记,实现了与宿主机的充分隔离修复。可随后因为runc文件较大且调用频繁导致的性能问题,不得不调整逻辑为仍用真实宿主机文件,但作为只读fd挂载并快速umount的方式修复,这也为后续内核提权漏洞导致的容器逃逸链路埋下了伏笔。
管道是用于进程间通信的单向通道,Linux的pipe_buffer结构体包含指向内存的page指针、标志位等其它字段。通过splice系统调用可以在管道和其它文件描述符之间传递数据(零拷贝)。
向管道写入1字节也会分配一个最低4KB的内存页,内核会增加PIPE_BUF_FLAG_CAN_MERGE标记,如果标记为1则说明内存页是自己分配的、尚有空余的内存页,可以继续追加写入。
Dirty Pipe

循环将每个pipe_buffer写满,使其均具有PIPE_BUF_FLAG_CAN_MERGE标记,随后循环读空每个pipe_buffer。由于标志位内存缺少初始化操作,此时每个pipe_buffer均被错误地标记为可追加状态。
通过splice读取目标fd文件1个字节进入管道,触发装载Page Cache指针,即可利用允许追加的标记特性实现覆写。限制为不可覆盖第一个字节、不可超过page长度(4KB)。此前runc只读fd埋下的伏笔,正好可以作为适配Dirty Pipe的容器逃逸链路。
Patch

Patch非常简单,增加标志位初始化就可以修复。
Scatterlist(SGL)是一种将零散内存区域整合为虚拟连续内存的数据结构,解密运算时输入和输出数据会使用同一个SGL(In-place)。
Linux内核提供了一个加解密Socket接口AF_ALG,用户可以绑定AEAD(Authenticated Encryption with Associated Data)模板执行加解密,authencesn是其中一个用于IPSec ESP加密的模板。
Copy Fail
使用AF_ALG Socket中的authencesn算法,通过splice(fd, pipe)&splice(pipe, socket)载入目标文件,内核以In-place方式解密时使用相同SGL,会将指针指向的Page Cache同时作为输入和输出区域(允许读写)。
1 | void memcpy_from_sglist(void *buf, struct scatterlist *sg, |
1 | static int crypto_authenc_esn_genicv(struct aead_request *req, |
authencesn算法会使用缓冲区边界外的4字节作为临时存储区,于是用户可控的Payload(seqno_lo)在In-place机制中获得了Page Cache的4字节写权限,循环偏移写入4*N字节后实现完整Shellcode覆写。
Container Escape
前面铺垫了那么多醋,都是为了包这碟饺子。由于众所周知的原因PoC有点敏感,因此分享一些和大佬们分析和验证过的思路&案例,相信大家结合上文&善用AI&参考链接&联网搜索可以很快构造出来。
- 在此类内核提权漏洞下,容器逃逸的核心围绕可获取到的宿主机fd展开。runc、ipset、nvidia-container-toolkit等程序会成为Payload的良好载体,其自身特性并不依赖主动挂载路径或是历史漏洞
- 程序的触发不一定需要人工被动交互,产品的功能接口、守护进程、计划任务等都可以成为触发点
- runc卡在
0a8e411和16612d7之间的版本或许会逃逸不成功,版本比较老了待验证
Patch

补丁看起来没选择修复authencesn的OOB,而是回滚了In-place优化,让输入输出不再使用相同Scatterlist。
从合订本可以看出不管是Linux内核还是runC,都在业务、安全、性能三角中艰难地寻求着平衡。Dirty Pipe、Copy Fail的多个优化MR单独看都很正常,对于AI自动化审计需要关注整体架构和单点功能变化导致的全局影响。
感谢chrisju、c0ss4ck、xkaneiki、yangyue、Danny-Wei等大佬们的帮助
https://github.com/Percivalll/Copy-Fail-CVE-2026-31431-Kubernetes-PoC
https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails
https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html
https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/
https://securitylabs.datadoghq.com/articles/dirty-pipe-container-escape-poc/
https://github.com/torvalds/linux/commit/4ceb5db9757aaeadcf8fbbf97d76bd42aa4df0d6
https://github.com/torvalds/linux/commit/abf09bed3cceadd809f0356065c2ada6cee90d4a
https://github.com/torvalds/linux/commit/19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619
https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b
https://github.com/opencontainers/runc/commit/16612d74de5f84977e50a9c8ead7f0e9e13b8628
https://github.com/torvalds/linux/commit/241699cd72a8489c9446ae3910ddd243e9b9061b
https://github.com/torvalds/linux/commit/f6dd975583bd8ce088400648fd9819e4691c8958
https://github.com/torvalds/linux/commit/9d2231c5d74e13b2a0546fee6737ee4446017903
https://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5
https://github.com/NVIDIA/nvidia-container-toolkit/blob/main/pkg/nvcdi/driver-wsl.go