diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MASTG-DEMO-0033.md b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MASTG-DEMO-0033.md new file mode 100644 index 00000000000..9940c27c71f --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MASTG-DEMO-0033.md @@ -0,0 +1,31 @@ +--- +platform: android +title: Verifying root detection techniques in applications via static analysis +code: [kotlin] +id: MASTG-DEMO-0033 +test: MASTG-TEST-0245 +--- + +### Sample + +The code snippet below shows sample code that performs root detection checks on the device. + +{{ MastgTest.kt }} + +### Steps + +1. Let's run our @MASTG-TOOL-0110 rule against the reversed java code. + +{{ ../../../../rules/mastg-android-root-detection.yml }} + +{{ run.sh }} + +### Observation + +The output reveals the presence of root detection mechanisms in the app, including the use of `Runtime.getRuntime().exec` to check for the `su` command. + +{{ output.txt }} + +### Evaluation + +The test fails because the app relies on only one root detection method. diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MastgTest.kt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MastgTest.kt new file mode 100644 index 00000000000..c38389eb4cf --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MastgTest.kt @@ -0,0 +1,97 @@ +package org.owasp.mastestapp + +import android.util.Log +import android.content.Context +import java.io.BufferedReader +import java.io.File +import java.io.IOException +import java.io.InputStreamReader + +class MastgTest(private val context: Context) { + + companion object { + private const val TAG = "RootCheck" + } + + fun mastgTest(): String { + return when { + checkRootFiles() || checkSuperUserApk() || checkSuCommand() || checkDangerousProperties() -> { + "Device is rooted" + } + else -> { + "Device is not rooted" + } + } + } + + private fun checkRootFiles(): Boolean { + val rootPaths = setOf( + "/system/app/Superuser.apk", + "/system/xbin/su", + "/system/bin/su", + "/sbin/su", + "/system/sd/xbin/su", + "/system/bin/.ext/.su", + "/system/usr/we-need-root/su-backup", + "/system/xbin/mu" + ) + rootPaths.forEach { path -> + if (File(path).exists()) { + Log.d(TAG, "Found root file: $path") + } + } + return rootPaths.any { path -> File(path).exists() } + } + + private fun checkSuperUserApk(): Boolean { + val superUserApk = File("/system/app/Superuser.apk") + val exists = superUserApk.exists() + if (exists) { + Log.d(TAG, "Found Superuser.apk") + } + return exists + } + + private fun checkSuCommand(): Boolean { + return try { + val process = Runtime.getRuntime().exec(arrayOf("which", "su")) + val reader = BufferedReader(InputStreamReader(process.inputStream)) + val result = reader.readLine() + if (result != null) { + Log.d(TAG, "su command found at: $result") + true + } else { + Log.d(TAG, "su command not found") + false + } + } catch (e: IOException) { + Log.e(TAG, "Error checking su command: ${e.message}", e) + false + } + } + + private fun checkDangerousProperties(): Boolean { + val dangerousProps = arrayOf("ro.debuggable", "ro.secure", "ro.build.tags") + dangerousProps.forEach { prop -> + val value = getSystemProperty(prop) + if (value != null) { + Log.d(TAG, "Dangerous property $prop: $value") + if (value.contains("debug")) { + return true + } + } + } + return false + } + + private fun getSystemProperty(prop: String): String? { + return try { + val process = Runtime.getRuntime().exec(arrayOf("getprop", prop)) + val reader = BufferedReader(InputStreamReader(process.inputStream)) + reader.readLine() + } catch (e: IOException) { + Log.e(TAG, "Error checking system property $prop: ${e.message}", e) + null + } + } +} diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MastgTest_reversed.java b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MastgTest_reversed.java new file mode 100644 index 00000000000..18a33316b65 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/MastgTest_reversed.java @@ -0,0 +1,79 @@ +package org.owasp.mastestapp; + +import android.content.Context; +import android.util.Log; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collection; +import kotlin.Metadata; +import kotlin.collections.CollectionsKt; +import kotlin.jvm.internal.Intrinsics; + +/* compiled from: MastgTest.kt */ +@Metadata(d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u000b\n\u0002\b\u0005\n\u0002\u0010\u000e\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\r\u0010\u0005\u001a\u00020\u0006H\u0000¢\u0006\u0002\b\u0007J\r\u0010\b\u001a\u00020\u0006H\u0000¢\u0006\u0002\b\tJ\b\u0010\n\u001a\u00020\u0006H\u0002J\u0006\u0010\u000b\u001a\u00020\fR\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\r"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "(Landroid/content/Context;)V", "checkRootFiles", "", "checkRootFiles$app_debug", "checkSuCommand", "checkSuCommand$app_debug", "checkSuperUserApk", "mastgTest", "", "app_debug"}, k = 1, mv = {1, 9, 0}, xi = 48) +/* loaded from: classes4.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() { + if (checkRootFiles$app_debug() || checkSuperUserApk() || checkSuCommand$app_debug()) { + return "Device is rooted"; + } + return "Device is not rooted"; + } + + public final boolean checkRootFiles$app_debug() { + Iterable rootPaths = CollectionsKt.listOf((Object[]) new String[]{"/system/app/Superuser.apk", "/system/xbin/su", "/system/bin/su", "/sbin/su", "/system/sd/xbin/su", "/system/bin/.ext/.su", "/system/usr/we-need-root/su-backup", "/system/xbin/mu"}); + Iterable $this$forEach$iv = rootPaths; + for (Object element$iv : $this$forEach$iv) { + String path = (String) element$iv; + if (new File(path).exists()) { + Log.d("RootCheck", "Found root file: " + path); + } + } + Iterable $this$any$iv = rootPaths; + if (($this$any$iv instanceof Collection) && ((Collection) $this$any$iv).isEmpty()) { + return false; + } + for (Object element$iv2 : $this$any$iv) { + if (new File((String) element$iv2).exists()) { + return true; + } + } + return false; + } + + private final boolean checkSuperUserApk() { + File superUserApk = new File("/system/app/Superuser.apk"); + if (superUserApk.exists()) { + Log.d("RootCheck", "Found Superuser.apk"); + } + return superUserApk.exists(); + } + + public final boolean checkSuCommand$app_debug() { + boolean z = false; + try { + Process process = Runtime.getRuntime().exec(new String[]{"which", "su"}); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String result = reader.readLine(); + if (result != null) { + Log.d("RootCheck", "su command found at: " + result); + z = true; + } else { + Log.d("RootCheck", "su command not found"); + } + } catch (IOException e) { + Log.d("RootCheck", "Error checking su command: " + e.getMessage()); + } + return z; + } +} diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/output.txt b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/output.txt new file mode 100644 index 00000000000..e8f260fb043 --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/output.txt @@ -0,0 +1,12 @@ + + +┌────────────────┐ +│ 1 Code Finding │ +└────────────────┘ + + MastgTest_reversed.java + ❱ rules.mastg-android-root-detection + Root detection mechanisms have been identified in this application. + + 65┆ Process process = Runtime.getRuntime().exec(new String[]{"which", "su"}); + diff --git a/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/run.sh b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/run.sh new file mode 100755 index 00000000000..76798449a0b --- /dev/null +++ b/demos/android/MASVS-RESILIENCE/MASTG-DEMO-0033/run.sh @@ -0,0 +1 @@ +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-root-detection.yml ./MastgTest_reversed.java --text > output.txt diff --git a/rules/mastg-android-root-detection.yml b/rules/mastg-android-root-detection.yml new file mode 100644 index 00000000000..ec4a78cc458 --- /dev/null +++ b/rules/mastg-android-root-detection.yml @@ -0,0 +1,27 @@ +rules: + - id: mastg-android-root-detection + languages: [java, kotlin] + severity: INFO + message: Root detection mechanisms have been identified in this application. + patterns: + - pattern-either: + - pattern: File("/system/app/Superuser.apk").exists() + - pattern: File("/system/xbin/su").exists() + - pattern: File("/system/bin/su").exists() + - pattern: File("/sbin/su").exists() + - pattern: File("/system/sd/xbin/su").exists() + - pattern: File("/system/bin/.ext/.su").exists() + - pattern: File("/system/usr/we-need-root/su-backup").exists() + - pattern: File("/system/xbin/mu").exists() + - pattern: Runtime.getRuntime().exec("which su") + - pattern: Runtime.getRuntime().exec(arrayOf("which", "su")) + - pattern: Runtime.getRuntime().exec(arrayOf("getprop", "ro.debuggable")) + - pattern: Runtime.getRuntime().exec(arrayOf("getprop", "ro.secure")) + - pattern: Runtime.getRuntime().exec(arrayOf("getprop", "ro.build.tags")) + - pattern: Runtime.getRuntime().exec($_) + - pattern-not: | + try { + Runtime.getRuntime().exec($_); + } catch (Exception e) { + $_; + } diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0245.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0245.md new file mode 100644 index 00000000000..c58b24c5dd8 --- /dev/null +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0245.md @@ -0,0 +1,38 @@ +--- +title: References to APIs for Root Detection +platform: android +id: MASTG-TEST-0245 +type: [static] +weakness: MASWE-0097 +best-practices: [] +false_negative_prone: true +apis: [Runtime.exec] +--- + +## Overview + +This test checks if the app tries to detect whether the device is rooted. It does not guarantee that the device is secure because some rooting tools might bypass the detection techniques described below. You can use this test as an indicator that the app includes some root detection. + +The testing process involves analyzing the device environment to identify common indicators of root access. This includes checking for the presence of: + +- root management tools - e.g. Magisk, KernelSU +- suspicious files or directories - e.g `/system/bin/su`, `/system/xbin/su` +- modified system properties - e.g. `ro.debuggable`, `ro.secure` + +## Steps + +1. Run @MASTG-TECH-0014 with a tool such as @MASTG-TOOL-0110 on the app binary to detect root detections that use `Runtime.exec`, `File.exists()` and `getprop` APIs. + +## Observation + +The output should include any instances of common root detection checks in the app binary. + +## Evaluation + +The test fails if the app does not implement root detection mechanisms. This test is not exhaustive and may not identify all possible root detection checks because the detections may: + +- be written in the native part of the app +- use different API than covered by this test +- be obfuscated + +Even if the test uncovers root detections, they might not be sufficient against more advanced rooting tools. The most effective way is to test an app against a set of rooting tools. This test should only verify that the developer included the intended detection mechanisms. diff --git a/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0246.md b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0246.md new file mode 100644 index 00000000000..c257849d572 --- /dev/null +++ b/tests-beta/android/MASVS-RESILIENCE/MASTG-TEST-0246.md @@ -0,0 +1,28 @@ +--- +title: Runtime Use Of Root Detection +platform: android +id: MASTG-TEST-0246 +type: [dynamic] +weakness: MASWE-0097 +best-practices: [] +--- + +## Overview + +This test is the dynamic counterpart to @MASTG-TEST-0245. + +## Steps + +1. **Monitor Application Behaviour:** + - Use tools like strace or similar utilities to trace how the app checks for root access. Look for interactions with the system, such as attempts to open su, check running processes, or read root-specific files. This analysis helps uncover how the app performs root detection and may reveal potential weaknesses. + +2. **Bypassing Root Detection Mechanisms** + - Run a dynamic analysis tool such as @MASTG-TOOL-0038 to attempt automated root detection bypass. Use commands to manipulate root checks and observe whether the app still correctly detects root access or if its security mechanisms can be bypassed. + +## Observation + +The output should include any observed instances of common root detection checks performed by the app and the results of the automated root detection bypass attempts. + +## Evaluation + +The test fails if no root detection mechanisms are identified, indicating that the app does not attempt to detect root access. However, this test is not exhaustive, as it relies on predefined bypass techniques that may not cover all possible root detection methods or may be outdated. Additionally, some applications may use more advanced detection mechanisms that automated tools cannot easily identify, requiring manual reverse engineering and deobfuscation to fully assess their effectiveness. diff --git a/tests/android/MASVS-RESILIENCE/MASTG-TEST-0045.md b/tests/android/MASVS-RESILIENCE/MASTG-TEST-0045.md index 9add1a327bd..bebe82fa192 100644 --- a/tests/android/MASVS-RESILIENCE/MASTG-TEST-0045.md +++ b/tests/android/MASVS-RESILIENCE/MASTG-TEST-0045.md @@ -8,6 +8,9 @@ title: Testing Root Detection masvs_v1_levels: - R profiles: [R] +status: deprecated +covered_by: [MASTG-TEST-0245, MASTG-TEST-0246] +deprecation_note: New version available in MASTG V2 --- ## Bypassing Root Detection