Zygisk module that defeats all versions of LSPosed/DirtySepolicy up to v2.2, and any detector using the same App-Zygote SELinux-probe technique.
DirtySepolicy v2.2 uses four detection methods from inside App Zygote:
-
contextExists()— writes context strings to/sys/fs/selinux/context, queries/sys/fs/selinux/access, and falls back to/proc/self/attr/currentto check if framework-injected SELinux types exist in the loaded kernel policy. -
checkSELinuxAccess()via kernel — resolves class/perm names from/sys/fs/selinux/class/and queries/sys/fs/selinux/accessdirectly, bypassing libselinux entirely. -
readStatus()/avdSeqNo— reads/sys/fs/selinux/statusfor sequence/policyload counters and readsseqnofrom/sys/fs/selinux/accessresponses to detect policy reloads. -
Indirect stock-context probes — checks allow rules between stock Android contexts that only exist because a framework injected them (e.g.
rootfs→tmpfs:associatefor Magisk).
This module defeats all four vectors:
| Hook | Target | Method |
|---|---|---|
write on /sys/fs/selinux/access |
Block writes containing hidden context substrings; stash query for read patching | Defeats contextExists() access fallback and hidden-context checkSELinuxAccess() |
read on /sys/fs/selinux/access |
Parse kernel response, mask hidden permission bits, mask exact-probe bits, rewrite seqno to 1 |
Defeats checkSELinuxAccess() and avdSeqNo detection |
read on /sys/fs/selinux/status |
Patch sequence and policyload fields to clean-boot values based on kernel version |
Defeats readStatus() policy-reload detection |
write on /sys/fs/selinux/context and /proc/self/attr/current |
Return EINVAL for writes containing hidden type substrings |
Defeats contextExists() first and last checks |
selinux_check_access / security_compute_av |
Same 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
| Pattern | Catches |
|---|---|
:magisk |
magisk, magisk_file, magisk_log_file, magisk32, ... |
:kitsune |
KitsuneMask types |
:apatch |
APatch types |
:ksu / :kernelsu |
KernelSU types |
:lsposed |
lsposed_file, any lsposed_* |
:xposed |
xposed_data, xposed_file, any xposed_* |
:riru |
riru_file, any riru_* |
:adbroot |
adbroot, adbroot_exec, adbroot_data_file |
:supersu / :supolicy |
SuperSU legacy types |
:su: |
AOSP u:r:su:s0 (exact — trailing colon avoids false positives) |
:zygisk |
Any generic zygisk_* artifact |
| scon | tcon | class | perm | Detects |
|---|---|---|---|---|
rootfs |
tmpfs |
filesystem |
associate |
Magisk |
kernel |
tmpfs |
fifo_file |
open |
Magisk |
kernel |
adb_data_file |
file |
read |
KernelSU |
system_server |
apk_data_file |
file |
execute |
LSPosed |
dex2oat |
dex2oat_exec |
file |
execute_no_trans |
Xposed |
zygote |
adb_data_file |
dir |
search |
ZygiskNext |
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.soPackage the flashable zip:
cd module
zip -r9 ../dirtysepbypass.zip module.prop customize.sh META-INF zygiskVia 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-
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=0The 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. - Kernel < 6.10:
-
Check hook logs:
su -c "logcat -d -s DirtySepBypass" -
Run the audit tool (reads raw kernel state, compares against hook tables):
su -c "python3 tools/audit.py"
If audit.py reports a LEAK:
- For new framework type names: Add the substring to
kHidden[]injni/module.cppandHOOK_BLOCKLISTintools/audit.py. - For new indirect stock-context probes: Add the exact tuple to
kHiddenExact[]injni/module.cppandHOOK_EXACT_PROBESintools/audit.py. - Rebuild, reflash, reboot, re-audit.
- 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/policyas 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.
| Requirement | Notes |
|---|---|
| arm64-v8a | Pre-built for arm64. Other ABIs: rebuild from source. |
| Android >= 10 | App Zygote (the detection surface) was added in Android 10. |
| Magisk >= 24 + Zygisk | Module requires Zygisk API v5. KitsuneMask also works. |
Apache 2.0