The Dirty COW Race Condition Attack
Introduction
Dirty COW (CVE-2016-5195) is a race condition vulnerability in the Linux kernel that affects every Linux-based OS. It is one of the most severe privilege escalation vulnerabilities ever found in the Linux kernel, having existed undetected for approximately nine years before disclosure. A local attacker without any privileges can use it to write to any read-only file — including /etc/passwd — gaining root access.
The name comes from the underlying kernel mechanism exploited: Copy-On-Write (COW).
Memory Mapping Using mmap()
Understanding mmap() is prerequisite to understanding the attack. mmap() maps a file (or anonymous memory) directly into the process’s virtual address space:
- Argument 1 — Starting address for the mapping.
NULLlets the kernel choose. - Argument 2 — Size of the mapping in bytes.
- Argument 3 — Protection flags:
PROT_READfor readable,PROT_WRITEfor writable. Must match the file’s open mode. - Argument 4 — Mapping type:
MAP_SHAREDorMAP_PRIVATE. - Argument 5 — The file descriptor to map.
- Argument 6 — Offset into the file.
0maps from the beginning.
MAP_SHARED: Changes to the mapped memory are visible to all processes that have the same file mapped. Writes go directly to the underlying physical memory (and thus the file).
MAP_PRIVATE: Creates a private copy of the mapped region for the process. When the process writes to the mapping, the kernel triggers Copy-On-Write: a new physical page is allocated, the content is copied from the original page to the new one, and the process’s page table entry is updated to point to the new page. Other processes continue to see the original page. The downside is the latency introduced by the copy operation — the modified page is called a dirty page.
Discard the Copied Memory
After a process has created a private copy via COW, it can manage that mapping with madvise(). When the private copy is no longer needed, passing MADV_DONTNEED to madvise() tells the kernel to discard the private copy. After this, the virtual memory entry for that region reverts to pointing back at the original read-only physical page.
This “discard and revert” behavior is the key ingredient in the race.
Mapping Read-Only Files
Normal OS behavior: if a file is opened read-only and mapped with MAP_PRIVATE, a normal user cannot write to the original file. However, the kernel does permit write() system calls into the virtual memory of a MAP_PRIVATE mapped region — because by definition, writes go to the private copy, not the original.
The write path via /proc/self/mem is an indirect mechanism that allows writing to any virtual memory address in the current process, bypassing the file’s original permission checks. This is the second ingredient.
The Dirty COW Vulnerability
A write through MAP_PRIVATE into a private copy requires three steps:
- Make a copy — allocate a new physical page and copy the content from the original (COW).
- Update the page table — redirect the virtual address to the new physical page.
- Write to memory — perform the actual write to the now-private page.
These three steps are not atomic. The kernel does not hold a lock across all three. This creates a race window between steps 2 and 3.
The attack uses two threads running concurrently:
- Thread 1 (writer): Repeatedly writes to the read-only file via
/proc/self/mem. Each write attempt triggers the COW sequence. - Thread 2 (madvisor): Repeatedly calls
madvise(addr, len, MADV_DONTNEED)on the mapped region to discard the private copy immediately after it’s created.
The race condition: if Thread 2 discards the private copy (reverting the page table to the original) after step 2 completes but before step 3 executes, Thread 1’s write lands on the original read-only physical page instead of the private copy. The kernel has already updated the page table to the private copy in step 2, then Thread 2 resets it — so step 3 writes to the address that now points back to the original. The read-only check was satisfied before step 2 (it was going to a private copy), so no permission error is raised.
The kernel designers did not add an extra permission check at step 3 because the assumption was that step 2 had already established that writes go to a private page. The race breaks that assumption.
Key difference from prior attacks: Dirty COW does not require a specific target program to exploit. The attacker writes their own exploit program, directly interacts with the kernel’s memory management subsystem, and modifies protected files without any privileged intermediary.
Reference
Introduction
- Race condition vulnerability의 일종이며 Linux기반의 모든 OS에 영향을 주는 취약점이다.
- 공격자는 읽기 모드라도 모든 protected file을 수정할 수 있게 된다.
- /etc/password file을 수정하여 root 계정을 만들어 버리면 root 권한을 탈취하게 된다.
Memory Mapping using mmap()
- 먼저 mmap() 함수를 살펴보고 시작한다. 여기서는 mmap()으로 mapping된 memory에 관한 이야기들을 할 것이다.
- 첫 번째 인자는 starting address for the mapped memory를 의미하고 NULL값은 kernel이 주소를 선택하는 것을 의미한다.
- 두 번째 인자는 mapped memory의 size.
- 세 번째 인자는 memory가 readable(PROT_READ)인지 writable(PROT_WRITE)인지 나타내고 open()의 속성과 동일해야한다.
- 네 번째 인자는 MAP_SHARED, MAP_PRIVATE같은 것을 넣는다.
- 다섯 번째 인자는 mapped 될 file을 넣는다.
- 여섯 번째 인자는 file의 mapping할 시작포인트에 관한 offset을 넣는데, 0이면 file 전체를 mapping하겠다는 의미이다.
- 이를 이해하기 위해서는 가상메모리에 대해 잘 알고 있어야 한다.
- MAP_SHARED를 사용하면 physical memory의 내용을 다른 프로세스들에서 다 볼 수 있다.
- MAP_PRIVATE를 사용하면 한 프로세스에서 내용을 수정하면 기존에 모두가 접근 가능했떤 영역에서 새로운 영역으로 내용을 복사하기 때문에 수정한 프로세스만이 그 내용을 볼 수 있게 되는 것이다. 오리지널 영역에 영향을 주기 싫을때에도 사용한다. 단점은 복사하는 딜레이가 생기는 것이다.
- MAP_PRIVATE의 경우에서 보는 것과 같이 다른 프로세스들이 같은 물리적 메모리에 mapping이 되어 그것을 공유하고 있다가 쓰기를 시도할때 가상 메모리의 paging이 일어나면서(이것을 dirty page라고 부른다) 새로운 물리적 주소가 할당되는데 이런 과정들을 Copy On Write라고 부르는 것이다.
Discard the Copied Memory
- Program이 private copy의 mapping이 끝나고 나면 madvise()로 memory를 관리한다.
- Mapping된 memory가 더 이상 필요없어서 버릴때 madvise()함수에서 MADV_DONTNEED 인자값을 사용한다.
- 이때 새로운 영역을 포인트 하던 것은 COW가 일어나기 전의 기존 영역으로 포인트하게 된다.
Mapping Read-Only Files
- Dirty COW 공격은 read-only file을 대상으로 한다.
- 보통의 os에서는 read-only file의 memory에 normal user가 어떤 것도 못 쓰게 하지만 linux의 경우 MAP_PRIVATE로 mapping 되었을 경우에는 예외적으로 normal user가 write() system call을 통해 read-only file에 쓰기가 가능하게 만든다.
- 이런 방식은 private memory 영역에만 새로운 것을 write하기 때문에 안전하다고 할 수 있다. 다른 유저는 알 수 없고 다른 영역도 건드리지 않기 때문이다.
- 근데 /proc/self/mem을 통해서 간접적으로 private memory 영역에 새로운 것을 쓰게 할 수 있다.
The Dirty COW Vulnerability
- write() system call을 통해 mapped memory에 무언가 쓰기 위해서는 다음 3가지 과정이 필요하다.
- make a copy of the mapped memory
- update the page table, so the virtual memory now points to the newly created physical memory
- write to the memory
- 위의 과정들은 atomic하지 않아서 다른 thread나 process에 의해 interrupted될 수 있다.
- 위에서 2, 3번째 과정 사이에서 만약 다른 thread나 process에 의해 madvise()가 MADV_DONTNEED와 함께 가상 메모리를 버리고 원래 메모리를 가리키게 된다면 원래 메모리이 위치에다가 write를 해버릴 수 있게 되는 것이다. 곧 race condition problem을 의미한다.
- 시스템 설계시 읽기 파일이라도 write system call이 가능하게 만든 이유는 2번 과정에서 이미 private memory에다가 쓴다는 가정이 있기 때문에 굳이 다른 check를 하지 않았기 때문이다.
- Dirty COW vulnerability를 위해서는 2개의 thread가 필요한데 1개는 write를 시도하는 것이고 나머지 하나는 discard the private copy of the mapped memory using madvise()이다.
- 이전 공격들과 조금 다른 점이라고 하면 다른 것들은 target programe이 있어서 그것을 공략하는데 이것은 system의 허점을 노려서 우리가 스스로 공격 프로그램을 짜고 우리 코드를 통해 protected file에 접근하여 정보를 수정하는 공격이다.