Hardening a rooted Android device against app detection
June 10, 2026 · 9 min read

Auditing from the app's eyes

You can't tell what a normal app sees from adb shell — shell has privileges an app never does. Three lenses for looking through the app's eyes, and the blind spot of each.

androidselinuxauditingnsenter
Cover illustration for “Auditing from the app's eyes”

To measure what a normal app could still observe, I stopped using adb shell. Shell lives in a different world than an app. adb shell runs as UID 2000 in the shell SELinux domain, while an app runs as UID 10000+ in untrusted_app and inside an isolated mount namespace that Shamiko sets up.

I relied on three lenses to see through the app’s eyes:

  1. su 10253 -z u:r:untrusted_app:s0 -c '<binary>' This perfectly matched the app’s UID and SELinux domain for testing gatekeepers like logd. Its blind spot: untrusted_app can’t execute /system/bin/sh.
  2. nsenter -t $(pidof com.target) -m cat /proc/cmdline Using nsenter(1), I entered the running app’s mount namespace. This revealed the filesystem without Magisk overlays. Its blind spot: it runs as root, bypassing SELinux constraints.
  3. Reading /proc/<pid>/... directly from shell. This showed me exactly what was mapped into the process, such as checking maps for Zygisk injection traces.

Through these lenses, the environment looked mostly clean. /proc was hidepid=invisible, Shamiko had isolated the namespaces perfectly, and there were zero injection hits. But two things leaked: adb_enabled=1 and LineageOS system services. Services like lineagehardware were still resolvable via ServiceManager.getService("lineagehardware") by native code.

I closed the service vector at the kernel layer by modifying the SELinux policy. I wrote a strict cross-product of denial rules preventing untrusted app domains from discovering Lineage services.

deny untrusted_app lineage_hardware_service   service_manager { find }
deny untrusted_app lineage_livedisplay_service service_manager { find }
deny isolated_app  lineage_hardware_service    service_manager { find }

By persisting this via sepolicy.rule in a Magisk module, the OS completely blinded apps at the kernel level without interfering with the system domains that rely on them. Hidden-API gating only works for Java layer logic; closing it at SELinux physically prevents discovery regardless of the app’s tricks.

Comments