Skip to content

Isolated-process mount-view detection - #24

Open
JingMatrix wants to merge 7 commits into
masterfrom
isolated
Open

Isolated-process mount-view detection#24
JingMatrix wants to merge 7 commits into
masterfrom
isolated

Conversation

@JingMatrix

Copy link
Copy Markdown
Owner

This variant detects root and module mounts from isolated processes, where a normal app cannot look. It ports two techniques, each credited to its original proof-of-concept:

  • a classic isolated service, after Privisolated (LSPosed/Privisolated): isolated processes inherit the AID_READPROC group, so it reads the /proc/<pid>/{mountinfo,mounts,mountstats} of every peer it is permitted to, scans each record for module markers, and runs the propagation-class differential Privisolated relies on;
  • a native zygote_next service, after ZygoteNextProbe (xiaotong6666/ZygoteNextProbe): forked into init's global mount namespace on Android 17, it reads its own /proc/self/mountinfo, where module mounts remain visible.

The dashboard is rebuilt in Jetpack Compose — foldable sections, a raw-data report and a logs panel, and export of either. The existing native toolkit (solist/vmap injection, /system remount, mountinfo) is folded in as a main-process integrity section, so every check reads from one screen. A file-access sweep over all visible pids records which sensitive /proc/<pid> entries an isolated reader can actually open, confirming SELinux confines the reach to a handful of same-domain peers.

The CI signs the release build itself with the repository secrets, so the
release build type must not attach the debug signing config, which named the
output app-release.apk and broke the rename-then-sign step.
Kernel-side mount hiders (e.g. KernelSU's mount_hide) erase module mount
records from /proc/<pid>/{mountinfo,mounts,mountstats} at the seq_file layer,
keyed on the reader, so every mount view an app reads is consistently scrubbed
-- defeating the marker scan, the cross-view differential, cross-file and
self-vs-init checks. The stat syscall family is not on that path, so it is
ground truth.

New shared core (recon.cpp / librecon.so), run identically in the main process
(libdemo), the native isolated probe (libmain) and the classic isolated Java
probe (ProcScanner via JNI):

- Reconciliation: statx(STATX_ATTR_MOUNT_ROOT) + statfs vs mountinfo. A path the
  kernel confirms as a mount root, or a single file backed by the userdata fs
  where a read-only partition is expected, that mountinfo omits is a hidden
  record. Immune to the filter and needs no other process.
- Structural (mountinfo-only, fire even where the hidden path is unreachable):
  orphaned parent id, and a gap in the peer-group id run collected as
  shared:N u master:N so it is dense in both a global and an app slave view.

Fix mount-trace false positives on clean devices:
- native mount.cpp: drop the mount-id arithmetic heuristics (root.id==parent+1,
  file-order-consecutive ids) that flagged clean phones and varied run to run;
  keep the root-tool source/path scan and move the peer-group check into recon.
- classic differential: normalize out the per-process app-data isolation tmpfs
  before comparing views, and only flag distinct > classes (injection adds a
  view; fewer is benign).
- markers: drop data_mirror (stock AOSP tree) and the generic overlay-option
  words; anchor the modules marker to /adb/modules so it no longer matches OEM
  paths such as com.oplus.moduleservices.

UI: split the main-process integrity checks into two families -- Injection
detections and Mount traces -- driven by a new per-check "type" field, with the
detailed reconciliation card (findings + probes) under the mount family.

dex2oat binds are intentionally not probed: SELinux denies apps getattr on
dex2oat_exec so stat cannot reach them, and they never enter the app namespace.
…c probe

Differential panel: each mount-view group now carries the processes it compares
-- pid, uid, propagation, and the full cmdline -- and the panel renders them as a
per-view table so a distinct-views-vs-classes mismatch shows WHICH processes
diverge, not just raw pids. The differing-records list is labelled A (largest
view) / B (2nd largest).

Full command line: display /proc/pid/cmdline (argv[0]) instead of the kernel's
15-char-truncated comm, in the differential samples and the per-process mount
cards; MonoBlock scrolls so long names are not cut.

Speed up the classic isolated probe (~2.4s -> ~0.8s on ~840 visible pids),
without changing any verdict input (recon / markerHits / differential / crossFile
/ selfVsInit all come from readable-mountinfo pids, still fully read):
- read comm/cmdline only for pids with a readable mount view (the only consumers);
  the hundreds of unreadable pids skip those two dead reads.
- uid from stat("/proc/pid").st_uid instead of reading and parsing status.
- read mountstats for self only (every other pid is a guaranteed EACCES and its
  differential is always a single view).
- sample 40 pids in the fileAccess aggregate (a hidepid-leak diagnostic, not a
  verdict input) instead of testing every pid -- tens of thousands of open() calls
  removed; result carries tested/visiblePids and a sampled flag.
- do not format+log a line for every unreadable pid.
The Privisolated differential was normalizing benign per-process mounts out
BEFORE counting views, so a genuine two-view split (e.g. a WebView sandboxed
process lacking the /storage FUSE+tmpfs view a permitted app has) was reported
as a single view. That baked our judgment into the raw data the UI shows.

Now the raw grouping keeps every mount -- distinctViews is the true count and the
panel shows both groups and the exact differing records. The benign judgment is a
separate layer: interpretedViews is computed by discounting the mounts Android
customises per process (app-data isolation tmpfs and the external-storage
subsystem -- /storage and the storage /mnt binds, matched by mountpoint since
their fs type varies tmpfs/fuse/sdcardfs), and ONLY that interpreted count drives
the verdict (interpreted > classes). A raw difference that collapses once those
are removed is flagged benignDifference, not a mismatch.

- FileView carries normalized (raw) and normalizedInterpreted.
- differentialByFile groups on raw for display, counts interpreted for the verdict,
  and tags each differing record benign via isBenignKey.
- UI shows views as raw->interpreted, a benign result state, and per-record
  [benign]/[ LEAK ] tags, so the raw truth and our reading are both visible; a
  non-benign record keeps interpreted > classes and still fires as a LEAK.
…lass pair

7c817ae keyed the verdict on interpretedViews -- raw views minus the storage/data
mounts Android customises per process -- on the theory that a same-class view
difference is benign per-process variation. That is unsound: Privisolated's
invariant is that processes in one propagation class have an identical view, so ANY
same-class difference IS the inconsistency, and a module can mount under the very
/storage, /mnt, /data/user paths the interpreted layer strips, so discounting them
by mountpoint just deletes the evidence. On a stock device two views arise only from
two classes (Chrome's isolated services shared:1 vs the WebView zygote master:1); a
third view within one class does not.

The verdict now fires on rawMismatch = (classes > 0 && distinctViews != classes) --
Privisolated's actual rule, both directions, with classes==0 (no shared:/master:
root seen) treated as not-applicable rather than an unconditional mismatch. The
interpreted / mismatch / benignDifference machinery and isPerProcessBenignMount are
gone; the storage/data predicate survives only to tag a differing record's shape for
display.

The surfaced records now come from a SAME-class pair: the diff buckets the distinct
views by class and, when a class holds more than one view, diffs two of them, so the
panel shows the master:1-vs-master:1 leak instead of the normal shared-vs-master
delta the two-largest heuristic used to surface. View rows render largest-first and
the two diffed views are tagged (A)/(B), so the A-only / B-only records are
attributable to a process group. The verdict reason lists per-file counts, e.g.
[mountinfo(3/2), mounts(3/2)], and no longer names a cause -- that stays in comments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant