Introduction: The Enduring Legacy of Dirty COW
The Dirty COW (Copy-On-Write) vulnerability, identified as CVE-2016-5195, was a significant flaw in the Linux kernel that allowed unprivileged local users to gain write access to read-only memory mappings. This privilege escalation vector had profound implications for Android devices, enabling root access on millions of phones. While patched years ago, understanding how such critical vulnerabilities are addressed at the kernel level is invaluable for security researchers, forensic analysts, and system architects. This article guides you through setting up a kernel forensics lab and reverse engineering the specific patch that mitigated Dirty COW, offering a deep dive into its mechanics and the forensic traces left behind.
Our journey will involve examining the kernel source code, understanding the nature of the race condition, and then analyzing the binary-level changes introduced by the patch. This hands-on approach will illuminate not only the technical solution but also the methodologies for investigating kernel-level security updates.
Background: Understanding CVE-2016-5195 (Dirty COW)
Dirty COW exploited a race condition in the Linux kernel’s memory subsystem, specifically within the copy-on-write mechanism. When a process attempts to write to a shared memory page, the kernel typically creates a private copy of that page for the process. Dirty COW’s flaw allowed an attacker to manipulate this mechanism. By simultaneously issuing a `madvise(MADV_DONTNEED)` call (which tells the kernel the process no longer needs a page) and a `ptrace` write (or similar direct memory write) to a read-only memory map, a race condition could be triggered.
The kernel’s `do_wp_page` function, responsible for handling write-protected pages, could be tricked into mapping a writable page without properly validating that the original page was truly being copied. This meant an attacker could write to read-only files (like `/system/bin/su` or `/etc/passwd`) that were mapped into memory, effectively achieving arbitrary write primitive to gain root privileges.
The Vulnerable Code Path (Pre-Patch)
The vulnerability lay primarily within how the kernel handled `get_user_pages` and `do_wp_page`. Prior to the patch, if a page was marked `FOLL_WRITE` but not `FOLL_COW` (Copy-On-Write), the kernel would sometimes mistakenly map a writable private page without ensuring the data integrity. Specifically, the race condition occurred around the logic that checks if a PTE (Page Table Entry) is dirty and writeable.
Setting Up the Android Kernel Forensics Lab
To conduct this analysis, you’ll need a suitable environment. The goal is to compare a vulnerable kernel image with a patched one. Ideally, this involves two Android devices or AOSP builds: one from before October 2016 and one after.
Required Tools and Resources:
- Vulnerable Android Device/Emulator: An Android device running a kernel version prior to Linux kernel 4.8, or an AOSP build specifically targeting an older kernel (e.g., AOSP 7.0 Nougat build released before the patch).
- Patched Android Device/Emulator: An Android device with a patched kernel (e.g., Android 7.1.1 or later, or a custom ROM including the fix).
- Kernel Source Code: Obtain the exact kernel source for both vulnerable and patched versions. You can often find this on the device manufacturer’s website or AOSP repositories.
- Development Workstation: A Linux machine with cross-compilation tools (e.g., `aarch64-linux-gnu-gcc`, `arm-linux-gnueabi-gcc`), `adb`, `fastboot`, `git`.
- Disassembler/Decompiler: IDA Pro or Ghidra for static analysis of kernel binaries.
- Binary Diffing Tool: Bindiff (IDA Pro plugin) or Ghidra’s built-in `diffrunner.py` for comparing vulnerable and patched binaries.
Setting Up Your Environment:
# Install essential tools on your Linux workstation (e.g., Ubuntu)c sudo apt update sudo apt install git build-essential bison flex libssl-dev libncurses-dev libc6-dev-i386 qemu-system-arm wget adb fastboot# Download AOSP prebuilts for toolchains (if not using distribution's cross-compilers)git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9cd aarch64-linux-android-4.9export PATH=$(pwd)/bin:$PATH# Obtain kernel source (example for a Nexus 5X running Android 7.0)git clone https://android.googlesource.com/kernel/msm.gitcd msmgit checkout android-msm-bullhead-3.10-nougat-mr1 # This might be vulnerable
Analyzing the Dirty COW Patch (Git Diff)
The Dirty COW vulnerability was primarily fixed by kernel commit `5b3f793b58c7343e17a08b31a681bb6570be7f3d`, titled “mm: enforce write-protection for COW pages.” This patch introduced crucial changes to how copy-on-write pages are handled, specifically ensuring that when `get_user_pages` is called with `FOLL_WRITE`, the kernel correctly applies write-protection if a new COW page is being created.
Key Changes in the Patch:
- Addition of `FOLL_WRITE` flag: The `get_user_pages` function in `mm/madvise.c` and related areas was updated to explicitly pass `FOLL_WRITE` when a writeable page is desired. This flag ensures the kernel performs proper permission checks.
- Refinement of `do_wp_page` logic: The `do_wp_page` function in `mm/memory.c` was modified to correctly handle cases where a page fault occurs on a write-protected, shared page. It now robustly ensures that `pte_mkwrite` and `pte_dirty` are applied only after a new, private copy of the page has been successfully created and linked.
Let’s examine a simplified `git diff` representation of the critical changes:
diff --git a/mm/memory.c b/mm/memory.cindex 2132132..abcdef1 100644--- a/mm/memory.c+++ b/mm/memory.c@@ -3455,6 +3455,10 @@ static int do_wp_page(struct vm_fault *vmf){ entry = pte_swp_mkwrite(entry); vmf->pte = entry; return 0;+}+ /* Ensure write-protection for COW pages is enforced */+ if (!pte_write(entry) && (vmf->flags & FOLL_WRITE)) {+ entry = pte_mkwrite(pte_dirty(entry));+ // Additional validation and atomicity checks added here+ } } ...diff --git a/mm/madvise.c b/mm/madvise.cindex 9876543..fedcba9 100644--- a/mm/madvise.c+++ b/mm/madvise.c@@ -218,7 +218,7 @@ static long madvise_remove(struct vm_area_struct *vma, { /* This needs get_user_pages for FOLL_WRITE to succeed or fail cleanly. */ ret = get_user_pages(vma->vm_start, npages,- FOLL_TOUCH, &page, NULL);+ FOLL_TOUCH | FOLL_WRITE, &page, NULL); /* Explicitly request write */ if (ret < 0) goto out; ...
The significant change is the explicit use of `FOLL_WRITE` in `get_user_pages` and the added checks within `do_wp_page` to ensure that a page marked for writing actually becomes writable only after a proper copy is made, preventing the race condition that enabled `madvise` to trick the kernel.
Reverse Engineering the Patch (Lab Walkthrough)
Now, let’s apply our forensic tools to the actual kernel binaries.
Step 1: Obtain Kernel Images
Extract the kernel image from your vulnerable and patched Android devices. This usually involves pulling the `boot.img` and then extracting the kernel from it.
# On vulnerable device (rooted or in recovery with adb shell)adb pull /dev/block/by-name/boot vulnerable_boot.img# Unpack the boot image (requires a tool like 'abootimg' or 'unpack_boot.py')python3 unpack_boot.py vulnerable_boot.img# You'll get a 'zImage' or 'Image' file. Decompress if necessary.mv vulnerable_boot.img.kernel vulnerable_kernel# Repeat for the patched deviceadb pull /dev/block/by-name/boot patched_boot.imgpython3 unpack_boot.py patched_boot.imgmv patched_boot.img.kernel patched_kernel
Step 2: Load into Disassembler (IDA Pro/Ghidra)
Load both `vulnerable_kernel` and `patched_kernel` into your disassembler. Identify the architecture (ARM/AArch64) and load as a raw binary. Manually define key functions like `do_wp_page`, `handle_pte_fault`, and `get_user_pages` based on kernel symbol maps or by searching for distinctive instruction patterns (prologues/epilogues and cross-references to memory management functions).
Step 3: Identify Vulnerable and Patched Assembly
Navigate to the `do_wp_page` function in both kernels. In the vulnerable kernel, you’ll find a path where `pte_mkwrite` might be called prematurely or without sufficient checks. The race condition is hard to see statically, but the patch will introduce new conditional jumps and function calls that weren’t present before.
# Vulnerable Kernel (Simplified ARM64 Assembly Pseudo-code for do_wp_page)do_wp_page: ... BL get_user_pages CMP W0, #0 B.LT error_path ... // Potentially unsafe branch if no explicit FOLL_WRITE or dirty check // leads to pte_mkwrite being called without proper COW validation BL pte_mkwrite ...
In the patched kernel, observe the added instructions, especially around the calls to `get_user_pages` and before any `pte_mkwrite` operations. You will see additional checks for `FOLL_WRITE` flags and more robust state management for the PTE.
# Patched Kernel (Simplified ARM64 Assembly Pseudo-code for do_wp_page)do_wp_page: ... MOV X1, XZR // Prepare for FOLL_WRITE check ORR X1, X1, #FOLL_WRITE // Set FOLL_WRITE bit in flags BL get_user_pages CMP W0, #0 B.LT error_path ... // NEW: Check for FOLL_WRITE flag and current PTE write status LDR W2, [SP, #frame_flags] // Load vmf->flags AND W2, W2, #FOLL_WRITE CBZ W2, .L_skip_write_check // If FOLL_WRITE not set, skip new check // ... more robust logic for pte_mkwrite ... BL pte_mkwrite_with_cow_check // Potentially new or modified helper .L_skip_write_check: ...
Step 4: Binary Diffing
Use your binary diffing tool (Bindiff, Ghidra’s `diffrunner.py`) to automate the comparison. Load both `vulnerable_kernel` and `patched_kernel`. The tool will highlight changed functions and basic blocks. `do_wp_page` and `get_user_pages` (and their respective callers) should show significant differences, indicating where the patch introduced new logic.
Focus on these highlighted areas. The diff will clearly show where new conditional branches, additional function calls (e.g., to a newly introduced helper function for COW validation), or modified register operations were added to enforce the write-protection correctly.
Forensic Significance and Conclusion
This deep dive into the Dirty COW patch provides more than just an academic exercise. For forensic analysts, understanding these kernel-level changes is crucial:
- Vulnerability Detection: By analyzing a device’s kernel binary, one can forensically determine if it was vulnerable to Dirty COW by comparing its `do_wp_page` and related functions against known patched versions.
- Incident Response: If a system was compromised via Dirty COW, understanding the exact mechanism helps in identifying indicators of compromise (IoCs) and assessing the extent of the breach.
- Patch Verification: This methodology can be applied to verify that security patches have been correctly applied and are effective, a vital step in maintaining system integrity.
Reverse engineering kernel patches, even for older vulnerabilities like Dirty COW, sharpens your skills in low-level analysis, race condition detection, and binary forensics. It demonstrates how subtle changes in kernel logic can have massive security implications and underscores the constant battle between attackers exploiting nuances and defenders fortifying the core of our operating systems.
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →