-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add more test cases covering the v1 MASTG-TEST-0048: RE Tools Detection (android) (by @appknox) #3848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ScreaMy7
wants to merge
19
commits into
OWASP:master
Choose a base branch
from
ScreaMy7:TEST-0048
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add more test cases covering the v1 MASTG-TEST-0048: RE Tools Detection (android) (by @appknox) #3848
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
5f68b92
adding port v1->v2 TEST-0048
ScreaMy7 1a4b978
fixed BEST files
ScreaMy7 598c18a
fix lint
ScreaMy7 b9f65b8
fix linter
ScreaMy7 79d4803
lint fix
ScreaMy7 2b8e0b2
changes added
ScreaMy7 597fcfe
lint fix
ScreaMy7 bd4a431
added MastgTest
ScreaMy7 34b3a80
added mastg test file
ScreaMy7 6f7b4ad
bug fix
ScreaMy7 a59e3f8
update demo
ScreaMy7 265faac
added changes
ScreaMy7 62c35c6
added best practices
ScreaMy7 0e38127
linter fix
ScreaMy7 0de6f02
Update
ScreaMy7 a9229a0
linter fix
ScreaMy7 a6cd717
simple script added
ScreaMy7 ef69db0
Merge branch 'master' into TEST-0048
cpholguera 38e6241
Fix TEST-0048 demos: thread-model alignment, ANSI-clean outputs, recu…
ScreaMy7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
39
demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MASTG-DEMO-0x48.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| --- | ||
| platform: android | ||
| title: Uses of Xposed/LSPosed Detection Techniques with 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/<pid>/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 | ||
|
|
||
| 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 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/<pid>/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. | ||
146 changes: 146 additions & 0 deletions
146
demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x48/MastgTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| 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`. | ||
|
ScreaMy7 marked this conversation as resolved.
Outdated
|
||
| // 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() | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.