Skip to content

MASTG-DEMO-0132: Root Detection Logic in Java/Kotlin Layer Only Protected by Identifier Renaming

Download MASTG-DEMO-0132 APK Open MASTG-DEMO-0132 Folder Build MASTG-DEMO-0132 APK

Sample

The sample code demonstrates an Android app that performs a root detection routine implemented in the Java/Kotlin layer. The app checks whether well-known root manager packages are installed on the device.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package org.owasp.mastestapp

import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log

// SUMMARY: This sample demonstrates a Java/Kotlin root detection routine based on root manager package names.
class MastgTest(private val context: Context) {

    // FAIL: [MASTG-TEST-0368] Due to weak code obfuscation, the root detection logic remains identifiable in the decompiled Java/Kotlin code despite minification.
    val shouldRunInMainThread = true

    private val rootManagerPackages = listOf(
        "com.topjohnwu.magisk",
        "eu.chainfire.supersu",
        "me.weishu.kernelsu"
    )

    fun mastgTest(): String {
        val results = DemoResults("0x51")

        return try {
            val detectedPackage = rootManagerPackages.firstOrNull(::isPackageInstalled)

            if (detectedPackage != null) {
                val message = "Detected root manager package '$detectedPackage' and triggered the root-detection branch. The test fails because this security-relevant logic remains identifiable in the decompiled Java/Kotlin code despite minification."
                Log.w("MASTG-DEMO-0132", message)
                results.add(Status.FAIL, message)
                closeApp()
            } else {
                results.add(Status.PASS, "The root-detection branch was not triggered in this execution.")
            }

            results.toJson()
        } catch (e: Exception) {
            results.add(Status.ERROR, e.toString())
            results.toJson()
        }
    }

