Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions best-practices/MASTG-BEST-0x48.md
Original file line number Diff line number Diff line change
@@ -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/<pid>/cmdline` and look for strings like `frida-server`, `frida-helper`, or `frida-agent`.
- **Thread names**: Look for Frida worker threads in `/proc/self/task/<tid>/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.
49 changes: 49 additions & 0 deletions best-practices/MASTG-BEST-0x49.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 39 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md
Original file line number Diff line number Diff line change
@@ -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/<tid>/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/<tid>/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.
147 changes: 147 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt
Original file line number Diff line number Diff line change
@@ -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<String> {
val needles = listOf("gum-js-loop", "gmain", "gdbus", "pool-frida", "frida")
val matches = mutableListOf<String>()

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<String> {
val needles = listOf("frida-agent", "libfrida", "frida-gadget", "gum-js-loop", "linjector", "/gum")
val hits = mutableSetOf<String>()
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()
}
}
Loading
Loading