From 5f68b92e04d8fcda56b1c115f5de54e84b22a792 Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Fri, 22 May 2026 00:44:40 +0530 Subject: [PATCH 01/18] adding port v1->v2 TEST-0048 --- best-practices/MASTG-BEST-0x48.md | 44 +++ best-practices/MASTG-BEST-0x49.md | 68 ++++ .../MASTG-DEMO-0x48/MASTG-DEMO-0x48.md | 48 +++ .../MASTG-DEMO-0x48/MastgTest.kt | Bin 0 -> 5528 bytes .../MASTG-DEMO-0x48/MastgTest_reversed.java | 251 +++++++++++++ .../MASTG-DEMO-0x48/output.txt | 73 ++++ .../MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh | 2 + .../MASTG-DEMO-0x49/MASTG-DEMO-0x49.md | 56 +++ .../MASTG-DEMO-0x49/output.txt | 6 + .../MASVS-RESILIENCE/MASTG-DEMO-0x49/run.sh | 2 + .../MASTG-DEMO-0x49/script.js | 140 ++++++++ .../MASTG-DEMO-0x4A/AndroidManifest.xml | 38 ++ .../MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 51 +++ .../MASTG-DEMO-0x4A/MastgTest.kt | 322 +++++++++++++++++ .../MASTG-DEMO-0x4A/MastgTest_reversed.java | 339 ++++++++++++++++++ .../MASTG-DEMO-0x4A/output.txt | 104 ++++++ .../MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh | 2 + .../MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md | 47 +++ .../MASTG-DEMO-0x4B/output.txt | 13 + .../MASVS-RESILIENCE/MASTG-DEMO-0x4B/run.sh | 2 + .../MASTG-DEMO-0x4B/script.js | 178 +++++++++ rules/mastg-android-frida-detection.yaml | 48 +++ rules/mastg-android-xposed-detection.yaml | 52 +++ .../MASVS-RESILIENCE/MASTG-TEST-0x48.md | 31 ++ .../MASVS-RESILIENCE/MASTG-TEST-0x49.md | 48 +++ 25 files changed, 1965 insertions(+) create mode 100644 best-practices/MASTG-BEST-0x48.md create mode 100644 best-practices/MASTG-BEST-0x49.md create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt create mode 100755 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt create mode 100755 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/run.sh create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt create mode 100755 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt create mode 100755 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/run.sh create mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js create mode 100644 rules/mastg-android-frida-detection.yaml create mode 100644 rules/mastg-android-xposed-detection.yaml create mode 100644 tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md create mode 100644 tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md diff --git a/best-practices/MASTG-BEST-0x48.md b/best-practices/MASTG-BEST-0x48.md new file mode 100644 index 00000000000..59f30f05124 --- /dev/null +++ b/best-practices/MASTG-BEST-0x48.md @@ -0,0 +1,44 @@ +--- +title: Resist Frida Instrumentation with Layered, Native-Backed Detection +alias: resist-frida-instrumentation-with-layered-native-backed-detection +id: MASTG-BEST-0x48 +platform: android +knowledge: [MASTG-KNOW-0030] +--- + +Frida is a user-mode instrumentation framework that injects an agent (`frida-agent.so`) into a target process, spawns native worker threads (`gum-js-loop`, `gmain`, `pool-frida`), and rewrites Java methods through ART trampolines. Detecting it from inside the app is necessary but not sufficient: every Java-side check is itself a method that Frida can hook and silence (see @MASTG-DEMO-0x49). The defense must be layered. + +## Java-side detection (necessary but defeatable on its own) + +Run *all* of the following — any single one is trivially defeated, but together they raise the cost of a generic bypass: + +- **Read `/proc/self/maps`** and flag any mapped file whose path contains `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, or `/gum`. Use a `BufferedReader` filter that loops until a clean line is returned, not a single `readLine()` call — Frida's @MASTG-DEMO-0x49 shows that a naive single-line filter leaks consecutive matches. +- **Enumerate `/proc/self/task//comm`** for instrumentation thread names (`gum-js-loop`, `gmain`, `pool-frida`). This catches Frida even when the agent is loaded via `memfd` and the on-disk `.so` is unmapped. +- **Avoid relying on the default `27042` port and the literal binary name `frida-server`.** Both are trivially changed (`./frida-server -l 0.0.0.0:12345` + binary rename). A port-scan / process-name check still belongs in the layered defense as a low-cost trip for lazy attackers, but it must never be your only signal. + +When *any* check fires, the app **must** alert the user and require explicit acceptance of liability before proceeding (see the dialog pattern in @MASTG-DEMO-0x48). Silently continuing — or worse, blocking only on a network call later — wastes the detection. + +## Native-side detection (the bypass-resistant layer) + +Java-side checks live in the same memory the attacker controls. Move the critical detection into JNI: + +- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `frida-gadget` injected via `LD_PRELOAD` and `frida-agent.so` loaded by `frida-server`, even when those files exist only as `/memfd:` mappings. +- Verify your own ELF segments against a build-time hash to catch bytecode/native patching (the first bypass category in @MASTG-TEST-0x48). +- Read `/proc/self/status` and inspect `TracerPid` for any non-zero value — catches `ptrace`-based attaches. + +A native check is harder to bypass because the attacker must hook `dlopen`/`dl_iterate_phdr` or patch the linker itself, not just a Java method. + +## Server-side attestation (the kernel-level layer) + +Kernel-mode interception (a rootkit that hooks syscalls, the third bypass category in @MASTG-TEST-0x48) cannot be reliably detected from user-space. Mitigate it by: + +- Requiring a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel still cannot forge the device-key-signed attestation, because the signing happens inside the TEE / StrongBox. +- Storing sensitive cryptographic material in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key is unrecoverable from a rooted device even if the userspace process is fully compromised. + +## What not to do + +- **Do not hardcode `27042` or `"127.0.0.1"` as your only Frida signal.** They survive in the demo because we deliberately mirror the v1 guidance, but in production an attacker rebinds the server in seconds. +- **Do not hook `Throwable.getStackTrace` yourself** as a way to verify your own detection — the attacker is hooking it too. Trust only the native-side and server-side results when the stakes are high. +- **Do not ship the detection as a single monolithic function** that returns a boolean. Frida can hook one function and you lose everything. Spread checks across multiple call sites, mix Java and native, and have the server reject calls when the cumulative score is below a threshold. + +For runtime checks, see the dynamic demo @MASTG-DEMO-0x48 (passing case) and bypass @MASTG-DEMO-0x49 (failing case). diff --git a/best-practices/MASTG-BEST-0x49.md b/best-practices/MASTG-BEST-0x49.md new file mode 100644 index 00000000000..e3f5fb7f298 --- /dev/null +++ b/best-practices/MASTG-BEST-0x49.md @@ -0,0 +1,68 @@ +--- +title: Resist Xposed/LSPosed Instrumentation with Layered Detection and Server Attestation +alias: resist-xposed-lsposed-instrumentation-with-layered-detection-and-server-attestation +id: MASTG-BEST-0x49 +platform: android +knowledge: [MASTG-KNOW-0030] +--- + +Modern LSPosed (1.9+) is a Zygisk-based reimplementation of Xposed that is designed to defeat the classic Java-side detection techniques the v1 MASTG-TEST-0048 guidance lists. LSPosed Manager can be hidden behind a randomized package name ("Hide LSPosed Manager"), `XposedBridge` is loaded into a separate module classloader that the host app cannot reach, the "Xposed API call protection" toggle additionally intercepts `Class.forName` lookups, and LSPlant (LSPosed's hooking engine) uses ART trampolines instead of injecting fields into `java.lang.reflect.Method`. A detection that relies on any one of those classic surfaces will be silent on a production-attacker device. The defense must be layered. + +## Permission considerations — do not ship with `QUERY_ALL_PACKAGES` + +Avoid `android.permission.QUERY_ALL_PACKAGES` for production apps. It is a Google Play sensitive permission restricted to a narrow list of approved use cases (security/antivirus, accessibility, file managers, etc.) and apps that cannot justify it under one of those categories are rejected at review. + +For Xposed Manager probing, use the manifest `` block instead: + +```xml + + + + + + + +``` + +`PackageManager.getPackageInfo(pkg, 0)` will then resolve the listed packages on Android 11+ without any sensitive permission. This is the technique demonstrated in @MASTG-DEMO-0x4A. + +## Java-side detection (layered, no single check is sufficient) + +Run *all* of the following — any single one is trivially defeated, but together they raise the cost of a generic bypass: + +- **Targeted Manager-package lookup** via `PackageManager.getPackageInfo` against the list above. Catches lazy installs that use a stock LSPosed Manager. Misses installs with "Hide LSPosed Manager" enabled — that case is covered by the next two checks. +- **`Modifier.isNative()` tripwire** on guaranteed-native methods (`System.currentTimeMillis`/`nanoTime`, `Object.notify`/`notifyAll`, `Thread.currentThread`). LSPlant clears the `kAccNative` access-flag bit when it hooks a native method; so does Frida's Java bridge. Acts as a high-confidence tripwire when an attacker hooks one of the audited methods. +- **`/proc/self/maps` scan** for any `/data/app//base.apk` that does not belong to your own package. LSPosed `mmap`s every active module's APK into the host process; this catches active modern modules. +- **Stack-trace probe**: force exceptions through likely-hooked methods (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan `Throwable.getStackTrace()` for `de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_` frames. Combine with `/proc/self/task//comm` enumeration for native worker threads. + +When *any* check fires, the app **must** alert the user and require explicit acceptance of liability before proceeding (see the dialog pattern in @MASTG-DEMO-0x4A). Silently continuing — or worse, blocking only on a network call later — wastes the detection. + +## What modern LSPosed has already neutralized — do not rely on these + +- **`Class.forName("de.robv.android.xposed.XposedBridge")`** — silenced by classloader isolation and by the "Xposed API call protection" toggle that LSPosed ships with by default. +- **`Method.getDeclaredFields()` for `xposedHookedMethodCallbacks` / `additionalInfo`** — a rovo89-era artefact. LSPlant uses ART trampolines and adds no fields to `java.lang.reflect.Method`. +- **`getInstalledApplications` enumeration** for "any app whose package id starts with `de.robv.android.xposed`" — useful only if the attacker hasn't enabled "Hide LSPosed Manager", which is a one-toggle action. + +Including these in the demo (as in @MASTG-DEMO-0x4A) is fine *as long as* they are not the only line of defense. + +## Native-side detection (the bypass-resistant layer) + +Java-side checks live in the same memory the attacker controls. Move the critical detection into JNI: + +- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `liblspd.so` and any LSPosed module SO that is loaded via memfd and would not appear as a `/data/app/.../base.apk` in `/proc/self/maps`. +- Verify your own DEX and `.so` segments against a build-time hash and pin the signing certificate via `PackageInfo.signingInfo` — this catches bytecode/native patching attacks where the attacker NOPs out the detection and re-signs the APK. + +## Server-side attestation (the kernel-level layer) + +Kernel-mode interception is the third bypass category in @MASTG-TEST-0x49 and cannot be reliably detected from user-space. Mitigate it by: + +- Requiring a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel still cannot forge the device-key-signed attestation, because the signing happens inside the TEE / StrongBox. +- Storing sensitive cryptographic material in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key is unrecoverable from a rooted device even if the userspace process is fully compromised. + +## What not to do + +- **Do not hardcode "Hide LSPosed Manager" packages.** The whole point of the feature is that the package id is randomized per-install. Use the multi-signal approach instead. +- **Do not return a single boolean from `isInstrumented()`.** An attacker hooks the function and you lose everything. Spread the checks across multiple call sites, mix Java and native, and let the server reject a session when the cumulative risk score is above a threshold. +- **Do not ship the alert dialog without a server check.** A Java-side dialog can be dismissed by the same bypass that silenced the detection (see @MASTG-DEMO-0x4B). The dialog is the *user-facing* control; the *authoritative* control must be on a server you operate. + +For runtime checks, see the dynamic demo @MASTG-DEMO-0x4A (passing case) and bypass @MASTG-DEMO-0x4B (failing case). diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md new file mode 100644 index 00000000000..a2556b11f61 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md @@ -0,0 +1,48 @@ +--- +platform: android +title: Uses of Frida Detection Techniques (Default Port, Process Enumeration and /proc/maps) +id: MASTG-DEMO-0x48 +code: [kotlin] +test: MASTG-TEST-0x48 +tools: [MASTG-TOOL-0110] +kind: pass +--- + +## Sample + +The snippet below shows sample code that performs three common Frida detection techniques used by Android apps as anti-instrumentation checks (see @MASTG-KNOW-0030 for more information about Detection of Reverse Engineering Tools): + +- Scanning the default `frida-server` TCP port (`127.0.0.1:27042`). +- Enumerating running processes via `/proc//cmdline` looking for `frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, etc. +- Reading `/proc/self/maps` looking for injected Frida artifacts (`frida-agent.so`, `libfrida`, `frida-gadget`, `gum`, `linjector`). + +These checks are well-known and trivially bypassable at runtime. + +{{ MastgTest.kt # MastgTest_reversed.java }} + +## Steps + +Run the static analysis rule against the decompiled code. + +{{ ../../../../rules/mastg-android-frida-detection.yaml }} + +{{ run.sh }} + +## Observation + +The output shows the semgrep rule matching the three detection routines. + +{{ output.txt }} + +## Evaluation + +The test passes because the app statically implements three independent Frida detection mechanisms — semgrep flags: + +- `Socket.connect` with `127.0.0.1:27042` (the default-port probe). +- The `/proc` enumeration that walks `/proc//cmdline` and the `frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, `gmain` string literals it greps for. +- The `/proc/self/maps` reader paired with the injected-library substrings (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`). + +!!! note + + This is a "kind: pass" demo for static presence only: the sample contains the detection mechanisms expected by the test. The dynamic counterpart @MASTG-DEMO-0x49 covers a runtime bypass that defeats all three of these checks. + diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8a7467e9b568615b4d94535ce19c33bbaa01faa GIT binary patch literal 5528 zcmc&&e{b715Z&MY6r51BG8QXoF<_gqu5+7Y>yWfT>|z+!pROB!M*Jz0@tEGiju`mW+q)M}kl)M}np|6F$*!PbMkrjB{gEtA&f+sLQ z_Ad5m^}Z2Vy1(Rwxo|dQ{uUu3qD)?Dt|d&HGG0(L-X37hZDbTIUSN{j^8!G#i+Q=c zh{*IL%WC*su#9WL-ia(5UFGUs?mV$tUQ3>3D_~b}WB4TwAT)sdbTXa&FgiRseK|UK z|K&HsXfTjV2L~b`h$G+|O>hv`(yn%nWkTCSSXcA@aIA7m{y(Gd_qIQ*O!Omm!TKE| zCrSaydf+X;=8;s<(=ttk7RfoF-5x!bndq;ecROb1!bZ>W&s4=%!cLMz<7hi;X+||V z7{nPsW2a#CdHLmW2?{=DE(2Uf z9agkI!(iChr!_KV;bsl_PcPda(Cm2f{AA32t@tuKn$CzXU;pNl2wCqF7HkI9I(g>k zjO)CV=VgpwA}L>8_w(DCVmHd6Y?1J6By$tnM5v+5Tv-ai&o5`Vp-+0jycfEh}E#%f8W!aK;M#*Y(?5wVlI~eBY zN7B4d_GnqyRXFUsO6TaLvx6HV*j`e`yruOu$ zlKD(kX&VLrZMbeB^y#>DOOOU-9ku-~VR~|br!JXBT4bnKNe43z7(>uPGuNO#3DGJ_ zG%XcL@-G>O4rsRP+DY9!cW`dA;yze}CR;E0*<|9z_$l6jvkV*wuD|4${ zNAX4XQ_%Djs_Y~6uugbtm%gD4DWg!zUO@*QD}5sQrOY6d!cB#yMZvj%NHk(3^0~}~ za54ZQuHV6|QW+_P(6Maj=(S-331O1Y0sEqBJQ3G27Ln5*GB1G_R5Fyuiu5$OC-Dd% znGtcR;hd9%3Adbp6?1wOZJV9(g1rtVF*Oyo3G8*ia262!(3WT#Q2nH+wKP}GDv^ME z*dvYlIyibSEmmx~3=^)ixy2+fgH1@oM%-v&OPwIuCpDwMH3 zA>p{w>j+d z3-zwmDXJ}NwbN$REy{V!o+3>J&s!%=bTaNH0WwvWhSnCI$J<)HS`vYMIqres)_o)l z9zOaiI>4WYP#^w!${rmYbYbsE+B+%2>KaiY%-EU%FLyiaDu3s&k1KNdN{H5q^G#QA zHb-|CM9&(XI7V?X_S;Hjeb)mO#S-^J(wHQ&U#4M@wyxbI$*Sk--rYRAdow-anml@I zMj39C?s18PZ6yF5T%%)`r6p>UKQGhTBOK%=4{17zO$>kNUKs|i8}{rKGC7o*e5R=p zIZl%eIG+;XQ_6R#apDRR;{JWSAPF1c1TaNfH$0cPqyg-8Q-*3s8+Yh;h_Kp@#Dd?r zUFYbC$?15RptFeD?`kIC@$Xv^&-a9(7M~0wsE7E-42b4FocGuP9F`jOz}C$ zDIgHSgw^Do*AZG!UkK=~8r-|7;B%+89^WHNJpgG9eJ&C6RR=A4+oVeOsd?`!hTqm1 z_;7_fX*BS^%BM}fKwY|wYEo&=(dF%qTmqp;6e~?*`$)7F2*kOCw3GxssivZvQxrOV zy1(dz&CfMHxf($opu>`~xdE)mqYkW>I>PZE@py4UG`~T)z*lb)ZFhH)&}Z0)3j}ub%sFn(^j;o+!?~RaCfq60Ha8 F!9QS2M`r*4 literal 0 HcmV?d00001 diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java new file mode 100644 index 00000000000..7eb76b7855d --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java @@ -0,0 +1,251 @@ +package org.owasp.mastestapp; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; +import androidx.compose.runtime.ComposerKt; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileReader; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import kotlin.Metadata; +import kotlin.Unit; +import kotlin.collections.CollectionsKt; +import kotlin.io.CloseableKt; +import kotlin.io.FilesKt; +import kotlin.io.TextStreamsKt; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.internal.Intrinsics; +import kotlin.text.StringsKt; + +/* compiled from: MastgTest.kt */ +@Metadata(d1 = {"\u0000.\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010 \n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u0007H\u0002J\b\u0010\u000b\u001a\u00020\fH\u0002J\u000e\u0010\r\u001a\b\u0012\u0004\u0012\u00020\u00070\u000eH\u0002J\u000e\u0010\u000f\u001a\b\u0012\u0004\u0012\u00020\u00070\u000eH\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u0010"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "mastgTest", "", "promptUserForLiability", "", "message", "checkFridaDefaultPort", "", "checkFridaProcesses", "", "checkFridaLibraries", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +/* loaded from: classes3.dex */ +public final class MastgTest { + public static final int $stable = 8; + private final Context context; + + public MastgTest(Context context) { + Intrinsics.checkNotNullParameter(context, "context"); + this.context = context; + } + + /* JADX WARN: Unsupported multi-entry loop pattern (BACK_EDGE: B:9:0x0023 -> B:30:0x003c). Please report as a decompilation issue!!! */ + public final String mastgTest() { + DemoResults r = new DemoResults("0x48"); + boolean anyFail = false; + try { + boolean portFound = checkFridaDefaultPort(); + if (portFound) { + r.add(Status.FAIL, "Frida default port (27042) is open — instrumentation detected."); + anyFail = true; + } else { + r.add(Status.PASS, "Frida default port (27042) is closed."); + } + } catch (Exception e) { + r.add(Status.ERROR, "Port check failed: " + e); + } + try { + List matches = checkFridaProcesses(); + if (!matches.isEmpty()) { + r.add(Status.FAIL, "Suspicious processes found: " + CollectionsKt.joinToString$default(matches, ", ", null, null, 0, null, null, 62, null)); + anyFail = true; + } else { + r.add(Status.PASS, "No Frida-related process names found under /proc."); + } + } catch (Exception e2) { + r.add(Status.ERROR, "Process enumeration failed: " + e2); + } + try { + List libsFound = checkFridaLibraries(); + if (!libsFound.isEmpty()) { + r.add(Status.FAIL, "Injected libraries detected in /proc/self/maps: " + CollectionsKt.joinToString$default(libsFound, ", ", null, null, 0, null, null, 62, null)); + anyFail = true; + } else { + r.add(Status.PASS, "No Frida libraries mapped into the process."); + } + } catch (Exception e3) { + r.add(Status.ERROR, "Maps check failed: " + e3); + } + if (anyFail) { + promptUserForLiability("Reverse-engineering or instrumentation tooling (Frida) was detected on this device. Continued use may compromise app security and data integrity. Tap \"Accept Liability\" to acknowledge the risk and continue, or \"Exit\" to close the app."); + } + return r.toJson(); + } + + private final void promptUserForLiability(final String message) { + Context context = this.context; + final Activity activity = context instanceof Activity ? (Activity) context : null; + if (activity == null) { + return; + } + new Handler(Looper.getMainLooper()).post(new Runnable() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda3 + @Override // java.lang.Runnable + public final void run() { + MastgTest.promptUserForLiability$lambda$2(activity, message); + } + }); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final void promptUserForLiability$lambda$2(final Activity activity, String message) { + Intrinsics.checkNotNullParameter(activity, "$activity"); + Intrinsics.checkNotNullParameter(message, "$message"); + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + new AlertDialog.Builder(activity).setTitle("Security Warning").setMessage(message).setCancelable(false).setPositiveButton("Accept Liability", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda1 + @Override // android.content.DialogInterface.OnClickListener + public final void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + } + }).setNegativeButton("Exit", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda2 + @Override // android.content.DialogInterface.OnClickListener + public final void onClick(DialogInterface dialogInterface, int i) { + MastgTest.promptUserForLiability$lambda$2$lambda$1(activity, dialogInterface, i); + } + }).show(); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final void promptUserForLiability$lambda$2$lambda$1(Activity activity, DialogInterface dialogInterface, int i) { + Intrinsics.checkNotNullParameter(activity, "$activity"); + activity.finishAffinity(); + } + + private final boolean checkFridaDefaultPort() throws IOException { + Socket socket = new Socket(); + try { + socket.connect(new InetSocketAddress("127.0.0.1", 27042), ComposerKt.invocationKey); + try { + socket.close(); + } catch (Exception e) { + } + return true; + } catch (Exception e2) { + try { + socket.close(); + } catch (Exception e3) { + } + return false; + } catch (Throwable th) { + try { + socket.close(); + } catch (Exception e4) { + } + throw th; + } + } + + private final List checkFridaProcesses() { + int i = 1; + List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", "frida-agent", "gum-js-loop", "gmain"}); + List matches = new ArrayList(); + File proc = new File("/proc"); + File[] pidDirs = proc.listFiles(new FileFilter() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0 + @Override // java.io.FileFilter + public final boolean accept(File file) { + return MastgTest.checkFridaProcesses$lambda$4(file); + } + }); + if (pidDirs == null) { + return matches; + } + int length = pidDirs.length; + int i2 = 0; + while (i2 < length) { + File pidDir = pidDirs[i2]; + File cmdline = new File(pidDir, "cmdline"); + if (cmdline.canRead()) { + try { + String name = StringsKt.trim((CharSequence) StringsKt.replace$default(FilesKt.readText$default(cmdline, null, i, null), (char) 0, ' ', false, 4, (Object) null)).toString(); + Iterator it = needles.iterator(); + while (true) { + if (it.hasNext()) { + String needle = (String) it.next(); + if (StringsKt.contains$default((CharSequence) name, (CharSequence) needle, false, 2, (Object) null)) { + matches.add(pidDir.getName() + ":" + name); + break; + } + } + } + } catch (Exception e) { + } + } + i2++; + i = 1; + } + return matches; + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final boolean checkFridaProcesses$lambda$4(File f) { + CharSequence $this$all$iv; + if (!f.isDirectory()) { + return false; + } + CharSequence name = f.getName(); + Intrinsics.checkNotNullExpressionValue(name, "getName(...)"); + CharSequence $this$all$iv2 = name; + int i = 0; + while (true) { + if (i < $this$all$iv2.length()) { + char element$iv = $this$all$iv2.charAt(i); + if (!Character.isDigit(element$iv)) { + $this$all$iv = null; + break; + } + i++; + } else { + $this$all$iv = 1; + break; + } + } + return $this$all$iv != null; + } + + private final List checkFridaLibraries() throws IOException { + final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + final Set hits = new LinkedHashSet(); + BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); + try { + BufferedReader br = bufferedReader; + TextStreamsKt.forEachLine(br, new Function1() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda4 + @Override // kotlin.jvm.functions.Function1 + public final Object invoke(Object obj) { + return MastgTest.checkFridaLibraries$lambda$6$lambda$5(needles, hits, (String) obj); + } + }); + Unit unit = Unit.INSTANCE; + CloseableKt.closeFinally(bufferedReader, null); + return CollectionsKt.toList(hits); + } finally { + } + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final Unit checkFridaLibraries$lambda$6$lambda$5(List needles, Set hits, String line) { + Intrinsics.checkNotNullParameter(needles, "$needles"); + Intrinsics.checkNotNullParameter(hits, "$hits"); + Intrinsics.checkNotNullParameter(line, "line"); + Iterator it = needles.iterator(); + while (it.hasNext()) { + String needle = (String) it.next(); + if (StringsKt.contains((CharSequence) line, (CharSequence) needle, true)) { + hits.add(needle); + } + } + return Unit.INSTANCE; + } +} diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt new file mode 100644 index 00000000000..cc5b3581559 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt @@ -0,0 +1,73 @@ + + +┌──────────────────┐ +│ 15 Code Findings │ +└──────────────────┘ + + MastgTest_reversed.java + ❱ rules.mastg-android-frida-detection-default-port + [MASVS-RESILIENCE-4] The app references the default frida-server TCP port (27042), suggesting a + port-scan-based Frida detection. + + 130┆ socket.connect(new InetSocketAddress("127.0.0.1", 27042), ComposerKt.invocationKey); + + ❱ rules.mastg-android-frida-detection-loopback-literal + [MASVS-RESILIENCE-4] The app references 127.0.0.1, which is typically combined with a port literal + to scan for frida-server. + + 130┆ socket.connect(new InetSocketAddress("127.0.0.1", 27042), ComposerKt.invocationKey); + + ❱ rules.mastg-android-frida-detection-frida-identifier-literals + [MASVS-RESILIENCE-4] The app contains string literals associated with Frida or Gum runtime artifacts + (frida-server, frida-helper, frida-agent, frida-gadget, libfrida, gum-js-loop, gmain, linjector, + re.frida). Strong indicator of Frida-detection logic. + + 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", + "frida-agent", "gum-js-loop", "gmain"}); + ⋮┆---------------------------------------- + 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", + "frida-agent", "gum-js-loop", "gmain"}); + ⋮┆---------------------------------------- + 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", + "frida-agent", "gum-js-loop", "gmain"}); + ⋮┆---------------------------------------- + 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", + "frida-agent", "gum-js-loop", "gmain"}); + ⋮┆---------------------------------------- + 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", + "frida-agent", "gum-js-loop", "gmain"}); + + ❱ rules.mastg-android-frida-detection-proc-enumeration + [MASVS-RESILIENCE-4] The app references /proc and cmdline, a common primitive pair for enumerating + running processes (e.g. to look for a frida-server process by name). + + 155┆ File proc = new File("/proc"); + ⋮┆---------------------------------------- + 169┆ File cmdline = new File(pidDir, "cmdline"); + + ❱ rules.mastg-android-frida-detection-frida-identifier-literals + [MASVS-RESILIENCE-4] The app contains string literals associated with Frida or Gum runtime artifacts + (frida-server, frida-helper, frida-agent, frida-gadget, libfrida, gum-js-loop, gmain, linjector, + re.frida). Strong indicator of Frida-detection logic. + + 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + + ❱ rules.mastg-android-frida-detection-proc-maps-read + [MASVS-RESILIENCE-4] The app references /proc/self/maps, the canonical primitive for detecting an + injected Frida agent or any other foreign library mapped into the process. + + 221┆ BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); + diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh new file mode 100755 index 00000000000..10087793875 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-frida-detection.yaml ./MastgTest_reversed.java > output.txt \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md new file mode 100644 index 00000000000..a07c7813c21 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md @@ -0,0 +1,56 @@ +--- +platform: android +title: Bypassing Frida Detection via API Hooking and Server Reconfiguration +id: MASTG-DEMO-0x49 +code: [kotlin] +test: MASTG-TEST-0x48 +tools: [MASTG-TOOL-0031] +kind: fail +--- + +## Sample + +This sample uses the same code as @MASTG-DEMO-0x48, which implements three Frida detection mechanisms: a default-port (`127.0.0.1:27042`) scan, a `/proc//cmdline` walk for `frida-server` / `frida-helper` / `frida-agent` / `gum-js-loop` / `gmain`, and a `/proc/self/maps` read for injected Frida libraries. This demo defeats all three by combining **server reconfiguration** — `frida-server` is renamed to `notfrida` on the device and bound to a non-default port — with **return-value tampering** — a Frida script hooks `Socket.connect`, `File.listFiles`, `BufferedReader.readLine`, and `FileReader` so each detection routine sees the values it would see on a clean device. + +See @MASTG-KNOW-0030 and @MASTG-KNOW-0032 for more context on bypassing runtime detection mechanisms. + +!!! note + This is a series of correlated tests. + + - @MASTG-DEMO-0x48 is a successful test (successful defense) that defend against Frida-detection mechanisms + - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x48 by using a runtime API-hooking bypass. + +{{ ../MASTG-DEMO-0x48/MastgTest.kt }} + +## Steps + +1. Install the app on a device (@MASTG-TECH-0005). +2. Push and rename `frida-server` on the device, then start it on a non-default port and forward it to the host: + + ```sh + adb push frida-server /data/local/tmp/notfrida + adb shell "chmod +x /data/local/tmp/notfrida && /data/local/tmp/notfrida -l 0.0.0.0:12345 &" + adb forward tcp:12345 tcp:12345 + ``` + +3. Run `run.sh` to spawn the app with the bypass script attached. +4. Click the **Start** button in the app and observe the in-app result and the Frida console output. +5. Stop the script by pressing `Ctrl+C` and/or `q` to quit the Frida CLI. + +{{ script.js # run.sh }} + +## Observation + +The frida console shows each hook firing — `Socket.connect` to `127.0.0.1:27042` is blocked, `File.listFiles("/proc")` returns a list with every Frida-named pid removed, and `BufferedReader.readLine` returns a sanitized view of `/proc/self/maps` — while the app reports **PASS** for all three Frida-detection checks. + +{{ output.txt }} + +## Evaluation + +The test fails because every detection routine has been neutralized at runtime: + +- **Default-port scan.** The `java.net.Socket.connect` hook intercepts connect attempts to `127.0.0.1:27042` and raises `ConnectException`, so `checkFridaDefaultPort()` reports the port closed. Server reconfiguration also moves `frida-server` to port `12345`, so the hardcoded probe would miss it even without the hook. +- **Process enumeration.** The `File.listFiles` hook filters `/proc` listings to drop any pid whose `cmdline` contains a Frida-related substring, and the `frida-server` binary has been renamed to `notfrida`, so `checkFridaProcesses()` finds nothing. +- **`/proc/self/maps` parsing.** The `BufferedReader.readLine` hook transparently drops any line that mentions `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, or `linjector`, so `checkFridaLibraries()` returns an empty list even though the agent is mapped into the process. + +The script also installs a top-level exception handler so any anti-tamper crash raised by the detection code is swallowed. This demonstrates why these three Frida-detection mechanisms must never be relied on as a single line of defence and must be combined with integrity-protected native checks and remote attestation. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt new file mode 100644 index 00000000000..50ed8e85a78 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt @@ -0,0 +1,6 @@ +[+] Frida detection bypass hooks installed +[+] Blocked Frida port probe +[+] Removed maps entry +[+] Removed maps entry +[+] Removed maps entry +[+] Removed maps entry diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/run.sh new file mode 100755 index 00000000000..4c033ae2b4a --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +frida -U -f org.owasp.mastestapp -l ./script.js -o output.txt diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js new file mode 100644 index 00000000000..3e182da8082 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js @@ -0,0 +1,140 @@ +Java.perform(function () { + + const PROC_KEYWORDS = [ + "frida-server", + "frida-agent", + "gum-js-loop", + "gmain" + ]; + + const MAPS_KEYWORDS = [ + "frida", + "gum-js-loop", + "libfrida" + ]; + + const File = Java.use("java.io.File"); + const FileInputStream = Java.use("java.io.FileInputStream"); + const BufferedReader = Java.use("java.io.BufferedReader"); + const Socket = Java.use("java.net.Socket"); + const InetSocketAddress = Java.use("java.net.InetSocketAddress"); + const ConnectException = Java.use("java.net.ConnectException"); + +const socketConnect = + Socket.connect.overload("java.net.SocketAddress", "int"); + +socketConnect.implementation = function (endpoint, timeout) { + + const addr = Java.cast(endpoint, InetSocketAddress); + const port = addr.getPort(); + + if (port === 27042) { + + console.log("[+] Blocked Frida port probe"); + + throw ConnectException.$new("Connection refused"); + } + + return socketConnect.call(this, endpoint, timeout); +}; + + const listFiles = File.listFiles.overload(); + + listFiles.implementation = function () { + + const files = listFiles.call(this); + + if (files === null) { + return files; + } + + if (this.getAbsolutePath() !== "/proc") { + return files; + } + + let filtered = []; + + for (let i = 0; i < files.length; i++) { + + try { + + const cmdline = + File.$new(files[i], "cmdline"); + + const fis = FileInputStream.$new(cmdline); + + const buffer = + Java.array("byte", new Array(256).fill(0)); + + const size = fis.read(buffer); + + fis.close(); + + if (size > 0) { + + let processName = ""; + + for (let j = 0; j < size; j++) { + processName += + String.fromCharCode(buffer[j] & 0xff); + } + + let hide = false; + + for (let k = 0; k < PROC_KEYWORDS.length; k++) { + + if (processName.indexOf(PROC_KEYWORDS[k]) !== -1) { + hide = true; + break; + } + } + + if (hide) { + console.log("[+] Hiding process: " + processName); + continue; + } + } + + } catch (e) {} + + filtered.push(files[i]); + } + + return Java.array("java.io.File", filtered); + }; + + + const readLine = + BufferedReader.readLine.overload(); + + readLine.implementation = function () { + + while (true) { + + const line = readLine.call(this); + + if (line === null) { + return null; + } + + let suspicious = false; + + for (let i = 0; i < MAPS_KEYWORDS.length; i++) { + + if (line.indexOf(MAPS_KEYWORDS[i]) !== -1) { + suspicious = true; + break; + } + } + + if (!suspicious) { + return line; + } + + console.log("[+] Removed maps entry"); + } + }; + + + console.log("[+] Frida detection bypass hooks installed"); +}); \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml new file mode 100644 index 00000000000..cc43d3d129d --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md new file mode 100644 index 00000000000..833dde0e63b --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -0,0 +1,51 @@ +--- +platform: android +title: Uses of Xposed/LSPosed Detection Techniques +id: MASTG-DEMO-0x4A +code: [kotlin] +test: MASTG-TEST-0x49 +tools: [MASTG-TOOL-0110] +kind: pass +--- + +## Sample + +The snippet below shows sample code that performs four Xposed/LSPosed/Frida detection techniques used by Android apps as anti-instrumentation checks (see @MASTG-KNOW-0030 for more information about Detection of Reverse Engineering Tools). The checks combine a `PackageManager` lookup for known framework Manager package ids, a reflection-based tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / thread probe that surfaces framework frames and native worker threads. + +- **Check 1 — Classic Manager-package lookup.** Targets a curated list of known LSPosed / Xposed / EdXposed Manager package ids declared in `` in the manifest, and calls `PackageManager.getPackageInfo(pkg, 0)` for each. Catches lazy installs of the Manager. + +- **Check 2 — Classic method-descriptor verification.** Resolves a set of guaranteed-native methods via reflection (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`) and checks that `Modifier.NATIVE` is still set on each. LSPlant clears `kAccNative` whenever it hooks a native method, and Frida's Java bridge does the same — so a missing bit on any of these methods is a strong tripwire. Only fires when the attacker actually targets one of the audited methods. + +- **Check 3 — Foreign DEX/APK in `/proc/self/maps`.** Reads the app's own memory map and flags any `/data/app//base.apk` path that does not belong to the app itself. LSPosed `mmap`s a module's `base.apk` into every hooked process. Permission-free. + +- **Check 4 — Stack-trace and thread inspection.** Forces three exceptions through calls an attacker is likely to hook — `PackageManager.getPackageInfo`, `Runtime.exec`, and `File.exists` — and scans each `Throwable.stackTrace` for class names belonging to known instrumentation frameworks. Also walks `Thread.getAllStackTraces()` and `/proc/self/task//comm` to catch native worker threads (Frida's `gum-js-loop`, etc.) that have no Java `Thread` representation. + +{{ MastgTest.kt # MastgTest_reversed.java # AndroidManifest.xml }} + +## Steps + +Run the static analysis rule against the decompiled code. + +{{ ../../../../rules/mastg-android-xposed-detection.yaml }} + +{{ run.sh }} + +## Observation + +The output shows the semgrep rule matching all four detection routines in the reversed Java. + +{{ output.txt }} + +## Evaluation + +The test passes because the app statically implements four independent Xposed/LSPosed/Frida detection mechanisms — semgrep flags: + +- `PackageManager.getPackageInfo` against the known Manager package ids (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`). +- The `Modifier.isNative` tripwire on the audited guaranteed-native methods. +- The `/proc/self/maps` reader paired with `/data/app/` inspection. +- The stack-trace / `/proc/self/task` probe via `Throwable.getStackTrace` / `Thread.getAllStackTraces`. + +!!! note + + This is a "kind: pass" demo for static presence only: the sample contains the detection mechanisms expected by the test. The dynamic counterpart @MASTG-DEMO-0x4B covers a Frida-based runtime bypass that defeats all four of these checks. + diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt new file mode 100644 index 00000000000..2db37441158 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt @@ -0,0 +1,322 @@ +package org.owasp.mastestapp + +// SUMMARY: This sample demonstrates four Xposed/LSPosed/Frida detection techniques. Two +// of them are classic Java-side techniques mirrored from the MASTG-TEST-0048 v1 guidance +// (intentionally vulnerable, easily bypassed, and on a modern LSPosed install often +// silent even without a bypass). The other two are the production-grade techniques we +// found to actually fire against modern LSPosed 1.9+ on Android 12+: +// +// 1. CLASSIC — PackageManager.getPackageInfo for known Xposed/LSPosed/EdXposed Manager +// package ids declared in in AndroidManifest.xml. No Play-restricted +// `QUERY_ALL_PACKAGES` permission needed. Catches lazy installs of the Manager; +// misses the stealth "Hide LSPosed Manager" mode where the Manager is renamed. +// 2. CLASSIC — Method-descriptor verification via reflection. Pick a small set of +// guaranteed-native methods (`System.currentTimeMillis` / `nanoTime`, +// `Object.notify` / `notifyAll`, `Thread.currentThread`); resolve them via +// reflection; if `Modifier.NATIVE` is no longer set on any one of them, LSPlant or +// Frida has hooked the method. Acts as a tripwire only — fires when the attacker +// targets one of the specific methods we audit. +// 3. STRONG — Walk /proc/self/maps for any foreign /data/app//base.apk mapping. +// When LSPosed injects a module into the host process, the module's base.apk is +// mmapped right next to the app's own one. Permission-free. +// 4. STRONG — Stack-trace inspection: deliberately call methods an attacker is likely +// to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) with +// arguments that force an exception, then walk the exception's stack trace and +// every live Java thread's stack looking for `de.robv.android.xposed.*`, +// `org.lsposed.lspd.*`, `LSPHooker_`, or `re.frida.*` frames. Also walks +// /proc/self/task//comm to catch native Frida workers (`gum-js-loop`, etc.) +// that have no Java Thread representation. Permission-free. +// +// When ANY check fires the app raises a "Security Warning" dialog and asks the user to +// accept liability before continuing, per the MASTG-TEST-0048 guidance. +// +// See MASTG-DEMO-0x4B for the bypass narrative. + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.pm.PackageManager +import android.os.Handler +import android.os.Looper +import java.io.BufferedReader +import java.io.File +import java.io.FileReader +import java.lang.reflect.Modifier + +class MastgTest(private val context: Context) { + + fun mastgTest(): String { + val r = DemoResults("0x4A") + var anyFail = false + + // Check 1 (classic): targeted PackageManager lookup against a curated list of known + // LSPosed / Xposed / EdXposed Manager package ids declared in in + // AndroidManifest.xml. This avoids the Play-restricted QUERY_ALL_PACKAGES permission. + // The trade-off is that Manager packages hidden behind randomized package names + // won't be caught here; those are covered by Checks 3 and 4. + try { + val managers = checkKnownXposedManagerPackages() + if (managers.isNotEmpty()) { + // FAIL: [MASTG-TEST-0x49] A known Xposed Manager package is installed. + r.add(Status.FAIL, "Xposed Manager package(s) installed: ${managers.joinToString(", ")}") + anyFail = true + } else { + // PASS: [MASTG-TEST-0x49] No known Xposed Manager package is installed. + r.add(Status.PASS, "No known Xposed Manager package found via PackageManager.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "Manager lookup failed: $e") + } + + // Check 2 (classic): method-descriptor verification. Acts as a tripwire — only fires + // if the attacker has hooked one of the specific guaranteed-native methods we audit. + try { + val tampered = checkHookedMethodSignatures() + if (tampered.isNotEmpty()) { + // FAIL: [MASTG-TEST-0x49] A guaranteed-native method has had its NATIVE bit cleared. + r.add(Status.FAIL, "Methods appear to be hooked: ${tampered.joinToString(", ")}") + anyFail = true + } else { + // PASS: [MASTG-TEST-0x49] Sensitive methods retain their original modifiers. + r.add(Status.PASS, "Audited native methods retain their NATIVE modifier.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "Method-descriptor check failed: $e") + } + + // Check 3 (strong): foreign DEX/APK mapped into our own process. + try { + val foreignDexes = checkForeignDexesInMaps() + if (foreignDexes.isNotEmpty()) { + // FAIL: [MASTG-TEST-0x49] A foreign APK/DEX is mapped into our process. + r.add(Status.FAIL, "Foreign DEX/APK mapped into process: ${foreignDexes.joinToString(", ")}") + anyFail = true + } else { + // PASS: [MASTG-TEST-0x49] No foreign DEX/APK is mapped. + r.add(Status.PASS, "No foreign DEX/APK mapped into process.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "/proc/self/maps inspection failed: $e") + } + + // Check 4 (strong): stack-trace inspection + native-thread enumeration. + try { + val frames = checkInstrumentationFramesInStacks() + if (frames.isNotEmpty()) { + // FAIL: [MASTG-TEST-0x49] An instrumentation framework frame was found on a stack. + r.add(Status.FAIL, "Instrumentation frames on stack: ${frames.joinToString(", ")}") + anyFail = true + } else { + // PASS: [MASTG-TEST-0x49] No instrumentation frames observed in any stack. + r.add(Status.PASS, "No Xposed/LSPosed/Frida frames found in any thread's stack.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "Stack-trace inspection failed: $e") + } + + if (anyFail) promptUserForLiability( + "Reverse-engineering or instrumentation tooling (Xposed/LSPosed/Frida) was " + + "detected on this device. Continued use may compromise app security and data " + + "integrity. Tap \"Accept Liability\" to acknowledge the risk and continue, or " + + "\"Exit\" to close the app." + ) + + return r.toJson() + } + + /** + * Per the MASTG-TEST-0048 guidance, when reverse-engineering tools are detected the + * app must alert the user and require them to accept liability before continuing. + * The dialog is dispatched to the main thread because `mastgTest()` runs on a worker + * thread (see `MainActivity.kt`). + */ + private fun promptUserForLiability(message: String) { + val activity = context as? Activity ?: return + Handler(Looper.getMainLooper()).post { + if (activity.isFinishing || activity.isDestroyed) return@post + AlertDialog.Builder(activity) + .setTitle("Security Warning") + .setMessage(message) + .setCancelable(false) + .setPositiveButton("Accept Liability") { d, _ -> d.dismiss() } + .setNegativeButton("Exit") { _, _ -> activity.finishAffinity() } + .show() + } + } + + /** + * Targeted `PackageManager.getPackageInfo` lookup for each known Xposed/LSPosed Manager + * package id. Those package ids are declared in `` in `AndroidManifest.xml`, + * which lets `getPackageInfo` see them on Android 11+ without holding the Play- + * restricted `QUERY_ALL_PACKAGES` permission. A Manager package id that resolves + * non-null means that flavour of the framework is installed on the device. + * + * Limitation: doesn't detect Managers that have been renamed via LSPosed's + * "Hide LSPosed Manager" feature. Those installs are caught by Checks 3 and 4. + */ + private fun checkKnownXposedManagerPackages(): List { + val knownManagers = listOf( + "de.robv.android.xposed.installer", // classic Xposed + "org.meowcat.edxposed.manager", // EdXposed + "io.va.exposed", // VirtualXposed + "com.solohsu.android.edxp.manager", // EdXposed (alt) + "org.lsposed.manager" // modern LSPosed + ) + val pm = context.packageManager + return knownManagers.filter { pkg -> + try { pm.getPackageInfo(pkg, 0); true } catch (_: PackageManager.NameNotFoundException) { false } + } + } + + /** + * Verifies that a small set of methods declared `native` on stock AOSP still report the + * NATIVE bit via reflection. LSPlant (LSPosed's hooking engine) clears `kAccNative` + * when it hooks a native method, so this acts as a tripwire for any attacker that + * targets one of these primitives. Frida's Java bridge also clears the bit when it + * hooks a native method via implementation-replacement. + * + * The set is curated: every entry must be `public final native` (or `public static + * native`) on every supported Android version. `android.os.Process.myPid` and + * `java.lang.Object.hashCode` are deliberately omitted — they delegate to libcore in + * Java and never carry NATIVE on a clean device. + */ + private fun checkHookedMethodSignatures(): List { + val tampered = mutableListOf() + val targets = mapOf( + "java.lang.System" to listOf("currentTimeMillis", "nanoTime"), + "java.lang.Object" to listOf("notify", "notifyAll"), + "java.lang.Thread" to listOf("currentThread") + ) + for ((cls, methods) in targets) { + try { + val c = Class.forName(cls) + for (mName in methods) { + val m = c.getDeclaredMethod(mName) + if (!Modifier.isNative(m.modifiers)) { + tampered.add("$cls.$mName (native bit cleared, modifiers=0x${Integer.toHexString(m.modifiers)})") + } + } + } catch (_: Exception) { /* ignore */ } + } + return tampered + } + + /** + * Reads `/proc/self/maps` and flags any `/data/app//base.apk` mapping that does NOT + * belong to our own package. When LSPosed injects a module into a host process, the + * module's `base.apk` is mmapped into that process and shows up in maps alongside the + * host app's APK. + */ + private fun checkForeignDexesInMaps(): List { + val ownPkg = context.packageName + val hits = LinkedHashSet() + BufferedReader(FileReader("/proc/self/maps")).use { br -> + br.forEachLine { line -> + val idx = line.indexOf("/data/app/") + if (idx < 0) return@forEachLine + val path = line.substring(idx).substringBefore(' ') + if (!path.endsWith(".apk")) return@forEachLine + if (path.contains("/$ownPkg-") || path.contains("/$ownPkg/")) return@forEachLine + val pkg = path.substringAfter("/data/app/").substringAfter('/').substringBefore('-') + hits.add(pkg) + } + } + return hits.toList() + } + + /** + * Two-pronged instrumentation probe: + * + * (a) Stack-trace inspection — Xposed/LSPosed hook handlers are Java code, so when a + * hooked method throws, the resulting `Throwable.stackTrace` carries the handler + * frames. We deliberately call three methods an attacker is likely to hook + * (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) with arguments + * that force an exception, then scan the captured stack for known framework class + * names. Also walks `Thread.getAllStackTraces()` for any Java thread parked + * inside a framework class. + * + * (b) Native-thread enumeration — Frida hooks do NOT add Java stack frames (the hook + * handler is V8/QuickJS native code), and Frida's worker threads (`gum-js-loop`, + * `gmain`, `pool-frida`) are native pthreads with no Java `Thread` object, so they + * are invisible to `Thread.getAllStackTraces()`. To catch them we walk + * `/proc/self/task//comm` directly — the kernel exposes every thread's name + * to its own process regardless of whether it's a Java thread. + */ + private fun checkInstrumentationFramesInStacks(): List { + val needles = listOf( + "de.robv.android.xposed", + "org.lsposed.lspd", + "org.lsposed.", + "lsphooker_", + "lsplant", + "edxposed", + "re.frida", + "gum-js" + ) + val hits = LinkedHashSet() + + fun scan(label: String, frames: Array?) { + if (frames == null) return + for (f in frames) { + val low = f.className.lowercase() + for (n in needles) { + if (low.contains(n.lowercase())) { + hits.add("$label: ${f.className}.${f.methodName}") + } + } + } + } + + // Probe 1: PackageManager.getPackageInfo (often hooked by root-bypass modules). + try { + context.packageManager.getPackageInfo("___xposed_probe_${System.nanoTime()}", 0) + } catch (e: Throwable) { + scan("getPackageInfo", e.stackTrace) + } + + // Probe 2: Runtime.exec (root-bypass modules typically hook this). + try { + Runtime.getRuntime().exec(arrayOf("/__xposed_probe_${System.nanoTime()}")) + } catch (e: Throwable) { + scan("Runtime.exec", e.stackTrace) + } + + // Probe 3: File.exists (root-bypass modules typically hook this too). + try { + File("/__xposed_probe_${System.nanoTime()}").exists() + val t = Throwable("post-File.exists probe") + scan("File.exists.probe", t.stackTrace) + } catch (e: Throwable) { + scan("File.exists", e.stackTrace) + } + + // Walk every live Java thread's stack. + try { + val all = Thread.getAllStackTraces() + for ((thread, frames) in all) { + scan("thread=${thread.name}", frames) + } + } catch (_: Throwable) { /* SecurityManager could block — ignore */ } + + // Native-thread enumeration via /proc/self/task//comm. Catches Frida workers + // that have no Java Thread representation (gum-js-loop, gmain, pool-frida, …). + val nativeThreadNeedles = listOf("gum-js", "gmain", "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker") + try { + val tasks = File("/proc/self/task").listFiles() ?: emptyArray() + for (task in tasks) { + val commFile = File(task, "comm") + if (!commFile.canRead()) continue + val name = try { commFile.readText().trim() } catch (_: Throwable) { continue } + val low = name.lowercase() + for (n in nativeThreadNeedles) { + if (low.contains(n)) { + hits.add("native-thread: $name") + break + } + } + } + } catch (_: Throwable) {} + + return hits.toList() + } +} diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java new file mode 100644 index 00000000000..623a210eaae --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java @@ -0,0 +1,339 @@ +package org.owasp.mastestapp; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.Looper; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import kotlin.Metadata; +import kotlin.TuplesKt; +import kotlin.Unit; +import kotlin.collections.CollectionsKt; +import kotlin.collections.MapsKt; +import kotlin.io.CloseableKt; +import kotlin.io.FilesKt; +import kotlin.io.TextStreamsKt; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.internal.ArrayIteratorKt; +import kotlin.jvm.internal.Intrinsics; +import kotlin.text.StringsKt; + +/* compiled from: MastgTest.kt */ +@Metadata(d1 = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0010 \n\u0002\b\u0004\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u0007H\u0002J\u000e\u0010\u000b\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002J\u000e\u0010\r\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002J\u000e\u0010\u000e\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002J\u000e\u0010\u000f\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u0010"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "mastgTest", "", "promptUserForLiability", "", "message", "checkKnownXposedManagerPackages", "", "checkHookedMethodSignatures", "checkForeignDexesInMaps", "checkInstrumentationFramesInStacks", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +/* loaded from: classes3.dex */ +public final class MastgTest { + public static final int $stable = 8; + private final Context context; + + public MastgTest(Context context) { + Intrinsics.checkNotNullParameter(context, "context"); + this.context = context; + } + + public final String mastgTest() { + DemoResults r = new DemoResults("0x4A"); + boolean anyFail = false; + try { + List managers = checkKnownXposedManagerPackages(); + if (!managers.isEmpty()) { + r.add(Status.FAIL, "Xposed Manager package(s) installed: " + CollectionsKt.joinToString$default(managers, ", ", null, null, 0, null, null, 62, null)); + anyFail = true; + } else { + r.add(Status.PASS, "No known Xposed Manager package found via PackageManager."); + } + } catch (Exception e) { + r.add(Status.ERROR, "Manager lookup failed: " + e); + } + try { + List tampered = checkHookedMethodSignatures(); + if (!tampered.isEmpty()) { + r.add(Status.FAIL, "Methods appear to be hooked: " + CollectionsKt.joinToString$default(tampered, ", ", null, null, 0, null, null, 62, null)); + anyFail = true; + } else { + r.add(Status.PASS, "Audited native methods retain their NATIVE modifier."); + } + } catch (Exception e2) { + r.add(Status.ERROR, "Method-descriptor check failed: " + e2); + } + try { + List foreignDexes = checkForeignDexesInMaps(); + if (!foreignDexes.isEmpty()) { + r.add(Status.FAIL, "Foreign DEX/APK mapped into process: " + CollectionsKt.joinToString$default(foreignDexes, ", ", null, null, 0, null, null, 62, null)); + anyFail = true; + } else { + r.add(Status.PASS, "No foreign DEX/APK mapped into process."); + } + } catch (Exception e3) { + r.add(Status.ERROR, "/proc/self/maps inspection failed: " + e3); + } + try { + List frames = checkInstrumentationFramesInStacks(); + if (!frames.isEmpty()) { + r.add(Status.FAIL, "Instrumentation frames on stack: " + CollectionsKt.joinToString$default(frames, ", ", null, null, 0, null, null, 62, null)); + anyFail = true; + } else { + r.add(Status.PASS, "No Xposed/LSPosed/Frida frames found in any thread's stack."); + } + } catch (Exception e4) { + r.add(Status.ERROR, "Stack-trace inspection failed: " + e4); + } + if (anyFail) { + promptUserForLiability("Reverse-engineering or instrumentation tooling (Xposed/LSPosed/Frida) was detected on this device. Continued use may compromise app security and data integrity. Tap \"Accept Liability\" to acknowledge the risk and continue, or \"Exit\" to close the app."); + } + return r.toJson(); + } + + private final void promptUserForLiability(final String message) { + Context context = this.context; + final Activity activity = context instanceof Activity ? (Activity) context : null; + if (activity == null) { + return; + } + new Handler(Looper.getMainLooper()).post(new Runnable() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda3 + @Override // java.lang.Runnable + public final void run() { + MastgTest.promptUserForLiability$lambda$2(activity, message); + } + }); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final void promptUserForLiability$lambda$2(final Activity activity, String message) { + Intrinsics.checkNotNullParameter(activity, "$activity"); + Intrinsics.checkNotNullParameter(message, "$message"); + if (activity.isFinishing() || activity.isDestroyed()) { + return; + } + new AlertDialog.Builder(activity).setTitle("Security Warning").setMessage(message).setCancelable(false).setPositiveButton("Accept Liability", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0 + @Override // android.content.DialogInterface.OnClickListener + public final void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + } + }).setNegativeButton("Exit", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda1 + @Override // android.content.DialogInterface.OnClickListener + public final void onClick(DialogInterface dialogInterface, int i) { + MastgTest.promptUserForLiability$lambda$2$lambda$1(activity, dialogInterface, i); + } + }).show(); + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final void promptUserForLiability$lambda$2$lambda$1(Activity activity, DialogInterface dialogInterface, int i) { + Intrinsics.checkNotNullParameter(activity, "$activity"); + activity.finishAffinity(); + } + + private final List checkKnownXposedManagerPackages() throws PackageManager.NameNotFoundException { + boolean z; + Iterable knownManagers = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); + PackageManager pm = this.context.getPackageManager(); + Iterable $this$filter$iv = knownManagers; + Collection destination$iv$iv = new ArrayList(); + for (Object element$iv$iv : $this$filter$iv) { + String pkg = (String) element$iv$iv; + try { + pm.getPackageInfo(pkg, 0); + z = true; + } catch (PackageManager.NameNotFoundException e) { + z = false; + } + if (z) { + destination$iv$iv.add(element$iv$iv); + } + } + return (List) destination$iv$iv; + } + + private final List checkHookedMethodSignatures() throws NoSuchMethodException, ClassNotFoundException, SecurityException { + List tampered = new ArrayList(); + Map targets = MapsKt.mapOf(TuplesKt.to("java.lang.System", CollectionsKt.listOf((Object[]) new String[]{"currentTimeMillis", "nanoTime"})), TuplesKt.to("java.lang.Object", CollectionsKt.listOf((Object[]) new String[]{"notify", "notifyAll"})), TuplesKt.to("java.lang.Thread", CollectionsKt.listOf("currentThread"))); + for (Map.Entry entry : targets.entrySet()) { + String cls = (String) entry.getKey(); + List methods = (List) entry.getValue(); + try { + Class c = Class.forName(cls); + for (String mName : methods) { + Method m = c.getDeclaredMethod(mName, new Class[0]); + if (!Modifier.isNative(m.getModifiers())) { + tampered.add(cls + "." + mName + " (native bit cleared, modifiers=0x" + Integer.toHexString(m.getModifiers()) + ")"); + } + } + } catch (Exception e) { + } + } + return tampered; + } + + private final List checkForeignDexesInMaps() throws IOException { + final String ownPkg = this.context.getPackageName(); + final LinkedHashSet hits = new LinkedHashSet(); + BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); + try { + BufferedReader br = bufferedReader; + TextStreamsKt.forEachLine(br, new Function1() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda2 + @Override // kotlin.jvm.functions.Function1 + public final Object invoke(Object obj) { + return MastgTest.checkForeignDexesInMaps$lambda$5$lambda$4(ownPkg, hits, (String) obj); + } + }); + Unit unit = Unit.INSTANCE; + CloseableKt.closeFinally(bufferedReader, null); + return CollectionsKt.toList(hits); + } finally { + } + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final Unit checkForeignDexesInMaps$lambda$5$lambda$4(String $ownPkg, LinkedHashSet hits, String line) { + Intrinsics.checkNotNullParameter(hits, "$hits"); + Intrinsics.checkNotNullParameter(line, "line"); + int idx = StringsKt.indexOf$default((CharSequence) line, "/data/app/", 0, false, 6, (Object) null); + if (idx < 0) { + return Unit.INSTANCE; + } + String strSubstring = line.substring(idx); + Intrinsics.checkNotNullExpressionValue(strSubstring, "substring(...)"); + String path = StringsKt.substringBefore$default(strSubstring, ' ', (String) null, 2, (Object) null); + if (!StringsKt.endsWith$default(path, ".apk", false, 2, (Object) null)) { + return Unit.INSTANCE; + } + if (StringsKt.contains$default((CharSequence) path, (CharSequence) ("/" + $ownPkg + "-"), false, 2, (Object) null) || StringsKt.contains$default((CharSequence) path, (CharSequence) ("/" + $ownPkg + "/"), false, 2, (Object) null)) { + return Unit.INSTANCE; + } + String pkg = StringsKt.substringBefore$default(StringsKt.substringAfter$default(StringsKt.substringAfter$default(path, "/data/app/", (String) null, 2, (Object) null), '/', (String) null, 2, (Object) null), '-', (String) null, 2, (Object) null); + hits.add(pkg); + return Unit.INSTANCE; + } + + /* JADX WARN: Removed duplicated region for block: B:34:0x015a A[Catch: all -> 0x01d5, TRY_LEAVE, TryCatch #3 {all -> 0x01d5, blocks: (B:25:0x0140, B:32:0x0156, B:34:0x015a, B:38:0x017a, B:39:0x0189, B:41:0x018f), top: B:66:0x0140 }] */ + /* + Code decompiled incorrectly, please refer to instructions dump. + */ + private final List checkInstrumentationFramesInStacks() { + int length; + int i; + List nativeThreadNeedles; + int i2 = 1; + List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum-js"}); + LinkedHashSet hits = new LinkedHashSet(); + try { + this.context.getPackageManager().getPackageInfo("___xposed_probe_" + System.nanoTime(), 0); + } catch (Throwable e) { + checkInstrumentationFramesInStacks$scan(needles, hits, "getPackageInfo", e.getStackTrace()); + } + try { + Runtime.getRuntime().exec(new String[]{"/__xposed_probe_" + System.nanoTime()}); + } catch (Throwable e2) { + checkInstrumentationFramesInStacks$scan(needles, hits, "Runtime.exec", e2.getStackTrace()); + } + try { + new File("/__xposed_probe_" + System.nanoTime()).exists(); + Throwable t = new Throwable("post-File.exists probe"); + checkInstrumentationFramesInStacks$scan(needles, hits, "File.exists.probe", t.getStackTrace()); + } catch (Throwable e3) { + checkInstrumentationFramesInStacks$scan(needles, hits, "File.exists", e3.getStackTrace()); + } + try { + Map all = Thread.getAllStackTraces(); + Intrinsics.checkNotNull(all); + for (Map.Entry entry : all.entrySet()) { + Thread thread = entry.getKey(); + StackTraceElement[] frames = entry.getValue(); + checkInstrumentationFramesInStacks$scan(needles, hits, "thread=" + thread.getName(), frames); + } + } catch (Throwable th) { + } + List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); + try { + File[] fileArrListFiles = new File("/proc/self/task").listFiles(); + if (fileArrListFiles == null) { + try { + fileArrListFiles = new File[0]; + File[] tasks = fileArrListFiles; + length = tasks.length; + i = 0; + while (i < length) { + File task = tasks[i]; + File commFile = new File(task, "comm"); + if (commFile.canRead()) { + try { + String name = StringsKt.trim((CharSequence) FilesKt.readText$default(commFile, null, i2, null)).toString(); + String low = name.toLowerCase(Locale.ROOT); + Intrinsics.checkNotNullExpressionValue(low, "toLowerCase(...)"); + Iterator it = nativeThreadNeedles2.iterator(); + while (true) { + if (!it.hasNext()) { + nativeThreadNeedles = nativeThreadNeedles2; + break; + } + String n = (String) it.next(); + nativeThreadNeedles = nativeThreadNeedles2; + try { + if (StringsKt.contains$default((CharSequence) low, (CharSequence) n, false, 2, (Object) null)) { + hits.add("native-thread: " + name); + break; + } + nativeThreadNeedles2 = nativeThreadNeedles; + } catch (Throwable th2) { + } + } + } catch (Throwable th3) { + nativeThreadNeedles = nativeThreadNeedles2; + } + } else { + nativeThreadNeedles = nativeThreadNeedles2; + } + i++; + nativeThreadNeedles2 = nativeThreadNeedles; + i2 = 1; + } + } catch (Throwable th4) { + } + } else { + File[] tasks2 = fileArrListFiles; + length = tasks2.length; + i = 0; + while (i < length) { + } + } + } catch (Throwable th5) { + } + return CollectionsKt.toList(hits); + } + + private static final void checkInstrumentationFramesInStacks$scan(List list, LinkedHashSet linkedHashSet, String label, StackTraceElement[] frames) { + if (frames == null) { + return; + } + Iterator it = ArrayIteratorKt.iterator(frames); + while (it.hasNext()) { + StackTraceElement f = (StackTraceElement) it.next(); + String className = f.getClassName(); + Intrinsics.checkNotNullExpressionValue(className, "getClassName(...)"); + String low = className.toLowerCase(Locale.ROOT); + Intrinsics.checkNotNullExpressionValue(low, "toLowerCase(...)"); + for (String n : list) { + String lowerCase = n.toLowerCase(Locale.ROOT); + Intrinsics.checkNotNullExpressionValue(lowerCase, "toLowerCase(...)"); + if (StringsKt.contains$default((CharSequence) low, (CharSequence) lowerCase, false, 2, (Object) null)) { + linkedHashSet.add(label + ": " + f.getClassName() + "." + f.getMethodName()); + } + } + } + } +} diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt new file mode 100644 index 00000000000..7e4adb38b83 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt @@ -0,0 +1,104 @@ + + +┌──────────────────┐ +│ 20 Code Findings │ +└──────────────────┘ + + MastgTest_reversed.java + ❱ rules.mastg-android-xposed-detection-manager-package-literal + [MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/EdXposed Manager + package ids. Typically used with PackageManager.getPackageInfo() as a presence probe. + + 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new + String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", + "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); + ⋮┆---------------------------------------- + 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new + String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", + "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); + ⋮┆---------------------------------------- + 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new + String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", + "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); + ⋮┆---------------------------------------- + 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new + String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", + "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); + ⋮┆---------------------------------------- + 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new + String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", + "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); + + ❱ rules.mastg-android-xposed-detection-modifier-isnative-tripwire + [MASVS-RESILIENCE-4] The app branches on a negated Modifier.isNative() check against a reflected + method's modifiers. LSPlant (LSPosed's hooking engine) and Frida's Java bridge clear kAccNative when + they hook a native method, so this is the canonical anti-hooking tripwire. A bare + Modifier.isNative() call is intentionally NOT matched, because reflection/serialization libraries + use it benignly. + + 172┆ if (!Modifier.isNative(m.getModifiers())) { + 173┆ tampered.add(cls + "." + mName + " (native bit cleared, modifiers=0x" + + Integer.toHexString(m.getModifiers()) + ")"); + 174┆ } + + ❱ rules.mastg-android-xposed-detection-foreign-dex-in-maps + [MASVS-RESILIENCE-4] The app opens /proc/self/maps from Java code. This is the canonical primitive + for detecting an injected module (an LSPosed module APK or a Frida agent .so) mmapped into the + process. Legitimate Java code rarely reads /proc/self/maps directly. + + 185┆ BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); + + ❱ rules.mastg-android-xposed-detection-instrumentation-needles + [MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/Frida framework + class prefixes (de.robv.android.xposed, org.lsposed., lsphooker_, lsplant, edxposed, re.frida) or + native worker thread names (gum-js, gum-js-loop, pool-frida, linjector), or references + /proc/self/task. These literals are the canonical needles for stack-trace inspection + (Thread.getAllStackTraces / Throwable.getStackTrace) and /proc/self/task//comm enumeration used + to detect active instrumentation. + + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", + "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- + js"}); + ⋮┆---------------------------------------- + 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", + "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); + ⋮┆---------------------------------------- + 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", + "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); + ⋮┆---------------------------------------- + 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", + "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); + ⋮┆---------------------------------------- + 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", + "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); + ⋮┆---------------------------------------- + 263┆ File[] fileArrListFiles = new File("/proc/self/task").listFiles(); + diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh new file mode 100755 index 00000000000..986c197312e --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-xposed-detection.yaml ./MastgTest_reversed.java > output.txt \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md new file mode 100644 index 00000000000..2d891c2521b --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md @@ -0,0 +1,47 @@ +--- +platform: android +title: Bypass of Xposed/LSPosed Detection via PackageManager, Method-Modifier, Maps and Stack-Trace Hooks +id: MASTG-DEMO-0x4B +code: [kotlin] +test: MASTG-TEST-0x49 +tools: [MASTG-TOOL-0031] +kind: fail +--- + +## Sample + +This sample uses the same code as @MASTG-DEMO-0x4A, which performs four Xposed/LSPosed/Frida detection techniques: a `PackageManager.getPackageInfo()` lookup for known framework Manager package ids, a reflection-based `Modifier.isNative()` tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / `/proc/self/task` probe that surfaces framework frames and native worker threads. This demo defeats all four with a Frida script that intercepts the Java APIs each detection routine relies on: `ApplicationPackageManager.getPackageInfo`, `Method.getModifiers`, `BufferedReader.readLine` (over `/proc/self/maps`), `Throwable.getStackTrace` / `Thread.getStackTrace` / `Thread.getAllStackTraces`, and `File.listFiles` (over `/proc/self/task`). + +See @MASTG-KNOW-0030 and @MASTG-KNOW-0032 for more context on bypassing runtime detection mechanisms. + +!!! note + This is a series of correlated tests. + + - @MASTG-DEMO-0x4A is a successful test (successful defense) that defends against Xposed/LSPosed/Frida detection mechanisms. + - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x4A by using a runtime API-hooking bypass. + +{{ ../MASTG-DEMO-0x4A/MastgTest.kt # script.js }} + +## Steps + +1. Install the app from @MASTG-DEMO-0x4A on a device where the Xposed/LSPosed framework is active and at least one module is scoped to `org.owasp.mastestapp` (@MASTG-TECH-0005). +2. Spawn the app with the Frida bypass attached: + +{{ run.sh }} + +3. Tap **Start** in the app and compare the result with @MASTG-DEMO-0x4A on the same device. + +## Observation + +The Frida console shows every hook firing while the on-screen result reports **PASS** for all four checks — and, critically, the liability prompt that @MASTG-DEMO-0x4A raises on detection never appears. + +{{ output.txt }} + +## Evaluation + +The test fails because every detection routine has been neutralized at runtime: + +- **Manager-package lookup.** A `getPackageInfo` hook throws `NameNotFoundException` for every known Xposed/LSPosed Manager package id, so `checkKnownXposedManagerPackages()` returns an empty list. +- **Method-modifier tripwire.** A `Method.getModifiers` hook re-ORs `Modifier.NATIVE` on every audited method whose bit has been cleared, so `checkHookedMethodSignatures()` reports the methods as untouched. +- **`/proc/self/maps` parsing.** A recursion-safe `BufferedReader.readLine` filter drops every `/proc/self/maps` line containing an Xposed-related package id before it reaches the app, so `checkForeignDexesInMaps()` returns empty. +- **Stack-trace and thread probe.** A `Throwable.getStackTrace` hook strips framework frames from every captured stack, and a `File.listFiles` hook hides instrumentation tids from the `/proc/self/task` walk before the app reads their `comm` files. Both halves of the check come back clean. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt new file mode 100644 index 00000000000..f57bdda7bcf --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt @@ -0,0 +1,13 @@ +[bypass] Demo 4 hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes). +[bypass] faking NameNotFoundException for de.robv.android.xposed.installer +[bypass] faking NameNotFoundException for org.meowcat.edxposed.manager +[bypass] faking NameNotFoundException for io.va.exposed +[bypass] faking NameNotFoundException for com.solohsu.android.edxp.manager +[bypass] faking NameNotFoundException for org.lsposed.manager +[bypass] dropping /proc/self/maps line: 6f9990f000-6f99911000 r-xp 001ee000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 6f99911000-6f99912000 r--p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 6f99912000-6f99913000 rw-p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] stripped 2 framework frames +[bypass] stripped 4 framework frames +[bypass] hiding /proc/self/task tid 12517 (comm=gmain) +[bypass] hiding /proc/self/task tid 12518 (comm=pool-frida) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/run.sh new file mode 100755 index 00000000000..4c033ae2b4a --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +frida -U -f org.owasp.mastestapp -l ./script.js -o output.txt diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js new file mode 100644 index 00000000000..d08280cc5fb --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js @@ -0,0 +1,178 @@ +Java.perform(function () { + + + var XPOSED_PKG_PREFIXES = [ + "de.robv.android.xposed", + "org.meowcat.edxposed", + "io.va.exposed", + "com.solohsu.android.edxp", + "org.lsposed", + "com.gauravssnl.bypassrootcheck", + "com.w311ang.disable_flag_keep_screen_on" + ]; + + var STACK_NEEDLES = [ + "de.robv.android.xposed", + "org.lsposed.lspd", + "org.lsposed.", + "lsphooker_", + "lsplant", + "edxposed", + "re.frida", + "gum-js" + ]; + + var THREAD_NAME_NEEDLES = [ + "gum-js", "gmain", "pool-frida", "frida", "linjector", + "lspd", "xposed", "lsphooker" + ]; + + var KNOWN_MANAGER_PACKAGES = [ + "de.robv.android.xposed.installer", + "org.meowcat.edxposed.manager", + "io.va.exposed", + "com.solohsu.android.edxp.manager", + "org.lsposed.manager" + ]; + + + var SUSPICIOUS_NATIVE_METHODS = { + "java.lang.System.currentTimeMillis": true, + "java.lang.System.nanoTime": true, + "java.lang.Object.notify": true, + "java.lang.Object.notifyAll": true, + "java.lang.Thread.currentThread": true + }; + + + var APM = Java.use("android.app.ApplicationPackageManager"); + var NameNotFoundException = Java.use("android.content.pm.PackageManager$NameNotFoundException"); + + APM.getPackageInfo.overload("java.lang.String", "int").implementation = function (name, flags) { + for (var i = 0; i < KNOWN_MANAGER_PACKAGES.length; i++) { + if (name === KNOWN_MANAGER_PACKAGES[i]) { + console.log("[bypass] faking NameNotFoundException for " + name); + throw NameNotFoundException.$new(name); + } + } + return this.getPackageInfo(name, flags); + }; + + + var Modifier = Java.use("java.lang.reflect.Modifier"); + var NATIVE_BIT = Modifier.NATIVE.value; + var Method = Java.use("java.lang.reflect.Method"); + + Method.getModifiers.implementation = function () { + var orig = this.getModifiers(); + try { + var key = this.getDeclaringClass().getName() + "." + this.getName(); + if (SUSPICIOUS_NATIVE_METHODS[key] && (orig & NATIVE_BIT) === 0) { + console.log("[bypass] restoring Modifier.NATIVE on " + key); + return orig | NATIVE_BIT; + } + } catch (e) { /* fall through */ } + return orig; + }; + + + var BufferedReader = Java.use("java.io.BufferedReader"); + BufferedReader.readLine.overload().implementation = function () { + while (true) { + var line = this.readLine(); + if (line === null) return null; + var dirty = false; + for (var i = 0; i < XPOSED_PKG_PREFIXES.length; i++) { + if (line.indexOf(XPOSED_PKG_PREFIXES[i]) !== -1) { dirty = true; break; } + } + if (!dirty) return line; + console.log("[bypass] dropping /proc/self/maps line: " + line); + } + }; + + var Throwable = Java.use("java.lang.Throwable"); + var ThreadCls = Java.use("java.lang.Thread"); + + function filterFrames(frames) { + if (frames === null) return frames; + var clean = []; + for (var i = 0; i < frames.length; i++) { + var name = (frames[i].getClassName() + "").toLowerCase(); + var dirty = false; + for (var j = 0; j < STACK_NEEDLES.length; j++) { + if (name.indexOf(STACK_NEEDLES[j].toLowerCase()) !== -1) { dirty = true; break; } + } + if (!dirty) clean.push(frames[i]); + } + if (clean.length === frames.length) return frames; + console.log("[bypass] stripped " + (frames.length - clean.length) + " framework frames"); + return Java.array("java.lang.StackTraceElement", clean); + } + + Throwable.getStackTrace.implementation = function () { + return filterFrames(this.getStackTrace()); + }; + ThreadCls.getStackTrace.implementation = function () { + return filterFrames(this.getStackTrace()); + }; + + ThreadCls.getAllStackTraces.implementation = function () { + + var raw = this.getAllStackTraces(); + var HashMap = Java.use("java.util.HashMap"); + var clean = HashMap.$new(); + var keys = raw.keySet().toArray(); + for (var i = 0; i < keys.length; i++) { + var t = keys[i]; + try { + clean.put(t, t.getStackTrace()); + } catch (e) { /* thread died between snapshot and re-walk — skip */ } + } + return clean; + }; + + var FileCls = Java.use("java.io.File"); + var FIS = Java.use("java.io.FileInputStream"); + + function readComm(tidDir) { + try { + var fis = FIS.$new(tidDir.getAbsolutePath() + "/comm"); + var buf = Java.array("byte", new Array(64).fill(0)); + var n = fis.read(buf); + fis.close(); + if (n <= 0) return ""; + var s = ""; + for (var i = 0; i < n; i++) s += String.fromCharCode(buf[i] & 0xff); + return s.trim(); + } catch (e) { return ""; } + } + + FileCls.listFiles.overload().implementation = function () { + var files = this.listFiles(); + if (files === null) return files; + var path = this.getAbsolutePath(); + if (path !== "/proc/self/task") return files; + var keep = []; + for (var i = 0; i < files.length; i++) { + var comm = readComm(files[i]).toLowerCase(); + var dirty = false; + for (var k = 0; k < THREAD_NAME_NEEDLES.length; k++) { + if (comm.indexOf(THREAD_NAME_NEEDLES[k]) !== -1) { dirty = true; break; } + } + if (dirty) { + console.log("[bypass] hiding /proc/self/task tid " + files[i].getName() + " (comm=" + comm + ")"); + continue; + } + keep.push(files[i]); + } + return Java.array("java.io.File", keep); + }; + + + Process.setExceptionHandler(function (details) { + console.log("[bypass] swallowed native exception: " + JSON.stringify(details)); + return true; + }); + + console.log("[bypass] Demo 4 hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes)."); +}); diff --git a/rules/mastg-android-frida-detection.yaml b/rules/mastg-android-frida-detection.yaml new file mode 100644 index 00000000000..252c98faeb7 --- /dev/null +++ b/rules/mastg-android-frida-detection.yaml @@ -0,0 +1,48 @@ +rules: + - id: mastg-android-frida-detection-default-port + severity: INFO + languages: [java] + metadata: + summary: Reference to the default frida-server TCP port (27042) + message: "[MASVS-RESILIENCE-4] The app references the default frida-server TCP port (27042), suggesting a port-scan-based Frida detection." + pattern: new InetSocketAddress($HOST, 27042) + + - id: mastg-android-frida-detection-loopback-literal + severity: INFO + languages: [java] + metadata: + summary: Reference to the loopback address typically used by frida-server (127.0.0.1) + message: "[MASVS-RESILIENCE-4] The app references 127.0.0.1, which is typically combined with a port literal to scan for frida-server." + pattern: '"127.0.0.1"' + + - id: mastg-android-frida-detection-frida-identifier-literals + severity: INFO + languages: [java] + metadata: + summary: String literals matching Frida / Gum identifiers + message: "[MASVS-RESILIENCE-4] The app contains string literals associated with Frida or Gum runtime artifacts (frida-server, frida-helper, frida-agent, frida-gadget, libfrida, gum-js-loop, gmain, linjector, re.frida). Strong indicator of Frida-detection logic." + patterns: + - pattern: $STR + - metavariable-regex: + metavariable: $STR + regex: '^"(frida-server|frida-helper|frida-agent|frida-gadget|libfrida|gum-js-loop|gum-js|gmain|linjector|re\.frida)"$' + + - id: mastg-android-frida-detection-proc-maps-read + severity: INFO + languages: [java] + metadata: + summary: Read of /proc/self/maps — generic instrumentation-detection primitive + message: "[MASVS-RESILIENCE-4] The app references /proc/self/maps, the canonical primitive for detecting an injected Frida agent or any other foreign library mapped into the process." + pattern: '"/proc/self/maps"' + + - id: mastg-android-frida-detection-proc-enumeration + severity: INFO + languages: [java] + metadata: + summary: Enumeration of /proc to look at running processes + message: "[MASVS-RESILIENCE-4] The app references /proc and cmdline, a common primitive pair for enumerating running processes (e.g. to look for a frida-server process by name)." + patterns: + - pattern: $STR + - metavariable-regex: + metavariable: $STR + regex: '^"(/proc|cmdline)"$' diff --git a/rules/mastg-android-xposed-detection.yaml b/rules/mastg-android-xposed-detection.yaml new file mode 100644 index 00000000000..136538b3f39 --- /dev/null +++ b/rules/mastg-android-xposed-detection.yaml @@ -0,0 +1,52 @@ +rules: + - id: mastg-android-xposed-detection-manager-package-literal + severity: INFO + languages: [java] + metadata: + summary: String literals matching known Xposed/LSPosed/EdXposed Manager package ids + message: "[MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/EdXposed Manager package ids. Typically used with PackageManager.getPackageInfo() as a presence probe." + patterns: + - pattern: $STR + - metavariable-regex: + metavariable: $STR + regex: '^"(de\.robv\.android\.xposed\.installer|org\.lsposed\.manager|org\.meowcat\.edxposed\.manager|com\.solohsu\.android\.edxp\.manager|io\.va\.exposed)"$' + + - id: mastg-android-xposed-detection-modifier-isnative-tripwire + severity: INFO + languages: [java] + metadata: + summary: Anti-hooking tripwire — negated Modifier.isNative() on a reflected method + message: "[MASVS-RESILIENCE-4] The app branches on a negated Modifier.isNative() check against a reflected method's modifiers." + pattern-either: + - pattern: if (!Modifier.isNative(...)) { ... } + - pattern: if (!java.lang.reflect.Modifier.isNative(...)) { ... } + - pattern: while (!Modifier.isNative(...)) { ... } + - pattern: boolean $X = !Modifier.isNative(...); + - pattern: return !Modifier.isNative(...); + + - id: mastg-android-xposed-detection-foreign-dex-in-maps + severity: INFO + languages: [java] + metadata: + summary: Java-side open of /proc/self/maps for reading + message: "[MASVS-RESILIENCE-4] The app opens /proc/self/maps from Java code. This is the canonical primitive for detecting an injected module (an LSPosed module APK or a Frida agent .so) mmapped into the process." + pattern-either: + - pattern: new FileReader("/proc/self/maps") + - pattern: new FileInputStream("/proc/self/maps") + - pattern: new RandomAccessFile("/proc/self/maps", ...) + - pattern: Files.readAllLines(Paths.get("/proc/self/maps")) + - pattern: Files.lines(Paths.get("/proc/self/maps")) + - pattern: Files.newBufferedReader(Paths.get("/proc/self/maps"), ...) + - pattern: Files.newInputStream(Paths.get("/proc/self/maps"), ...) + + - id: mastg-android-xposed-detection-instrumentation-needles + severity: INFO + languages: [java] + metadata: + summary: String literals matching instrumentation framework class prefixes, worker thread names, or /proc/self/task + message: "[MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/Frida framework class prefixes (de.robv.android.xposed, org.lsposed., lsphooker_, lsplant, edxposed, re.frida) or native worker thread names (gum-js, gum-js-loop, pool-frida, linjector), or references /proc/self/task." + patterns: + - pattern: $STR + - metavariable-regex: + metavariable: $STR + regex: '^"(de\.robv\.android\.xposed|org\.lsposed\.lspd|org\.lsposed\.|lsphooker_?|lsplant|edxposed|re\.frida|gum-js(-loop)?|pool-frida|linjector|/proc/self/task)"$' diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md new file mode 100644 index 00000000000..8cb4d6575e3 --- /dev/null +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md @@ -0,0 +1,31 @@ +--- +platform: android +title: Testing Runtime Use of Frida +id: MASTG-TEST-0x48 +type: [dynamic] +weakness: MASWE-0098 +best-practices: [MASTG-BEST-0x48] +profiles: [R] +knowledge: [MASTG-KNOW-0030] +--- + +## Overview + +This test verifies whether app detect well-known Frida detection patterns — connecting to the default `frida-server` TCP port (`127.0.0.1:27042`), grepping `/proc//cmdline` for `frida-server`/`frida-helper`, or scanning `/proc/self/maps` for `frida-agent.so`/`libfrida`/`gum-js-loop` — provide little resilience against an attacker that controls the device. Each of these signals can be neutralized with a few lines of Frida JavaScript or by reconfiguring the `frida-server` deployment (renaming the binary, binding a non-default port). If these mechanisms are the only line of defense, an attacker can trivially run instrumentation against the app and the protection is illusory. + +This test checks whether an app's Frida detection routines actually resist an in-process Frida bypass that hooks the underlying `Socket`/`File`/`FileReader` APIs and returns the values the detection logic expects when no instrumentation is active. + +## Steps + +1. Install the app on a device with `frida-server` running. +2. Use @MASTG-TOOL-0001 to attach to the app and exercise the function(s) that perform Frida detection. +3. Use @MASTG-TECH-0033 to attempt to hook the APIs used by each detection routine (`java.net.Socket.connect`, `java.io.File.listFiles`, `java.io.BufferedReader.readLine`) to return clean values. +4. Re-run the app's detection function and capture the result. + +## Observation + +The output should contain a list of detection routines that the app executed, the APIs they queried, and the value those APIs returned both with and without the bypass script attached. + +## Evaluation + +The test case fails if the app fails to detect Frida when app normally under `frida-server` is running and a generic API-hooking bypass is attached. diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md new file mode 100644 index 00000000000..c719bf6b4d5 --- /dev/null +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md @@ -0,0 +1,48 @@ +--- +platform: android +title: Runtime Use of Xposed/LSPosed Detection Mechanisms +id: MASTG-TEST-0x49 +apis: [PackageManager, Method, BufferedReader, Throwable, Thread, File] +type: [dynamic] +weakness: MASWE-0098 +best-practices: [MASTG-BEST-0x49] +profiles: [R] +knowledge: [MASTG-KNOW-0030] +--- + +## Overview + +Android apps that try to detect the Xposed/LSPosed framework with classic Java-only techniques fall into two camps: + +- **Easily-bypassed-and-often-silent** techniques mirrored from the MASTG-TEST-0048 v1 guidance — a targeted `PackageManager.getPackageInfo` lookup for known Manager package ids, and reflection-based verification that `Modifier.NATIVE` is still set on a curated list of guaranteed-native methods. These remain useful as low-cost tripwires for lazy attacker setups but are silent against any modern LSPosed install with "Hide LSPosed Manager" enabled and against module deployments that don't touch the methods the app audits. +- **Stronger** techniques that actually fire against modern LSPosed 1.9+ on Android 12+ — scanning `/proc/self/maps` for foreign DEX/APK mappings that LSPosed introduces into every hooked process, and a stack-trace probe through likely-hooked methods (plus `/proc/self/task//comm` enumeration to catch native Frida workers). + +This test verifies whether an app's Xposed/LSPosed detection actually resists a modern LSPosed deployment plus a generic Frida-based bypass that hooks the underlying Java APIs. + +The four detection techniques exercised by the linked demo are: + +- **Manager-package lookup** via `PackageManager.getPackageInfo` against a fixed list declared in `` (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`). +- **Method-descriptor verification** via `java.lang.reflect.Method.getModifiers()` for a small set of guaranteed-native methods (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`). If `Modifier.NATIVE` is cleared on any one of them, LSPlant or Frida has hooked the method. +- **Foreign DEX/APK in `/proc/self/maps`** — flag any `/data/app//base.apk` mapping whose package id is not the app itself. LSPosed `mmap`s a module's APK into every hooked process. +- **Stack-trace inspection + native-thread enumeration** — force exceptions through methods an attacker is likely to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan the resulting `Throwable.stackTrace` for framework class names (`de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_`, `re.frida.*`, …). Also walks `Thread.getAllStackTraces()` and `/proc/self/task//comm` to catch native instrumentation threads (`gum-js-loop`, `gmain`, `pool-frida`). + +When any of these detections fires, the app MUST alert the user and require liability acceptance before continuing (see the dialog pattern in the linked demos). + +## Steps + +1. Install the app on a rooted device with Magisk + Zygisk + LSPosed 1.9+ active and at least one LSPosed module scoped to the app. +2. Run @MASTG-TOOL-0001 to attach to the app and apply a generic Java-side bypass that hooks `ApplicationPackageManager.getPackageInfo` (to throw `NameNotFoundException` for Manager package ids), `Method.getModifiers` (to re-OR `Modifier.NATIVE` on the audited methods), `BufferedReader.readLine` (to scrub `/proc/self/maps` of foreign DEX/APK paths), `Throwable.getStackTrace` / `Thread.getStackTrace` / `Thread.getAllStackTraces` (to strip framework frames), and `File.listFiles` against `/proc/self/task` (to hide instrumentation tids). +3. Trigger the app's Xposed detection function and capture the result. + +## Observation + +The output should contain a list of detection routines, the APIs they queried (`PackageManager.getPackageInfo`, `Method.modifiers`, `/proc/self/maps`, `Throwable.stackTrace`, `Thread.getAllStackTraces`, `/proc/self/task//comm`), and the value those APIs returned with and without the bypass attached. It should also indicate whether the app raised the liability-acceptance dialog when the checks fired. + +## Evaluation + +The test case fails if **any** of the following hold: + +- The app reports "no Xposed detected" while at least one LSPosed module is loaded into the process and a generic Java-side bypass is attached, or +- A check fires but the app does not raise an alert/liability-acceptance dialog to the user before proceeding. + +Note that this Java-side test only covers the API-hooking bypass category. Bypass via app-code patching and via kernel-level syscall interception are out of scope and must be addressed by complementary controls (DEX/native integrity hashing, signing-certificate pinning, and server-side attestation such as the Play Integrity API). From 1a4b97810fdda00d4663ef73c66ba12762258583 Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Fri, 22 May 2026 11:23:50 +0530 Subject: [PATCH 02/18] fixed BEST files --- best-practices/MASTG-BEST-0x48.md | 44 +++++++++----------- best-practices/MASTG-BEST-0x49.md | 67 +++++++++---------------------- 2 files changed, 39 insertions(+), 72 deletions(-) diff --git a/best-practices/MASTG-BEST-0x48.md b/best-practices/MASTG-BEST-0x48.md index 59f30f05124..ecde4c578b9 100644 --- a/best-practices/MASTG-BEST-0x48.md +++ b/best-practices/MASTG-BEST-0x48.md @@ -6,39 +6,35 @@ platform: android knowledge: [MASTG-KNOW-0030] --- -Frida is a user-mode instrumentation framework that injects an agent (`frida-agent.so`) into a target process, spawns native worker threads (`gum-js-loop`, `gmain`, `pool-frida`), and rewrites Java methods through ART trampolines. Detecting it from inside the app is necessary but not sufficient: every Java-side check is itself a method that Frida can hook and silence (see @MASTG-DEMO-0x49). The defense must be layered. +Frida injects an agent (`frida-agent.so`) into the target process, starts native worker threads (`gum-js-loop`, `gmain`, `pool-frida`), and rewrites Java methods through ART trampolines. Every Java-side check is itself a Java method, so Frida can hook the check and silence it (see @MASTG-DEMO-0x49). Use multiple layers, and let the layer the attacker cannot reach decide who gets into the sensitive flow. -## Java-side detection (necessary but defeatable on its own) +Order layers from strongest to weakest. Gate the sensitive flow on the strongest. -Run *all* of the following — any single one is trivially defeated, but together they raise the cost of a generic bypass: +## 1. Server-side attestation -- **Read `/proc/self/maps`** and flag any mapped file whose path contains `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, or `/gum`. Use a `BufferedReader` filter that loops until a clean line is returned, not a single `readLine()` call — Frida's @MASTG-DEMO-0x49 shows that a naive single-line filter leaks consecutive matches. -- **Enumerate `/proc/self/task//comm`** for instrumentation thread names (`gum-js-loop`, `gmain`, `pool-frida`). This catches Frida even when the agent is loaded via `memfd` and the on-disk `.so` is unmapped. -- **Avoid relying on the default `27042` port and the literal binary name `frida-server`.** Both are trivially changed (`./frida-server -l 0.0.0.0:12345` + binary rename). A port-scan / process-name check still belongs in the layered defense as a low-cost trip for lazy attackers, but it must never be your only signal. +User-space code cannot reliably detect kernel-mode hooks. The only signal an attacker on a compromised device cannot forge is one signed inside the TEE / StrongBox and checked off-device. -When *any* check fires, the app **must** alert the user and require explicit acceptance of liability before proceeding (see the dialog pattern in @MASTG-DEMO-0x48). Silently continuing — or worse, blocking only on a network call later — wastes the detection. +- Require a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel cannot forge the device-key-signed attestation — the signing happens inside the TEE / StrongBox. +- Store secrets in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key cannot be pulled off a rooted device even if user-space is fully compromised. +- Have the server combine the attestation verdict with the score from the client-side checks below. Use client checks as inputs to the server's decision, not as a gate on their own. -## Native-side detection (the bypass-resistant layer) +## 2. Native-side detection -Java-side checks live in the same memory the attacker controls. Move the critical detection into JNI: +Java-side checks run in memory the attacker controls. Move the important detection into JNI, so a bypass has to hook `dlopen` / `dl_iterate_phdr` or patch the linker itself — not just a Java method. -- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `frida-gadget` injected via `LD_PRELOAD` and `frida-agent.so` loaded by `frida-server`, even when those files exist only as `/memfd:` mappings. -- Verify your own ELF segments against a build-time hash to catch bytecode/native patching (the first bypass category in @MASTG-TEST-0x48). -- Read `/proc/self/status` and inspect `TracerPid` for any non-zero value — catches `ptrace`-based attaches. +- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `frida-gadget` loaded via `LD_PRELOAD` and `frida-agent.so` loaded by `frida-server`, even when those files exist only as `/memfd:` mappings. +- Hash your own ELF segments and DEX at build time and check them at runtime. This catches the app-patching bypass — an attacker who NOPs out the detection and re-signs the APK. +- Read `/proc/self/status` and check `TracerPid`. A non-zero value means something is `ptrace`-attached. +- Send the native verdict to your server with the attestation token. Don't let JNI return a plain boolean that Java code reads and acts on — that boolean is itself something Frida can hook. -A native check is harder to bypass because the attacker must hook `dlopen`/`dl_iterate_phdr` or patch the linker itself, not just a Java method. +## 3. Java-side tripwires -## Server-side attestation (the kernel-level layer) +Java-side checks are cheap and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. -Kernel-mode interception (a rootkit that hooks syscalls, the third bypass category in @MASTG-TEST-0x48) cannot be reliably detected from user-space. Mitigate it by: +- **Read `/proc/self/maps`** and flag any path that contains `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, or `/gum`. Use a `BufferedReader` filter that loops until a clean line comes back, not a single `readLine()` call — @MASTG-DEMO-0x49 shows that a single-line filter leaks consecutive matches. +- **Enumerate `/proc/self/task//comm`** for instrumentation thread names (`gum-js-loop`, `gmain`, `pool-frida`). This catches Frida even when the agent was loaded from `memfd` and the on-disk `.so` is unmapped. +- **Scan for `frida-server`-like processes**, but don't rely on the default `27042` port or the binary name `frida-server`. Both are trivially changed (`./frida-server -l 0.0.0.0:12345` + binary rename — see @MASTG-DEMO-0x49). A port / name check still belongs as a cheap trip for lazy attackers, but it must never be your only signal. -- Requiring a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel still cannot forge the device-key-signed attestation, because the signing happens inside the TEE / StrongBox. -- Storing sensitive cryptographic material in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key is unrecoverable from a rooted device even if the userspace process is fully compromised. +When any check fires, the app **must** show the user a warning and require them to accept the risk before continuing (see the dialog in @MASTG-DEMO-0x48). -## What not to do - -- **Do not hardcode `27042` or `"127.0.0.1"` as your only Frida signal.** They survive in the demo because we deliberately mirror the v1 guidance, but in production an attacker rebinds the server in seconds. -- **Do not hook `Throwable.getStackTrace` yourself** as a way to verify your own detection — the attacker is hooking it too. Trust only the native-side and server-side results when the stakes are high. -- **Do not ship the detection as a single monolithic function** that returns a boolean. Frida can hook one function and you lose everything. Spread checks across multiple call sites, mix Java and native, and have the server reject calls when the cumulative score is below a threshold. - -For runtime checks, see the dynamic demo @MASTG-DEMO-0x48 (passing case) and bypass @MASTG-DEMO-0x49 (failing case). +For runtime checks, see the passing demo @MASTG-DEMO-0x48 and the bypass demo @MASTG-DEMO-0x49. diff --git a/best-practices/MASTG-BEST-0x49.md b/best-practices/MASTG-BEST-0x49.md index e3f5fb7f298..4897c38353b 100644 --- a/best-practices/MASTG-BEST-0x49.md +++ b/best-practices/MASTG-BEST-0x49.md @@ -6,63 +6,34 @@ platform: android knowledge: [MASTG-KNOW-0030] --- -Modern LSPosed (1.9+) is a Zygisk-based reimplementation of Xposed that is designed to defeat the classic Java-side detection techniques the v1 MASTG-TEST-0048 guidance lists. LSPosed Manager can be hidden behind a randomized package name ("Hide LSPosed Manager"), `XposedBridge` is loaded into a separate module classloader that the host app cannot reach, the "Xposed API call protection" toggle additionally intercepts `Class.forName` lookups, and LSPlant (LSPosed's hooking engine) uses ART trampolines instead of injecting fields into `java.lang.reflect.Method`. A detection that relies on any one of those classic surfaces will be silent on a production-attacker device. The defense must be layered. +Modern LSPosed (1.9+) is built to defeat the classic Java-only checks from the v1 MASTG-TEST-0048 guidance. Its Manager app can hide behind a random package name, `XposedBridge` lives in a classloader your app can't reach, the "Xposed API call protection" toggle blocks `Class.forName` lookups, and its hooking engine (LSPlant) leaves no fields on `java.lang.reflect.Method` to find. Any single Java-side check is easy to silence. Use multiple layers, and let the layer the attacker can't reach be the one that decides access to sensitive flows. -## Permission considerations — do not ship with `QUERY_ALL_PACKAGES` +## 1. Server-side attestation -Avoid `android.permission.QUERY_ALL_PACKAGES` for production apps. It is a Google Play sensitive permission restricted to a narrow list of approved use cases (security/antivirus, accessibility, file managers, etc.) and apps that cannot justify it under one of those categories are rejected at review. +A rooted device can lie to the app about its own state. The only check the attacker can't fake is one signed by the device's secure hardware (TEE / StrongBox) and verified by your server. -For Xposed Manager probing, use the manifest `` block instead: +- Require a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel cannot forge the device-key-signed attestation — the signing happens inside the TEE / StrongBox. +- Store secrets in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key cannot be pulled off a rooted device even if user-space is fully compromised. +- Treat the client-side checks below as inputs to the server's decision, not as a gate on their own. -```xml - - - - - - - -``` +## 2. Native-side detection -`PackageManager.getPackageInfo(pkg, 0)` will then resolve the listed packages on Android 11+ without any sensitive permission. This is the technique demonstrated in @MASTG-DEMO-0x4A. +Java-side checks run in memory the attacker controls. Put the important detection in JNI, so a bypass has to hook the linker itself, not just a Java method. -## Java-side detection (layered, no single check is sufficient) +- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `liblspd.so` and any LSPosed module SO loaded via memfd — neither shows up as a `/data/app/.../base.apk` in `/proc/self/maps`. +- Hash your own DEX and `.so` segments at build time and check them at runtime. Pin the signing certificate via `PackageInfo.signingInfo`. This catches the app-patching bypass — an attacker who NOPs out the detection and re-signs the APK. +- Send the native verdict to your server with the attestation token. Don't let JNI return a plain boolean that Java code reads and acts on — that boolean is itself something Frida can hook. -Run *all* of the following — any single one is trivially defeated, but together they raise the cost of a generic bypass: +## 3. Java-side tripwires -- **Targeted Manager-package lookup** via `PackageManager.getPackageInfo` against the list above. Catches lazy installs that use a stock LSPosed Manager. Misses installs with "Hide LSPosed Manager" enabled — that case is covered by the next two checks. -- **`Modifier.isNative()` tripwire** on guaranteed-native methods (`System.currentTimeMillis`/`nanoTime`, `Object.notify`/`notifyAll`, `Thread.currentThread`). LSPlant clears the `kAccNative` access-flag bit when it hooks a native method; so does Frida's Java bridge. Acts as a high-confidence tripwire when an attacker hooks one of the audited methods. -- **`/proc/self/maps` scan** for any `/data/app//base.apk` that does not belong to your own package. LSPosed `mmap`s every active module's APK into the host process; this catches active modern modules. -- **Stack-trace probe**: force exceptions through likely-hooked methods (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan `Throwable.getStackTrace()` for `de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_` frames. Combine with `/proc/self/task//comm` enumeration for native worker threads. +Java-side checks are trivial and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. -When *any* check fires, the app **must** alert the user and require explicit acceptance of liability before proceeding (see the dialog pattern in @MASTG-DEMO-0x4A). Silently continuing — or worse, blocking only on a network call later — wastes the detection. +- **Look up known Manager packages** with `PackageManager.getPackageInfo` against a fixed list (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`) declared in `` (see the permission note below). Misses installs with "Hide LSPosed Manager" enabled — the next checks cover that case. +- **`Modifier.isNative()` tripwire** on methods that should always be native (`System.currentTimeMillis`/`nanoTime`, `Object.notify`/`notifyAll`, `Thread.currentThread`). LSPlant clears the `kAccNative` flag when it hooks a native method; so does Frida's Java bridge. If the bit is clear on any of these, something has hooked them. +- **`/proc/self/maps` scan** for any `/data/app//base.apk` that is not yours. LSPosed `mmap`s every active module's APK into the host process, so a foreign `base.apk` means an active module. +- **Stack-trace + thread probe**: force exceptions through methods an attacker is likely to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan `Throwable.getStackTrace()` for `de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_`, `re.frida.*` frames. Walk `Thread.getAllStackTraces()` and `/proc/self/task//comm` for native worker threads (`gum-js-loop`, `gmain`, `pool-frida`). -## What modern LSPosed has already neutralized — do not rely on these +When any check fires, the app **must** show the user a warning and require them to accept the risk before continuing (see the dialog in @MASTG-DEMO-0x4A). -- **`Class.forName("de.robv.android.xposed.XposedBridge")`** — silenced by classloader isolation and by the "Xposed API call protection" toggle that LSPosed ships with by default. -- **`Method.getDeclaredFields()` for `xposedHookedMethodCallbacks` / `additionalInfo`** — a rovo89-era artefact. LSPlant uses ART trampolines and adds no fields to `java.lang.reflect.Method`. -- **`getInstalledApplications` enumeration** for "any app whose package id starts with `de.robv.android.xposed`" — useful only if the attacker hasn't enabled "Hide LSPosed Manager", which is a one-toggle action. -Including these in the demo (as in @MASTG-DEMO-0x4A) is fine *as long as* they are not the only line of defense. - -## Native-side detection (the bypass-resistant layer) - -Java-side checks live in the same memory the attacker controls. Move the critical detection into JNI: - -- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `liblspd.so` and any LSPosed module SO that is loaded via memfd and would not appear as a `/data/app/.../base.apk` in `/proc/self/maps`. -- Verify your own DEX and `.so` segments against a build-time hash and pin the signing certificate via `PackageInfo.signingInfo` — this catches bytecode/native patching attacks where the attacker NOPs out the detection and re-signs the APK. - -## Server-side attestation (the kernel-level layer) - -Kernel-mode interception is the third bypass category in @MASTG-TEST-0x49 and cannot be reliably detected from user-space. Mitigate it by: - -- Requiring a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel still cannot forge the device-key-signed attestation, because the signing happens inside the TEE / StrongBox. -- Storing sensitive cryptographic material in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key is unrecoverable from a rooted device even if the userspace process is fully compromised. - -## What not to do - -- **Do not hardcode "Hide LSPosed Manager" packages.** The whole point of the feature is that the package id is randomized per-install. Use the multi-signal approach instead. -- **Do not return a single boolean from `isInstrumented()`.** An attacker hooks the function and you lose everything. Spread the checks across multiple call sites, mix Java and native, and let the server reject a session when the cumulative risk score is above a threshold. -- **Do not ship the alert dialog without a server check.** A Java-side dialog can be dismissed by the same bypass that silenced the detection (see @MASTG-DEMO-0x4B). The dialog is the *user-facing* control; the *authoritative* control must be on a server you operate. - -For runtime checks, see the dynamic demo @MASTG-DEMO-0x4A (passing case) and bypass @MASTG-DEMO-0x4B (failing case). +For runtime checks, see the passing demo @MASTG-DEMO-0x4A and the bypass demo @MASTG-DEMO-0x4B. From 598c18ac558d148990e6ba031ced64946cf21b8b Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Fri, 22 May 2026 11:28:15 +0530 Subject: [PATCH 03/18] fix lint --- best-practices/MASTG-BEST-0x49.md | 1 - .../android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md | 1 - .../android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 1 - .../android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md | 2 -- 4 files changed, 5 deletions(-) diff --git a/best-practices/MASTG-BEST-0x49.md b/best-practices/MASTG-BEST-0x49.md index 4897c38353b..03ac4a13b31 100644 --- a/best-practices/MASTG-BEST-0x49.md +++ b/best-practices/MASTG-BEST-0x49.md @@ -35,5 +35,4 @@ Java-side checks are trivial and catch lazy attacker setups. They are not enough When any check fires, the app **must** show the user a warning and require them to accept the risk before continuing (see the dialog in @MASTG-DEMO-0x4A). - For runtime checks, see the passing demo @MASTG-DEMO-0x4A and the bypass demo @MASTG-DEMO-0x4B. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md index a2556b11f61..6d107616109 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md @@ -45,4 +45,3 @@ The test passes because the app statically implements three independent Frida de !!! note This is a "kind: pass" demo for static presence only: the sample contains the detection mechanisms expected by the test. The dynamic counterpart @MASTG-DEMO-0x49 covers a runtime bypass that defeats all three of these checks. - diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md index 833dde0e63b..7369ab79821 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -48,4 +48,3 @@ The test passes because the app statically implements four independent Xposed/LS !!! note This is a "kind: pass" demo for static presence only: the sample contains the detection mechanisms expected by the test. The dynamic counterpart @MASTG-DEMO-0x4B covers a Frida-based runtime bypass that defeats all four of these checks. - diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md index 2d891c2521b..200078d4460 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md @@ -26,9 +26,7 @@ See @MASTG-KNOW-0030 and @MASTG-KNOW-0032 for more context on bypassing runtime 1. Install the app from @MASTG-DEMO-0x4A on a device where the Xposed/LSPosed framework is active and at least one module is scoped to `org.owasp.mastestapp` (@MASTG-TECH-0005). 2. Spawn the app with the Frida bypass attached: - {{ run.sh }} - 3. Tap **Start** in the app and compare the result with @MASTG-DEMO-0x4A on the same device. ## Observation From b9f65b8838c7fc8fa19a90371c2764902751ad29 Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Fri, 22 May 2026 11:30:42 +0530 Subject: [PATCH 04/18] fix linter --- .../MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md index 7369ab79821..b99836dead2 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -13,11 +13,8 @@ kind: pass The snippet below shows sample code that performs four Xposed/LSPosed/Frida detection techniques used by Android apps as anti-instrumentation checks (see @MASTG-KNOW-0030 for more information about Detection of Reverse Engineering Tools). The checks combine a `PackageManager` lookup for known framework Manager package ids, a reflection-based tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / thread probe that surfaces framework frames and native worker threads. - **Check 1 — Classic Manager-package lookup.** Targets a curated list of known LSPosed / Xposed / EdXposed Manager package ids declared in `` in the manifest, and calls `PackageManager.getPackageInfo(pkg, 0)` for each. Catches lazy installs of the Manager. - - **Check 2 — Classic method-descriptor verification.** Resolves a set of guaranteed-native methods via reflection (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`) and checks that `Modifier.NATIVE` is still set on each. LSPlant clears `kAccNative` whenever it hooks a native method, and Frida's Java bridge does the same — so a missing bit on any of these methods is a strong tripwire. Only fires when the attacker actually targets one of the audited methods. - - **Check 3 — Foreign DEX/APK in `/proc/self/maps`.** Reads the app's own memory map and flags any `/data/app//base.apk` path that does not belong to the app itself. LSPosed `mmap`s a module's `base.apk` into every hooked process. Permission-free. - - **Check 4 — Stack-trace and thread inspection.** Forces three exceptions through calls an attacker is likely to hook — `PackageManager.getPackageInfo`, `Runtime.exec`, and `File.exists` — and scans each `Throwable.stackTrace` for class names belonging to known instrumentation frameworks. Also walks `Thread.getAllStackTraces()` and `/proc/self/task//comm` to catch native worker threads (Frida's `gum-js-loop`, etc.) that have no Java `Thread` representation. {{ MastgTest.kt # MastgTest_reversed.java # AndroidManifest.xml }} From 79d4803eb0900a7020deb0b3276422b4aa9f5f9d Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Fri, 22 May 2026 12:19:42 +0530 Subject: [PATCH 05/18] lint fix --- .../android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md index b99836dead2..e2ecb3ce700 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -12,7 +12,7 @@ kind: pass The snippet below shows sample code that performs four Xposed/LSPosed/Frida detection techniques used by Android apps as anti-instrumentation checks (see @MASTG-KNOW-0030 for more information about Detection of Reverse Engineering Tools). The checks combine a `PackageManager` lookup for known framework Manager package ids, a reflection-based tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / thread probe that surfaces framework frames and native worker threads. -- **Check 1 — Classic Manager-package lookup.** Targets a curated list of known LSPosed / Xposed / EdXposed Manager package ids declared in `` in the manifest, and calls `PackageManager.getPackageInfo(pkg, 0)` for each. Catches lazy installs of the Manager. +- **Check 1 — Classic Manager-package lookup.** Targets a curated list of known LSPosed / Xposed / EdXposed Manager package ids declared in `` in the manifest, and calls `PackageManager.getPackageInfo(pkg, 0)` for each. Catches lazy installs of the Manager. - **Check 2 — Classic method-descriptor verification.** Resolves a set of guaranteed-native methods via reflection (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`) and checks that `Modifier.NATIVE` is still set on each. LSPlant clears `kAccNative` whenever it hooks a native method, and Frida's Java bridge does the same — so a missing bit on any of these methods is a strong tripwire. Only fires when the attacker actually targets one of the audited methods. - **Check 3 — Foreign DEX/APK in `/proc/self/maps`.** Reads the app's own memory map and flags any `/data/app//base.apk` path that does not belong to the app itself. LSPosed `mmap`s a module's `base.apk` into every hooked process. Permission-free. - **Check 4 — Stack-trace and thread inspection.** Forces three exceptions through calls an attacker is likely to hook — `PackageManager.getPackageInfo`, `Runtime.exec`, and `File.exists` — and scans each `Throwable.stackTrace` for class names belonging to known instrumentation frameworks. Also walks `Thread.getAllStackTraces()` and `/proc/self/task//comm` to catch native worker threads (Frida's `gum-js-loop`, etc.) that have no Java `Thread` representation. From 2b8e0b2ac692ebbf8978c3f65ffe0437835e0bbd Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Fri, 22 May 2026 15:21:56 +0530 Subject: [PATCH 06/18] changes added --- best-practices/MASTG-BEST-0x48.md | 21 ++-- best-practices/MASTG-BEST-0x49.md | 28 ++--- .../MASTG-DEMO-0x48/MASTG-DEMO-0x48.md | 26 ++--- .../MASTG-DEMO-0x48/MastgTest.kt | Bin 5528 -> 0 bytes .../MASTG-DEMO-0x49/MASTG-DEMO-0x49.md | 42 ++------ .../MASTG-DEMO-0x49/output.txt | 9 +- .../MASTG-DEMO-0x49/script.js | 68 ++++++------ .../MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 25 ++--- .../MASTG-DEMO-0x4A/MastgTest.kt | 102 +----------------- .../MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh | 2 +- .../MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md | 28 ++--- .../MASTG-DEMO-0x4B/output.txt | 13 +-- .../MASTG-DEMO-0x4B/script.js | 15 ++- .../MASVS-RESILIENCE/MASTG-TEST-0x48.md | 13 ++- .../MASVS-RESILIENCE/MASTG-TEST-0x49.md | 31 ++---- 15 files changed, 131 insertions(+), 292 deletions(-) diff --git a/best-practices/MASTG-BEST-0x48.md b/best-practices/MASTG-BEST-0x48.md index ecde4c578b9..e27ba1d0444 100644 --- a/best-practices/MASTG-BEST-0x48.md +++ b/best-practices/MASTG-BEST-0x48.md @@ -1,6 +1,6 @@ --- -title: Resist Frida Instrumentation with Layered, Native-Backed Detection -alias: resist-frida-instrumentation-with-layered-native-backed-detection +title: Detecting Frida Instrumentation +alias: detecting-frida-instrumentation id: MASTG-BEST-0x48 platform: android knowledge: [MASTG-KNOW-0030] @@ -8,9 +8,7 @@ knowledge: [MASTG-KNOW-0030] Frida injects an agent (`frida-agent.so`) into the target process, starts native worker threads (`gum-js-loop`, `gmain`, `pool-frida`), and rewrites Java methods through ART trampolines. Every Java-side check is itself a Java method, so Frida can hook the check and silence it (see @MASTG-DEMO-0x49). Use multiple layers, and let the layer the attacker cannot reach decide who gets into the sensitive flow. -Order layers from strongest to weakest. Gate the sensitive flow on the strongest. - -## 1. Server-side attestation +## Play Integrity User-space code cannot reliably detect kernel-mode hooks. The only signal an attacker on a compromised device cannot forge is one signed inside the TEE / StrongBox and checked off-device. @@ -18,23 +16,24 @@ User-space code cannot reliably detect kernel-mode hooks. The only signal an att - Store secrets in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key cannot be pulled off a rooted device even if user-space is fully compromised. - Have the server combine the attestation verdict with the score from the client-side checks below. Use client checks as inputs to the server's decision, not as a gate on their own. -## 2. Native-side detection +## Native Probes Java-side checks run in memory the attacker controls. Move the important detection into JNI, so a bypass has to hook `dlopen` / `dl_iterate_phdr` or patch the linker itself — not just a Java method. - Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `frida-gadget` loaded via `LD_PRELOAD` and `frida-agent.so` loaded by `frida-server`, even when those files exist only as `/memfd:` mappings. -- Hash your own ELF segments and DEX at build time and check them at runtime. This catches the app-patching bypass — an attacker who NOPs out the detection and re-signs the APK. - Read `/proc/self/status` and check `TracerPid`. A non-zero value means something is `ptrace`-attached. - Send the native verdict to your server with the attestation token. Don't let JNI return a plain boolean that Java code reads and acts on — that boolean is itself something Frida can hook. -## 3. Java-side tripwires +## Memory and Process Inspection -Java-side checks are cheap and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. +These checks are cheap and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. - **Read `/proc/self/maps`** and flag any path that contains `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, or `/gum`. Use a `BufferedReader` filter that loops until a clean line comes back, not a single `readLine()` call — @MASTG-DEMO-0x49 shows that a single-line filter leaks consecutive matches. - **Enumerate `/proc/self/task//comm`** for instrumentation thread names (`gum-js-loop`, `gmain`, `pool-frida`). This catches Frida even when the agent was loaded from `memfd` and the on-disk `.so` is unmapped. - **Scan for `frida-server`-like processes**, but don't rely on the default `27042` port or the binary name `frida-server`. Both are trivially changed (`./frida-server -l 0.0.0.0:12345` + binary rename — see @MASTG-DEMO-0x49). A port / name check still belongs as a cheap trip for lazy attackers, but it must never be your only signal. -When any check fires, the app **must** show the user a warning and require them to accept the risk before continuing (see the dialog in @MASTG-DEMO-0x48). +## Code Integrity + +Frida's Java bridge clears `kAccNative` when it hooks a native method. Auditing that bit on methods that should always be native catches the in-process hook. -For runtime checks, see the passing demo @MASTG-DEMO-0x48 and the bypass demo @MASTG-DEMO-0x49. +- Audit a small set of guaranteed-native methods with `java.lang.reflect.Method.getModifiers()` and require `Modifier.NATIVE` to still be set. Frida's Java bridge (and LSPlant) clear that bit when they hook a native method. diff --git a/best-practices/MASTG-BEST-0x49.md b/best-practices/MASTG-BEST-0x49.md index 03ac4a13b31..3efddd3629d 100644 --- a/best-practices/MASTG-BEST-0x49.md +++ b/best-practices/MASTG-BEST-0x49.md @@ -1,14 +1,14 @@ --- -title: Resist Xposed/LSPosed Instrumentation with Layered Detection and Server Attestation -alias: resist-xposed-lsposed-instrumentation-with-layered-detection-and-server-attestation +title: Detecting Xposed/LSPosed Instrumentation +alias: detecting-xposed-lsposed-instrumentation id: MASTG-BEST-0x49 platform: android knowledge: [MASTG-KNOW-0030] --- -Modern LSPosed (1.9+) is built to defeat the classic Java-only checks from the v1 MASTG-TEST-0048 guidance. Its Manager app can hide behind a random package name, `XposedBridge` lives in a classloader your app can't reach, the "Xposed API call protection" toggle blocks `Class.forName` lookups, and its hooking engine (LSPlant) leaves no fields on `java.lang.reflect.Method` to find. Any single Java-side check is easy to silence. Use multiple layers, and let the layer the attacker can't reach be the one that decides access to sensitive flows. +LSPosed is built to defeat classic Java-only Xposed detection checks. Its Manager app can hide behind a random package name, `XposedBridge` lives in a classloader your app can't reach, the "Xposed API call protection" toggle blocks `Class.forName` lookups, and its hooking engine (LSPlant) leaves no fields on `java.lang.reflect.Method` to find. Any single Java-side check is easy to silence. Use multiple layers, and let the layer the attacker can't reach be the one that decides access to sensitive flows. -## 1. Server-side attestation +## Play Integrity A rooted device can lie to the app about its own state. The only check the attacker can't fake is one signed by the device's secure hardware (TEE / StrongBox) and verified by your server. @@ -16,23 +16,23 @@ A rooted device can lie to the app about its own state. The only check the attac - Store secrets in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key cannot be pulled off a rooted device even if user-space is fully compromised. - Treat the client-side checks below as inputs to the server's decision, not as a gate on their own. -## 2. Native-side detection +## Native Probes Java-side checks run in memory the attacker controls. Put the important detection in JNI, so a bypass has to hook the linker itself, not just a Java method. - Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `liblspd.so` and any LSPosed module SO loaded via memfd — neither shows up as a `/data/app/.../base.apk` in `/proc/self/maps`. -- Hash your own DEX and `.so` segments at build time and check them at runtime. Pin the signing certificate via `PackageInfo.signingInfo`. This catches the app-patching bypass — an attacker who NOPs out the detection and re-signs the APK. - Send the native verdict to your server with the attestation token. Don't let JNI return a plain boolean that Java code reads and acts on — that boolean is itself something Frida can hook. -## 3. Java-side tripwires +## Memory and Process Inspection -Java-side checks are trivial and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. +These checks are trivial and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. -- **Look up known Manager packages** with `PackageManager.getPackageInfo` against a fixed list (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`) declared in `` (see the permission note below). Misses installs with "Hide LSPosed Manager" enabled — the next checks cover that case. -- **`Modifier.isNative()` tripwire** on methods that should always be native (`System.currentTimeMillis`/`nanoTime`, `Object.notify`/`notifyAll`, `Thread.currentThread`). LSPlant clears the `kAccNative` flag when it hooks a native method; so does Frida's Java bridge. If the bit is clear on any of these, something has hooked them. -- **`/proc/self/maps` scan** for any `/data/app//base.apk` that is not yours. LSPosed `mmap`s every active module's APK into the host process, so a foreign `base.apk` means an active module. -- **Stack-trace + thread probe**: force exceptions through methods an attacker is likely to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan `Throwable.getStackTrace()` for `de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_`, `re.frida.*` frames. Walk `Thread.getAllStackTraces()` and `/proc/self/task//comm` for native worker threads (`gum-js-loop`, `gmain`, `pool-frida`). +- **Look up known Manager packages** with `PackageManager.getPackageInfo` against a fixed list (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`) declared in ``. Misses installs with "Hide LSPosed Manager" enabled — the checks below cover that case. +- **Scan `/proc/self/maps`** for any `/data/app//base.apk` that is not yours. LSPosed `mmap`s every active module's APK into the host process, so a foreign `base.apk` means an active module. +- **Probe stack traces and thread names**: force exceptions through methods an attacker is likely to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan `Throwable.getStackTrace()` for `de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_`, `re.frida.*` frames. Walk `Thread.getAllStackTraces()` and `/proc/self/task//comm` for native worker threads (`gum-js-loop`, `gmain`, `pool-frida`). -When any check fires, the app **must** show the user a warning and require them to accept the risk before continuing (see the dialog in @MASTG-DEMO-0x4A). +## Code Integrity -For runtime checks, see the passing demo @MASTG-DEMO-0x4A and the bypass demo @MASTG-DEMO-0x4B. +LSPlant clears `kAccNative` when it hooks a native method. Auditing that bit on methods that should always be native catches the in-process hook. + +- Audit a small set of guaranteed-native methods (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`) with `java.lang.reflect.Method.getModifiers()` and require `Modifier.NATIVE` to still be set. LSPlant (and Frida's Java bridge) clear that bit when they hook a native method. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md index 6d107616109..f4b6edf550f 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md @@ -10,19 +10,13 @@ kind: pass ## Sample -The snippet below shows sample code that performs three common Frida detection techniques used by Android apps as anti-instrumentation checks (see @MASTG-KNOW-0030 for more information about Detection of Reverse Engineering Tools): - -- Scanning the default `frida-server` TCP port (`127.0.0.1:27042`). -- Enumerating running processes via `/proc//cmdline` looking for `frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, etc. -- Reading `/proc/self/maps` looking for injected Frida artifacts (`frida-agent.so`, `libfrida`, `frida-gadget`, `gum`, `linjector`). - -These checks are well-known and trivially bypassable at runtime. +The snippet below shows sample code that performs three common Frida detection techniques used by Android apps as anti-instrumentation checks: a TCP scan of the default `frida-server` port (`127.0.0.1:27042`), a `/proc//cmdline` walk for `frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, `gmain`, and a `/proc/self/maps` read for injected Frida artifacts (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`). {{ MastgTest.kt # MastgTest_reversed.java }} ## Steps -Run the static analysis rule against the decompiled code. +Let's run our semgrep rule against the sample code. {{ ../../../../rules/mastg-android-frida-detection.yaml }} @@ -30,18 +24,16 @@ Run the static analysis rule against the decompiled code. ## Observation -The output shows the semgrep rule matching the three detection routines. +The output shows the semgrep rule matching the detection checks. {{ output.txt }} ## Evaluation -The test passes because the app statically implements three independent Frida detection mechanisms — semgrep flags: - -- `Socket.connect` with `127.0.0.1:27042` (the default-port probe). -- The `/proc` enumeration that walks `/proc//cmdline` and the `frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, `gmain` string literals it greps for. -- The `/proc/self/maps` reader paired with the injected-library substrings (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`). - -!!! note +The test case passes because the app statically implements three independent Frida detection mechanisms, all flagged by semgrep. Review each of the reported instances in `MastgTest_reversed.java`: - This is a "kind: pass" demo for static presence only: the sample contains the detection mechanisms expected by the test. The dynamic counterpart @MASTG-DEMO-0x49 covers a runtime bypass that defeats all three of these checks. +- Line 130 opens a TCP socket to `127.0.0.1:27042` — the default `frida-server` port-scan probe. +- Line 153 declares the process-name needle list (`frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, `gmain`) consumed by the `/proc` enumeration. +- Lines 155 and 169 enumerate `/proc` and read each `/proc//cmdline` to match running processes against those needles. +- Line 219 declares the injected-library needle list (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`) used to scan foreign mappings. +- Line 221 opens `/proc/self/maps` from Java to detect a Frida agent or any other foreign library mapped into the process. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt index f8a7467e9b568615b4d94535ce19c33bbaa01faa..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 5528 zcmc&&e{b715Z&MY6r51BG8QXoF<_gqu5+7Y>yWfT>|z+!pROB!M*Jz0@tEGiju`mW+q)M}kl)M}np|6F$*!PbMkrjB{gEtA&f+sLQ z_Ad5m^}Z2Vy1(Rwxo|dQ{uUu3qD)?Dt|d&HGG0(L-X37hZDbTIUSN{j^8!G#i+Q=c zh{*IL%WC*su#9WL-ia(5UFGUs?mV$tUQ3>3D_~b}WB4TwAT)sdbTXa&FgiRseK|UK z|K&HsXfTjV2L~b`h$G+|O>hv`(yn%nWkTCSSXcA@aIA7m{y(Gd_qIQ*O!Omm!TKE| zCrSaydf+X;=8;s<(=ttk7RfoF-5x!bndq;ecROb1!bZ>W&s4=%!cLMz<7hi;X+||V z7{nPsW2a#CdHLmW2?{=DE(2Uf z9agkI!(iChr!_KV;bsl_PcPda(Cm2f{AA32t@tuKn$CzXU;pNl2wCqF7HkI9I(g>k zjO)CV=VgpwA}L>8_w(DCVmHd6Y?1J6By$tnM5v+5Tv-ai&o5`Vp-+0jycfEh}E#%f8W!aK;M#*Y(?5wVlI~eBY zN7B4d_GnqyRXFUsO6TaLvx6HV*j`e`yruOu$ zlKD(kX&VLrZMbeB^y#>DOOOU-9ku-~VR~|br!JXBT4bnKNe43z7(>uPGuNO#3DGJ_ zG%XcL@-G>O4rsRP+DY9!cW`dA;yze}CR;E0*<|9z_$l6jvkV*wuD|4${ zNAX4XQ_%Djs_Y~6uugbtm%gD4DWg!zUO@*QD}5sQrOY6d!cB#yMZvj%NHk(3^0~}~ za54ZQuHV6|QW+_P(6Maj=(S-331O1Y0sEqBJQ3G27Ln5*GB1G_R5Fyuiu5$OC-Dd% znGtcR;hd9%3Adbp6?1wOZJV9(g1rtVF*Oyo3G8*ia262!(3WT#Q2nH+wKP}GDv^ME z*dvYlIyibSEmmx~3=^)ixy2+fgH1@oM%-v&OPwIuCpDwMH3 zA>p{w>j+d z3-zwmDXJ}NwbN$REy{V!o+3>J&s!%=bTaNH0WwvWhSnCI$J<)HS`vYMIqres)_o)l z9zOaiI>4WYP#^w!${rmYbYbsE+B+%2>KaiY%-EU%FLyiaDu3s&k1KNdN{H5q^G#QA zHb-|CM9&(XI7V?X_S;Hjeb)mO#S-^J(wHQ&U#4M@wyxbI$*Sk--rYRAdow-anml@I zMj39C?s18PZ6yF5T%%)`r6p>UKQGhTBOK%=4{17zO$>kNUKs|i8}{rKGC7o*e5R=p zIZl%eIG+;XQ_6R#apDRR;{JWSAPF1c1TaNfH$0cPqyg-8Q-*3s8+Yh;h_Kp@#Dd?r zUFYbC$?15RptFeD?`kIC@$Xv^&-a9(7M~0wsE7E-42b4FocGuP9F`jOz}C$ zDIgHSgw^Do*AZG!UkK=~8r-|7;B%+89^WHNJpgG9eJ&C6RR=A4+oVeOsd?`!hTqm1 z_;7_fX*BS^%BM}fKwY|wYEo&=(dF%qTmqp;6e~?*`$)7F2*kOCw3GxssivZvQxrOV zy1(dz&CfMHxf($opu>`~xdE)mqYkW>I>PZE@py4UG`~T)z*lb)ZFhH)&}Z0)3j}ub%sFn(^j;o+!?~RaCfq60Ha8 F!9QS2M`r*4 diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md index a07c7813c21..b1b5aabe3b7 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md @@ -1,6 +1,6 @@ --- platform: android -title: Bypassing Frida Detection via API Hooking and Server Reconfiguration +title: Bypassing Frida Detection via API Hooking id: MASTG-DEMO-0x49 code: [kotlin] test: MASTG-TEST-0x48 @@ -10,47 +10,27 @@ kind: fail ## Sample -This sample uses the same code as @MASTG-DEMO-0x48, which implements three Frida detection mechanisms: a default-port (`127.0.0.1:27042`) scan, a `/proc//cmdline` walk for `frida-server` / `frida-helper` / `frida-agent` / `gum-js-loop` / `gmain`, and a `/proc/self/maps` read for injected Frida libraries. This demo defeats all three by combining **server reconfiguration** — `frida-server` is renamed to `notfrida` on the device and bound to a non-default port — with **return-value tampering** — a Frida script hooks `Socket.connect`, `File.listFiles`, `BufferedReader.readLine`, and `FileReader` so each detection routine sees the values it would see on a clean device. +This demo defeats three Frida detection checks with a Frida script that hooks the Java APIs each routine depends on (`Socket.connect`, `File.listFiles`, `BufferedReader.readLine`) and reconfigures `frida-server` on different port. -See @MASTG-KNOW-0030 and @MASTG-KNOW-0032 for more context on bypassing runtime detection mechanisms. - -!!! note - This is a series of correlated tests. - - - @MASTG-DEMO-0x48 is a successful test (successful defense) that defend against Frida-detection mechanisms - - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x48 by using a runtime API-hooking bypass. - -{{ ../MASTG-DEMO-0x48/MastgTest.kt }} +{{ ../MASTG-DEMO-0x48/MastgTest.kt # script.js }} ## Steps -1. Install the app on a device (@MASTG-TECH-0005). -2. Push and rename `frida-server` on the device, then start it on a non-default port and forward it to the host: +1. Install the app, then push and rename `frida-server` on the device and start it on a non-default port. +2. Spawn the app with the Frida bypass attached. - ```sh - adb push frida-server /data/local/tmp/notfrida - adb shell "chmod +x /data/local/tmp/notfrida && /data/local/tmp/notfrida -l 0.0.0.0:12345 &" - adb forward tcp:12345 tcp:12345 - ``` - -3. Run `run.sh` to spawn the app with the bypass script attached. -4. Click the **Start** button in the app and observe the in-app result and the Frida console output. -5. Stop the script by pressing `Ctrl+C` and/or `q` to quit the Frida CLI. - -{{ script.js # run.sh }} +{{ run.sh }} ## Observation -The frida console shows each hook firing — `Socket.connect` to `127.0.0.1:27042` is blocked, `File.listFiles("/proc")` returns a list with every Frida-named pid removed, and `BufferedReader.readLine` returns a sanitized view of `/proc/self/maps` — while the app reports **PASS** for all three Frida-detection checks. +The Frida console shows every hook firing while the app reports **PASS** for all three Frida-detection checks. {{ output.txt }} ## Evaluation -The test fails because every detection routine has been neutralized at runtime: - -- **Default-port scan.** The `java.net.Socket.connect` hook intercepts connect attempts to `127.0.0.1:27042` and raises `ConnectException`, so `checkFridaDefaultPort()` reports the port closed. Server reconfiguration also moves `frida-server` to port `12345`, so the hardcoded probe would miss it even without the hook. -- **Process enumeration.** The `File.listFiles` hook filters `/proc` listings to drop any pid whose `cmdline` contains a Frida-related substring, and the `frida-server` binary has been renamed to `notfrida`, so `checkFridaProcesses()` finds nothing. -- **`/proc/self/maps` parsing.** The `BufferedReader.readLine` hook transparently drops any line that mentions `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, or `linjector`, so `checkFridaLibraries()` returns an empty list even though the agent is mapped into the process. +The test case fails because every detection routine has been bypassed at runtime: -The script also installs a top-level exception handler so any anti-tamper crash raised by the detection code is swallowed. This demonstrates why these three Frida-detection mechanisms must never be relied on as a single line of defence and must be combined with integrity-protected native checks and remote attestation. +- The `Socket.connect` hook raises `ConnectException` for `127.0.0.1:27042`, and `frida-server` was moved to a non-default port. +- The `File.listFiles` hook filters `/proc` to drop Frida-named pids, and the `frida-server` binary was renamed to `notfrida`. +- The `BufferedReader.readLine` hook drops `/proc/self/maps` lines mentioning Frida artifacts (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`). diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt index 50ed8e85a78..1e9751827ee 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt @@ -1,6 +1,7 @@ [+] Frida detection bypass hooks installed [+] Blocked Frida port probe -[+] Removed maps entry -[+] Removed maps entry -[+] Removed maps entry -[+] Removed maps entry +[+] File.listFiles(/proc) intercepted (process-enumeration bypass active) +[+] Removed maps entry: 7c5700a000-7c57951000 r--p 00000000 00:05 287218 /memfd:frida-agent-64.so (deleted) +[+] Removed maps entry: 7c57952000-7c58694000 r-xp 00947000 00:05 287218 /memfd:frida-agent-64.so (deleted) +[+] Removed maps entry: 7c58694000-7c58765000 r--p 01688000 00:05 287218 /memfd:frida-agent-64.so (deleted) +[+] Removed maps entry: 7c58766000-7c58781000 rw-p 01759000 00:05 287218 /memfd:frida-agent-64.so (deleted) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js index 3e182da8082..6d60cad7643 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js @@ -38,69 +38,63 @@ socketConnect.implementation = function (endpoint, timeout) { return socketConnect.call(this, endpoint, timeout); }; - const listFiles = File.listFiles.overload(); - - listFiles.implementation = function () { - - const files = listFiles.call(this); - - if (files === null) { - return files; - } - - if (this.getAbsolutePath() !== "/proc") { - return files; - } + let procListFilesLogged = false; + function filterProcEntries(files) { let filtered = []; - for (let i = 0; i < files.length; i++) { - try { - - const cmdline = - File.$new(files[i], "cmdline"); - + const cmdline = File.$new(files[i], "cmdline"); const fis = FileInputStream.$new(cmdline); - - const buffer = - Java.array("byte", new Array(256).fill(0)); - + const buffer = Java.array("byte", new Array(256).fill(0)); const size = fis.read(buffer); - fis.close(); - if (size > 0) { - let processName = ""; - for (let j = 0; j < size; j++) { - processName += - String.fromCharCode(buffer[j] & 0xff); + processName += String.fromCharCode(buffer[j] & 0xff); } - let hide = false; - for (let k = 0; k < PROC_KEYWORDS.length; k++) { - if (processName.indexOf(PROC_KEYWORDS[k]) !== -1) { hide = true; break; } } - if (hide) { console.log("[+] Hiding process: " + processName); continue; } } - } catch (e) {} - filtered.push(files[i]); } - return Java.array("java.io.File", filtered); + } + + function noteProcIntercepted() { + if (!procListFilesLogged) { + procListFilesLogged = true; + console.log("[+] File.listFiles(/proc) intercepted (process-enumeration bypass active)"); + } + } + + const listFilesNoArg = File.listFiles.overload(); + listFilesNoArg.implementation = function () { + const files = listFilesNoArg.call(this); + if (files === null) return files; + if (this.getAbsolutePath() !== "/proc") return files; + noteProcIntercepted(); + return filterProcEntries(files); + }; + + const listFilesFilter = File.listFiles.overload("java.io.FileFilter"); + listFilesFilter.implementation = function (filter) { + const files = listFilesFilter.call(this, filter); + if (files === null) return files; + if (this.getAbsolutePath() !== "/proc") return files; + noteProcIntercepted(); + return filterProcEntries(files); }; @@ -131,7 +125,7 @@ socketConnect.implementation = function (endpoint, timeout) { return line; } - console.log("[+] Removed maps entry"); + console.log("[+] Removed maps entry: " + line); } }; diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md index e2ecb3ce700..bb1610727ba 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -10,18 +10,13 @@ kind: pass ## Sample -The snippet below shows sample code that performs four Xposed/LSPosed/Frida detection techniques used by Android apps as anti-instrumentation checks (see @MASTG-KNOW-0030 for more information about Detection of Reverse Engineering Tools). The checks combine a `PackageManager` lookup for known framework Manager package ids, a reflection-based tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / thread probe that surfaces framework frames and native worker threads. - -- **Check 1 — Classic Manager-package lookup.** Targets a curated list of known LSPosed / Xposed / EdXposed Manager package ids declared in `` in the manifest, and calls `PackageManager.getPackageInfo(pkg, 0)` for each. Catches lazy installs of the Manager. -- **Check 2 — Classic method-descriptor verification.** Resolves a set of guaranteed-native methods via reflection (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`) and checks that `Modifier.NATIVE` is still set on each. LSPlant clears `kAccNative` whenever it hooks a native method, and Frida's Java bridge does the same — so a missing bit on any of these methods is a strong tripwire. Only fires when the attacker actually targets one of the audited methods. -- **Check 3 — Foreign DEX/APK in `/proc/self/maps`.** Reads the app's own memory map and flags any `/data/app//base.apk` path that does not belong to the app itself. LSPosed `mmap`s a module's `base.apk` into every hooked process. Permission-free. -- **Check 4 — Stack-trace and thread inspection.** Forces three exceptions through calls an attacker is likely to hook — `PackageManager.getPackageInfo`, `Runtime.exec`, and `File.exists` — and scans each `Throwable.stackTrace` for class names belonging to known instrumentation frameworks. Also walks `Thread.getAllStackTraces()` and `/proc/self/task//comm` to catch native worker threads (Frida's `gum-js-loop`, etc.) that have no Java `Thread` representation. +The snippet below shows sample code that performs four Xposed/LSPosed/Frida detection techniques used by Android apps as anti-instrumentation checks. The checks combine a `PackageManager` lookup for known framework Manager package ids, a reflection-based tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / thread probe that surfaces framework frames and native worker threads. {{ MastgTest.kt # MastgTest_reversed.java # AndroidManifest.xml }} ## Steps -Run the static analysis rule against the decompiled code. +Let's run our semgrep rule against the sample code. {{ ../../../../rules/mastg-android-xposed-detection.yaml }} @@ -29,19 +24,15 @@ Run the static analysis rule against the decompiled code. ## Observation -The output shows the semgrep rule matching all four detection routines in the reversed Java. +The rule has identified some instances in the code file where detection checks are placed. {{ output.txt }} ## Evaluation -The test passes because the app statically implements four independent Xposed/LSPosed/Frida detection mechanisms — semgrep flags: - -- `PackageManager.getPackageInfo` against the known Manager package ids (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`). -- The `Modifier.isNative` tripwire on the audited guaranteed-native methods. -- The `/proc/self/maps` reader paired with `/data/app/` inspection. -- The stack-trace / `/proc/self/task` probe via `Throwable.getStackTrace` / `Thread.getAllStackTraces`. - -!!! note +The test case passes because the app statically implements four independent Xposed/LSPosed/Frida detection mechanisms: - This is a "kind: pass" demo for static presence only: the sample contains the detection mechanisms expected by the test. The dynamic counterpart @MASTG-DEMO-0x4B covers a Frida-based runtime bypass that defeats all four of these checks. +- Line 143 declares the list of known Xposed/LSPosed/EdXposed Manager package ids (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`) consumed by a `PackageManager.getPackageInfo` presence probe. +- Lines 172–174 implement the `Modifier.isNative` tripwire: LSPlant (LSPosed's hooking engine) and Frida's Java bridge clear the `kAccNative` bit when they hook a native method, and this branch flags any audited method whose reflected modifiers no longer report native. +- Line 185 opens `/proc/self/maps` from Java to scan for foreign DEX/APK mappings (e.g. an LSPosed module APK or a Frida agent `.so`) injected into the process address space. +- Lines 232 and 261 declare the framework/needle string literals (`de.robv.android.xposed`, `org.lsposed.`, `lsphooker_`, `lsplant`, `edxposed`, `re.frida`, `gum-js`, `gmain`, `pool-frida`, `linjector`, …) used to inspect stack traces via `Throwable.getStackTrace` / `Thread.getAllStackTraces`; line 263 enumerates `/proc/self/task` to cross-check `comm` values against those native worker thread needles. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt index 2db37441158..8c3f61f1637 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt @@ -1,37 +1,4 @@ package org.owasp.mastestapp - -// SUMMARY: This sample demonstrates four Xposed/LSPosed/Frida detection techniques. Two -// of them are classic Java-side techniques mirrored from the MASTG-TEST-0048 v1 guidance -// (intentionally vulnerable, easily bypassed, and on a modern LSPosed install often -// silent even without a bypass). The other two are the production-grade techniques we -// found to actually fire against modern LSPosed 1.9+ on Android 12+: -// -// 1. CLASSIC — PackageManager.getPackageInfo for known Xposed/LSPosed/EdXposed Manager -// package ids declared in in AndroidManifest.xml. No Play-restricted -// `QUERY_ALL_PACKAGES` permission needed. Catches lazy installs of the Manager; -// misses the stealth "Hide LSPosed Manager" mode where the Manager is renamed. -// 2. CLASSIC — Method-descriptor verification via reflection. Pick a small set of -// guaranteed-native methods (`System.currentTimeMillis` / `nanoTime`, -// `Object.notify` / `notifyAll`, `Thread.currentThread`); resolve them via -// reflection; if `Modifier.NATIVE` is no longer set on any one of them, LSPlant or -// Frida has hooked the method. Acts as a tripwire only — fires when the attacker -// targets one of the specific methods we audit. -// 3. STRONG — Walk /proc/self/maps for any foreign /data/app//base.apk mapping. -// When LSPosed injects a module into the host process, the module's base.apk is -// mmapped right next to the app's own one. Permission-free. -// 4. STRONG — Stack-trace inspection: deliberately call methods an attacker is likely -// to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) with -// arguments that force an exception, then walk the exception's stack trace and -// every live Java thread's stack looking for `de.robv.android.xposed.*`, -// `org.lsposed.lspd.*`, `LSPHooker_`, or `re.frida.*` frames. Also walks -// /proc/self/task//comm to catch native Frida workers (`gum-js-loop`, etc.) -// that have no Java Thread representation. Permission-free. -// -// When ANY check fires the app raises a "Security Warning" dialog and asks the user to -// accept liability before continuing, per the MASTG-TEST-0048 guidance. -// -// See MASTG-DEMO-0x4B for the bypass narrative. - import android.app.Activity import android.app.AlertDialog import android.content.Context @@ -49,11 +16,6 @@ class MastgTest(private val context: Context) { val r = DemoResults("0x4A") var anyFail = false - // Check 1 (classic): targeted PackageManager lookup against a curated list of known - // LSPosed / Xposed / EdXposed Manager package ids declared in in - // AndroidManifest.xml. This avoids the Play-restricted QUERY_ALL_PACKAGES permission. - // The trade-off is that Manager packages hidden behind randomized package names - // won't be caught here; those are covered by Checks 3 and 4. try { val managers = checkKnownXposedManagerPackages() if (managers.isNotEmpty()) { @@ -68,8 +30,6 @@ class MastgTest(private val context: Context) { r.add(Status.ERROR, "Manager lookup failed: $e") } - // Check 2 (classic): method-descriptor verification. Acts as a tripwire — only fires - // if the attacker has hooked one of the specific guaranteed-native methods we audit. try { val tampered = checkHookedMethodSignatures() if (tampered.isNotEmpty()) { @@ -84,7 +44,6 @@ class MastgTest(private val context: Context) { r.add(Status.ERROR, "Method-descriptor check failed: $e") } - // Check 3 (strong): foreign DEX/APK mapped into our own process. try { val foreignDexes = checkForeignDexesInMaps() if (foreignDexes.isNotEmpty()) { @@ -99,7 +58,6 @@ class MastgTest(private val context: Context) { r.add(Status.ERROR, "/proc/self/maps inspection failed: $e") } - // Check 4 (strong): stack-trace inspection + native-thread enumeration. try { val frames = checkInstrumentationFramesInStacks() if (frames.isNotEmpty()) { @@ -124,12 +82,6 @@ class MastgTest(private val context: Context) { return r.toJson() } - /** - * Per the MASTG-TEST-0048 guidance, when reverse-engineering tools are detected the - * app must alert the user and require them to accept liability before continuing. - * The dialog is dispatched to the main thread because `mastgTest()` runs on a worker - * thread (see `MainActivity.kt`). - */ private fun promptUserForLiability(message: String) { val activity = context as? Activity ?: return Handler(Looper.getMainLooper()).post { @@ -143,17 +95,6 @@ class MastgTest(private val context: Context) { .show() } } - - /** - * Targeted `PackageManager.getPackageInfo` lookup for each known Xposed/LSPosed Manager - * package id. Those package ids are declared in `` in `AndroidManifest.xml`, - * which lets `getPackageInfo` see them on Android 11+ without holding the Play- - * restricted `QUERY_ALL_PACKAGES` permission. A Manager package id that resolves - * non-null means that flavour of the framework is installed on the device. - * - * Limitation: doesn't detect Managers that have been renamed via LSPosed's - * "Hide LSPosed Manager" feature. Those installs are caught by Checks 3 and 4. - */ private fun checkKnownXposedManagerPackages(): List { val knownManagers = listOf( "de.robv.android.xposed.installer", // classic Xposed @@ -168,18 +109,6 @@ class MastgTest(private val context: Context) { } } - /** - * Verifies that a small set of methods declared `native` on stock AOSP still report the - * NATIVE bit via reflection. LSPlant (LSPosed's hooking engine) clears `kAccNative` - * when it hooks a native method, so this acts as a tripwire for any attacker that - * targets one of these primitives. Frida's Java bridge also clears the bit when it - * hooks a native method via implementation-replacement. - * - * The set is curated: every entry must be `public final native` (or `public static - * native`) on every supported Android version. `android.os.Process.myPid` and - * `java.lang.Object.hashCode` are deliberately omitted — they delegate to libcore in - * Java and never carry NATIVE on a clean device. - */ private fun checkHookedMethodSignatures(): List { val tampered = mutableListOf() val targets = mapOf( @@ -201,12 +130,6 @@ class MastgTest(private val context: Context) { return tampered } - /** - * Reads `/proc/self/maps` and flags any `/data/app//base.apk` mapping that does NOT - * belong to our own package. When LSPosed injects a module into a host process, the - * module's `base.apk` is mmapped into that process and shows up in maps alongside the - * host app's APK. - */ private fun checkForeignDexesInMaps(): List { val ownPkg = context.packageName val hits = LinkedHashSet() @@ -224,24 +147,7 @@ class MastgTest(private val context: Context) { return hits.toList() } - /** - * Two-pronged instrumentation probe: - * - * (a) Stack-trace inspection — Xposed/LSPosed hook handlers are Java code, so when a - * hooked method throws, the resulting `Throwable.stackTrace` carries the handler - * frames. We deliberately call three methods an attacker is likely to hook - * (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) with arguments - * that force an exception, then scan the captured stack for known framework class - * names. Also walks `Thread.getAllStackTraces()` for any Java thread parked - * inside a framework class. - * - * (b) Native-thread enumeration — Frida hooks do NOT add Java stack frames (the hook - * handler is V8/QuickJS native code), and Frida's worker threads (`gum-js-loop`, - * `gmain`, `pool-frida`) are native pthreads with no Java `Thread` object, so they - * are invisible to `Thread.getAllStackTraces()`. To catch them we walk - * `/proc/self/task//comm` directly — the kernel exposes every thread's name - * to its own process regardless of whether it's a Java thread. - */ + private fun checkInstrumentationFramesInStacks(): List { val needles = listOf( "de.robv.android.xposed", @@ -267,21 +173,18 @@ class MastgTest(private val context: Context) { } } - // Probe 1: PackageManager.getPackageInfo (often hooked by root-bypass modules). try { context.packageManager.getPackageInfo("___xposed_probe_${System.nanoTime()}", 0) } catch (e: Throwable) { scan("getPackageInfo", e.stackTrace) } - // Probe 2: Runtime.exec (root-bypass modules typically hook this). try { Runtime.getRuntime().exec(arrayOf("/__xposed_probe_${System.nanoTime()}")) } catch (e: Throwable) { scan("Runtime.exec", e.stackTrace) } - // Probe 3: File.exists (root-bypass modules typically hook this too). try { File("/__xposed_probe_${System.nanoTime()}").exists() val t = Throwable("post-File.exists probe") @@ -290,7 +193,6 @@ class MastgTest(private val context: Context) { scan("File.exists", e.stackTrace) } - // Walk every live Java thread's stack. try { val all = Thread.getAllStackTraces() for ((thread, frames) in all) { @@ -298,8 +200,6 @@ class MastgTest(private val context: Context) { } } catch (_: Throwable) { /* SecurityManager could block — ignore */ } - // Native-thread enumeration via /proc/self/task//comm. Catches Frida workers - // that have no Java Thread representation (gum-js-loop, gmain, pool-frida, …). val nativeThreadNeedles = listOf("gum-js", "gmain", "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker") try { val tasks = File("/proc/self/task").listFiles() ?: emptyArray() diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh index 986c197312e..c04cc62431e 100755 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -NO_COLOR=true semgrep -c ../../../../rules/mastg-android-xposed-detection.yaml ./MastgTest_reversed.java > output.txt \ No newline at end of file +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-xposed-detection.yaml ./MastgTest_reversed.java -text -o output.txt \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md index 200078d4460..2a4386b0f8d 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md @@ -10,36 +10,28 @@ kind: fail ## Sample -This sample uses the same code as @MASTG-DEMO-0x4A, which performs four Xposed/LSPosed/Frida detection techniques: a `PackageManager.getPackageInfo()` lookup for known framework Manager package ids, a reflection-based `Modifier.isNative()` tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / `/proc/self/task` probe that surfaces framework frames and native worker threads. This demo defeats all four with a Frida script that intercepts the Java APIs each detection routine relies on: `ApplicationPackageManager.getPackageInfo`, `Method.getModifiers`, `BufferedReader.readLine` (over `/proc/self/maps`), `Throwable.getStackTrace` / `Thread.getStackTrace` / `Thread.getAllStackTraces`, and `File.listFiles` (over `/proc/self/task`). - -See @MASTG-KNOW-0030 and @MASTG-KNOW-0032 for more context on bypassing runtime detection mechanisms. - -!!! note - This is a series of correlated tests. - - - @MASTG-DEMO-0x4A is a successful test (successful defense) that defends against Xposed/LSPosed/Frida detection mechanisms. - - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x4A by using a runtime API-hooking bypass. +This demo defeats four Xposed/LSPosed detection checks with a Frida script that hooks the Java APIs each routine depends on (`getPackageInfo`, `Method.getModifiers`, `BufferedReader.readLine`, `Throwable.getStackTrace`, `File.listFiles`). {{ ../MASTG-DEMO-0x4A/MastgTest.kt # script.js }} ## Steps -1. Install the app from @MASTG-DEMO-0x4A on a device where the Xposed/LSPosed framework is active and at least one module is scoped to `org.owasp.mastestapp` (@MASTG-TECH-0005). -2. Spawn the app with the Frida bypass attached: +1. Install the app on a device where the Xposed/LSPosed framework is active and at least one module is scoped to `org.owasp.mastestapp`. +2. Spawn the app with the Frida bypass attached. + {{ run.sh }} -3. Tap **Start** in the app and compare the result with @MASTG-DEMO-0x4A on the same device. ## Observation -The Frida console shows every hook firing while the on-screen result reports **PASS** for all four checks — and, critically, the liability prompt that @MASTG-DEMO-0x4A raises on detection never appears. +The Frida console shows every hook firing while the app reports **PASS** for all four detection checks. {{ output.txt }} ## Evaluation -The test fails because every detection routine has been neutralized at runtime: +The test case fails because every detection routine has been bypassed at runtime: -- **Manager-package lookup.** A `getPackageInfo` hook throws `NameNotFoundException` for every known Xposed/LSPosed Manager package id, so `checkKnownXposedManagerPackages()` returns an empty list. -- **Method-modifier tripwire.** A `Method.getModifiers` hook re-ORs `Modifier.NATIVE` on every audited method whose bit has been cleared, so `checkHookedMethodSignatures()` reports the methods as untouched. -- **`/proc/self/maps` parsing.** A recursion-safe `BufferedReader.readLine` filter drops every `/proc/self/maps` line containing an Xposed-related package id before it reaches the app, so `checkForeignDexesInMaps()` returns empty. -- **Stack-trace and thread probe.** A `Throwable.getStackTrace` hook strips framework frames from every captured stack, and a `File.listFiles` hook hides instrumentation tids from the `/proc/self/task` walk before the app reads their `comm` files. Both halves of the check come back clean. +- The `getPackageInfo` hook throws `NameNotFoundException` for every known Xposed/LSPosed Manager id. +- The `Method.getModifiers` hook re-sets `Modifier.NATIVE` on audited methods. +- The `BufferedReader.readLine` hook drops lines mentioning Xposed-related package ids. +- The `Throwable.getStackTrace` hook strips framework frames, and the `File.listFiles` hook hides instrumentation tids from `/proc/self/task`. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt index f57bdda7bcf..f27b9661dd5 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt @@ -1,13 +1,14 @@ -[bypass] Demo 4 hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes). +[bypass] hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes). [bypass] faking NameNotFoundException for de.robv.android.xposed.installer [bypass] faking NameNotFoundException for org.meowcat.edxposed.manager [bypass] faking NameNotFoundException for io.va.exposed [bypass] faking NameNotFoundException for com.solohsu.android.edxp.manager [bypass] faking NameNotFoundException for org.lsposed.manager -[bypass] dropping /proc/self/maps line: 6f9990f000-6f99911000 r-xp 001ee000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk -[bypass] dropping /proc/self/maps line: 6f99911000-6f99912000 r--p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk -[bypass] dropping /proc/self/maps line: 6f99912000-6f99913000 rw-p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] Method.getModifiers intercepted (Modifier.NATIVE tripwire bypass active) +[bypass] dropping /proc/self/maps line: 7c4c894000-7c4c896000 r-xp 001ee000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 7c4c896000-7c4c897000 r--p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 7c4c897000-7c4c898000 rw-p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk [bypass] stripped 2 framework frames [bypass] stripped 4 framework frames -[bypass] hiding /proc/self/task tid 12517 (comm=gmain) -[bypass] hiding /proc/self/task tid 12518 (comm=pool-frida) +[bypass] hiding /proc/self/task tid 14110 (comm=gmain) +[bypass] hiding /proc/self/task tid 14111 (comm=pool-frida) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js index d08280cc5fb..300c05efb3c 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js @@ -63,13 +63,20 @@ Java.perform(function () { var NATIVE_BIT = Modifier.NATIVE.value; var Method = Java.use("java.lang.reflect.Method"); + var getModifiersLogged = false; Method.getModifiers.implementation = function () { var orig = this.getModifiers(); try { var key = this.getDeclaringClass().getName() + "." + this.getName(); - if (SUSPICIOUS_NATIVE_METHODS[key] && (orig & NATIVE_BIT) === 0) { - console.log("[bypass] restoring Modifier.NATIVE on " + key); - return orig | NATIVE_BIT; + if (SUSPICIOUS_NATIVE_METHODS[key]) { + if (!getModifiersLogged) { + getModifiersLogged = true; + console.log("[bypass] Method.getModifiers intercepted (Modifier.NATIVE tripwire bypass active)"); + } + if ((orig & NATIVE_BIT) === 0) { + console.log("[bypass] restoring Modifier.NATIVE on " + key); + return orig | NATIVE_BIT; + } } } catch (e) { /* fall through */ } return orig; @@ -174,5 +181,5 @@ Java.perform(function () { return true; }); - console.log("[bypass] Demo 4 hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes)."); + console.log("[bypass] hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes)."); }); diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md index 8cb4d6575e3..03ef5000eef 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md @@ -11,21 +11,20 @@ knowledge: [MASTG-KNOW-0030] ## Overview -This test verifies whether app detect well-known Frida detection patterns — connecting to the default `frida-server` TCP port (`127.0.0.1:27042`), grepping `/proc//cmdline` for `frida-server`/`frida-helper`, or scanning `/proc/self/maps` for `frida-agent.so`/`libfrida`/`gum-js-loop` — provide little resilience against an attacker that controls the device. Each of these signals can be neutralized with a few lines of Frida JavaScript or by reconfiguring the `frida-server` deployment (renaming the binary, binding a non-default port). If these mechanisms are the only line of defense, an attacker can trivially run instrumentation against the app and the protection is illusory. +If an application relies exclusively on standard, local environment signatures to detect instrumentation frameworks, an attacker with full control over the host device can easily bypass these checks. -This test checks whether an app's Frida detection routines actually resist an in-process Frida bypass that hooks the underlying `Socket`/`File`/`FileReader` APIs and returns the values the detection logic expects when no instrumentation is active. +Commonly implemented detection patterns—such as attempting connections to the default Frida-server TCP port (`127.0.0.1:27042`), searching `/proc//cmdline` for strings like `frida-server` or `frida-helper`, and scanning `/proc/self/maps` for artifacts like `frida-agent.so`, `libfrida`, or `gum-js-loop`—provide insufficient resilience. Because these checks reside entirely within the user space controlled by the analyst, they represent superficial barriers. This test verifies whether the application's runtime protection can be trivially neutralized by hooking the underlying Java or system APIs to spoof expected clean-device responses. ## Steps 1. Install the app on a device with `frida-server` running. -2. Use @MASTG-TOOL-0001 to attach to the app and exercise the function(s) that perform Frida detection. -3. Use @MASTG-TECH-0033 to attempt to hook the APIs used by each detection routine (`java.net.Socket.connect`, `java.io.File.listFiles`, `java.io.BufferedReader.readLine`) to return clean values. -4. Re-run the app's detection function and capture the result. +2. Use @MASTG-TOOL-0001 to attach to the app and test the function that perform Frida detection. +3. Run @MASTG-TECH-0033 to attempt to hook the APIs used by each detection routine to return clean values. ## Observation -The output should contain a list of detection routines that the app executed, the APIs they queried, and the value those APIs returned both with and without the bypass script attached. +The output should contain a list of detection routines that the app executed, the APIs they queried, and the value those APIs returned with a Frida script attached. ## Evaluation -The test case fails if the app fails to detect Frida when app normally under `frida-server` is running and a generic API-hooking bypass is attached. +The test case fails if the app fails to detect Frida when `frida-server` is attached to the process. diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md index c719bf6b4d5..b1e780867cf 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md @@ -12,37 +12,20 @@ knowledge: [MASTG-KNOW-0030] ## Overview -Android apps that try to detect the Xposed/LSPosed framework with classic Java-only techniques fall into two camps: +Well-known Xposed/LSPosed detection patterns a `PackageManager.getPackageInfo` lookup for known Manager package ids, a reflection-based `Modifier.isNative` tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings, and a stack-trace / `/proc/self/task` probe for framework frames and native worker threads provide little resilience against an attacker that controls the device. Each of these signals can be neutralized with a few lines of Frida JavaScript that hooks the underlying Java APIs and returns the values the detection logic expects on a clean device. -- **Easily-bypassed-and-often-silent** techniques mirrored from the MASTG-TEST-0048 v1 guidance — a targeted `PackageManager.getPackageInfo` lookup for known Manager package ids, and reflection-based verification that `Modifier.NATIVE` is still set on a curated list of guaranteed-native methods. These remain useful as low-cost tripwires for lazy attacker setups but are silent against any modern LSPosed install with "Hide LSPosed Manager" enabled and against module deployments that don't touch the methods the app audits. -- **Stronger** techniques that actually fire against modern LSPosed 1.9+ on Android 12+ — scanning `/proc/self/maps` for foreign DEX/APK mappings that LSPosed introduces into every hooked process, and a stack-trace probe through likely-hooked methods (plus `/proc/self/task//comm` enumeration to catch native Frida workers). - -This test verifies whether an app's Xposed/LSPosed detection actually resists a modern LSPosed deployment plus a generic Frida-based bypass that hooks the underlying Java APIs. - -The four detection techniques exercised by the linked demo are: - -- **Manager-package lookup** via `PackageManager.getPackageInfo` against a fixed list declared in `` (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`). -- **Method-descriptor verification** via `java.lang.reflect.Method.getModifiers()` for a small set of guaranteed-native methods (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`). If `Modifier.NATIVE` is cleared on any one of them, LSPlant or Frida has hooked the method. -- **Foreign DEX/APK in `/proc/self/maps`** — flag any `/data/app//base.apk` mapping whose package id is not the app itself. LSPosed `mmap`s a module's APK into every hooked process. -- **Stack-trace inspection + native-thread enumeration** — force exceptions through methods an attacker is likely to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan the resulting `Throwable.stackTrace` for framework class names (`de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_`, `re.frida.*`, …). Also walks `Thread.getAllStackTraces()` and `/proc/self/task//comm` to catch native instrumentation threads (`gum-js-loop`, `gmain`, `pool-frida`). - -When any of these detections fires, the app MUST alert the user and require liability acceptance before continuing (see the dialog pattern in the linked demos). +This test checks whether an app's Xposed/LSPosed detection routines actually resist a runtime API-hooking bypass. ## Steps -1. Install the app on a rooted device with Magisk + Zygisk + LSPosed 1.9+ active and at least one LSPosed module scoped to the app. -2. Run @MASTG-TOOL-0001 to attach to the app and apply a generic Java-side bypass that hooks `ApplicationPackageManager.getPackageInfo` (to throw `NameNotFoundException` for Manager package ids), `Method.getModifiers` (to re-OR `Modifier.NATIVE` on the audited methods), `BufferedReader.readLine` (to scrub `/proc/self/maps` of foreign DEX/APK paths), `Throwable.getStackTrace` / `Thread.getStackTrace` / `Thread.getAllStackTraces` (to strip framework frames), and `File.listFiles` against `/proc/self/task` (to hide instrumentation tids). -3. Trigger the app's Xposed detection function and capture the result. +1. Install the app on a rooted device with LSPosed active and at least one module scoped to the app. +2. Use @MASTG-TOOL-0001 to attach to the app and test the function that perform Xposed/LSPosed detection. +3. Run @MASTG-TECH-0033 to attempt to hook the APIs used by each detection routine to return clean values. ## Observation -The output should contain a list of detection routines, the APIs they queried (`PackageManager.getPackageInfo`, `Method.modifiers`, `/proc/self/maps`, `Throwable.stackTrace`, `Thread.getAllStackTraces`, `/proc/self/task//comm`), and the value those APIs returned with and without the bypass attached. It should also indicate whether the app raised the liability-acceptance dialog when the checks fired. +The output should contain a list of detection routines that the app executed, the APIs they queried, and the value those APIs returned both with the a frida script attached. ## Evaluation -The test case fails if **any** of the following hold: - -- The app reports "no Xposed detected" while at least one LSPosed module is loaded into the process and a generic Java-side bypass is attached, or -- A check fires but the app does not raise an alert/liability-acceptance dialog to the user before proceeding. - -Note that this Java-side test only covers the API-hooking bypass category. Bypass via app-code patching and via kernel-level syscall interception are out of scope and must be addressed by complementary controls (DEX/native integrity hashing, signing-certificate pinning, and server-side attestation such as the Play Integrity API). +The test case fails if the app fails to detect Xposed/LSPosed when at least one LSPosed module is loaded into the process. From 597fcfebed7f6f85b2bc17309e1ba5489400a0db Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Fri, 22 May 2026 15:25:22 +0530 Subject: [PATCH 07/18] lint fix --- tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md | 2 +- tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md index 03ef5000eef..c6d675da898 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md @@ -11,7 +11,7 @@ knowledge: [MASTG-KNOW-0030] ## Overview -If an application relies exclusively on standard, local environment signatures to detect instrumentation frameworks, an attacker with full control over the host device can easily bypass these checks. +If an application relies exclusively on standard, local environment signatures to detect instrumentation frameworks, an attacker with full control over the host device can easily bypass these checks. Commonly implemented detection patterns—such as attempting connections to the default Frida-server TCP port (`127.0.0.1:27042`), searching `/proc//cmdline` for strings like `frida-server` or `frida-helper`, and scanning `/proc/self/maps` for artifacts like `frida-agent.so`, `libfrida`, or `gum-js-loop`—provide insufficient resilience. Because these checks reside entirely within the user space controlled by the analyst, they represent superficial barriers. This test verifies whether the application's runtime protection can be trivially neutralized by hooking the underlying Java or system APIs to spoof expected clean-device responses. diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md index b1e780867cf..091be63b185 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md @@ -12,7 +12,7 @@ knowledge: [MASTG-KNOW-0030] ## Overview -Well-known Xposed/LSPosed detection patterns a `PackageManager.getPackageInfo` lookup for known Manager package ids, a reflection-based `Modifier.isNative` tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings, and a stack-trace / `/proc/self/task` probe for framework frames and native worker threads provide little resilience against an attacker that controls the device. Each of these signals can be neutralized with a few lines of Frida JavaScript that hooks the underlying Java APIs and returns the values the detection logic expects on a clean device. +Well-known Xposed/LSPosed detection patterns a `PackageManager.getPackageInfo` lookup for known Manager package ids, a reflection-based `Modifier.isNative` tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings, and a stack-trace / `/proc/self/task` probe for framework frames and native worker threads provide little resilience against an attacker that controls the device. Each of these signals can be neutralized with a few lines of Frida JavaScript that hooks the underlying Java APIs and returns the values the detection logic expects on a clean device. This test checks whether an app's Xposed/LSPosed detection routines actually resist a runtime API-hooking bypass. From bd4a4310c004a4ffa334ef1b29198c27622c70a0 Mon Sep 17 00:00:00 2001 From: ScreaMy7 Date: Mon, 25 May 2026 11:04:51 +0530 Subject: [PATCH 08/18] added MastgTest --- .../MASTG-DEMO-0x48/MastgTest.kt | Bin 0 -> 4620 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b57cfd933cd512d4c4131ce586648d9b02171162 100644 GIT binary patch literal 4620 zcmcIn>u%dN7~O9_1t%1Zj3sJX6zC9a?b0T}IwWlnXV{+t1tU>56NwZ^Dov0Y*hB0I z_ar+XsjKZYMbW81T#4j&Ip6KD;^`Hi3#QFH(Vw`jl9F2|tm9QRnaHxz#xbrkqh*%h zTe6=zc`e=T#g08u z=fa5WOz>=d^+*eh!%8 z%E)W(1iR)1^G@ElJ=SNCP6+)aOQR(ua!yhJJPkva3WoZ^yHFD_diT3>r7oD9k`QP;eZpV6In9wwbj%6iz8xW#gYji(3|*xji>vw*@HHwg|_R4 zEIYkRmU5RaSSs$ zGCPjo@^m@y?^^;I&UE0nB1ZT&VST5W&%PZa3Y25k8p;$eftfd!!C+z76Y^m-*S<@L z12#-3B)18^QR2syN?$7 zs#~DzM(n4t@Jw9GR3v^zNmT={8eO#Hw-{d~nJ4iIAej|uZD1UYf0l8_30N_wPszI2 zb6&9z(SAxjk#&xJh!|o4j-m7?^gIv^ii?r<%1fnzNj7YQf8g}tjdVD%W->(h#_SIh z35hzFru_gk!qrA$Fgg8))vD=;ZC`EYG>zhWf`CS0!G+rCJnWS;o_WK3r~wBS!?=CL zT0+EL?Xh4&XIw*R;y_@Mxo{_3s_=$hoK)Jn!Th3x@yXICLJ1#9CGCQO{P}Zd0KOl9 zdKi5xvT399Hg01f>nywwQ~sES{6kJ1mwl?NsxUCT{>PPt#S19*^6Ws|DIz02Ihs|-Q6+lBPR7#j3#i+ zfG}fA2E2Gttny87aN}3v3ziUV8PmrxW9mcqHHe!uvuLxKi}*D0}7TbzYY{mv*PXmBYW5DA+*(P~dtVR@RPE zo9ua?w-zCgT^-VPG@BIm(B&hJJoi|USIO)^8nT(TCS*8mw#fOM5HP9sg2@ByW#5KcLwj>_blEtlqiNnK9`-p3F&Ukf8Agj}?G8HuPxF1? z0iB?x^<%S?q+HP@i@0Xn-a9;6@nk<_uV*-IXayNaImcZwo+d_A1y4o1^~2US+hSYO z1ox+MP%?DxzD(=PSgCH0Mo1V!qe-1rG)OY}=OHJDkPtQwo7d?y?Z(qmV!9$lkM06| zK5EOoM|1|U$*Eu^C&Xo_kk#x>PKG`{x7{hhukuAZGnn6N}`?f@(5Xawtp zNf7=+o-U3l&F|1I@UyasRDOMu&~8c;l0+?fm?+y!&XqRe4Tn9H_qDh;cSsIF;qTW? io{em$4fOQk5AUmA`t|;QO`>@DPE+G*ifB1mPW}UAd>}Re literal 0 HcmV?d00001 From 34b3a8043e460832655e2a6eb87a63f39e4e8d58 Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Mon, 25 May 2026 12:36:36 +0530 Subject: [PATCH 09/18] added mastg test file --- .../MASTG-DEMO-0x48/MastgTest.kt | Bin 4620 -> 4620 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt index b57cfd933cd512d4c4131ce586648d9b02171162..f09b1290e2414cfd792b91ffa2c6a268540ec972 100644 GIT binary patch delta 15 WcmeBC=~0;=w^4Q}$L7Nvo}2(Jbp=`g delta 15 WcmeBC=~0;=w^4ox$L2#EUYr0ed<9zo From 6f7b4adb0f3831e03377beb4064ccef94cdf4ab2 Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Mon, 25 May 2026 13:01:08 +0530 Subject: [PATCH 10/18] bug fix --- .../MASTG-DEMO-0x48/MastgTest.kt | Bin 4620 -> 5118 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt index f09b1290e2414cfd792b91ffa2c6a268540ec972..09603ad7135caf6b227eb8d113f9e9998fbd473e 100644 GIT binary patch delta 904 zcmah{&ubGw6egQZsI|!^U`r8=&uD21%@0CBEfx&6k%E@mY`utw$?j}BX0sD^CjEhe zp7f%iJn$&Rvmg@jZcqLj9z6L6coS!`iPRG7o@U?s-uJ!lz4zksui00*j~B)bJZGI+ zEtjEQTfMyq_wUpi_N}75(y)t_y@jg}pdK=pz%CKsQb`?2UD#m+=oPw6{Y_T>Kipb{G2m|LVI2qANPgVZ19;x4H@f(x!bY9`6P&us|i0_@EEneC2!u z6)+NlH%#}xwnpm;*rg$jXth!f56$Uhq=w(j>2z68-z!TZTBZElIDW}aVI{seG;Akk z@oA!klU8Qqgl*LVp&uR2{i3IDQ2#{_HJyX_6fUJ^uIq6vvk;b`&U{+PA8Ajg)F9QB^uHbA zgJiBy&_NNtP7zy$`gfo*bMWX7T9@bwhqv3Bc_^UZstp}ILao8fgTloD8rRdy*+F4R bAD&J3WC1^?pQD$Z$9t9)S3JFzbzuAk-1iz# delta 434 zcmeyT-lH->Zlf$W)8uz7s*~q4)dOj{No?$sdzfcWKFBCP*^5PlGoUCxIkmVrwRkck zlknsUpu%@72K)suQH8Yp(!7+()~qQI%@XWTsmXF|5|cMDhq6IbPM*c4&a9xIFnK>) z?&Lo9)ezPEi;riCFiE(Wag#9%!cRzIu9z2 zWD7_~W}1SAB1CGki-3y~M6+H|YGO)AYDI~LCXiW>lbD>Uq0XSLqoA&!K6$1<`{W#9 U$;m!~{A>`@H^&JsU}EF~0GXAD!2kdN From a59e3f859d1e8f045eed1c84c9786519b96fae84 Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Tue, 26 May 2026 13:07:22 +0530 Subject: [PATCH 11/18] update demo --- .../MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh | 2 +- .../MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 10 +- .../MASTG-DEMO-0x4A/MastgTest.kt | 98 ++--------- .../MASTG-DEMO-0x4A/MastgTest_reversed.java | 152 ++---------------- .../MASTG-DEMO-0x4A/output.txt | 122 +++----------- .../MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh | 2 +- rules/mastg-android-xposed-detection.yaml | 50 ++---- 7 files changed, 64 insertions(+), 372 deletions(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh index 10087793875..d8adbd1bfc5 100755 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -NO_COLOR=true semgrep -c ../../../../rules/mastg-android-frida-detection.yaml ./MastgTest_reversed.java > output.txt \ No newline at end of file +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-frida-detection.yaml ./MastgTest_reversed.java --text -o output.txt \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md index bb1610727ba..92edd903894 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -10,7 +10,7 @@ kind: pass ## Sample -The snippet below shows sample code that performs four Xposed/LSPosed/Frida detection techniques used by Android apps as anti-instrumentation checks. The checks combine a `PackageManager` lookup for known framework Manager package ids, a reflection-based tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process, and a stack-trace / thread probe that surfaces framework frames and native worker threads. +The snippet below shows sample code that performs two Xposed/LSPosed detection techniques used by Android apps as anti-instrumentation checks. The checks combine a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process and a stack-trace probe that surfaces framework frames left by hooked methods and parked framework workers. {{ MastgTest.kt # MastgTest_reversed.java # AndroidManifest.xml }} @@ -30,9 +30,7 @@ The rule has identified some instances in the code file where detection checks a ## Evaluation -The test case passes because the app statically implements four independent Xposed/LSPosed/Frida detection mechanisms: +TThe test case passes because the app statically implements two independent Xposed/LSPosed detection mechanisms: -- Line 143 declares the list of known Xposed/LSPosed/EdXposed Manager package ids (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`) consumed by a `PackageManager.getPackageInfo` presence probe. -- Lines 172–174 implement the `Modifier.isNative` tripwire: LSPlant (LSPosed's hooking engine) and Frida's Java bridge clear the `kAccNative` bit when they hook a native method, and this branch flags any audited method whose reflected modifiers no longer report native. -- Line 185 opens `/proc/self/maps` from Java to scan for foreign DEX/APK mappings (e.g. an LSPosed module APK or a Frida agent `.so`) injected into the process address space. -- Lines 232 and 261 declare the framework/needle string literals (`de.robv.android.xposed`, `org.lsposed.`, `lsphooker_`, `lsplant`, `edxposed`, `re.frida`, `gum-js`, `gmain`, `pool-frida`, `linjector`, …) used to inspect stack traces via `Throwable.getStackTrace` / `Thread.getAllStackTraces`; line 263 enumerates `/proc/self/task` to cross-check `comm` values against those native worker thread needles. +- Line 114 opens `/proc/self/maps` to scan for foreign DEX/APK mappings injected into the process address space. +- Line 153 declares the framework needle string literals (`de.robv.android.xposed`, `org.lsposed.lspd`, `lsphooker_`, `lsplant`, `edxposed`, `re.frida`) used to inspect stack traces via `Throwable.getStackTrace` and `Thread.getAllStackTraces` for Xposed/LSPosed bridge frames. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt index 8c3f61f1637..13da15e9889 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt @@ -1,14 +1,13 @@ package org.owasp.mastestapp + import android.app.Activity import android.app.AlertDialog import android.content.Context -import android.content.pm.PackageManager import android.os.Handler import android.os.Looper import java.io.BufferedReader import java.io.File import java.io.FileReader -import java.lang.reflect.Modifier class MastgTest(private val context: Context) { @@ -16,42 +15,13 @@ class MastgTest(private val context: Context) { val r = DemoResults("0x4A") var anyFail = false - try { - val managers = checkKnownXposedManagerPackages() - if (managers.isNotEmpty()) { - // FAIL: [MASTG-TEST-0x49] A known Xposed Manager package is installed. - r.add(Status.FAIL, "Xposed Manager package(s) installed: ${managers.joinToString(", ")}") - anyFail = true - } else { - // PASS: [MASTG-TEST-0x49] No known Xposed Manager package is installed. - r.add(Status.PASS, "No known Xposed Manager package found via PackageManager.") - } - } catch (e: Exception) { - r.add(Status.ERROR, "Manager lookup failed: $e") - } - - try { - val tampered = checkHookedMethodSignatures() - if (tampered.isNotEmpty()) { - // FAIL: [MASTG-TEST-0x49] A guaranteed-native method has had its NATIVE bit cleared. - r.add(Status.FAIL, "Methods appear to be hooked: ${tampered.joinToString(", ")}") - anyFail = true - } else { - // PASS: [MASTG-TEST-0x49] Sensitive methods retain their original modifiers. - r.add(Status.PASS, "Audited native methods retain their NATIVE modifier.") - } - } catch (e: Exception) { - r.add(Status.ERROR, "Method-descriptor check failed: $e") - } - + try { val foreignDexes = checkForeignDexesInMaps() if (foreignDexes.isNotEmpty()) { - // FAIL: [MASTG-TEST-0x49] A foreign APK/DEX is mapped into our process. r.add(Status.FAIL, "Foreign DEX/APK mapped into process: ${foreignDexes.joinToString(", ")}") anyFail = true } else { - // PASS: [MASTG-TEST-0x49] No foreign DEX/APK is mapped. r.add(Status.PASS, "No foreign DEX/APK mapped into process.") } } catch (e: Exception) { @@ -61,11 +31,9 @@ class MastgTest(private val context: Context) { try { val frames = checkInstrumentationFramesInStacks() if (frames.isNotEmpty()) { - // FAIL: [MASTG-TEST-0x49] An instrumentation framework frame was found on a stack. r.add(Status.FAIL, "Instrumentation frames on stack: ${frames.joinToString(", ")}") anyFail = true } else { - // PASS: [MASTG-TEST-0x49] No instrumentation frames observed in any stack. r.add(Status.PASS, "No Xposed/LSPosed/Frida frames found in any thread's stack.") } } catch (e: Exception) { @@ -73,7 +41,7 @@ class MastgTest(private val context: Context) { } if (anyFail) promptUserForLiability( - "Reverse-engineering or instrumentation tooling (Xposed/LSPosed/Frida) was " + + "Reverse-engineering or instrumentation tooling (Xposed/LSPosed) was " + "detected on this device. Continued use may compromise app security and data " + "integrity. Tap \"Accept Liability\" to acknowledge the risk and continue, or " + "\"Exit\" to close the app." @@ -95,40 +63,6 @@ class MastgTest(private val context: Context) { .show() } } - private fun checkKnownXposedManagerPackages(): List { - val knownManagers = listOf( - "de.robv.android.xposed.installer", // classic Xposed - "org.meowcat.edxposed.manager", // EdXposed - "io.va.exposed", // VirtualXposed - "com.solohsu.android.edxp.manager", // EdXposed (alt) - "org.lsposed.manager" // modern LSPosed - ) - val pm = context.packageManager - return knownManagers.filter { pkg -> - try { pm.getPackageInfo(pkg, 0); true } catch (_: PackageManager.NameNotFoundException) { false } - } - } - - private fun checkHookedMethodSignatures(): List { - val tampered = mutableListOf() - val targets = mapOf( - "java.lang.System" to listOf("currentTimeMillis", "nanoTime"), - "java.lang.Object" to listOf("notify", "notifyAll"), - "java.lang.Thread" to listOf("currentThread") - ) - for ((cls, methods) in targets) { - try { - val c = Class.forName(cls) - for (mName in methods) { - val m = c.getDeclaredMethod(mName) - if (!Modifier.isNative(m.modifiers)) { - tampered.add("$cls.$mName (native bit cleared, modifiers=0x${Integer.toHexString(m.modifiers)})") - } - } - } catch (_: Exception) { /* ignore */ } - } - return tampered - } private fun checkForeignDexesInMaps(): List { val ownPkg = context.packageName @@ -147,7 +81,6 @@ class MastgTest(private val context: Context) { return hits.toList() } - private fun checkInstrumentationFramesInStacks(): List { val needles = listOf( "de.robv.android.xposed", @@ -156,8 +89,7 @@ class MastgTest(private val context: Context) { "lsphooker_", "lsplant", "edxposed", - "re.frida", - "gum-js" + "re.frida" ) val hits = LinkedHashSet() @@ -173,18 +105,21 @@ class MastgTest(private val context: Context) { } } + try { context.packageManager.getPackageInfo("___xposed_probe_${System.nanoTime()}", 0) } catch (e: Throwable) { scan("getPackageInfo", e.stackTrace) } + try { Runtime.getRuntime().exec(arrayOf("/__xposed_probe_${System.nanoTime()}")) } catch (e: Throwable) { scan("Runtime.exec", e.stackTrace) } + try { File("/__xposed_probe_${System.nanoTime()}").exists() val t = Throwable("post-File.exists probe") @@ -193,6 +128,7 @@ class MastgTest(private val context: Context) { scan("File.exists", e.stackTrace) } + try { val all = Thread.getAllStackTraces() for ((thread, frames) in all) { @@ -200,23 +136,7 @@ class MastgTest(private val context: Context) { } } catch (_: Throwable) { /* SecurityManager could block — ignore */ } - val nativeThreadNeedles = listOf("gum-js", "gmain", "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker") - try { - val tasks = File("/proc/self/task").listFiles() ?: emptyArray() - for (task in tasks) { - val commFile = File(task, "comm") - if (!commFile.canRead()) continue - val name = try { commFile.readText().trim() } catch (_: Throwable) { continue } - val low = name.lowercase() - for (n in nativeThreadNeedles) { - if (low.contains(n)) { - hits.add("native-thread: $name") - break - } - } - } - } catch (_: Throwable) {} - return hits.toList() } + } diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java index 623a210eaae..cc9cdb6a9e0 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java @@ -4,29 +4,21 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.content.pm.PackageManager; import android.os.Handler; import android.os.Looper; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import kotlin.Metadata; -import kotlin.TuplesKt; import kotlin.Unit; import kotlin.collections.CollectionsKt; -import kotlin.collections.MapsKt; import kotlin.io.CloseableKt; -import kotlin.io.FilesKt; import kotlin.io.TextStreamsKt; import kotlin.jvm.functions.Function1; import kotlin.jvm.internal.ArrayIteratorKt; @@ -34,7 +26,7 @@ import kotlin.text.StringsKt; /* compiled from: MastgTest.kt */ -@Metadata(d1 = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0010 \n\u0002\b\u0004\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u0007H\u0002J\u000e\u0010\u000b\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002J\u000e\u0010\r\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002J\u000e\u0010\u000e\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002J\u000e\u0010\u000f\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u0010"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "mastgTest", "", "promptUserForLiability", "", "message", "checkKnownXposedManagerPackages", "", "checkHookedMethodSignatures", "checkForeignDexesInMaps", "checkInstrumentationFramesInStacks", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +@Metadata(d1 = {"\u0000(\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0010 \n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u0007H\u0002J\u000e\u0010\u000b\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002J\u000e\u0010\r\u001a\b\u0012\u0004\u0012\u00020\u00070\fH\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u000e"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "mastgTest", "", "promptUserForLiability", "", "message", "checkForeignDexesInMaps", "", "checkInstrumentationFramesInStacks", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) /* loaded from: classes3.dex */ public final class MastgTest { public static final int $stable = 8; @@ -48,28 +40,6 @@ public MastgTest(Context context) { public final String mastgTest() { DemoResults r = new DemoResults("0x4A"); boolean anyFail = false; - try { - List managers = checkKnownXposedManagerPackages(); - if (!managers.isEmpty()) { - r.add(Status.FAIL, "Xposed Manager package(s) installed: " + CollectionsKt.joinToString$default(managers, ", ", null, null, 0, null, null, 62, null)); - anyFail = true; - } else { - r.add(Status.PASS, "No known Xposed Manager package found via PackageManager."); - } - } catch (Exception e) { - r.add(Status.ERROR, "Manager lookup failed: " + e); - } - try { - List tampered = checkHookedMethodSignatures(); - if (!tampered.isEmpty()) { - r.add(Status.FAIL, "Methods appear to be hooked: " + CollectionsKt.joinToString$default(tampered, ", ", null, null, 0, null, null, 62, null)); - anyFail = true; - } else { - r.add(Status.PASS, "Audited native methods retain their NATIVE modifier."); - } - } catch (Exception e2) { - r.add(Status.ERROR, "Method-descriptor check failed: " + e2); - } try { List foreignDexes = checkForeignDexesInMaps(); if (!foreignDexes.isEmpty()) { @@ -78,8 +48,8 @@ public final String mastgTest() { } else { r.add(Status.PASS, "No foreign DEX/APK mapped into process."); } - } catch (Exception e3) { - r.add(Status.ERROR, "/proc/self/maps inspection failed: " + e3); + } catch (Exception e) { + r.add(Status.ERROR, "/proc/self/maps inspection failed: " + e); } try { List frames = checkInstrumentationFramesInStacks(); @@ -89,11 +59,11 @@ public final String mastgTest() { } else { r.add(Status.PASS, "No Xposed/LSPosed/Frida frames found in any thread's stack."); } - } catch (Exception e4) { - r.add(Status.ERROR, "Stack-trace inspection failed: " + e4); + } catch (Exception e2) { + r.add(Status.ERROR, "Stack-trace inspection failed: " + e2); } if (anyFail) { - promptUserForLiability("Reverse-engineering or instrumentation tooling (Xposed/LSPosed/Frida) was detected on this device. Continued use may compromise app security and data integrity. Tap \"Accept Liability\" to acknowledge the risk and continue, or \"Exit\" to close the app."); + promptUserForLiability("Reverse-engineering or instrumentation tooling (Xposed/LSPosed) was detected on this device. Continued use may compromise app security and data integrity. Tap \"Accept Liability\" to acknowledge the risk and continue, or \"Exit\" to close the app."); } return r.toJson(); } @@ -138,47 +108,6 @@ public final void onClick(DialogInterface dialogInterface, int i) { activity.finishAffinity(); } - private final List checkKnownXposedManagerPackages() throws PackageManager.NameNotFoundException { - boolean z; - Iterable knownManagers = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); - PackageManager pm = this.context.getPackageManager(); - Iterable $this$filter$iv = knownManagers; - Collection destination$iv$iv = new ArrayList(); - for (Object element$iv$iv : $this$filter$iv) { - String pkg = (String) element$iv$iv; - try { - pm.getPackageInfo(pkg, 0); - z = true; - } catch (PackageManager.NameNotFoundException e) { - z = false; - } - if (z) { - destination$iv$iv.add(element$iv$iv); - } - } - return (List) destination$iv$iv; - } - - private final List checkHookedMethodSignatures() throws NoSuchMethodException, ClassNotFoundException, SecurityException { - List tampered = new ArrayList(); - Map targets = MapsKt.mapOf(TuplesKt.to("java.lang.System", CollectionsKt.listOf((Object[]) new String[]{"currentTimeMillis", "nanoTime"})), TuplesKt.to("java.lang.Object", CollectionsKt.listOf((Object[]) new String[]{"notify", "notifyAll"})), TuplesKt.to("java.lang.Thread", CollectionsKt.listOf("currentThread"))); - for (Map.Entry entry : targets.entrySet()) { - String cls = (String) entry.getKey(); - List methods = (List) entry.getValue(); - try { - Class c = Class.forName(cls); - for (String mName : methods) { - Method m = c.getDeclaredMethod(mName, new Class[0]); - if (!Modifier.isNative(m.getModifiers())) { - tampered.add(cls + "." + mName + " (native bit cleared, modifiers=0x" + Integer.toHexString(m.getModifiers()) + ")"); - } - } - } catch (Exception e) { - } - } - return tampered; - } - private final List checkForeignDexesInMaps() throws IOException { final String ownPkg = this.context.getPackageName(); final LinkedHashSet hits = new LinkedHashSet(); @@ -188,7 +117,7 @@ private final List checkForeignDexesInMaps() throws IOException { TextStreamsKt.forEachLine(br, new Function1() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda2 @Override // kotlin.jvm.functions.Function1 public final Object invoke(Object obj) { - return MastgTest.checkForeignDexesInMaps$lambda$5$lambda$4(ownPkg, hits, (String) obj); + return MastgTest.checkForeignDexesInMaps$lambda$4$lambda$3(ownPkg, hits, (String) obj); } }); Unit unit = Unit.INSTANCE; @@ -199,7 +128,7 @@ public final Object invoke(Object obj) { } /* JADX INFO: Access modifiers changed from: private */ - public static final Unit checkForeignDexesInMaps$lambda$5$lambda$4(String $ownPkg, LinkedHashSet hits, String line) { + public static final Unit checkForeignDexesInMaps$lambda$4$lambda$3(String $ownPkg, LinkedHashSet hits, String line) { Intrinsics.checkNotNullParameter(hits, "$hits"); Intrinsics.checkNotNullParameter(line, "line"); int idx = StringsKt.indexOf$default((CharSequence) line, "/data/app/", 0, false, 6, (Object) null); @@ -220,16 +149,8 @@ public final Object invoke(Object obj) { return Unit.INSTANCE; } - /* JADX WARN: Removed duplicated region for block: B:34:0x015a A[Catch: all -> 0x01d5, TRY_LEAVE, TryCatch #3 {all -> 0x01d5, blocks: (B:25:0x0140, B:32:0x0156, B:34:0x015a, B:38:0x017a, B:39:0x0189, B:41:0x018f), top: B:66:0x0140 }] */ - /* - Code decompiled incorrectly, please refer to instructions dump. - */ private final List checkInstrumentationFramesInStacks() { - int length; - int i; - List nativeThreadNeedles; - int i2 = 1; - List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum-js"}); + List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida"}); LinkedHashSet hits = new LinkedHashSet(); try { this.context.getPackageManager().getPackageInfo("___xposed_probe_" + System.nanoTime(), 0); @@ -258,61 +179,6 @@ private final List checkInstrumentationFramesInStacks() { } } catch (Throwable th) { } - List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); - try { - File[] fileArrListFiles = new File("/proc/self/task").listFiles(); - if (fileArrListFiles == null) { - try { - fileArrListFiles = new File[0]; - File[] tasks = fileArrListFiles; - length = tasks.length; - i = 0; - while (i < length) { - File task = tasks[i]; - File commFile = new File(task, "comm"); - if (commFile.canRead()) { - try { - String name = StringsKt.trim((CharSequence) FilesKt.readText$default(commFile, null, i2, null)).toString(); - String low = name.toLowerCase(Locale.ROOT); - Intrinsics.checkNotNullExpressionValue(low, "toLowerCase(...)"); - Iterator it = nativeThreadNeedles2.iterator(); - while (true) { - if (!it.hasNext()) { - nativeThreadNeedles = nativeThreadNeedles2; - break; - } - String n = (String) it.next(); - nativeThreadNeedles = nativeThreadNeedles2; - try { - if (StringsKt.contains$default((CharSequence) low, (CharSequence) n, false, 2, (Object) null)) { - hits.add("native-thread: " + name); - break; - } - nativeThreadNeedles2 = nativeThreadNeedles; - } catch (Throwable th2) { - } - } - } catch (Throwable th3) { - nativeThreadNeedles = nativeThreadNeedles2; - } - } else { - nativeThreadNeedles = nativeThreadNeedles2; - } - i++; - nativeThreadNeedles2 = nativeThreadNeedles; - i2 = 1; - } - } catch (Throwable th4) { - } - } else { - File[] tasks2 = fileArrListFiles; - length = tasks2.length; - i = 0; - while (i < length) { - } - } - } catch (Throwable th5) { - } return CollectionsKt.toList(hits); } diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt index 7e4adb38b83..cc3fb44ec55 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt @@ -1,104 +1,30 @@ - - -┌──────────────────┐ -│ 20 Code Findings │ -└──────────────────┘ + + +┌─────────────────┐ +│ 6 Code Findings │ +└─────────────────┘ MastgTest_reversed.java - ❱ rules.mastg-android-xposed-detection-manager-package-literal - [MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/EdXposed Manager - package ids. Typically used with PackageManager.getPackageInfo() as a presence probe. - - 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new - String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", - "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); - ⋮┆---------------------------------------- - 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new - String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", - "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); - ⋮┆---------------------------------------- - 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new - String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", - "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); - ⋮┆---------------------------------------- - 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new - String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", - "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); - ⋮┆---------------------------------------- - 143┆ Iterable knownManagers = CollectionsKt.listOf((Object[]) new - String[]{"de.robv.android.xposed.installer", "org.meowcat.edxposed.manager", - "io.va.exposed", "com.solohsu.android.edxp.manager", "org.lsposed.manager"}); + ❱ rules.mastg-android-xposed-detection-foreign-dex-in-maps + [MASVS-RESILIENCE-4] The app references /proc/self/maps and /data/app/ — the canonical primitive + pair for detecting a foreign module APK (LSPosed module) mmapped into the process. + + 114┆ BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); + ⋮┆---------------------------------------- + 134┆ int idx = StringsKt.indexOf$default((CharSequence) line, "/data/app/", 0, false, 6, + (Object) null); + ⋮┆---------------------------------------- + 147┆ String pkg = StringsKt.substringBefore$default(StringsKt.substringAfter$default(StringsKt.s + ubstringAfter$default(path, "/data/app/", (String) null, 2, (Object) null), '/', (String) + null, 2, (Object) null), '-', (String) null, 2, (Object) null); - ❱ rules.mastg-android-xposed-detection-modifier-isnative-tripwire - [MASVS-RESILIENCE-4] The app branches on a negated Modifier.isNative() check against a reflected - method's modifiers. LSPlant (LSPosed's hooking engine) and Frida's Java bridge clear kAccNative when - they hook a native method, so this is the canonical anti-hooking tripwire. A bare - Modifier.isNative() call is intentionally NOT matched, because reflection/serialization libraries - use it benignly. + ❱ rules.mastg-android-xposed-detection-stack-trace-probe + [MASVS-RESILIENCE-4] The app inspects stack traces (Thread.getAllStackTraces / + Throwable.getStackTrace / StackTraceElement.getClassName) for instrumentation-framework class names. + Common anti-hooking primitive. - 172┆ if (!Modifier.isNative(m.getModifiers())) { - 173┆ tampered.add(cls + "." + mName + " (native bit cleared, modifiers=0x" + - Integer.toHexString(m.getModifiers()) + ")"); - 174┆ } - - ❱ rules.mastg-android-xposed-detection-foreign-dex-in-maps - [MASVS-RESILIENCE-4] The app opens /proc/self/maps from Java code. This is the canonical primitive - for detecting an injected module (an LSPosed module APK or a Frida agent .so) mmapped into the - process. Legitimate Java code rarely reads /proc/self/maps directly. - - 185┆ BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); - - ❱ rules.mastg-android-xposed-detection-instrumentation-needles - [MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/Frida framework - class prefixes (de.robv.android.xposed, org.lsposed., lsphooker_, lsplant, edxposed, re.frida) or - native worker thread names (gum-js, gum-js-loop, pool-frida, linjector), or references - /proc/self/task. These literals are the canonical needles for stack-trace inspection - (Thread.getAllStackTraces / Throwable.getStackTrace) and /proc/self/task//comm enumeration used - to detect active instrumentation. - - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 232┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"de.robv.android.xposed", - "org.lsposed.lspd", "org.lsposed.", "lsphooker_", "lsplant", "edxposed", "re.frida", "gum- - js"}); - ⋮┆---------------------------------------- - 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", - "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); - ⋮┆---------------------------------------- - 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", - "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); - ⋮┆---------------------------------------- - 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", - "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); + 173┆ Map all = Thread.getAllStackTraces(); ⋮┆---------------------------------------- - 261┆ List nativeThreadNeedles2 = CollectionsKt.listOf((Object[]) new String[]{"gum-js", "gmain", - "pool-frida", "frida", "linjector", "lspd", "xposed", "lsphooker"}); + 192┆ String className = f.getClassName(); ⋮┆---------------------------------------- - 263┆ File[] fileArrListFiles = new File("/proc/self/task").listFiles(); - + 200┆ linkedHashSet.add(label + ": " + f.getClassName() + "." + f.getMethodName()); diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh index c04cc62431e..da2e344f215 100755 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -NO_COLOR=true semgrep -c ../../../../rules/mastg-android-xposed-detection.yaml ./MastgTest_reversed.java -text -o output.txt \ No newline at end of file +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-xposed-detection.yaml ./MastgTest_reversed.java --text -o output.txt \ No newline at end of file diff --git a/rules/mastg-android-xposed-detection.yaml b/rules/mastg-android-xposed-detection.yaml index 136538b3f39..ac95e9e3590 100644 --- a/rules/mastg-android-xposed-detection.yaml +++ b/rules/mastg-android-xposed-detection.yaml @@ -1,52 +1,34 @@ rules: - - id: mastg-android-xposed-detection-manager-package-literal - severity: INFO + - id: mastg-android-xposed-detection-foreign-dex-in-maps + severity: WARNING languages: [java] metadata: - summary: String literals matching known Xposed/LSPosed/EdXposed Manager package ids - message: "[MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/EdXposed Manager package ids. Typically used with PackageManager.getPackageInfo() as a presence probe." + summary: /proc/self/maps inspection combined with /data/app/ path checks + message: "[MASVS-RESILIENCE-4] The app references /proc/self/maps and /data/app/ — the canonical primitive pair for detecting a foreign module APK (LSPosed module) mmapped into the process." patterns: - pattern: $STR - metavariable-regex: metavariable: $STR - regex: '^"(de\.robv\.android\.xposed\.installer|org\.lsposed\.manager|org\.meowcat\.edxposed\.manager|com\.solohsu\.android\.edxp\.manager|io\.va\.exposed)"$' - - - id: mastg-android-xposed-detection-modifier-isnative-tripwire - severity: INFO - languages: [java] - metadata: - summary: Anti-hooking tripwire — negated Modifier.isNative() on a reflected method - message: "[MASVS-RESILIENCE-4] The app branches on a negated Modifier.isNative() check against a reflected method's modifiers." - pattern-either: - - pattern: if (!Modifier.isNative(...)) { ... } - - pattern: if (!java.lang.reflect.Modifier.isNative(...)) { ... } - - pattern: while (!Modifier.isNative(...)) { ... } - - pattern: boolean $X = !Modifier.isNative(...); - - pattern: return !Modifier.isNative(...); + regex: '^"(/proc/self/maps|/data/app/)"$' - - id: mastg-android-xposed-detection-foreign-dex-in-maps - severity: INFO + - id: mastg-android-xposed-detection-stack-trace-probe + severity: WARNING languages: [java] metadata: - summary: Java-side open of /proc/self/maps for reading - message: "[MASVS-RESILIENCE-4] The app opens /proc/self/maps from Java code. This is the canonical primitive for detecting an injected module (an LSPosed module APK or a Frida agent .so) mmapped into the process." + summary: Stack-trace inspection for instrumentation-framework class names + message: "[MASVS-RESILIENCE-4] The app inspects stack traces (Thread.getAllStackTraces / Throwable.getStackTrace / StackTraceElement.getClassName) for instrumentation-framework class names. Common anti-hooking primitive." pattern-either: - - pattern: new FileReader("/proc/self/maps") - - pattern: new FileInputStream("/proc/self/maps") - - pattern: new RandomAccessFile("/proc/self/maps", ...) - - pattern: Files.readAllLines(Paths.get("/proc/self/maps")) - - pattern: Files.lines(Paths.get("/proc/self/maps")) - - pattern: Files.newBufferedReader(Paths.get("/proc/self/maps"), ...) - - pattern: Files.newInputStream(Paths.get("/proc/self/maps"), ...) + - pattern: $T.getAllStackTraces() + - pattern: $F.getClassName() - - id: mastg-android-xposed-detection-instrumentation-needles - severity: INFO + - id: mastg-android-xposed-detection-proc-task-comm-enumeration + severity: WARNING languages: [java] metadata: - summary: String literals matching instrumentation framework class prefixes, worker thread names, or /proc/self/task - message: "[MASVS-RESILIENCE-4] The app contains string literals matching known Xposed/LSPosed/Frida framework class prefixes (de.robv.android.xposed, org.lsposed., lsphooker_, lsplant, edxposed, re.frida) or native worker thread names (gum-js, gum-js-loop, pool-frida, linjector), or references /proc/self/task." + summary: Enumeration of /proc/self/task//comm for instrumentation worker thread names + message: "[MASVS-RESILIENCE-4] The app references /proc/self/task or known native instrumentation worker thread names (gum-js-loop, gmain, pool-frida) used to detect Frida/LSPosed workers that have no Java Thread representation." patterns: - pattern: $STR - metavariable-regex: metavariable: $STR - regex: '^"(de\.robv\.android\.xposed|org\.lsposed\.lspd|org\.lsposed\.|lsphooker_?|lsplant|edxposed|re\.frida|gum-js(-loop)?|pool-frida|linjector|/proc/self/task)"$' + regex: '^"(/proc/self/task|gum-js-loop|gum-js|gmain|pool-frida)"$' From 265faacca7746052c3a6709c81f90c5a3eb6f1a9 Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Thu, 28 May 2026 10:22:48 +0530 Subject: [PATCH 12/18] added changes --- .../MASTG-DEMO-0x48/MASTG-DEMO-0x48.md | 10 +- .../MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh | 2 +- .../MASTG-DEMO-0x49/MASTG-DEMO-0x49.md | 26 +++-- .../MASTG-DEMO-0x4A/AndroidManifest.xml | 38 ------- .../MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 10 +- .../MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md | 28 +++-- .../MASTG-DEMO-0x4B/output.txt | 16 +-- .../MASTG-DEMO-0x4B/script.js | 105 +----------------- ...yaml => mastg-android-frida-detection.yml} | 0 rules/mastg-android-xposed-detection.yml | 34 ++++++ .../MASVS-RESILIENCE/MASTG-TEST-0x48.md | 10 +- .../MASVS-RESILIENCE/MASTG-TEST-0x49.md | 16 ++- 12 files changed, 97 insertions(+), 198 deletions(-) delete mode 100644 demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml rename rules/{mastg-android-frida-detection.yaml => mastg-android-frida-detection.yml} (100%) create mode 100644 rules/mastg-android-xposed-detection.yml diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md index f4b6edf550f..c9eabe7e633 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md @@ -1,6 +1,6 @@ --- platform: android -title: Uses of Frida Detection Techniques (Default Port, Process Enumeration and /proc/maps) +title: Uses of Xposed/LSPosed Detection Techniques with Semgrep id: MASTG-DEMO-0x48 code: [kotlin] test: MASTG-TEST-0x48 @@ -16,21 +16,21 @@ The snippet below shows sample code that performs three common Frida detection t ## Steps -Let's run our semgrep rule against the sample code. +Let's run our @MASTG-TOOL-0110 rule against the sample code. -{{ ../../../../rules/mastg-android-frida-detection.yaml }} +{{ ../../../../rules/mastg-android-frida-detection.yml }} {{ run.sh }} ## Observation -The output shows the semgrep rule matching the detection checks. +The output contains the locations of all Frida detection checks in the code. {{ output.txt }} ## Evaluation -The test case passes because the app statically implements three independent Frida detection mechanisms, all flagged by semgrep. Review each of the reported instances in `MastgTest_reversed.java`: +The test case passes because the app statically implements three independent Frida detection mechanisms. Review each of the reported instances: - Line 130 opens a TCP socket to `127.0.0.1:27042` — the default `frida-server` port-scan probe. - Line 153 declares the process-name needle list (`frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, `gmain`) consumed by the `/proc` enumeration. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh index d8adbd1bfc5..39fa8d8738d 100755 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -NO_COLOR=true semgrep -c ../../../../rules/mastg-android-frida-detection.yaml ./MastgTest_reversed.java --text -o output.txt \ No newline at end of file +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-frida-detection.yml ./MastgTest_reversed.java --text -o output.txt \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md index b1b5aabe3b7..3e794100776 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md @@ -1,6 +1,6 @@ --- platform: android -title: Bypassing Frida Detection via API Hooking +title: Bypassing Frida Detection via API Hooking and Frida-Server Reconfiguration id: MASTG-DEMO-0x49 code: [kotlin] test: MASTG-TEST-0x48 @@ -10,20 +10,28 @@ kind: fail ## Sample -This demo defeats three Frida detection checks with a Frida script that hooks the Java APIs each routine depends on (`Socket.connect`, `File.listFiles`, `BufferedReader.readLine`) and reconfigures `frida-server` on different port. +This sample uses the same code as @MASTG-DEMO-0x48, which implements three independent Frida detection routines: a TCP probe of the default `frida-server` port (`127.0.0.1:27042`), a `/proc//cmdline` walk for Frida process-name needles, and a `/proc/self/maps` scan for injected Frida artifacts. This demo demonstrates bypassing all three checks with a Frida script that hooks the Java APIs each routine depends on (`Socket.connect`, `File.listFiles`, `BufferedReader.readLine`) and reconfigures `frida-server` to listen on a non-default port under a renamed binary, so the port probe fails, the `/proc` enumeration sees no Frida-named pids, and `/proc/self/maps` reports no Frida mappings. + +!!! note + This is a series of correlated tests. + - @MASTG-DEMO-0x48 is a successful test (successful defense/failed attack) against a Frida instrumentation attack. + - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x48 by using a more "complex" attack. {{ ../MASTG-DEMO-0x48/MastgTest.kt # script.js }} ## Steps -1. Install the app, then push and rename `frida-server` on the device and start it on a non-default port. -2. Spawn the app with the Frida bypass attached. +1. Use @MASTG-TECH-0005 to install the app. +2. Make sure you have @MASTG-TOOL-0031 installed on your machine; push and rename the `frida-server` binary on the device (e.g., to `notfrida`) and start it on a non-default port. +3. Run `run.sh` to spawn the app with the bypass script. +4. Click the **Start** button. +5. Stop the script by pressing `Ctrl+C` and/or `q` to quit the Frida CLI. -{{ run.sh }} +{{ script.js # run.sh }} ## Observation -The Frida console shows every hook firing while the app reports **PASS** for all three Frida-detection checks. +The output contains the trace lines emitted by each hook as it intercepts a detection probe {{ output.txt }} @@ -31,6 +39,6 @@ The Frida console shows every hook firing while the app reports **PASS** for all The test case fails because every detection routine has been bypassed at runtime: -- The `Socket.connect` hook raises `ConnectException` for `127.0.0.1:27042`, and `frida-server` was moved to a non-default port. -- The `File.listFiles` hook filters `/proc` to drop Frida-named pids, and the `frida-server` binary was renamed to `notfrida`. -- The `BufferedReader.readLine` hook drops `/proc/self/maps` lines mentioning Frida artifacts (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`). +- The `Socket.connect` hook raises `ConnectException` for `127.0.0.1:27042`, and `frida-server` was moved to a non-default port so no fallback scan succeeds. +- The `File.listFiles` hook filters `/proc` to drop Frida-named pids, and the `frida-server` binary was renamed (e.g., to `notfrida`) so a name-based scan would also miss it. +- The `BufferedReader.readLine` hook drops `/proc/self/maps` lines mentioning Frida artifacts (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`), including the four `/memfd:frida-agent-64.so` entries shown in the output. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml deleted file mode 100644 index cc43d3d129d..00000000000 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/AndroidManifest.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md index 92edd903894..79a4b1920a0 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -1,6 +1,6 @@ --- platform: android -title: Uses of Xposed/LSPosed Detection Techniques +title: Static Detection of Xposed/LSPosed Hooks using semgrep id: MASTG-DEMO-0x4A code: [kotlin] test: MASTG-TEST-0x49 @@ -12,19 +12,19 @@ kind: pass The snippet below shows sample code that performs two Xposed/LSPosed detection techniques used by Android apps as anti-instrumentation checks. The checks combine a `/proc/self/maps` scan for foreign DEX/APK mappings injected into the process and a stack-trace probe that surfaces framework frames left by hooked methods and parked framework workers. -{{ MastgTest.kt # MastgTest_reversed.java # AndroidManifest.xml }} +{{ MastgTest.kt # MastgTest_reversed.java }} ## Steps -Let's run our semgrep rule against the sample code. +Let's run our @MASTG-TOOL-0110 rule against the sample code. -{{ ../../../../rules/mastg-android-xposed-detection.yaml }} +{{ ../../../../rules/mastg-android-xposed-detection.yml }} {{ run.sh }} ## Observation -The rule has identified some instances in the code file where detection checks are placed. +The output contains the locations of all Xposed/LSPosed detection checks in the code. {{ output.txt }} diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md index 2a4386b0f8d..24d3896a2c3 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md @@ -1,6 +1,6 @@ --- platform: android -title: Bypass of Xposed/LSPosed Detection via PackageManager, Method-Modifier, Maps and Stack-Trace Hooks +title: Bypassing Xposed/LSPosed Detection via API Hooking id: MASTG-DEMO-0x4B code: [kotlin] test: MASTG-TEST-0x49 @@ -10,20 +10,28 @@ kind: fail ## Sample -This demo defeats four Xposed/LSPosed detection checks with a Frida script that hooks the Java APIs each routine depends on (`getPackageInfo`, `Method.getModifiers`, `BufferedReader.readLine`, `Throwable.getStackTrace`, `File.listFiles`). +This demo defeats both Xposed/LSPosed detection checks from @MASTG-DEMO-0x4A with a Frida script that hooks the Java APIs each routine depends on (`BufferedReader.readLine`, `Throwable.getStackTrace`). -{{ ../MASTG-DEMO-0x4A/MastgTest.kt # script.js }} +!!! note + This is a series of correlated tests. + - @MASTG-DEMO-0x4A is a successful test (successful defense/failed attack) against an Xposed/LSPosed instrumentation attack. + - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x4A by using a more "complex" attack. + +{{ ../MASTG-DEMO-0x4A/MastgTest.kt }} ## Steps -1. Install the app on a device where the Xposed/LSPosed framework is active and at least one module is scoped to `org.owasp.mastestapp`. -2. Spawn the app with the Frida bypass attached. +1. Install the app on a device (@MASTG-TECH-0005) where the Xposed/LSPosed framework is active and at least one module is scoped to `org.owasp.mastestapp`. +2. Make sure you have @MASTG-TOOL-0031 installed on your machine and the frida-server running on the device. +3. Run `run.sh` to spawn the app with the bypass script. +4. Click the **Start** button. +5. Stop the script by pressing `Ctrl+C` and/or `q` to quit the Frida CLI. -{{ run.sh }} +{{ script.js # run.sh }} ## Observation -The Frida console shows every hook firing while the app reports **PASS** for all four detection checks. +The output contains the trace lines emitted by each hook as it intercepts a detection probe, while the app reports **PASS** for detection checks. {{ output.txt }} @@ -31,7 +39,5 @@ The Frida console shows every hook firing while the app reports **PASS** for all The test case fails because every detection routine has been bypassed at runtime: -- The `getPackageInfo` hook throws `NameNotFoundException` for every known Xposed/LSPosed Manager id. -- The `Method.getModifiers` hook re-sets `Modifier.NATIVE` on audited methods. -- The `BufferedReader.readLine` hook drops lines mentioning Xposed-related package ids. -- The `Throwable.getStackTrace` hook strips framework frames, and the `File.listFiles` hook hides instrumentation tids from `/proc/self/task`. +- The `BufferedReader.readLine` hook drops `/proc/self/maps` lines whose mapped path contains an Xposed-related package id, so the foreign-DEX scan returns empty. +- The `Throwable.getStackTrace` hook strips frames whose class name matches a framework needle (`de.robv.android.xposed`, `org.lsposed.lspd`, `lsphooker_`, …), so the stack-trace probe finds no Xposed/LSPosed frames. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt index f27b9661dd5..5e6893630fd 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt @@ -1,14 +1,6 @@ -[bypass] hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes). -[bypass] faking NameNotFoundException for de.robv.android.xposed.installer -[bypass] faking NameNotFoundException for org.meowcat.edxposed.manager -[bypass] faking NameNotFoundException for io.va.exposed -[bypass] faking NameNotFoundException for com.solohsu.android.edxp.manager -[bypass] faking NameNotFoundException for org.lsposed.manager -[bypass] Method.getModifiers intercepted (Modifier.NATIVE tripwire bypass active) -[bypass] dropping /proc/self/maps line: 7c4c894000-7c4c896000 r-xp 001ee000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk -[bypass] dropping /proc/self/maps line: 7c4c896000-7c4c897000 r--p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk -[bypass] dropping /proc/self/maps line: 7c4c897000-7c4c898000 rw-p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] Demo 4 hooks installed (/proc/self/maps + stack-trace probes). +[bypass] dropping /proc/self/maps line: 780346a000-780346c000 r-xp 001ee000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 780346c000-780346d000 r--p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 780346d000-780346e000 rw-p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk [bypass] stripped 2 framework frames [bypass] stripped 4 framework frames -[bypass] hiding /proc/self/task tid 14110 (comm=gmain) -[bypass] hiding /proc/self/task tid 14111 (comm=pool-frida) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js index 300c05efb3c..1006322bef7 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js @@ -18,70 +18,9 @@ Java.perform(function () { "lsphooker_", "lsplant", "edxposed", - "re.frida", - "gum-js" + "re.frida" ]; - var THREAD_NAME_NEEDLES = [ - "gum-js", "gmain", "pool-frida", "frida", "linjector", - "lspd", "xposed", "lsphooker" - ]; - - var KNOWN_MANAGER_PACKAGES = [ - "de.robv.android.xposed.installer", - "org.meowcat.edxposed.manager", - "io.va.exposed", - "com.solohsu.android.edxp.manager", - "org.lsposed.manager" - ]; - - - var SUSPICIOUS_NATIVE_METHODS = { - "java.lang.System.currentTimeMillis": true, - "java.lang.System.nanoTime": true, - "java.lang.Object.notify": true, - "java.lang.Object.notifyAll": true, - "java.lang.Thread.currentThread": true - }; - - - var APM = Java.use("android.app.ApplicationPackageManager"); - var NameNotFoundException = Java.use("android.content.pm.PackageManager$NameNotFoundException"); - - APM.getPackageInfo.overload("java.lang.String", "int").implementation = function (name, flags) { - for (var i = 0; i < KNOWN_MANAGER_PACKAGES.length; i++) { - if (name === KNOWN_MANAGER_PACKAGES[i]) { - console.log("[bypass] faking NameNotFoundException for " + name); - throw NameNotFoundException.$new(name); - } - } - return this.getPackageInfo(name, flags); - }; - - - var Modifier = Java.use("java.lang.reflect.Modifier"); - var NATIVE_BIT = Modifier.NATIVE.value; - var Method = Java.use("java.lang.reflect.Method"); - - var getModifiersLogged = false; - Method.getModifiers.implementation = function () { - var orig = this.getModifiers(); - try { - var key = this.getDeclaringClass().getName() + "." + this.getName(); - if (SUSPICIOUS_NATIVE_METHODS[key]) { - if (!getModifiersLogged) { - getModifiersLogged = true; - console.log("[bypass] Method.getModifiers intercepted (Modifier.NATIVE tripwire bypass active)"); - } - if ((orig & NATIVE_BIT) === 0) { - console.log("[bypass] restoring Modifier.NATIVE on " + key); - return orig | NATIVE_BIT; - } - } - } catch (e) { /* fall through */ } - return orig; - }; - var BufferedReader = Java.use("java.io.BufferedReader"); BufferedReader.readLine.overload().implementation = function () { @@ -124,7 +63,7 @@ Java.perform(function () { }; ThreadCls.getAllStackTraces.implementation = function () { - + var raw = this.getAllStackTraces(); var HashMap = Java.use("java.util.HashMap"); var clean = HashMap.$new(); @@ -138,48 +77,10 @@ Java.perform(function () { return clean; }; - var FileCls = Java.use("java.io.File"); - var FIS = Java.use("java.io.FileInputStream"); - - function readComm(tidDir) { - try { - var fis = FIS.$new(tidDir.getAbsolutePath() + "/comm"); - var buf = Java.array("byte", new Array(64).fill(0)); - var n = fis.read(buf); - fis.close(); - if (n <= 0) return ""; - var s = ""; - for (var i = 0; i < n; i++) s += String.fromCharCode(buf[i] & 0xff); - return s.trim(); - } catch (e) { return ""; } - } - - FileCls.listFiles.overload().implementation = function () { - var files = this.listFiles(); - if (files === null) return files; - var path = this.getAbsolutePath(); - if (path !== "/proc/self/task") return files; - var keep = []; - for (var i = 0; i < files.length; i++) { - var comm = readComm(files[i]).toLowerCase(); - var dirty = false; - for (var k = 0; k < THREAD_NAME_NEEDLES.length; k++) { - if (comm.indexOf(THREAD_NAME_NEEDLES[k]) !== -1) { dirty = true; break; } - } - if (dirty) { - console.log("[bypass] hiding /proc/self/task tid " + files[i].getName() + " (comm=" + comm + ")"); - continue; - } - keep.push(files[i]); - } - return Java.array("java.io.File", keep); - }; - - Process.setExceptionHandler(function (details) { console.log("[bypass] swallowed native exception: " + JSON.stringify(details)); return true; }); - console.log("[bypass] hooks installed (Manager-pkg, NATIVE modifier, /proc/self/maps, stack & thread probes)."); + console.log("[bypass] Demo 4 hooks installed (/proc/self/maps + stack-trace probes)."); }); diff --git a/rules/mastg-android-frida-detection.yaml b/rules/mastg-android-frida-detection.yml similarity index 100% rename from rules/mastg-android-frida-detection.yaml rename to rules/mastg-android-frida-detection.yml diff --git a/rules/mastg-android-xposed-detection.yml b/rules/mastg-android-xposed-detection.yml new file mode 100644 index 00000000000..ac95e9e3590 --- /dev/null +++ b/rules/mastg-android-xposed-detection.yml @@ -0,0 +1,34 @@ +rules: + - id: mastg-android-xposed-detection-foreign-dex-in-maps + severity: WARNING + languages: [java] + metadata: + summary: /proc/self/maps inspection combined with /data/app/ path checks + message: "[MASVS-RESILIENCE-4] The app references /proc/self/maps and /data/app/ — the canonical primitive pair for detecting a foreign module APK (LSPosed module) mmapped into the process." + patterns: + - pattern: $STR + - metavariable-regex: + metavariable: $STR + regex: '^"(/proc/self/maps|/data/app/)"$' + + - id: mastg-android-xposed-detection-stack-trace-probe + severity: WARNING + languages: [java] + metadata: + summary: Stack-trace inspection for instrumentation-framework class names + message: "[MASVS-RESILIENCE-4] The app inspects stack traces (Thread.getAllStackTraces / Throwable.getStackTrace / StackTraceElement.getClassName) for instrumentation-framework class names. Common anti-hooking primitive." + pattern-either: + - pattern: $T.getAllStackTraces() + - pattern: $F.getClassName() + + - id: mastg-android-xposed-detection-proc-task-comm-enumeration + severity: WARNING + languages: [java] + metadata: + summary: Enumeration of /proc/self/task//comm for instrumentation worker thread names + message: "[MASVS-RESILIENCE-4] The app references /proc/self/task or known native instrumentation worker thread names (gum-js-loop, gmain, pool-frida) used to detect Frida/LSPosed workers that have no Java Thread representation." + patterns: + - pattern: $STR + - metavariable-regex: + metavariable: $STR + regex: '^"(/proc/self/task|gum-js-loop|gum-js|gmain|pool-frida)"$' diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md index c6d675da898..3ad72666375 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md @@ -1,8 +1,8 @@ --- platform: android -title: Testing Runtime Use of Frida +title: Runtime Use of Frida Detection Techniques id: MASTG-TEST-0x48 -type: [dynamic] +type: [dynamic, hooks] weakness: MASWE-0098 best-practices: [MASTG-BEST-0x48] profiles: [R] @@ -11,9 +11,7 @@ knowledge: [MASTG-KNOW-0030] ## Overview -If an application relies exclusively on standard, local environment signatures to detect instrumentation frameworks, an attacker with full control over the host device can easily bypass these checks. - -Commonly implemented detection patterns—such as attempting connections to the default Frida-server TCP port (`127.0.0.1:27042`), searching `/proc//cmdline` for strings like `frida-server` or `frida-helper`, and scanning `/proc/self/maps` for artifacts like `frida-agent.so`, `libfrida`, or `gum-js-loop`—provide insufficient resilience. Because these checks reside entirely within the user space controlled by the analyst, they represent superficial barriers. This test verifies whether the application's runtime protection can be trivially neutralized by hooking the underlying Java or system APIs to spoof expected clean-device responses. +The test verifies whether the app's Frida detection logic can be trivially neutralized by hooking the Java or system APIs it relies on. If the app implements only standard local-environment signature checks — such as connecting to the default `frida-server` TCP port (`127.0.0.1:27042`), walking `/proc/self/task//comm` for Frida worker thread names like `gum-js-loop`, `gmain`, or `pool-frida`, and scanning `/proc/self/maps` for artifacts like `frida-agent.so` or `libfrida`, an attacker with full control over the host device can spoof clean responses from each underlying API and disable the protection at runtime. This can lead to instrumentation going undetected, allowing the attacker to inspect or modify sensitive runtime behavior despite the apparent presence of an anti-Frida defense. ## Steps @@ -27,4 +25,4 @@ The output should contain a list of detection routines that the app executed, th ## Evaluation -The test case fails if the app fails to detect Frida when `frida-server` is attached to the process. +The test case fails if the app continues to operate normally after each detection routine receives spoofed clean values from the hooked APIs, indicating that the Frida detection logic can be neutralized at the API level. diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md index 091be63b185..db9247cb5ce 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md @@ -1,9 +1,9 @@ --- platform: android -title: Runtime Use of Xposed/LSPosed Detection Mechanisms +title: Runtime Use of Xposed/LSPosed Detection Techniques id: MASTG-TEST-0x49 apis: [PackageManager, Method, BufferedReader, Throwable, Thread, File] -type: [dynamic] +type: [dynamic, hooks] weakness: MASWE-0098 best-practices: [MASTG-BEST-0x49] profiles: [R] @@ -12,15 +12,13 @@ knowledge: [MASTG-KNOW-0030] ## Overview -Well-known Xposed/LSPosed detection patterns a `PackageManager.getPackageInfo` lookup for known Manager package ids, a reflection-based `Modifier.isNative` tripwire on guaranteed-native methods, a `/proc/self/maps` scan for foreign DEX/APK mappings, and a stack-trace / `/proc/self/task` probe for framework frames and native worker threads provide little resilience against an attacker that controls the device. Each of these signals can be neutralized with a few lines of Frida JavaScript that hooks the underlying Java APIs and returns the values the detection logic expects on a clean device. - -This test checks whether an app's Xposed/LSPosed detection routines actually resist a runtime API-hooking bypass. +This test verifies whether the app detects Xposed/LSPosed at runtime using common local-environment signatures, such as a `/proc/self/maps` scan for foreign APK/DEX mappings injected into the process (an LSPosed module's `base.apk`) and a stack-trace probe that forces exceptions through likely-hooked methods (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scans the captured `Throwable.stackTrace` plus every live thread's `Thread.getAllStackTraces` for framework class names (`de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_`). Because these checks rely entirely on user-space APIs controlled by the attacker, they can be silently disabled by hooking the underlying Java calls to return the values expected on a clean device, leaving the framework and its modules undetected. ## Steps -1. Install the app on a rooted device with LSPosed active and at least one module scoped to the app. -2. Use @MASTG-TOOL-0001 to attach to the app and test the function that perform Xposed/LSPosed detection. -3. Run @MASTG-TECH-0033 to attempt to hook the APIs used by each detection routine to return clean values. +1. Use @MASTG-TECH-0005 to install the app on a rooted device with LSPosed active and at least one module scoped to the app. +2. Use @MASTG-TECH-0043 to hook the relevant API calls. +3. Exercise the app extensively to trigger as many flows as possible and enter sensitive data wherever you can. ## Observation @@ -28,4 +26,4 @@ The output should contain a list of detection routines that the app executed, th ## Evaluation -The test case fails if the app fails to detect Xposed/LSPosed when at least one LSPosed module is loaded into the process. +The test case fails if the app continues to operate normally after each detection routine receives spoofed clean values from the hooked APIs, indicating that the Xposed/LSPosed detection logic can be neutralized at the API level. From 62c35c6b83171b08d2baa76834cfd75b0c8f12b0 Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Thu, 28 May 2026 10:52:00 +0530 Subject: [PATCH 13/18] added best practices --- best-practices/MASTG-BEST-0x48.md | 46 +++++++++++++++-------------- best-practices/MASTG-BEST-0x49.md | 48 +++++++++++++++++-------------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/best-practices/MASTG-BEST-0x48.md b/best-practices/MASTG-BEST-0x48.md index e27ba1d0444..2b616b3f141 100644 --- a/best-practices/MASTG-BEST-0x48.md +++ b/best-practices/MASTG-BEST-0x48.md @@ -6,34 +6,36 @@ platform: android knowledge: [MASTG-KNOW-0030] --- -Frida injects an agent (`frida-agent.so`) into the target process, starts native worker threads (`gum-js-loop`, `gmain`, `pool-frida`), and rewrites Java methods through ART trampolines. Every Java-side check is itself a Java method, so Frida can hook the check and silence it (see @MASTG-DEMO-0x49). Use multiple layers, and let the layer the attacker cannot reach decide who gets into the sensitive flow. +Implement multiple independent Frida detection mechanisms to increase the effort required for an attacker to successfully instrument the application. Frida detection should be treated as a layer of defense-in-depth rather than a foolproof solution, as sophisticated attackers can bypass most user-space checks. -## Play Integrity +## Frida Detection Techniques -User-space code cannot reliably detect kernel-mode hooks. The only signal an attacker on a compromised device cannot forge is one signed inside the TEE / StrongBox and checked off-device. +### TCP Port Scan +Frida's default `frida-server` listens on TCP port `27042`. Attempting to connect to this port on `127.0.0.1` can detect a running server. However, attackers often change the default port. -- Require a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel cannot forge the device-key-signed attestation — the signing happens inside the TEE / StrongBox. -- Store secrets in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key cannot be pulled off a rooted device even if user-space is fully compromised. -- Have the server combine the attestation verdict with the score from the client-side checks below. Use client checks as inputs to the server's decision, not as a gate on their own. +### Procfs Enumeration +Scanning `/proc` for artifacts related to Frida is a common technique: +- **Process names**: Enumerate running processes by walking `/proc//cmdline` and look for strings like `frida-server`, `frida-helper`, or `frida-agent`. +- **Thread names**: Look for Frida worker threads in `/proc/self/task//comm` such as `gum-js-loop`, `gmain`, or `pool-frida`. +- **Memory Maps**: Scan `/proc/self/maps` for injected libraries or artifacts like `frida-agent.so`, `libfrida`, `frida-gadget`, or `linjector`. -## Native Probes +### Frida Gadget Detection +On non-rooted devices, Frida is often used by embedding the `frida-gadget` shared library into the APK. This can be detected by: +- **Scanning Memory Maps**: Looking for `libfrida-gadget.so` (or any renamed version of the gadget library) in `/proc/self/maps`. +- **Native Library Enumeration**: Using `System.loadLibrary` hooks or `dladdr` in native code to inspect loaded libraries for Frida-related symbols or names. -Java-side checks run in memory the attacker controls. Move the important detection into JNI, so a bypass has to hook `dlopen` / `dl_iterate_phdr` or patch the linker itself — not just a Java method. +### Memory Scanning for Artifacts +Scan the process memory for known Frida strings, such as "LIBFRIDA", which is present in various versions of Frida's libraries. This can be done by iterating through memory mappings and performing a signature-based search. -- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `frida-gadget` loaded via `LD_PRELOAD` and `frida-agent.so` loaded by `frida-server`, even when those files exist only as `/memfd:` mappings. -- Read `/proc/self/status` and check `TracerPid`. A non-zero value means something is `ptrace`-attached. -- Send the native verdict to your server with the attestation token. Don't let JNI return a plain boolean that Java code reads and acts on — that boolean is itself something Frida can hook. +### Detecting Hooking Trampolines +Frida's `Interceptor` works by inserting trampolines (indirect jump vectors) at the beginning of functions. Detecting these jumps in critical native functions can reveal that they have been hooked. -## Memory and Process Inspection +## Countermeasures and Limitations -These checks are cheap and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. +Since these checks rely on user-space APIs controlled by the attacker, they can be silently disabled by hooking the underlying Java or system calls to return spoofed clean values. -- **Read `/proc/self/maps`** and flag any path that contains `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, or `/gum`. Use a `BufferedReader` filter that loops until a clean line comes back, not a single `readLine()` call — @MASTG-DEMO-0x49 shows that a single-line filter leaks consecutive matches. -- **Enumerate `/proc/self/task//comm`** for instrumentation thread names (`gum-js-loop`, `gmain`, `pool-frida`). This catches Frida even when the agent was loaded from `memfd` and the on-disk `.so` is unmapped. -- **Scan for `frida-server`-like processes**, but don't rely on the default `27042` port or the binary name `frida-server`. Both are trivially changed (`./frida-server -l 0.0.0.0:12345` + binary rename — see @MASTG-DEMO-0x49). A port / name check still belongs as a cheap trip for lazy attackers, but it must never be your only signal. - -## Code Integrity - -Frida's Java bridge clears `kAccNative` when it hooks a native method. Auditing that bit on methods that should always be native catches the in-process hook. - -- Audit a small set of guaranteed-native methods with `java.lang.reflect.Method.getModifiers()` and require `Modifier.NATIVE` to still be set. Frida's Java bridge (and LSPlant) clear that bit when they hook a native method. +To improve resilience: +- **Use Native Implementation**: Perform these checks in C/C++ via the NDK to make them harder to hook than Java/Kotlin APIs. +- **Direct System Calls**: Use `syscall()` to bypass libc wrappers that are easily hooked. +- **Combine with Integrity Checks**: Use code integrity checks to detect if the detection logic itself has been tampered with. +- **Silent Detection**: Instead of crashing immediately upon detection, change the app's behavior subtly or report the detection to a backend server to avoid tipping off the attacker. diff --git a/best-practices/MASTG-BEST-0x49.md b/best-practices/MASTG-BEST-0x49.md index 3efddd3629d..1f5a09f393b 100644 --- a/best-practices/MASTG-BEST-0x49.md +++ b/best-practices/MASTG-BEST-0x49.md @@ -6,33 +6,37 @@ platform: android knowledge: [MASTG-KNOW-0030] --- -LSPosed is built to defeat classic Java-only Xposed detection checks. Its Manager app can hide behind a random package name, `XposedBridge` lives in a classloader your app can't reach, the "Xposed API call protection" toggle blocks `Class.forName` lookups, and its hooking engine (LSPlant) leaves no fields on `java.lang.reflect.Method` to find. Any single Java-side check is easy to silence. Use multiple layers, and let the layer the attacker can't reach be the one that decides access to sensitive flows. +Employ various techniques to detect the presence of the Xposed Framework or its modern derivatives like LSPosed and EdXposed. These frameworks modify the Android Runtime (ART) to allow hooking of Java methods, which can be used to bypass security controls or steal sensitive data. -## Play Integrity +## Xposed Detection Techniques -A rooted device can lie to the app about its own state. The only check the attacker can't fake is one signed by the device's secure hardware (TEE / StrongBox) and verified by your server. +### Stack Trace Analysis +Xposed leaves artifacts in the call stack when a hooked method is executed. Throwing a `Throwable` and inspecting the stack trace can reveal framework-related classes: +- `de.robv.android.xposed.XposedBridge` +- `org.lsposed.lspd` +- `lsphooker_` +- `lsplant` -- Require a **[Play Integrity API](https://developer.android.com/google/play/integrity) verdict** from your server before unlocking sensitive flows. A compromised kernel cannot forge the device-key-signed attestation — the signing happens inside the TEE / StrongBox. -- Store secrets in the [Android Keystore](https://developer.android.com/privacy-and-security/keystore) with `setIsStrongBoxBacked(true)`. The key cannot be pulled off a rooted device even if user-space is fully compromised. -- Treat the client-side checks below as inputs to the server's decision, not as a gate on their own. +### Memory Mapping Scan +Scan `/proc/self/maps` for foreign APK or DEX files mapped into the process's address space. Xposed and LSPosed modules often inject their own code, which can be identified by looking for entries containing: +- Paths to the Xposed/LSPosed manager app. +- Package names of known modules (e.g., checking for `/data/app/` paths of module APKs). -## Native Probes +### Checking for Known Files and Packages +Check for the presence of the Xposed installer app or framework files: +- Package names: `de.robv.android.xposed.installer`, `org.lsposed.manager`. +- System files: `/system/bin/app_process` (if it has been modified to support Xposed). -Java-side checks run in memory the attacker controls. Put the important detection in JNI, so a bypass has to hook the linker itself, not just a Java method. +## Countermeasures and Limitations -- Use [`dl_iterate_phdr`](https://man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html) from a small `.so` to walk every shared object loaded into the process. Flag anything outside `/data/app//`, `/system/`, `/apex/`, or `/vendor/`. This catches `liblspd.so` and any LSPosed module SO loaded via memfd — neither shows up as a `/data/app/.../base.apk` in `/proc/self/maps`. -- Send the native verdict to your server with the attestation token. Don't let JNI return a plain boolean that Java code reads and acts on — that boolean is itself something Frida can hook. +Modern instrumentation frameworks like LSPosed and EdXposed are highly effective at bypassing detection checks by default. Because they operate within the Android Runtime (ART), they can intercept and modify any Java API the application uses for its own defense. -## Memory and Process Inspection +- **Selective Hooking (Scoping)**: Modern frameworks allow users to enable hooks only for specific applications. This prevents "global" artifacts (like modified system files or globally visible processes) from being easily detected by apps not currently being targeted. +- **API Spoofing**: The framework can hook the very APIs used to detect it. For example, it can intercept `PackageManager.getPackageInfo` to hide its own manager app, or `BufferedReader.readLine` to filter out its own entries from `/proc/self/maps`. +- **Stack Trace Cleaning**: Frameworks often automatically strip their own class names (`de.robv.android.xposed.*`) from `Throwable.getStackTrace` and `Thread.getStackTrace` results, making the stack trace appear legitimate even when running within a hooked environment. -These checks are trivial and catch lazy attacker setups. They are not enough on their own — any single one can be defeated — but together they raise the cost of a generic bypass. - -- **Look up known Manager packages** with `PackageManager.getPackageInfo` against a fixed list (`de.robv.android.xposed.installer`, `org.lsposed.manager`, `org.meowcat.edxposed.manager`, `io.va.exposed`, `com.solohsu.android.edxp.manager`) declared in ``. Misses installs with "Hide LSPosed Manager" enabled — the checks below cover that case. -- **Scan `/proc/self/maps`** for any `/data/app//base.apk` that is not yours. LSPosed `mmap`s every active module's APK into the host process, so a foreign `base.apk` means an active module. -- **Probe stack traces and thread names**: force exceptions through methods an attacker is likely to hook (`PackageManager.getPackageInfo`, `Runtime.exec`, `File.exists`) and scan `Throwable.getStackTrace()` for `de.robv.android.xposed.*`, `org.lsposed.lspd.*`, `LSPHooker_`, `re.frida.*` frames. Walk `Thread.getAllStackTraces()` and `/proc/self/task//comm` for native worker threads (`gum-js-loop`, `gmain`, `pool-frida`). - -## Code Integrity - -LSPlant clears `kAccNative` when it hooks a native method. Auditing that bit on methods that should always be native catches the in-process hook. - -- Audit a small set of guaranteed-native methods (`System.currentTimeMillis` / `nanoTime`, `Object.notify` / `notifyAll`, `Thread.currentThread`) with `java.lang.reflect.Method.getModifiers()` and require `Modifier.NATIVE` to still be set. LSPlant (and Frida's Java bridge) clear that bit when they hook a native method. +To enhance detection: +- **Native Probes**: Implement detection logic in native code (C/C++) using the NDK. Native code is harder (though not impossible) to hook than Java methods and can use direct system calls to bypass Java-level spoofing. +- **Method Integrity Checks**: Use the NDK to inspect the `ArtMethod` structure of critical Java methods. Xposed often modifies these structures (e.g., changing the entry point to a native trampoline) to facilitate hooking. +- **Anti-Hooking**: Implement checks to detect if critical methods have been hooked (e.g., by checking for known trampolines in the native implementation of Java methods or verifying the method's access flags). +- **Silent Detection**: Instead of crashing immediately upon detection, change the app's behavior subtly or report the detection to a backend server to avoid tipping off the attacker. From 0e381272c43fb82bb62fb4e116fb781106f86dcb Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Thu, 28 May 2026 11:04:33 +0530 Subject: [PATCH 14/18] linter fix --- best-practices/MASTG-BEST-0x48.md | 8 ++++++++ best-practices/MASTG-BEST-0x49.md | 7 +++++++ .../MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/best-practices/MASTG-BEST-0x48.md b/best-practices/MASTG-BEST-0x48.md index 2b616b3f141..3b867a49c97 100644 --- a/best-practices/MASTG-BEST-0x48.md +++ b/best-practices/MASTG-BEST-0x48.md @@ -11,23 +11,30 @@ Implement multiple independent Frida detection mechanisms to increase the effort ## Frida Detection Techniques ### TCP Port Scan + Frida's default `frida-server` listens on TCP port `27042`. Attempting to connect to this port on `127.0.0.1` can detect a running server. However, attackers often change the default port. ### Procfs Enumeration + Scanning `/proc` for artifacts related to Frida is a common technique: + - **Process names**: Enumerate running processes by walking `/proc//cmdline` and look for strings like `frida-server`, `frida-helper`, or `frida-agent`. - **Thread names**: Look for Frida worker threads in `/proc/self/task//comm` such as `gum-js-loop`, `gmain`, or `pool-frida`. - **Memory Maps**: Scan `/proc/self/maps` for injected libraries or artifacts like `frida-agent.so`, `libfrida`, `frida-gadget`, or `linjector`. ### Frida Gadget Detection + On non-rooted devices, Frida is often used by embedding the `frida-gadget` shared library into the APK. This can be detected by: + - **Scanning Memory Maps**: Looking for `libfrida-gadget.so` (or any renamed version of the gadget library) in `/proc/self/maps`. - **Native Library Enumeration**: Using `System.loadLibrary` hooks or `dladdr` in native code to inspect loaded libraries for Frida-related symbols or names. ### Memory Scanning for Artifacts + Scan the process memory for known Frida strings, such as "LIBFRIDA", which is present in various versions of Frida's libraries. This can be done by iterating through memory mappings and performing a signature-based search. ### Detecting Hooking Trampolines + Frida's `Interceptor` works by inserting trampolines (indirect jump vectors) at the beginning of functions. Detecting these jumps in critical native functions can reveal that they have been hooked. ## Countermeasures and Limitations @@ -35,6 +42,7 @@ Frida's `Interceptor` works by inserting trampolines (indirect jump vectors) at Since these checks rely on user-space APIs controlled by the attacker, they can be silently disabled by hooking the underlying Java or system calls to return spoofed clean values. To improve resilience: + - **Use Native Implementation**: Perform these checks in C/C++ via the NDK to make them harder to hook than Java/Kotlin APIs. - **Direct System Calls**: Use `syscall()` to bypass libc wrappers that are easily hooked. - **Combine with Integrity Checks**: Use code integrity checks to detect if the detection logic itself has been tampered with. diff --git a/best-practices/MASTG-BEST-0x49.md b/best-practices/MASTG-BEST-0x49.md index 1f5a09f393b..3fc84bee12a 100644 --- a/best-practices/MASTG-BEST-0x49.md +++ b/best-practices/MASTG-BEST-0x49.md @@ -11,19 +11,25 @@ Employ various techniques to detect the presence of the Xposed Framework or its ## Xposed Detection Techniques ### Stack Trace Analysis + Xposed leaves artifacts in the call stack when a hooked method is executed. Throwing a `Throwable` and inspecting the stack trace can reveal framework-related classes: + - `de.robv.android.xposed.XposedBridge` - `org.lsposed.lspd` - `lsphooker_` - `lsplant` ### Memory Mapping Scan + Scan `/proc/self/maps` for foreign APK or DEX files mapped into the process's address space. Xposed and LSPosed modules often inject their own code, which can be identified by looking for entries containing: + - Paths to the Xposed/LSPosed manager app. - Package names of known modules (e.g., checking for `/data/app/` paths of module APKs). ### Checking for Known Files and Packages + Check for the presence of the Xposed installer app or framework files: + - Package names: `de.robv.android.xposed.installer`, `org.lsposed.manager`. - System files: `/system/bin/app_process` (if it has been modified to support Xposed). @@ -36,6 +42,7 @@ Modern instrumentation frameworks like LSPosed and EdXposed are highly effective - **Stack Trace Cleaning**: Frameworks often automatically strip their own class names (`de.robv.android.xposed.*`) from `Throwable.getStackTrace` and `Thread.getStackTrace` results, making the stack trace appear legitimate even when running within a hooked environment. To enhance detection: + - **Native Probes**: Implement detection logic in native code (C/C++) using the NDK. Native code is harder (though not impossible) to hook than Java methods and can use direct system calls to bypass Java-level spoofing. - **Method Integrity Checks**: Use the NDK to inspect the `ArtMethod` structure of critical Java methods. Xposed often modifies these structures (e.g., changing the entry point to a native trampoline) to facilitate hooking. - **Anti-Hooking**: Implement checks to detect if critical methods have been hooked (e.g., by checking for known trampolines in the native implementation of Java methods or verifying the method's access flags). diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md index 79a4b1920a0..0d8238f3ecb 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -30,7 +30,7 @@ The output contains the locations of all Xposed/LSPosed detection checks in the ## Evaluation -TThe test case passes because the app statically implements two independent Xposed/LSPosed detection mechanisms: +The test case passes because the app statically implements two independent Xposed/LSPosed detection mechanisms: - Line 114 opens `/proc/self/maps` to scan for foreign DEX/APK mappings injected into the process address space. - Line 153 declares the framework needle string literals (`de.robv.android.xposed`, `org.lsposed.lspd`, `lsphooker_`, `lsplant`, `edxposed`, `re.frida`) used to inspect stack traces via `Throwable.getStackTrace` and `Thread.getAllStackTraces` for Xposed/LSPosed bridge frames. From 0de6f027e2b8a09c431dd0ebd707e2a5beb0e254 Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Thu, 28 May 2026 12:16:23 +0530 Subject: [PATCH 15/18] Update --- .../MASTG-DEMO-0x48/MastgTest.kt | 6 ++++ .../MASTG-DEMO-0x48/output.txt | 13 ++++--- .../MASTG-DEMO-0x4A/MastgTest.kt | 3 ++ .../MASTG-DEMO-0x4A/output.txt | 4 +-- rules/mastg-android-frida-detection.yml | 10 +++--- rules/mastg-android-xposed-detection.yaml | 34 ------------------- rules/mastg-android-xposed-detection.yml | 11 ------ 7 files changed, 22 insertions(+), 59 deletions(-) delete mode 100644 rules/mastg-android-xposed-detection.yaml diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt index 09603ad7135..ace2b96e4a3 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt @@ -1,5 +1,11 @@ package org.owasp.mastestapp +// SUMMARY: This sample demonstrates three common Frida detection techniques used by Android +// apps as anti-instrumentation checks: scanning the default Frida TCP port (27042), +// enumerating running processes for `frida-server`/`frida-helper`, and reading +// `/proc/self/maps` for injected libraries such as `frida-agent.so`, `libfrida` or `gum`. +// All three checks are well-known and trivially bypassable (see MASTG-DEMO-0x49). + import android.app.Activity import android.app.AlertDialog import android.content.Context diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt index cc5b3581559..a9260b09388 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt @@ -5,19 +5,19 @@ └──────────────────┘ MastgTest_reversed.java - ❱ rules.mastg-android-frida-detection-default-port + ❯❱ [rules.mastg-android-frida-detection-default-port[ [MASVS-RESILIENCE-4] The app references the default frida-server TCP port (27042), suggesting a port-scan-based Frida detection. 130┆ socket.connect(new InetSocketAddress("127.0.0.1", 27042), ComposerKt.invocationKey); - ❱ rules.mastg-android-frida-detection-loopback-literal + ❯❱ [1mrules.mastg-android-frida-detection-loopback-literal[ [MASVS-RESILIENCE-4] The app references 127.0.0.1, which is typically combined with a port literal to scan for frida-server. 130┆ socket.connect(new InetSocketAddress("127.0.0.1", 27042), ComposerKt.invocationKey); - ❱ rules.mastg-android-frida-detection-frida-identifier-literals + ❯❱ [rules.mastg-android-frida-detection-frida-identifier-literals[ [MASVS-RESILIENCE-4] The app contains string literals associated with Frida or Gum runtime artifacts (frida-server, frida-helper, frida-agent, frida-gadget, libfrida, gum-js-loop, gmain, linjector, re.frida). Strong indicator of Frida-detection logic. @@ -37,7 +37,7 @@ 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", "frida-agent", "gum-js-loop", "gmain"}); - ❱ rules.mastg-android-frida-detection-proc-enumeration + ❯❱ [1mrules.mastg-android-frida-detection-proc-enumeration[ [MASVS-RESILIENCE-4] The app references /proc and cmdline, a common primitive pair for enumerating running processes (e.g. to look for a frida-server process by name). @@ -45,7 +45,7 @@ ⋮┆---------------------------------------- 169┆ File cmdline = new File(pidDir, "cmdline"); - ❱ rules.mastg-android-frida-detection-frida-identifier-literals + ❯❱ [1mrules.mastg-android-frida-detection-frida-identifier-literals[ [MASVS-RESILIENCE-4] The app contains string literals associated with Frida or Gum runtime artifacts (frida-server, frida-helper, frida-agent, frida-gadget, libfrida, gum-js-loop, gmain, linjector, re.frida). Strong indicator of Frida-detection logic. @@ -65,9 +65,8 @@ 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); - ❱ rules.mastg-android-frida-detection-proc-maps-read + ❯❱ [rules.mastg-android-frida-detection-proc-maps-read[ [MASVS-RESILIENCE-4] The app references /proc/self/maps, the canonical primitive for detecting an injected Frida agent or any other foreign library mapped into the process. 221┆ BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); - diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt index 13da15e9889..d428a84efdd 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt @@ -1,5 +1,8 @@ package org.owasp.mastestapp +// SUMMARY: This sample demonstrates two production-grade Xposed/LSPosed detection +// techniques that actually fire against modern LSPosed. + import android.app.Activity import android.app.AlertDialog import android.content.Context diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt index cc3fb44ec55..a088b427422 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt @@ -5,7 +5,7 @@ └─────────────────┘ MastgTest_reversed.java - ❱ rules.mastg-android-xposed-detection-foreign-dex-in-maps + ❯❱ [rules.mastg-android-xposed-detection-foreign-dex-in-maps[ [MASVS-RESILIENCE-4] The app references /proc/self/maps and /data/app/ — the canonical primitive pair for detecting a foreign module APK (LSPosed module) mmapped into the process. @@ -18,7 +18,7 @@ ubstringAfter$default(path, "/data/app/", (String) null, 2, (Object) null), '/', (String) null, 2, (Object) null), '-', (String) null, 2, (Object) null); - ❱ rules.mastg-android-xposed-detection-stack-trace-probe + ❯❱ [rules.mastg-android-xposed-detection-stack-trace-probe[ [MASVS-RESILIENCE-4] The app inspects stack traces (Thread.getAllStackTraces / Throwable.getStackTrace / StackTraceElement.getClassName) for instrumentation-framework class names. Common anti-hooking primitive. diff --git a/rules/mastg-android-frida-detection.yml b/rules/mastg-android-frida-detection.yml index 252c98faeb7..0af936e298f 100644 --- a/rules/mastg-android-frida-detection.yml +++ b/rules/mastg-android-frida-detection.yml @@ -1,6 +1,6 @@ rules: - id: mastg-android-frida-detection-default-port - severity: INFO + severity: WARNING languages: [java] metadata: summary: Reference to the default frida-server TCP port (27042) @@ -8,7 +8,7 @@ rules: pattern: new InetSocketAddress($HOST, 27042) - id: mastg-android-frida-detection-loopback-literal - severity: INFO + severity: WARNING languages: [java] metadata: summary: Reference to the loopback address typically used by frida-server (127.0.0.1) @@ -16,7 +16,7 @@ rules: pattern: '"127.0.0.1"' - id: mastg-android-frida-detection-frida-identifier-literals - severity: INFO + severity: WARNING languages: [java] metadata: summary: String literals matching Frida / Gum identifiers @@ -28,7 +28,7 @@ rules: regex: '^"(frida-server|frida-helper|frida-agent|frida-gadget|libfrida|gum-js-loop|gum-js|gmain|linjector|re\.frida)"$' - id: mastg-android-frida-detection-proc-maps-read - severity: INFO + severity: WARNING languages: [java] metadata: summary: Read of /proc/self/maps — generic instrumentation-detection primitive @@ -36,7 +36,7 @@ rules: pattern: '"/proc/self/maps"' - id: mastg-android-frida-detection-proc-enumeration - severity: INFO + severity: WARNING languages: [java] metadata: summary: Enumeration of /proc to look at running processes diff --git a/rules/mastg-android-xposed-detection.yaml b/rules/mastg-android-xposed-detection.yaml deleted file mode 100644 index ac95e9e3590..00000000000 --- a/rules/mastg-android-xposed-detection.yaml +++ /dev/null @@ -1,34 +0,0 @@ -rules: - - id: mastg-android-xposed-detection-foreign-dex-in-maps - severity: WARNING - languages: [java] - metadata: - summary: /proc/self/maps inspection combined with /data/app/ path checks - message: "[MASVS-RESILIENCE-4] The app references /proc/self/maps and /data/app/ — the canonical primitive pair for detecting a foreign module APK (LSPosed module) mmapped into the process." - patterns: - - pattern: $STR - - metavariable-regex: - metavariable: $STR - regex: '^"(/proc/self/maps|/data/app/)"$' - - - id: mastg-android-xposed-detection-stack-trace-probe - severity: WARNING - languages: [java] - metadata: - summary: Stack-trace inspection for instrumentation-framework class names - message: "[MASVS-RESILIENCE-4] The app inspects stack traces (Thread.getAllStackTraces / Throwable.getStackTrace / StackTraceElement.getClassName) for instrumentation-framework class names. Common anti-hooking primitive." - pattern-either: - - pattern: $T.getAllStackTraces() - - pattern: $F.getClassName() - - - id: mastg-android-xposed-detection-proc-task-comm-enumeration - severity: WARNING - languages: [java] - metadata: - summary: Enumeration of /proc/self/task//comm for instrumentation worker thread names - message: "[MASVS-RESILIENCE-4] The app references /proc/self/task or known native instrumentation worker thread names (gum-js-loop, gmain, pool-frida) used to detect Frida/LSPosed workers that have no Java Thread representation." - patterns: - - pattern: $STR - - metavariable-regex: - metavariable: $STR - regex: '^"(/proc/self/task|gum-js-loop|gum-js|gmain|pool-frida)"$' diff --git a/rules/mastg-android-xposed-detection.yml b/rules/mastg-android-xposed-detection.yml index ac95e9e3590..a9640d783f6 100644 --- a/rules/mastg-android-xposed-detection.yml +++ b/rules/mastg-android-xposed-detection.yml @@ -21,14 +21,3 @@ rules: - pattern: $T.getAllStackTraces() - pattern: $F.getClassName() - - id: mastg-android-xposed-detection-proc-task-comm-enumeration - severity: WARNING - languages: [java] - metadata: - summary: Enumeration of /proc/self/task//comm for instrumentation worker thread names - message: "[MASVS-RESILIENCE-4] The app references /proc/self/task or known native instrumentation worker thread names (gum-js-loop, gmain, pool-frida) used to detect Frida/LSPosed workers that have no Java Thread representation." - patterns: - - pattern: $STR - - metavariable-regex: - metavariable: $STR - regex: '^"(/proc/self/task|gum-js-loop|gum-js|gmain|pool-frida)"$' From a9229a041264ff01fdeeefe98a0a7b013f99411f Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Thu, 28 May 2026 12:18:46 +0530 Subject: [PATCH 16/18] linter fix --- .../android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md index 24d3896a2c3..747df8ad968 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md @@ -31,7 +31,7 @@ This demo defeats both Xposed/LSPosed detection checks from @MASTG-DEMO-0x4A wit ## Observation -The output contains the trace lines emitted by each hook as it intercepts a detection probe, while the app reports **PASS** for detection checks. +The output contains the trace lines emitted by each hook as it intercepts a detection probe, while the app reports **PASS** for detection checks. {{ output.txt }} From a6cd717c65f6658fca698005fef24b722bbc9725 Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Thu, 28 May 2026 14:36:08 +0530 Subject: [PATCH 17/18] simple script added --- .../MASTG-DEMO-0x49/output.txt | 22 ++- .../MASTG-DEMO-0x49/script.js | 179 +++++++----------- 2 files changed, 79 insertions(+), 122 deletions(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt index 1e9751827ee..6df28e3155c 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt @@ -1,7 +1,15 @@ -[+] Frida detection bypass hooks installed -[+] Blocked Frida port probe -[+] File.listFiles(/proc) intercepted (process-enumeration bypass active) -[+] Removed maps entry: 7c5700a000-7c57951000 r--p 00000000 00:05 287218 /memfd:frida-agent-64.so (deleted) -[+] Removed maps entry: 7c57952000-7c58694000 r-xp 00947000 00:05 287218 /memfd:frida-agent-64.so (deleted) -[+] Removed maps entry: 7c58694000-7c58765000 r--p 01688000 00:05 287218 /memfd:frida-agent-64.so (deleted) -[+] Removed maps entry: 7c58766000-7c58781000 rw-p 01759000 00:05 287218 /memfd:frida-agent-64.so (deleted) +[*] Installing Frida detection bypass hooks + +[*] All Frida detection hooks installed + +[+] checkFridaDefaultPort() intercepted +- The port probe for 127.0.0.1:27042 was bypassed by forcing the method to return false. +- This prevents the application from detecting the default frida-server listener. + +[+] checkFridaThreads() intercepted +- The /proc/self/task enumeration was bypassed by returning an empty thread list. +- Frida-related thread names such as `gum-js-loop`, `gmain`, `gdbus`, and `pool-frida` are hidden from the application. + +[+] checkFridaLibraries() intercepted +- The /proc/self/maps scan was bypassed by returning an empty library match list. +- Frida artifacts such as `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, and `linjector` are hidden from detection. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js index 6d60cad7643..f33a17630cf 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js @@ -1,134 +1,83 @@ +// frida -U -f org.owasp.mastestapp -l bypass.js + Java.perform(function () { - const PROC_KEYWORDS = [ - "frida-server", - "frida-agent", - "gum-js-loop", - "gmain" - ]; - - const MAPS_KEYWORDS = [ - "frida", - "gum-js-loop", - "libfrida" - ]; - - const File = Java.use("java.io.File"); - const FileInputStream = Java.use("java.io.FileInputStream"); - const BufferedReader = Java.use("java.io.BufferedReader"); - const Socket = Java.use("java.net.Socket"); - const InetSocketAddress = Java.use("java.net.InetSocketAddress"); - const ConnectException = Java.use("java.net.ConnectException"); - -const socketConnect = - Socket.connect.overload("java.net.SocketAddress", "int"); - -socketConnect.implementation = function (endpoint, timeout) { - - const addr = Java.cast(endpoint, InetSocketAddress); - const port = addr.getPort(); - - if (port === 27042) { - - console.log("[+] Blocked Frida port probe"); - - throw ConnectException.$new("Connection refused"); - } - - return socketConnect.call(this, endpoint, timeout); -}; - - let procListFilesLogged = false; - - function filterProcEntries(files) { - let filtered = []; - for (let i = 0; i < files.length; i++) { - try { - const cmdline = File.$new(files[i], "cmdline"); - const fis = FileInputStream.$new(cmdline); - const buffer = Java.array("byte", new Array(256).fill(0)); - const size = fis.read(buffer); - fis.close(); - if (size > 0) { - let processName = ""; - for (let j = 0; j < size; j++) { - processName += String.fromCharCode(buffer[j] & 0xff); - } - let hide = false; - for (let k = 0; k < PROC_KEYWORDS.length; k++) { - if (processName.indexOf(PROC_KEYWORDS[k]) !== -1) { - hide = true; - break; - } - } - if (hide) { - console.log("[+] Hiding process: " + processName); - continue; - } - } - } catch (e) {} - filtered.push(files[i]); - } - return Java.array("java.io.File", filtered); - } - - function noteProcIntercepted() { - if (!procListFilesLogged) { - procListFilesLogged = true; - console.log("[+] File.listFiles(/proc) intercepted (process-enumeration bypass active)"); - } - } - - const listFilesNoArg = File.listFiles.overload(); - listFilesNoArg.implementation = function () { - const files = listFilesNoArg.call(this); - if (files === null) return files; - if (this.getAbsolutePath() !== "/proc") return files; - noteProcIntercepted(); - return filterProcEntries(files); - }; + const ArrayList = Java.use("java.util.ArrayList"); + const MastgTest = Java.use("org.owasp.mastestapp.MastgTest"); + + console.log("[*] Installing Frida detection bypass hooks"); + + + MastgTest.checkFridaDefaultPort.implementation = function () { + + console.log("\n[+] checkFridaDefaultPort() intercepted"); + + console.log( + "- The port probe for 127.0.0.1:27042 was bypassed " + + "by forcing the method to return false." + ); + + console.log( + "- This prevents the application from detecting " + + "the default frida-server listener." + ); - const listFilesFilter = File.listFiles.overload("java.io.FileFilter"); - listFilesFilter.implementation = function (filter) { - const files = listFilesFilter.call(this, filter); - if (files === null) return files; - if (this.getAbsolutePath() !== "/proc") return files; - noteProcIntercepted(); - return filterProcEntries(files); + return false; }; - const readLine = - BufferedReader.readLine.overload(); + MastgTest.checkFridaThreads.implementation = function () { - readLine.implementation = function () { + console.log("\n[+] checkFridaThreads() intercepted"); - while (true) { + console.log( + "- The /proc/self/task enumeration was bypassed " + + "by returning an empty thread list." + ); - const line = readLine.call(this); + console.log( + "- Frida-related thread names such as " + + "`gum-js-loop`, `gmain`, `gdbus`, and `pool-frida` " + + "are hidden from the application." + ); + + return ArrayList.$new(); + }; - if (line === null) { - return null; - } - let suspicious = false; - for (let i = 0; i < MAPS_KEYWORDS.length; i++) { + MastgTest.checkFridaLibraries.implementation = function () { - if (line.indexOf(MAPS_KEYWORDS[i]) !== -1) { - suspicious = true; - break; - } - } + console.log("\n[+] checkFridaLibraries() intercepted"); - if (!suspicious) { - return line; - } + console.log( + "- The /proc/self/maps scan was bypassed " + + "by returning an empty library match list." + ); - console.log("[+] Removed maps entry: " + line); - } + console.log( + "- Frida artifacts such as `frida-agent`, " + + "`libfrida`, `frida-gadget`, `gum-js-loop`, " + + "and `linjector` are hidden from detection." + ); + + return ArrayList.$new(); }; + MastgTest.promptUserForLiability.implementation = function (msg) { + + console.log("\n[+] promptUserForLiability() intercepted"); + + console.log( + "- The anti-tampering warning dialog was suppressed " + + "to prevent the user prompt from appearing." + ); + + console.log("- Dialog message:"); + console.log(" " + msg); + + return; + }; - console.log("[+] Frida detection bypass hooks installed"); + console.log("\n[*] All Frida detection hooks installed"); }); \ No newline at end of file From 38e62416bb97c1a8604e4d30ce7bd04f7d28a6db Mon Sep 17 00:00:00 2001 From: ScreaM <70141504+ScreaMy7@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:45:17 +0530 Subject: [PATCH 18/18] Fix TEST-0048 demos: thread-model alignment, ANSI-clean outputs, recursion-safe bypass scripts --- .../MASTG-DEMO-0x48/MASTG-DEMO-0x48.md | 12 +- .../MASTG-DEMO-0x48/MastgTest.kt | 5 +- .../MASTG-DEMO-0x48/MastgTest_reversed.java | 50 +++-- .../MASTG-DEMO-0x48/output.txt | 55 +++--- .../MASTG-DEMO-0x49/MASTG-DEMO-0x49.md | 12 +- .../MASTG-DEMO-0x49/output.txt | 25 +-- .../MASTG-DEMO-0x49/script.js | 176 +++++++++++------- .../MASTG-DEMO-0x4A/output.txt | 5 +- .../MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh | 2 +- .../MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md | 2 +- .../MASTG-DEMO-0x4B/output.txt | 6 +- .../MASTG-DEMO-0x4B/script.js | 30 +-- rules/mastg-android-frida-detection.yml | 6 +- .../MASVS-RESILIENCE/MASTG-TEST-0x48.md | 6 +- .../MASVS-RESILIENCE/MASTG-TEST-0x49.md | 2 +- 15 files changed, 211 insertions(+), 183 deletions(-) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md index c9eabe7e633..9d28bc923e3 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md @@ -1,6 +1,6 @@ --- platform: android -title: Uses of Xposed/LSPosed Detection Techniques with Semgrep +title: Static Detection of Frida using Semgrep id: MASTG-DEMO-0x48 code: [kotlin] test: MASTG-TEST-0x48 @@ -10,7 +10,7 @@ kind: pass ## Sample -The snippet below shows sample code that performs three common Frida detection techniques used by Android apps as anti-instrumentation checks: a TCP scan of the default `frida-server` port (`127.0.0.1:27042`), a `/proc//cmdline` walk for `frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, `gmain`, and a `/proc/self/maps` read for injected Frida artifacts (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`). +The snippet below shows sample code that performs three common Frida detection techniques used by Android apps as anti-instrumentation checks: a TCP scan of the default `frida-server` port (`127.0.0.1:27042`), a `/proc/self/task//comm` walk for Frida worker thread names (`gum-js-loop`, `gmain`, `gdbus`, `pool-frida`, `frida`), and a `/proc/self/maps` read for injected Frida artifacts (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`). {{ MastgTest.kt # MastgTest_reversed.java }} @@ -33,7 +33,7 @@ The output contains the locations of all Frida detection checks in the code. The test case passes because the app statically implements three independent Frida detection mechanisms. Review each of the reported instances: - Line 130 opens a TCP socket to `127.0.0.1:27042` — the default `frida-server` port-scan probe. -- Line 153 declares the process-name needle list (`frida-server`, `frida-helper`, `frida-agent`, `gum-js-loop`, `gmain`) consumed by the `/proc` enumeration. -- Lines 155 and 169 enumerate `/proc` and read each `/proc//cmdline` to match running processes against those needles. -- Line 219 declares the injected-library needle list (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`) used to scan foreign mappings. -- Line 221 opens `/proc/self/maps` from Java to detect a Frida agent or any other foreign library mapped into the process. +- Line 152 declares the thread-name needle list (`gum-js-loop`, `gmain`, `gdbus`, `pool-frida`, `frida`) consumed by the `/proc/self/task` enumeration. +- Lines 154 and 165 enumerate `/proc/self/task` and read each `/proc/self/task//comm` to match the process's own thread names against those needles. +- Line 213 declares the injected-library needle list (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`, `/gum`) used to scan foreign mappings. +- Line 215 opens `/proc/self/maps` from Java to detect a Frida agent or any other foreign library mapped into the process. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt index ace2b96e4a3..a009ac5c424 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt @@ -2,8 +2,9 @@ package org.owasp.mastestapp // SUMMARY: This sample demonstrates three common Frida detection techniques used by Android // apps as anti-instrumentation checks: scanning the default Frida TCP port (27042), -// enumerating running processes for `frida-server`/`frida-helper`, and reading -// `/proc/self/maps` for injected libraries such as `frida-agent.so`, `libfrida` or `gum`. +// enumerating thread names under `/proc/self/task` for Frida worker threads such as +// `gum-js-loop`/`gmain`, and reading `/proc/self/maps` for injected libraries such as +// `frida-agent.so`, `libfrida` or `gum`. // All three checks are well-known and trivially bypassable (see MASTG-DEMO-0x49). import android.app.Activity diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java index 7eb76b7855d..9f1772c0c7c 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java @@ -30,7 +30,7 @@ import kotlin.text.StringsKt; /* compiled from: MastgTest.kt */ -@Metadata(d1 = {"\u0000.\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010 \n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u0007H\u0002J\b\u0010\u000b\u001a\u00020\fH\u0002J\u000e\u0010\r\u001a\b\u0012\u0004\u0012\u00020\u00070\u000eH\u0002J\u000e\u0010\u000f\u001a\b\u0012\u0004\u0012\u00020\u00070\u000eH\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u0010"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "mastgTest", "", "promptUserForLiability", "", "message", "checkFridaDefaultPort", "", "checkFridaProcesses", "", "checkFridaLibraries", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +@Metadata(d1 = {"\u0000.\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010 \n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\t2\u0006\u0010\n\u001a\u00020\u0007H\u0002J\b\u0010\u000b\u001a\u00020\fH\u0002J\u000e\u0010\r\u001a\b\u0012\u0004\u0012\u00020\u00070\u000eH\u0002J\u000e\u0010\u000f\u001a\b\u0012\u0004\u0012\u00020\u00070\u000eH\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u0010"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "mastgTest", "", "promptUserForLiability", "", "message", "checkFridaDefaultPort", "", "checkFridaThreads", "", "checkFridaLibraries", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) /* loaded from: classes3.dex */ public final class MastgTest { public static final int $stable = 8; @@ -57,15 +57,15 @@ public final String mastgTest() { r.add(Status.ERROR, "Port check failed: " + e); } try { - List matches = checkFridaProcesses(); + List matches = checkFridaThreads(); if (!matches.isEmpty()) { - r.add(Status.FAIL, "Suspicious processes found: " + CollectionsKt.joinToString$default(matches, ", ", null, null, 0, null, null, 62, null)); + r.add(Status.FAIL, "Suspicious threads found in this process: " + CollectionsKt.joinToString$default(matches, ", ", null, null, 0, null, null, 62, null)); anyFail = true; } else { - r.add(Status.PASS, "No Frida-related process names found under /proc."); + r.add(Status.PASS, "No Frida-related thread names found under /proc/self/task."); } } catch (Exception e2) { - r.add(Status.ERROR, "Process enumeration failed: " + e2); + r.add(Status.ERROR, "Thread enumeration failed: " + e2); } try { List libsFound = checkFridaLibraries(); @@ -90,7 +90,7 @@ private final void promptUserForLiability(final String message) { if (activity == null) { return; } - new Handler(Looper.getMainLooper()).post(new Runnable() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda3 + new Handler(Looper.getMainLooper()).post(new Runnable() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda2 @Override // java.lang.Runnable public final void run() { MastgTest.promptUserForLiability$lambda$2(activity, message); @@ -105,12 +105,12 @@ public final void run() { if (activity.isFinishing() || activity.isDestroyed()) { return; } - new AlertDialog.Builder(activity).setTitle("Security Warning").setMessage(message).setCancelable(false).setPositiveButton("Accept Liability", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda1 + new AlertDialog.Builder(activity).setTitle("Security Warning").setMessage(message).setCancelable(false).setPositiveButton("Accept Liability", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0 @Override // android.content.DialogInterface.OnClickListener public final void onClick(DialogInterface dialogInterface, int i) { dialogInterface.dismiss(); } - }).setNegativeButton("Exit", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda2 + }).setNegativeButton("Exit", new DialogInterface.OnClickListener() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda1 @Override // android.content.DialogInterface.OnClickListener public final void onClick(DialogInterface dialogInterface, int i) { MastgTest.promptUserForLiability$lambda$2$lambda$1(activity, dialogInterface, i); @@ -148,34 +148,30 @@ private final boolean checkFridaDefaultPort() throws IOException { } } - private final List checkFridaProcesses() { - int i = 1; - List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", "frida-agent", "gum-js-loop", "gmain"}); + private final List checkFridaThreads() { + List needles = CollectionsKt.listOf((Object[]) new String[]{"gum-js-loop", "gmain", "gdbus", "pool-frida", "frida"}); List matches = new ArrayList(); - File proc = new File("/proc"); - File[] pidDirs = proc.listFiles(new FileFilter() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0 + File taskDir = new File("/proc/self/task"); + File[] tids = taskDir.listFiles(new FileFilter() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda3 @Override // java.io.FileFilter public final boolean accept(File file) { - return MastgTest.checkFridaProcesses$lambda$4(file); + return MastgTest.checkFridaThreads$lambda$4(file); } }); - if (pidDirs == null) { + if (tids == null) { return matches; } - int length = pidDirs.length; - int i2 = 0; - while (i2 < length) { - File pidDir = pidDirs[i2]; - File cmdline = new File(pidDir, "cmdline"); - if (cmdline.canRead()) { + for (File tid : tids) { + File commFile = new File(tid, "comm"); + if (commFile.canRead()) { try { - String name = StringsKt.trim((CharSequence) StringsKt.replace$default(FilesKt.readText$default(cmdline, null, i, null), (char) 0, ' ', false, 4, (Object) null)).toString(); + String name = StringsKt.trim((CharSequence) FilesKt.readText$default(commFile, null, 1, null)).toString(); Iterator it = needles.iterator(); while (true) { if (it.hasNext()) { String needle = (String) it.next(); - if (StringsKt.contains$default((CharSequence) name, (CharSequence) needle, false, 2, (Object) null)) { - matches.add(pidDir.getName() + ":" + name); + if (StringsKt.contains((CharSequence) name, (CharSequence) needle, true)) { + matches.add(tid.getName() + ":" + name); break; } } @@ -183,14 +179,12 @@ public final boolean accept(File file) { } catch (Exception e) { } } - i2++; - i = 1; } return matches; } /* JADX INFO: Access modifiers changed from: private */ - public static final boolean checkFridaProcesses$lambda$4(File f) { + public static final boolean checkFridaThreads$lambda$4(File f) { CharSequence $this$all$iv; if (!f.isDirectory()) { return false; @@ -248,4 +242,4 @@ public final Object invoke(Object obj) { } return Unit.INSTANCE; } -} +} \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt index a9260b09388..10c5e73f6ee 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt @@ -1,72 +1,65 @@ ┌──────────────────┐ -│ 15 Code Findings │ +│ 12 Code Findings │ └──────────────────┘ MastgTest_reversed.java - ❯❱ [rules.mastg-android-frida-detection-default-port[ + ❯❱ rules.mastg-android-frida-detection-default-port [MASVS-RESILIENCE-4] The app references the default frida-server TCP port (27042), suggesting a port-scan-based Frida detection. 130┆ socket.connect(new InetSocketAddress("127.0.0.1", 27042), ComposerKt.invocationKey); - ❯❱ [1mrules.mastg-android-frida-detection-loopback-literal[ + ❯❱ rules.mastg-android-frida-detection-loopback-literal [MASVS-RESILIENCE-4] The app references 127.0.0.1, which is typically combined with a port literal to scan for frida-server. 130┆ socket.connect(new InetSocketAddress("127.0.0.1", 27042), ComposerKt.invocationKey); - ❯❱ [rules.mastg-android-frida-detection-frida-identifier-literals[ + ❯❱ rules.mastg-android-frida-detection-frida-identifier-literals [MASVS-RESILIENCE-4] The app contains string literals associated with Frida or Gum runtime artifacts (frida-server, frida-helper, frida-agent, frida-gadget, libfrida, gum-js-loop, gmain, linjector, re.frida). Strong indicator of Frida-detection logic. - 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", - "frida-agent", "gum-js-loop", "gmain"}); + 152┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"gum-js-loop", "gmain", + "gdbus", "pool-frida", "frida"}); ⋮┆---------------------------------------- - 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", - "frida-agent", "gum-js-loop", "gmain"}); - ⋮┆---------------------------------------- - 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", - "frida-agent", "gum-js-loop", "gmain"}); - ⋮┆---------------------------------------- - 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", - "frida-agent", "gum-js-loop", "gmain"}); - ⋮┆---------------------------------------- - 153┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-server", "frida-helper", - "frida-agent", "gum-js-loop", "gmain"}); + 152┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"gum-js-loop", "gmain", + "gdbus", "pool-frida", "frida"}); - ❯❱ [1mrules.mastg-android-frida-detection-proc-enumeration[ - [MASVS-RESILIENCE-4] The app references /proc and cmdline, a common primitive pair for enumerating - running processes (e.g. to look for a frida-server process by name). - - 155┆ File proc = new File("/proc"); + ❯❱ rules.mastg-android-frida-detection-proc-enumeration + [MASVS-RESILIENCE-4] The app references /proc, /proc/self/task, cmdline or comm, common primitives + for enumerating running processes or thread names (e.g. to look for a frida-server process or Frida + worker threads such as gum-js-loop/gmain). + + 154┆ File taskDir = new File("/proc/self/task"); ⋮┆---------------------------------------- - 169┆ File cmdline = new File(pidDir, "cmdline"); + 165┆ File commFile = new File(tid, "comm"); - ❯❱ [1mrules.mastg-android-frida-detection-frida-identifier-literals[ + ❯❱ rules.mastg-android-frida-detection-frida-identifier-literals [MASVS-RESILIENCE-4] The app contains string literals associated with Frida or Gum runtime artifacts (frida-server, frida-helper, frida-agent, frida-gadget, libfrida, gum-js-loop, gmain, linjector, re.frida). Strong indicator of Frida-detection logic. - 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); ⋮┆---------------------------------------- - 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); ⋮┆---------------------------------------- - 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); ⋮┆---------------------------------------- - 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); ⋮┆---------------------------------------- - 219┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); - ❯❱ [rules.mastg-android-frida-detection-proc-maps-read[ + ❯❱ rules.mastg-android-frida-detection-proc-maps-read [MASVS-RESILIENCE-4] The app references /proc/self/maps, the canonical primitive for detecting an injected Frida agent or any other foreign library mapped into the process. - 221┆ BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); + 215┆ BufferedReader bufferedReader = new BufferedReader(new FileReader("/proc/self/maps")); + diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md index 3e794100776..d13fb9191fd 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md @@ -10,12 +10,12 @@ kind: fail ## Sample -This sample uses the same code as @MASTG-DEMO-0x48, which implements three independent Frida detection routines: a TCP probe of the default `frida-server` port (`127.0.0.1:27042`), a `/proc//cmdline` walk for Frida process-name needles, and a `/proc/self/maps` scan for injected Frida artifacts. This demo demonstrates bypassing all three checks with a Frida script that hooks the Java APIs each routine depends on (`Socket.connect`, `File.listFiles`, `BufferedReader.readLine`) and reconfigures `frida-server` to listen on a non-default port under a renamed binary, so the port probe fails, the `/proc` enumeration sees no Frida-named pids, and `/proc/self/maps` reports no Frida mappings. +This sample uses the same code as @MASTG-DEMO-0x48, which implements three independent Frida detection routines: a TCP probe of the default `frida-server` port (`127.0.0.1:27042`), a `/proc/self/task//comm` walk for Frida worker thread names (`gum-js-loop`, `gmain`, `gdbus`, `pool-frida`), and a `/proc/self/maps` scan for injected Frida artifacts. This demo demonstrates bypassing all three checks with a Frida script that hooks the Java APIs each routine depends on (`Socket.connect`, `File.listFiles`, `BufferedReader.readLine`), combined with reconfiguring `frida-server` to listen on a non-default port under a renamed binary so the default-port probe finds no listener even before the `Socket.connect` hook fires. !!! note This is a series of correlated tests. - @MASTG-DEMO-0x48 is a successful test (successful defense/failed attack) against a Frida instrumentation attack. - - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x48 by using a more "complex" attack. + - This test is a failed test (failed defense/successful attack) against the defenses of @MASTG-DEMO-0x48 by using a more "complex" attack. {{ ../MASTG-DEMO-0x48/MastgTest.kt # script.js }} @@ -31,7 +31,7 @@ This sample uses the same code as @MASTG-DEMO-0x48, which implements three indep ## Observation -The output contains the trace lines emitted by each hook as it intercepts a detection probe +The output contains the trace lines emitted by each hook as it intercepts a detection probe. {{ output.txt }} @@ -39,6 +39,6 @@ The output contains the trace lines emitted by each hook as it intercepts a dete The test case fails because every detection routine has been bypassed at runtime: -- The `Socket.connect` hook raises `ConnectException` for `127.0.0.1:27042`, and `frida-server` was moved to a non-default port so no fallback scan succeeds. -- The `File.listFiles` hook filters `/proc` to drop Frida-named pids, and the `frida-server` binary was renamed (e.g., to `notfrida`) so a name-based scan would also miss it. -- The `BufferedReader.readLine` hook drops `/proc/self/maps` lines mentioning Frida artifacts (`frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, `linjector`), including the four `/memfd:frida-agent-64.so` entries shown in the output. +- The `Socket.connect` hook blocks the probe to `127.0.0.1:27042`, so the port check finds no `frida-server` listener. +- The `File.listFiles` hook hides Frida worker threads (`gmain`, `pool-frida`, `gdbus`) from the `/proc/self/task` scan. +- The `BufferedReader.readLine` hook drops the Frida `/proc/self/maps` lines. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt index 6df28e3155c..8c4a1bd8aad 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt @@ -1,15 +1,10 @@ -[*] Installing Frida detection bypass hooks - -[*] All Frida detection hooks installed - -[+] checkFridaDefaultPort() intercepted -- The port probe for 127.0.0.1:27042 was bypassed by forcing the method to return false. -- This prevents the application from detecting the default frida-server listener. - -[+] checkFridaThreads() intercepted -- The /proc/self/task enumeration was bypassed by returning an empty thread list. -- Frida-related thread names such as `gum-js-loop`, `gmain`, `gdbus`, and `pool-frida` are hidden from the application. - -[+] checkFridaLibraries() intercepted -- The /proc/self/maps scan was bypassed by returning an empty library match list. -- Frida artifacts such as `frida-agent`, `libfrida`, `frida-gadget`, `gum-js-loop`, and `linjector` are hidden from detection. +[+] Frida detection bypass hooks installed +[+] Blocked Frida default-port probe (127.0.0.1:27042) +[+] File.listFiles(/proc/self/task) intercepted (thread-enumeration bypass active) +[+] Hiding Frida thread: gmain +[+] Hiding Frida thread: pool-frida +[+] Hiding Frida thread: gdbus +[+] Removed maps entry: 7405c1c000-7406563000 r--p 00000000 00:05 2231284 /memfd:frida-agent-64.so (deleted) +[+] Removed maps entry: 7406564000-74072a6000 r-xp 00947000 00:05 2231284 /memfd:frida-agent-64.so (deleted) +[+] Removed maps entry: 74072a6000-7407377000 r--p 01688000 00:05 2231284 /memfd:frida-agent-64.so (deleted) +[+] Removed maps entry: 7407378000-7407393000 rw-p 01759000 00:05 2231284 /memfd:frida-agent-64.so (deleted) diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js index f33a17630cf..3c7a70da301 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js @@ -1,83 +1,121 @@ -// frida -U -f org.owasp.mastestapp -l bypass.js +// frida -U -f org.owasp.mastestapp -l script.js Java.perform(function () { - const ArrayList = Java.use("java.util.ArrayList"); - const MastgTest = Java.use("org.owasp.mastestapp.MastgTest"); - console.log("[*] Installing Frida detection bypass hooks"); - - - MastgTest.checkFridaDefaultPort.implementation = function () { - - console.log("\n[+] checkFridaDefaultPort() intercepted"); - - console.log( - "- The port probe for 127.0.0.1:27042 was bypassed " + - "by forcing the method to return false." - ); - - console.log( - "- This prevents the application from detecting " + - "the default frida-server listener." - ); - - return false; + const THREAD_KEYWORDS = [ + "gum-js-loop", + "gmain", + "gdbus", + "pool-frida", + "frida" + ]; + + + const MAPS_KEYWORDS = [ + "frida-agent", + "libfrida", + "frida-gadget", + "gum-js-loop", + "linjector", + "/gum" + ]; + + const File = Java.use("java.io.File"); + const FileInputStream = Java.use("java.io.FileInputStream"); + const BufferedReader = Java.use("java.io.BufferedReader"); + const Socket = Java.use("java.net.Socket"); + const InetSocketAddress = Java.use("java.net.InetSocketAddress"); + const ConnectException = Java.use("java.net.ConnectException"); + + + const socketConnect = Socket.connect.overload("java.net.SocketAddress", "int"); + socketConnect.implementation = function (endpoint, timeout) { + const addr = Java.cast(endpoint, InetSocketAddress); + if (addr.getPort() === 27042) { + console.log("[+] Blocked Frida default-port probe (127.0.0.1:27042)"); + throw ConnectException.$new("Connection refused"); + } + return socketConnect.call(this, endpoint, timeout); }; - MastgTest.checkFridaThreads.implementation = function () { - - console.log("\n[+] checkFridaThreads() intercepted"); - - console.log( - "- The /proc/self/task enumeration was bypassed " + - "by returning an empty thread list." - ); - - console.log( - "- Frida-related thread names such as " + - "`gum-js-loop`, `gmain`, `gdbus`, and `pool-frida` " + - "are hidden from the application." - ); - - return ArrayList.$new(); + function commName(tidDir) { + try { + const comm = File.$new(tidDir, "comm"); + if (!comm.canRead()) return ""; + const fis = FileInputStream.$new(comm); + const buffer = Java.array("byte", new Array(64).fill(0)); + const size = fis.read(buffer); + fis.close(); + if (size <= 0) return ""; + let name = ""; + for (let j = 0; j < size; j++) { + name += String.fromCharCode(buffer[j] & 0xff); + } + return name.trim(); + } catch (e) { + return ""; + } + } + + function filterTaskEntries(tids) { + const kept = []; + for (let i = 0; i < tids.length; i++) { + const name = commName(tids[i]); + let hide = false; + for (let k = 0; k < THREAD_KEYWORDS.length; k++) { + if (name.indexOf(THREAD_KEYWORDS[k]) !== -1) { hide = true; break; } + } + if (hide) { + console.log("[+] Hiding Frida thread: " + name); + continue; + } + kept.push(tids[i]); + } + return Java.array("java.io.File", kept); + } + + let taskListLogged = false; + function noteTaskIntercepted() { + if (!taskListLogged) { + taskListLogged = true; + console.log("[+] File.listFiles(/proc/self/task) intercepted (thread-enumeration bypass active)"); + } + } + + const listFilesFilter = File.listFiles.overload("java.io.FileFilter"); + listFilesFilter.implementation = function (filter) { + const files = listFilesFilter.call(this, filter); + if (files === null) return files; + if (this.getAbsolutePath() !== "/proc/self/task") return files; + noteTaskIntercepted(); + return filterTaskEntries(files); }; - - - MastgTest.checkFridaLibraries.implementation = function () { - - console.log("\n[+] checkFridaLibraries() intercepted"); - - console.log( - "- The /proc/self/maps scan was bypassed " + - "by returning an empty library match list." - ); - - console.log( - "- Frida artifacts such as `frida-agent`, " + - "`libfrida`, `frida-gadget`, `gum-js-loop`, " + - "and `linjector` are hidden from detection." - ); - - return ArrayList.$new(); + const listFilesNoArg = File.listFiles.overload(); + listFilesNoArg.implementation = function () { + const files = listFilesNoArg.call(this); + if (files === null) return files; + if (this.getAbsolutePath() !== "/proc/self/task") return files; + noteTaskIntercepted(); + return filterTaskEntries(files); }; - MastgTest.promptUserForLiability.implementation = function (msg) { - - console.log("\n[+] promptUserForLiability() intercepted"); - - console.log( - "- The anti-tampering warning dialog was suppressed " + - "to prevent the user prompt from appearing." - ); - - console.log("- Dialog message:"); - console.log(" " + msg); - return; + const readLine = BufferedReader.readLine.overload(); + readLine.implementation = function () { + while (true) { + const line = readLine.call(this); + if (line === null) return null; + let suspicious = false; + for (let i = 0; i < MAPS_KEYWORDS.length; i++) { + if (line.indexOf(MAPS_KEYWORDS[i]) !== -1) { suspicious = true; break; } + } + if (!suspicious) return line; + console.log("[+] Removed maps entry: " + line); + } }; - console.log("\n[*] All Frida detection hooks installed"); -}); \ No newline at end of file + console.log("[+] Frida detection bypass hooks installed"); +}); diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt index a088b427422..0ca36e9fc85 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt @@ -5,7 +5,7 @@ └─────────────────┘ MastgTest_reversed.java - ❯❱ [rules.mastg-android-xposed-detection-foreign-dex-in-maps[ + ❯❱ rules.mastg-android-xposed-detection-foreign-dex-in-maps [MASVS-RESILIENCE-4] The app references /proc/self/maps and /data/app/ — the canonical primitive pair for detecting a foreign module APK (LSPosed module) mmapped into the process. @@ -18,7 +18,7 @@ ubstringAfter$default(path, "/data/app/", (String) null, 2, (Object) null), '/', (String) null, 2, (Object) null), '-', (String) null, 2, (Object) null); - ❯❱ [rules.mastg-android-xposed-detection-stack-trace-probe[ + ❯❱ rules.mastg-android-xposed-detection-stack-trace-probe [MASVS-RESILIENCE-4] The app inspects stack traces (Thread.getAllStackTraces / Throwable.getStackTrace / StackTraceElement.getClassName) for instrumentation-framework class names. Common anti-hooking primitive. @@ -28,3 +28,4 @@ 192┆ String className = f.getClassName(); ⋮┆---------------------------------------- 200┆ linkedHashSet.add(label + ": " + f.getClassName() + "." + f.getMethodName()); + diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh index da2e344f215..b993f16d19f 100755 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -NO_COLOR=true semgrep -c ../../../../rules/mastg-android-xposed-detection.yaml ./MastgTest_reversed.java --text -o output.txt \ No newline at end of file +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-xposed-detection.yml ./MastgTest_reversed.java --text -o output.txt \ No newline at end of file diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md index 747df8ad968..608683c7c4d 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md @@ -15,7 +15,7 @@ This demo defeats both Xposed/LSPosed detection checks from @MASTG-DEMO-0x4A wit !!! note This is a series of correlated tests. - @MASTG-DEMO-0x4A is a successful test (successful defense/failed attack) against an Xposed/LSPosed instrumentation attack. - - This test is a failed test (failed defence/successful attack) against the defenses of @MASTG-DEMO-0x4A by using a more "complex" attack. + - This test is a failed test (failed defense/successful attack) against the defenses of @MASTG-DEMO-0x4A by using a more "complex" attack. {{ ../MASTG-DEMO-0x4A/MastgTest.kt }} diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt index 5e6893630fd..bb1dbb95938 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt @@ -1,6 +1,6 @@ [bypass] Demo 4 hooks installed (/proc/self/maps + stack-trace probes). -[bypass] dropping /proc/self/maps line: 780346a000-780346c000 r-xp 001ee000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk -[bypass] dropping /proc/self/maps line: 780346c000-780346d000 r--p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk -[bypass] dropping /proc/self/maps line: 780346d000-780346e000 rw-p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 73fcf0b000-73fcf0d000 r-xp 001ee000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 73fcf0d000-73fcf0e000 r--p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk +[bypass] dropping /proc/self/maps line: 73fcf0e000-73fcf0f000 rw-p 001ef000 103:25 289690 /data/app/~~626HEFLllMTG5SvJsEco2A==/com.gauravssnl.bypassrootcheck.pro-Uy4OWQFppVQf6I8Iy1gRQg==/base.apk [bypass] stripped 2 framework frames [bypass] stripped 4 framework frames diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js index 1006322bef7..4e66755574a 100644 --- a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js @@ -23,9 +23,11 @@ Java.perform(function () { var BufferedReader = Java.use("java.io.BufferedReader"); - BufferedReader.readLine.overload().implementation = function () { + + var readLine = BufferedReader.readLine.overload(); + readLine.implementation = function () { while (true) { - var line = this.readLine(); + var line = readLine.call(this); if (line === null) return null; var dirty = false; for (var i = 0; i < XPOSED_PKG_PREFIXES.length; i++) { @@ -55,24 +57,28 @@ Java.perform(function () { return Java.array("java.lang.StackTraceElement", clean); } - Throwable.getStackTrace.implementation = function () { - return filterFrames(this.getStackTrace()); - }; - ThreadCls.getStackTrace.implementation = function () { - return filterFrames(this.getStackTrace()); + + var throwableGetStackTrace = Throwable.getStackTrace.overload(); + throwableGetStackTrace.implementation = function () { + return filterFrames(throwableGetStackTrace.call(this)); }; - ThreadCls.getAllStackTraces.implementation = function () { + var threadGetStackTrace = ThreadCls.getStackTrace.overload(); + threadGetStackTrace.implementation = function () { + return filterFrames(threadGetStackTrace.call(this)); + }; - var raw = this.getAllStackTraces(); + var threadGetAllStackTraces = ThreadCls.getAllStackTraces.overload(); + threadGetAllStackTraces.implementation = function () { + var raw = threadGetAllStackTraces.call(this); var HashMap = Java.use("java.util.HashMap"); var clean = HashMap.$new(); var keys = raw.keySet().toArray(); for (var i = 0; i < keys.length; i++) { - var t = keys[i]; try { - clean.put(t, t.getStackTrace()); - } catch (e) { /* thread died between snapshot and re-walk — skip */ } + var t = Java.cast(keys[i], ThreadCls); + clean.put(t, filterFrames(threadGetStackTrace.call(t))); + } catch (e) { } } return clean; }; diff --git a/rules/mastg-android-frida-detection.yml b/rules/mastg-android-frida-detection.yml index 0af936e298f..83051ee4ae2 100644 --- a/rules/mastg-android-frida-detection.yml +++ b/rules/mastg-android-frida-detection.yml @@ -39,10 +39,10 @@ rules: severity: WARNING languages: [java] metadata: - summary: Enumeration of /proc to look at running processes - message: "[MASVS-RESILIENCE-4] The app references /proc and cmdline, a common primitive pair for enumerating running processes (e.g. to look for a frida-server process by name)." + summary: Enumeration of /proc or /proc/self/task to inspect process/thread names + message: "[MASVS-RESILIENCE-4] The app references /proc, /proc/self/task, cmdline or comm, common primitives for enumerating running processes or thread names (e.g. to look for a frida-server process or Frida worker threads such as gum-js-loop/gmain)." patterns: - pattern: $STR - metavariable-regex: metavariable: $STR - regex: '^"(/proc|cmdline)"$' + regex: '^"(/proc|/proc/self/task|cmdline|comm)"$' diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md index 3ad72666375..11b03c15763 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md @@ -15,9 +15,9 @@ The test verifies whether the app's Frida detection logic can be trivially neutr ## Steps -1. Install the app on a device with `frida-server` running. -2. Use @MASTG-TOOL-0001 to attach to the app and test the function that perform Frida detection. -3. Run @MASTG-TECH-0033 to attempt to hook the APIs used by each detection routine to return clean values. + 1. Use @MASTG-TECH-0005 to install the app on a device with `frida-server` running. + 2. Use @MASTG-TECH-0043 to hook the relevant API calls. + 3. Exercise the app extensively to trigger as many flows as possible and enter sensitive data wherever you can. ## Observation diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md index db9247cb5ce..60683ecdea7 100644 --- a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md @@ -22,7 +22,7 @@ This test verifies whether the app detects Xposed/LSPosed at runtime using commo ## Observation -The output should contain a list of detection routines that the app executed, the APIs they queried, and the value those APIs returned both with the a frida script attached. + The output should contain a list of detection routines that the app executed, the APIs they queried, and the value those APIs returned both without and with a Frida script attached. ## Evaluation