    @Suppress("DEPRECATION")
    private fun isPackageInstalled(packageName: String): Boolean {
        val packageManager = context.packageManager
        return try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                packageManager.getPackageInfo(
                    packageName,
                    PackageManager.PackageInfoFlags.of(0)
                )
            } else {
                packageManager.getPackageInfo(packageName, 0)
            }
            true
        } catch (_: PackageManager.NameNotFoundException) {
            false
        }
    }

    private fun closeApp() {
        val activity = context as? Activity ?: return
        activity.finishAffinity()
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <queries>
        <package android:name="com.topjohnwu.magisk" />
        <package android:name="eu.chainfire.supersu" />
        <package android:name="me.weishu.kernelsu" />
    </queries>

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MASTestApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:windowSoftInputMode="adjustResize"
            android:theme="@style/Theme.MASTestApp">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Regardless of whether this protection mechanism is sufficient to detect root in a real-world scenario, the purpose of this demo is to show that the obfuscation applied to the Java/Kotlin layer is not enough to prevent an attacker from reverse engineering the root detection logic with reasonable effort.

The app is obfuscated with R8, which applies identifier renaming and some code shrinking. However, no string encryption or control flow obfuscation is applied. See Obfuscation for reference on common obfuscation techniques.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    buildTypes {
        debug {
            isMinifyEnabled = true
            isDebuggable = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

-keepnames class org.owasp.mastestapp.MastgTest
-keepclassmembers class org.owasp.mastestapp.MastgTest {
    public boolean getShouldRunInMainThread();
}

Steps

  1. Use jadx to decompile the app.

Observation

The output contains the reverse-engineered Java/Kotlin code.

Evaluation

The test case fails because when performing reverse engineering on the minified Java/Kotlin layer it is possible to identify and understand the security-relevant root detection logic with little effort.

In MastgTest_reversed.java, lines 21, 24, and 32 show that R8 shortened member names (f7310a, f7311b, and a()), so some identifier renaming was clearly applied. However, lines 29, 47, 50, 52, 59, and 64 still reveal the monitored package names, the PackageManager lookups, the root detection message, and the finishAffinity() call, as the strings are not encrypted.

MastgTest_reversed.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package org.owasp.mastestapp;

import a3.i;
import a3.m;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import h1.AbstractC0489n;
import java.util.Iterator;
import java.util.List;
import kotlin.Metadata;
import t1.AbstractC0902k;

/* JADX INFO: loaded from: classes.dex */
@Metadata(d1 = {"\u0000\u0010\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0010\u000b\n\u0002\b\u0005\b\u0007\u0018\u00002\u00020\u0001R\u001a\u0010\u0003\u001a\u00020\u00028\u0006X\u0086\u0006\f\n\u0004\b\u0003\u0010\u0004\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0007"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "", "shouldRunInMainThread", "Z", "getShouldRunInMainThread", "()Z", "app_release"}, k = 1, mv = {2, 0, 0}, xi = 48)
public final class MastgTest {

    /* JADX INFO: renamed from: a, reason: collision with root package name */
    public final Context f7310a;

    /* JADX INFO: renamed from: b, reason: collision with root package name */
    public final List f7311b;

    public MastgTest(Context context) {
        AbstractC0902k.e(context, "context");
        this.f7310a = context;
        this.f7311b = AbstractC0489n.I0("com.topjohnwu.magisk", "eu.chainfire.supersu", "me.weishu.kernelsu");
    }

    public final String a() {
        Context context;
        Object next;
        i iVar = new i();
        try {
            Iterator it = this.f7311b.iterator();
            while (true) {
                boolean zHasNext = it.hasNext();
                context = this.f7310a;
                if (!zHasNext) {
                    next = null;
                    break;
                }
                next = it.next();
                String str = (String) next;
                PackageManager packageManager = context.getPackageManager();
                try {
                    if (Build.VERSION.SDK_INT >= 33) {
                        packageManager.getPackageInfo(str, PackageManager.PackageInfoFlags.of(0L));
                    } else {
                        packageManager.getPackageInfo(str, 0);
                    }
                } catch (PackageManager.NameNotFoundException unused) {
                }
            }
            String str2 = (String) next;
            if (str2 != null) {
                String str3 = "Detected root manager package '" + str2 + "'. The app closes when root-related packages are found.";
                Log.w("MASTG-DEMO-0132", str3);
                iVar.a(m.f4493g, str3);
                Activity activity = context instanceof Activity ? (Activity) context : null;
                if (activity != null) {
                    activity.finishAffinity();
                }
            } else {
                iVar.a(m.f4494h, "No monitored root manager package was found.");
            }
            return iVar.d();
        } catch (Exception e3) {
            iVar.a(m.f4495i, e3.toString());
            return iVar.d();
        }
    }

    public final boolean getShouldRunInMainThread() {
        return true;
    }
}

This means that the reverse-engineered code still makes it straightforward to identify that the app is checking for root manager packages and closing the app when one is found, despite having some obfuscation applied in the form of identifier renaming.

Additional Context:

In AndroidManifest_reversed.xml, lines 14 to 16 show the package visibility queries for the monitored root manager packages.

AndroidManifest_reversed.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="35"
    android:compileSdkVersionCodename="15"
    package="org.owasp.mastestapp"
    platformBuildVersionCode="35"
    platformBuildVersionName="15">
    <uses-sdk
        android:minSdkVersion="29"
        android:targetSdkVersion="35"/>
    <queries>
        <package android:name="com.topjohnwu.magisk"/>
        <package android:name="eu.chainfire.supersu"/>
        <package android:name="me.weishu.kernelsu"/>
    </queries>
    <uses-permission android:name="android.permission.INTERNET"/>
    <permission
        android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
        android:protectionLevel="signature"/>
    <uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
    <application
        android:theme="@style/Theme.MASTestApp"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:extractNativeLibs="false"
        android:fullBackupContent="@xml/backup_rules"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:dataExtractionRules="@xml/data_extraction_rules">
        <activity
            android:theme="@style/Theme.MASTestApp"
            android:name="org.owasp.mastestapp.MainActivity"
            android:exported="true"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            android:authorities="org.owasp.mastestapp.androidx-startup">
            <meta-data
                android:name="androidx.emoji2.text.EmojiCompatInitializer"
                android:value="androidx.startup"/>
            <meta-data
                android:name="androidx.lifecycle.ProcessLifecycleInitializer"
                android:value="androidx.startup"/>
            <meta-data
                android:name="androidx.profileinstaller.ProfileInstallerInitializer"
                android:value="androidx.startup"/>
        </provider>
        <receiver
            android:name="androidx.profileinstaller.ProfileInstallReceiver"
            android:permission="android.permission.DUMP"
            android:enabled="true"
            android:exported="true"
            android:directBootAware="false">
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SKIP_FILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/>
            </intent-filter>
            <intent-filter>
                <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/>
            </intent-filter>
        </receiver>
    </application>
</manifest>