diff --git a/best-practices/MASTG-BEST-0x48.md b/best-practices/MASTG-BEST-0x48.md new file mode 100644 index 00000000000..3b867a49c97 --- /dev/null +++ b/best-practices/MASTG-BEST-0x48.md @@ -0,0 +1,49 @@ +--- +title: Detecting Frida Instrumentation +alias: detecting-frida-instrumentation +id: MASTG-BEST-0x48 +platform: android +knowledge: [MASTG-KNOW-0030] +--- + +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. + +## 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 + +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. +- **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 new file mode 100644 index 00000000000..3fc84bee12a --- /dev/null +++ b/best-practices/MASTG-BEST-0x49.md @@ -0,0 +1,49 @@ +--- +title: Detecting Xposed/LSPosed Instrumentation +alias: detecting-xposed-lsposed-instrumentation +id: MASTG-BEST-0x49 +platform: android +knowledge: [MASTG-KNOW-0030] +--- + +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. + +## 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). + +## Countermeasures and Limitations + +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. + +- **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. + +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. 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..9d28bc923e3 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md @@ -0,0 +1,39 @@ +--- +platform: android +title: Static Detection of Frida using Semgrep +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: 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 }} + +## Steps + +Let's run our @MASTG-TOOL-0110 rule against the sample code. + +{{ ../../../../rules/mastg-android-frida-detection.yml }} + +{{ run.sh }} + +## Observation + +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. 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 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 new file mode 100644 index 00000000000..a009ac5c424 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt @@ -0,0 +1,147 @@ +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 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 +import android.app.AlertDialog +import android.content.Context +import android.os.Handler +import android.os.Looper +import java.io.BufferedReader +import java.io.File +import java.io.FileReader +import java.net.InetSocketAddress +import java.net.Socket + +class MastgTest(private val context: Context) { + + fun mastgTest(): String { + val r = DemoResults("0x48") + var anyFail = false + + + try { + val portFound = checkFridaDefaultPort() + if (portFound) { + // FAIL: [MASTG-TEST-0x48] Frida was detected via the default port 27042. + r.add(Status.FAIL, "Frida default port (27042) is open — instrumentation detected.") + anyFail = true + } else { + // PASS: [MASTG-TEST-0x48] No process is listening on port 27042. + r.add(Status.PASS, "Frida default port (27042) is closed.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "Port check failed: $e") + } + + + try { + val matches = checkFridaThreads() + if (matches.isNotEmpty()) { + // FAIL: [MASTG-TEST-0x48] A suspicious thread name was found in this process. + r.add(Status.FAIL, "Suspicious threads found in this process: ${matches.joinToString(", ")}") + anyFail = true + } else { + // PASS: [MASTG-TEST-0x48] No suspicious thread names were found. + r.add(Status.PASS, "No Frida-related thread names found under /proc/self/task.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "Thread enumeration failed: $e") + } + + + try { + val libsFound = checkFridaLibraries() + if (libsFound.isNotEmpty()) { + // FAIL: [MASTG-TEST-0x48] An injected Frida library was found in /proc/self/maps. + r.add(Status.FAIL, "Injected libraries detected in /proc/self/maps: ${libsFound.joinToString(", ")}") + anyFail = true + } else { + // PASS: [MASTG-TEST-0x48] /proc/self/maps does not contain any Frida artifacts. + r.add(Status.PASS, "No Frida libraries mapped into the process.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "Maps check failed: $e") + } + + 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 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() + } + } + + private fun checkFridaDefaultPort(): Boolean { + val socket = Socket() + return try { + socket.connect(InetSocketAddress("127.0.0.1", 27042), 200) + true + } catch (e: Exception) { + false + } finally { + try { socket.close() } catch (_: Exception) {} + } + } + + + private fun checkFridaThreads(): List { + val needles = listOf("gum-js-loop", "gmain", "gdbus", "pool-frida", "frida") + val matches = mutableListOf() + + val taskDir = File("/proc/self/task") + val tids = taskDir.listFiles { f -> f.isDirectory && f.name.all { it.isDigit() } } ?: return matches + + for (tid in tids) { + val commFile = File(tid, "comm") + if (!commFile.canRead()) continue + val name = try { + commFile.readText().trim() + } catch (_: Exception) { continue } + for (needle in needles) { + if (name.contains(needle, ignoreCase = true)) { + matches.add("${tid.name}:$name") + break + } + } + } + return matches + } + + + private fun checkFridaLibraries(): List { + val needles = listOf("frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum") + val hits = mutableSetOf() + BufferedReader(FileReader("/proc/self/maps")).use { br -> + br.forEachLine { line -> + for (needle in needles) { + if (line.contains(needle, ignoreCase = true)) { + hits.add(needle) + } + } + } + } + return hits.toList() + } +} \ No newline at end of file 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..9f1772c0c7c --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest_reversed.java @@ -0,0 +1,245 @@ +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", "", "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; + 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 = checkFridaThreads(); + if (!matches.isEmpty()) { + 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 thread names found under /proc/self/task."); + } + } catch (Exception e2) { + r.add(Status.ERROR, "Thread 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$$ExternalSyntheticLambda2 + @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 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 checkFridaThreads() { + List needles = CollectionsKt.listOf((Object[]) new String[]{"gum-js-loop", "gmain", "gdbus", "pool-frida", "frida"}); + List matches = new ArrayList(); + 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.checkFridaThreads$lambda$4(file); + } + }); + if (tids == null) { + return matches; + } + for (File tid : tids) { + File commFile = new File(tid, "comm"); + if (commFile.canRead()) { + try { + 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((CharSequence) name, (CharSequence) needle, true)) { + matches.add(tid.getName() + ":" + name); + break; + } + } + } + } catch (Exception e) { + } + } + } + return matches; + } + + /* JADX INFO: Access modifiers changed from: private */ + public static final boolean checkFridaThreads$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; + } +} \ 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 new file mode 100644 index 00000000000..10c5e73f6ee --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/output.txt @@ -0,0 +1,65 @@ + + +┌──────────────────┐ +│ 12 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. + + 152┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"gum-js-loop", "gmain", + "gdbus", "pool-frida", "frida"}); + ⋮┆---------------------------------------- + 152┆ List needles = CollectionsKt.listOf((Object[]) new String[]{"gum-js-loop", "gmain", + "gdbus", "pool-frida", "frida"}); + + ❯❱ 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"); + ⋮┆---------------------------------------- + 165┆ File commFile = new File(tid, "comm"); + + ❯❱ 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. + + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 213┆ final List needles = CollectionsKt.listOf((Object[]) new String[]{"frida-agent", + "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum"}); + ⋮┆---------------------------------------- + 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 + [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. + + 215┆ 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..39fa8d8738d --- /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.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 new file mode 100644 index 00000000000..d13fb9191fd --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/MASTG-DEMO-0x49.md @@ -0,0 +1,44 @@ +--- +platform: android +title: Bypassing Frida Detection via API Hooking and Frida-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 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 defense/successful attack) against the defenses of @MASTG-DEMO-0x48 by using a more "complex" attack. + +{{ ../MASTG-DEMO-0x48/MastgTest.kt # script.js }} + +## Steps + +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. + +{{ script.js # run.sh }} + +## Observation + +The output contains the trace lines emitted by each hook as it intercepts a detection probe. + +{{ output.txt }} + +## Evaluation + +The test case fails because every detection routine has been bypassed at runtime: + +- 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 new file mode 100644 index 00000000000..8c4a1bd8aad --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/output.txt @@ -0,0 +1,10 @@ +[+] 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/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..3c7a70da301 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x49/script.js @@ -0,0 +1,121 @@ +// frida -U -f org.owasp.mastestapp -l script.js + +Java.perform(function () { + + + 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); + }; + + + 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); + }; + + 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); + }; + + + 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("[+] Frida detection bypass hooks installed"); +}); 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..0d8238f3ecb --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MASTG-DEMO-0x4A.md @@ -0,0 +1,36 @@ +--- +platform: android +title: Static Detection of Xposed/LSPosed Hooks using semgrep +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 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 }} + +## Steps + +Let's run our @MASTG-TOOL-0110 rule against the sample code. + +{{ ../../../../rules/mastg-android-xposed-detection.yml }} + +{{ run.sh }} + +## Observation + +The output contains the locations of all Xposed/LSPosed detection checks in the code. + +{{ output.txt }} + +## Evaluation + +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. 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..d428a84efdd --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest.kt @@ -0,0 +1,145 @@ +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 +import android.os.Handler +import android.os.Looper +import java.io.BufferedReader +import java.io.File +import java.io.FileReader + +class MastgTest(private val context: Context) { + + fun mastgTest(): String { + val r = DemoResults("0x4A") + var anyFail = false + + + try { + val foreignDexes = checkForeignDexesInMaps() + if (foreignDexes.isNotEmpty()) { + r.add(Status.FAIL, "Foreign DEX/APK mapped into process: ${foreignDexes.joinToString(", ")}") + anyFail = true + } else { + r.add(Status.PASS, "No foreign DEX/APK mapped into process.") + } + } catch (e: Exception) { + r.add(Status.ERROR, "/proc/self/maps inspection failed: $e") + } + + try { + val frames = checkInstrumentationFramesInStacks() + if (frames.isNotEmpty()) { + r.add(Status.FAIL, "Instrumentation frames on stack: ${frames.joinToString(", ")}") + anyFail = true + } else { + 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) 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 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() + } + } + + 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() + } + + private fun checkInstrumentationFramesInStacks(): List { + val needles = listOf( + "de.robv.android.xposed", + "org.lsposed.lspd", + "org.lsposed.", + "lsphooker_", + "lsplant", + "edxposed", + "re.frida" + ) + 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}") + } + } + } + } + + + 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") + scan("File.exists.probe", t.stackTrace) + } catch (e: Throwable) { + scan("File.exists", e.stackTrace) + } + + + try { + val all = Thread.getAllStackTraces() + for ((thread, frames) in all) { + scan("thread=${thread.name}", frames) + } + } catch (_: Throwable) { /* SecurityManager could block — ignore */ } + + 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..cc9cdb6a9e0 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/MastgTest_reversed.java @@ -0,0 +1,205 @@ +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 java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +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.Unit; +import kotlin.collections.CollectionsKt; +import kotlin.io.CloseableKt; +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\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; + 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 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 e) { + r.add(Status.ERROR, "/proc/self/maps inspection failed: " + e); + } + 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 e2) { + r.add(Status.ERROR, "Stack-trace inspection failed: " + e2); + } + if (anyFail) { + 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(); + } + + 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 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$4$lambda$3(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$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); + 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; + } + + private final List checkInstrumentationFramesInStacks() { + 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); + } 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) { + } + 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..0ca36e9fc85 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4A/output.txt @@ -0,0 +1,31 @@ + + +┌─────────────────┐ +│ 6 Code Findings │ +└─────────────────┘ + + MastgTest_reversed.java + ❯❱ 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-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. + + 173┆ Map all = Thread.getAllStackTraces(); + ⋮┆---------------------------------------- + 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 new file mode 100755 index 00000000000..b993f16d19f --- /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.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 new file mode 100644 index 00000000000..608683c7c4d --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/MASTG-DEMO-0x4B.md @@ -0,0 +1,43 @@ +--- +platform: android +title: Bypassing Xposed/LSPosed Detection via API Hooking +id: MASTG-DEMO-0x4B +code: [kotlin] +test: MASTG-TEST-0x49 +tools: [MASTG-TOOL-0031] +kind: fail +--- + +## Sample + +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`). + +!!! 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 defense/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 (@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. + +{{ script.js # run.sh }} + +## 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. + +{{ output.txt }} + +## Evaluation + +The test case fails because every detection routine has been bypassed at runtime: + +- 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 new file mode 100644 index 00000000000..bb1dbb95938 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/output.txt @@ -0,0 +1,6 @@ +[bypass] Demo 4 hooks installed (/proc/self/maps + stack-trace probes). +[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/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..4e66755574a --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x4B/script.js @@ -0,0 +1,92 @@ +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" + ]; + + + var BufferedReader = Java.use("java.io.BufferedReader"); + + var readLine = BufferedReader.readLine.overload(); + readLine.implementation = function () { + while (true) { + var line = readLine.call(this); + 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); + } + + + var throwableGetStackTrace = Throwable.getStackTrace.overload(); + throwableGetStackTrace.implementation = function () { + return filterFrames(throwableGetStackTrace.call(this)); + }; + + var threadGetStackTrace = ThreadCls.getStackTrace.overload(); + threadGetStackTrace.implementation = function () { + return filterFrames(threadGetStackTrace.call(this)); + }; + + 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++) { + try { + var t = Java.cast(keys[i], ThreadCls); + clean.put(t, filterFrames(threadGetStackTrace.call(t))); + } catch (e) { } + } + return clean; + }; + + Process.setExceptionHandler(function (details) { + console.log("[bypass] swallowed native exception: " + JSON.stringify(details)); + return true; + }); + + console.log("[bypass] Demo 4 hooks installed (/proc/self/maps + stack-trace probes)."); +}); diff --git a/rules/mastg-android-frida-detection.yml b/rules/mastg-android-frida-detection.yml new file mode 100644 index 00000000000..83051ee4ae2 --- /dev/null +++ b/rules/mastg-android-frida-detection.yml @@ -0,0 +1,48 @@ +rules: + - id: mastg-android-frida-detection-default-port + severity: WARNING + 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: WARNING + 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: WARNING + 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: WARNING + 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: WARNING + languages: [java] + metadata: + 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|/proc/self/task|cmdline|comm)"$' diff --git a/rules/mastg-android-xposed-detection.yml b/rules/mastg-android-xposed-detection.yml new file mode 100644 index 00000000000..a9640d783f6 --- /dev/null +++ b/rules/mastg-android-xposed-detection.yml @@ -0,0 +1,23 @@ +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() + 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..11b03c15763 --- /dev/null +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x48.md @@ -0,0 +1,28 @@ +--- +platform: android +title: Runtime Use of Frida Detection Techniques +id: MASTG-TEST-0x48 +type: [dynamic, hooks] +weakness: MASWE-0098 +best-practices: [MASTG-BEST-0x48] +profiles: [R] +knowledge: [MASTG-KNOW-0030] +--- + +## Overview + +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 + + 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 + +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 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 new file mode 100644 index 00000000000..60683ecdea7 --- /dev/null +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0x49.md @@ -0,0 +1,29 @@ +--- +platform: android +title: Runtime Use of Xposed/LSPosed Detection Techniques +id: MASTG-TEST-0x49 +apis: [PackageManager, Method, BufferedReader, Throwable, Thread, File] +type: [dynamic, hooks] +weakness: MASWE-0098 +best-practices: [MASTG-BEST-0x49] +profiles: [R] +knowledge: [MASTG-KNOW-0030] +--- + +## Overview + +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. 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 + + 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 + +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.