Linux内核提权与容器逃逸途径
Contents 1. CVE-2016-51951.1. Dirty COW1.2. Patch2. CVE-2019-57 2026-5-4 11:33:37 Author: hosch3n.github.io(查看原文) 阅读量:0 收藏

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

图片由AI生成

攻击者可以只读权限打开/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
2
3
4
5
6
7
$ ldd /usr/bin/runc
linux-vdso.so.1 (0x00007fff07bb0000)
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007fa5fbb05000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa5fbae4000)
libseccomp.so.2 => /lib/x86_64-linux-gnu/libseccomp.so.2 (0x00007fa5fba9b000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa5fb8db000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa5fc4bc000)

动态链接库中可以构造__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

图片由AI生成

循环将每个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void memcpy_from_sglist(void *buf, struct scatterlist *sg,
unsigned int start, unsigned int nbytes);

void memcpy_to_sglist(struct scatterlist *sg, unsigned int start,
const void *buf, unsigned int nbytes);

void memcpy_sglist(struct scatterlist *dst, struct scatterlist *src,
unsigned int nbytes);


static inline void scatterwalk_map_and_copy(void *buf, struct scatterlist *sg,
unsigned int start,
unsigned int nbytes, int out)
{
if (out)
memcpy_to_sglist(sg, start, buf, nbytes);
else
memcpy_from_sglist(buf, sg, start, nbytes);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int crypto_authenc_esn_genicv(struct aead_request *req,
unsigned int flags)
{
struct crypto_aead *authenc_esn = crypto_aead_reqtfm(req);
struct authenc_esn_request_ctx *areq_ctx = aead_request_ctx(req);
struct crypto_authenc_esn_ctx *ctx = crypto_aead_ctx(authenc_esn);
struct crypto_ahash *auth = ctx->auth;
u8 *hash = areq_ctx->tail;
struct ahash_request *ahreq = (void *)(areq_ctx->tail + ctx->reqoff);
unsigned int authsize = crypto_aead_authsize(authenc_esn);
unsigned int assoclen = req->assoclen;
unsigned int cryptlen = req->cryptlen;
struct scatterlist *dst = req->dst;
u32 tmp[2];

if (!authsize)
return 0;


scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);

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卡在0a8e41116612d7之间的版本或许会逃逸不成功,版本比较老了待验证

Patch

补丁看起来没选择修复authencesn的OOB,而是回滚了In-place优化,让输入输出不再使用相同Scatterlist。

从合订本可以看出不管是Linux内核还是runC,都在业务、安全、性能三角中艰难地寻求着平衡。Dirty Pipe、Copy Fail的多个优化MR单独看都很正常,对于AI自动化审计需要关注整体架构和单点功能变化导致的全局影响。

感谢chrisju、c0ss4ck、xkaneiki、yangyue、Danny-Wei等大佬们的帮助

  1. https://github.com/q3k/cve-2019-5736-poc

  2. https://github.com/twistlock/RunC-CVE-2019-5736

  3. https://github.com/DataDog/security-labs-pocs/blob/main/proof-of-concept-exploits/dirtypipe-container-breakout/

  4. https://github.com/theori-io/copy-fail-CVE-2026-31431

  5. https://github.com/Percivalll/Copy-Fail-CVE-2026-31431-Kubernetes-PoC

  6. https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails

  7. https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html

  8. https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/

  9. https://unit42.paloaltonetworks.com/docker-patched-the-most-severe-copy-vulnerability-to-date-with-cve-2019-14271/

  10. https://bestwing.me/CVE-2020-15257-anaylysis.html

  11. https://dirtypipe.cm4all.com/

  12. https://www.anquanke.com/post/id/270067

  13. https://securitylabs.datadoghq.com/articles/dirty-pipe-container-escape-poc/

  14. https://bestwing.me/CVE-2024-21626-container-escape.html

  15. https://xint.io/blog/copy-fail-linux-distributions

  16. https://docs.kernel.org/core-api/mm-api.html

  17. https://github.com/torvalds/linux/commit/4ceb5db9757aaeadcf8fbbf97d76bd42aa4df0d6

  18. https://github.com/torvalds/linux/commit/abf09bed3cceadd809f0356065c2ada6cee90d4a

  19. https://github.com/torvalds/linux/commit/19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619

  20. https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b

  21. https://github.com/opencontainers/runc/commit/16612d74de5f84977e50a9c8ead7f0e9e13b8628

  22. https://github.com/opencontainers/runc/issues/1980

  23. https://github.com/opencontainers/runc/pull/1984

  24. https://github.com/opencontainers/runc/issues/3422

  25. https://github.com/torvalds/linux/commit/241699cd72a8489c9446ae3910ddd243e9b9061b

  26. https://github.com/torvalds/linux/commit/f6dd975583bd8ce088400648fd9819e4691c8958

  27. https://github.com/torvalds/linux/commit/9d2231c5d74e13b2a0546fee6737ee4446017903

  28. https://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5

  29. https://github.com/NVIDIA/nvidia-container-toolkit/blob/main/pkg/nvcdi/driver-wsl.go


文章来源: https://hosch3n.github.io/2026/05/04/Linux%E5%86%85%E6%A0%B8%E6%8F%90%E6%9D%83%E4%B8%8E%E5%AE%B9%E5%99%A8%E9%80%83%E9%80%B8%E9%80%94%E5%BE%84/
如有侵权请联系:admin#unsafe.sh