DirtySepolicy Bypass

June 19, 2026 · View on GitHub

Downloads

Zygisk module that defeats all versions of LSPosed/DirtySepolicy up to v2.2, and any detector using the same App-Zygote SELinux-probe technique.

How it works

DirtySepolicy v2.2 uses four detection methods from inside App Zygote:

  1. contextExists() — writes context strings to /sys/fs/selinux/context, queries /sys/fs/selinux/access, and falls back to /proc/self/attr/current to check if framework-injected SELinux types exist in the loaded kernel policy.

  2. checkSELinuxAccess() via kernel — resolves class/perm names from /sys/fs/selinux/class/ and queries /sys/fs/selinux/access directly, bypassing libselinux entirely.

  3. readStatus() / avdSeqNo — reads /sys/fs/selinux/status for sequence/policyload counters and reads seqno from /sys/fs/selinux/access responses to detect policy reloads.

  4. Indirect stock-context probes — checks allow rules between stock Android contexts that only exist because a framework injected them (e.g. rootfs→tmpfs:associate for Magisk).

This module defeats all four vectors:

HookTargetMethod
write on /sys/fs/selinux/accessBlock writes containing hidden context substrings; stash query for read patchingDefeats contextExists() access fallback and hidden-context checkSELinuxAccess()
read on /sys/fs/selinux/accessParse kernel response, mask hidden permission bits, mask exact-probe bits, rewrite seqno to 1Defeats checkSELinuxAccess() and avdSeqNo detection
read on /sys/fs/selinux/statusPatch sequence and policyload fields to clean-boot values based on kernel versionDefeats readStatus() policy-reload detection
write on /sys/fs/selinux/context and /proc/self/attr/currentReturn EINVAL for writes containing hidden type substringsDefeats contextExists() first and last checks
selinux_check_access / security_compute_avSame logic via libselinux API (defense-in-depth for older detectors)Defeats v1.x/v2.0/v2.1 and any libselinux-based detector

All hooks are installed via Zygisk PLT hooking across every loaded .so in every app and system_server process.

Hidden type patterns

PatternCatches
:magiskmagisk, magisk_file, magisk_log_file, magisk32, ...
:kitsuneKitsuneMask types
:apatchAPatch types
:ksu / :kernelsuKernelSU types
:lsposedlsposed_file, any lsposed_*
:xposedxposed_data, xposed_file, any xposed_*
:riruriru_file, any riru_*
:adbrootadbroot, adbroot_exec, adbroot_data_file
:supersu / :supolicySuperSU legacy types
:su:AOSP u:r:su:s0 (exact — trailing colon avoids false positives)
:zygiskAny generic zygisk_* artifact

Exact-match probe table (indirect stock-context probes)

scontconclasspermDetects
rootfstmpfsfilesystemassociateMagisk
kerneltmpfsfifo_fileopenMagisk
kerneladb_data_filefilereadKernelSU
system_serverapk_data_filefileexecuteLSPosed
dex2oatdex2oat_execfileexecute_no_transXposed
zygoteadb_data_filedirsearchZygiskNext

Build

Compile natively on Termux (arm64):

cd jni

aarch64-linux-android-clang++ \
  -std=c++17 -fno-exceptions -fno-rtti \
  -fPIC -shared -O2 \
  -fvisibility=hidden -fvisibility-inlines-hidden \
  -fdata-sections -ffunction-sections \
  -nostdlib++ \
  -Wall -Wextra \
  -Wl,--hash-style=both \
  -Wl,--gc-sections \
  -Wl,-z,lazy \
  -Wl,-z,norelro \
  -Wl,-soname,libdirtysepbypass.so \
  -o ../module/zygisk/arm64-v8a.so \
  module.cpp -llog

patchelf --remove-rpath ../module/zygisk/arm64-v8a.so

Debug build

Add -DDEBUG to get verbose logging (every hook action, blocked write, patched response, internal failures):

aarch64-linux-android-clang++ \
  -std=c++17 -fno-exceptions -fno-rtti \
  -fPIC -shared -O2 -DDEBUG \
  -fvisibility=hidden -fvisibility-inlines-hidden \
  -fdata-sections -ffunction-sections \
  -nostdlib++ \
  -Wall -Wextra \
  -Wl,--hash-style=both \
  -Wl,--gc-sections \
  -Wl,-z,lazy \
  -Wl,-z,norelro \
  -Wl,-soname,libdirtysepbypass.so \
  -o ../module/zygisk/arm64-v8a.so \
  module.cpp -llog

Package the flashable zip:

cd module
zip -r9 ../dirtysepbypass.zip module.prop customize.sh META-INF zygisk

Install

Via Magisk app: Modules > Install from storage > select dirtysepbypass.zip > Reboot.

Or from a root shell:

su -c "magisk --install-module /sdcard/dirtysepbypass.zip"
su -c reboot

Verify

  1. Open the DirtySepolicy app. It displays the device fingerprint and kernel version at the top, then the detection result in the center. A successful bypass looks like:

    OK: no dirty sepolicy found
    INFO: sequence=0 policyload=0
    

    The expected counter values depend on kernel version:

    • Kernel < 6.10: sequence=0 policyload=0
    • Kernel >= 6.10: sequence=4 policyload=1

    Any WARNING: line (e.g. found Magisk, found KernelSU, sequence=7 policyload=2) means a detection vector is leaking.

  2. Check hook logs:

    su -c "logcat -d -s DirtySepBypass"
    
  3. Run the audit tool (reads raw kernel state, compares against hook tables):

    su -c "python3 tools/audit.py"
    

Troubleshooting

If the bypass isn't working on your device, install the debug build (dirtysepbypass-debug.zip from Releases) instead of the regular one. It logs every hook action, blocked write, patched response, and internal failure. After rebooting, open DirtySepolicy, then dump the logs:

su -c "logcat -d -s DirtySepBypass" > debug_logs.txt

If you're building from source, add -DDEBUG to the compile command (see Debug build).

Please include debug logs when opening an issue — without them there's no way to diagnose what's happening on your device.

Extending the bypass

If audit.py reports a LEAK:

  • For new framework type names: Add the substring to kHidden[] in jni/module.cpp and HOOK_BLOCKLIST in tools/audit.py.
  • For new indirect stock-context probes: Add the exact tuple to kHiddenExact[] in jni/module.cpp and HOOK_EXACT_PROBES in tools/audit.py.
  • Rebuild, reflash, reboot, re-audit.

Limitations

  • Substring blocklist. New frameworks with novel type names need manual addition.
  • Exact-match table. New indirect probes using stock contexts need manual addition.
  • Raw policy parsing. A detector that reads /sys/fs/selinux/policy as a binary blob and parses type names directly could bypass all userspace hooks. No current detector does this.
  • Kernel-level counters. The status and seqno patches assume specific clean-boot values (kernel < 6.10: seq=0/policyload=0; kernel >= 6.10: seq=4/policyload=1). Non-standard boot sequences or OEM policy loaders could produce different baselines.

Compatibility

RequirementNotes
arm64-v8aPre-built for arm64. Other ABIs: rebuild from source.
Android >= 10App Zygote (the detection surface) was added in Android 10.
Magisk >= 24 + ZygiskModule requires Zygisk API v5. KitsuneMask also works.

License

Apache 2.0