"Summary of root detection methods of common Java layer anti-debugging technologies"

Intro

This series is a series of excellent works of students. The attachment apk, code, etc. are located in my project, and you can pick them up by yourself:

https://github.com/r0ysue/AndroidSecurityStudy

"Summary of root detection methods of common Java layer anti-debugging technologies"

The Android system is designed to protect the security and stability of users' devices and to control access to the system. Therefore, the Android system will restrict unauthorized operations such as root. When penetrating Android applications, most technologies require root permissions to install various tools, thereby endangering the security of the application. A summary of Root detection methods, throwing bricks to attract jade

Execute the 'which su' command to detect su

The su command can be used to switch to other users. Android phones generally do not have su executable files in the system for security reasons, so based on checking whether there is a su file in the system, it can be judged whether the system has been rooted.

The following command tests a rooted mobile phone, and you can see the existence of su

bullhead:/ $ which su
which su
/system/bin/su

And another Huawei mobile phone is not rooted, su is not detected

D:\Users\wyz\Desktop\Fastboot>adb shell
HWEVR:/ $ which su
which su
1|HWEVR:/ $

Use java to detect whether there is a su command

 package com.example.checksu;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import android.widget.Toast;
 public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);

        if(checkSuExists())
        {
    
    
            Toast.makeText(getApplicationContext(), "检测到su命令",
                    Toast.LENGTH_SHORT).show();
        }
        else
        {
    
    
            Toast.makeText(getApplicationContext(),"没有检测到su命令",
                    Toast.LENGTH_SHORT).show();
        }
    }

     public boolean checkSuExists() {
    
    
         Process process = null;
         try {
    
    
             process = Runtime.getRuntime().exec(new String[] {
    
     "which", "su" });
             BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
             return in.readLine() != null;
         } catch (Throwable t) {
    
    
             return false;
         } finally {
    
    
             if (process != null) process.destroy();
         }
     }
}

The detection results of Huawei mobile phones that have not been rooted

Detection results of rooted N5x

Detect illegal binary files in common directories

The N5x that the author has rooted has su and magisk in some environment variable paths

bullhead:/system/bin $ which magick
which magick
1|bullhead:/system/bin $ which magisk
which magisk
/system/bin/magisk
bullhead:/system/bin $ which su
which su
/system/bin/su
1|bullhead:/system/bin $ echo $PATH | grep /system/bin
echo $PATH | grep /system/bin
/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin

Therefore, some illegal binary files can be detected by traversing the system PATH. The java implementation is as follows

package com.example.checkpath;

import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;


public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        checkForBinary("magisk");
        checkForBinary("su");
        checkForBinary("busybox");
    }

    private static final String[] suPaths = {
    
    
            "/data/local/",
            "/data/local/bin/",
            "/data/local/xbin/",
            "/sbin/",
            "/su/bin/",
            "/system/bin/",
            "/system/bin/.ext/",
            "/system/bin/failsafe/",
            "/system/sd/xbin/",
            "/system/usr/we-need-root/",
            "/system/xbin/",
            "/cache/",
            "/data/",
            "/dev/"
    };

    public void checkForBinary(String filename) {
    
    

        String[] pathsArray = this.getPaths();

        boolean flag = false;

        for (String path : pathsArray) {
    
    
            String completePath = path + filename;
            File f = new File(path, filename);
            boolean fileExists = f.exists();
            if (fileExists) {
    
    
                Toast.makeText(getApplicationContext(), "检测到非法的二进制文件: "+path+filename, Toast.LENGTH_LONG).show();
            }
        }
    }

    private String[] getPaths() {
    
    
        ArrayList<String> paths = new ArrayList<>(Arrays.asList(suPaths));

        String sysPaths = System.getenv("PATH");

        // If we can't get the path variable just return the static paths
        if (sysPaths == null || "".equals(sysPaths)) {
    
    
            return paths.toArray(new String[0]);
        }

        for (String path : sysPaths.split(":")) {
    
    

            if (!path.endsWith("/")) {
    
    
                path = path + '/';
            }

            if (!paths.contains(path)) {
    
    
                paths.add(path);
            }
        }

        return paths.toArray(new String[0]);
    }
}

N5x running results

Determine whether SELinux is enabled,

However, this method may be outdated and needs to be combined with other methods to determine whether the phone is rooted

Use java reflection to get the value of ro.build.selinux to judge

package com.example.checkselinuxenabled;

import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.lang.reflect.Method;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        if(isSelinuxFlagInEnabled())
        {
    
    
            Toast.makeText(getApplicationContext(), "SELinux开启",
                    Toast.LENGTH_SHORT).show();
        }
        else
        {
    
    
            Toast.makeText(getApplicationContext(), "SELinux没有开启",
                    Toast.LENGTH_SHORT).show();
        }
    }

    private boolean isSelinuxFlagInEnabled() {
    
    
        try {
    
    
            Class<?> c = Class.forName("android.os.SystemProperties");
            Method get = c.getMethod("get", String.class);
            String selinux = (String) get.invoke(c, "ro.build.selinux");
            return "1".equals(selinux);
        } catch (Exception ignored) {
    
    

        }
        return false;
    }
}

Check ro.debuggable and ro.secure values

"ro.debuggable" is a property in the Android OS that indicates whether the device has the Android Debug Bridge (ADB) feature enabled. When this property is set to '1', the device will have ADB functionality enabled, allowing developers to use various tools and commands for debugging and testing on the device. In a production environment, this attribute should usually be set to "0" to increase device security. If this property is set to '1', anyone can use ADB commands to access the device, potentially opening the device to attacks or other security issues

"ro.secure" is a system property (System Property) in the Android operating system, which controls whether the device has security enhancements enabled. If the value of this attribute is "1", it means that the device has security hardening measures turned on. If the value of this attribute is "0", it means that the security hardening measures are not enabled on the device.

However, the ro.secure value of the N5x that I have rooted is 1, so I cannot blindly believe in ro.secure

bullhead:/ $ getprop ro.debuggable
getprop ro.debuggable
1
bullhead:/ $ getprop ro.secure
getprop ro.secure
1

Therefore, the detection logic is to check whether the ro.debuggable attribute is true. If it is true, the running environment of the APP is likely to be the Root environment.
Use java code to implement

package com.example.checkdebuggableandsecure;

import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Scanner;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        checkForDangerousProps();
    }

    private void checkForDangerousProps() {
    
    

        final Map<String, String> dangerousProps = new HashMap<>();
        dangerousProps.put("ro.debuggable", "1");

        boolean result = false;

        String[] lines = propsReader();

        if (lines == null){
    
    
            // Could not read, assume false;
            return;
        }

        for (String line : lines) {
    
    
            for (String key : dangerousProps.keySet()) {
    
    
                if (line.contains(key)) {
    
    
                    String badValue = dangerousProps.get(key);
                    badValue = "[" + badValue + "]";
                    if (line.contains(badValue)) {
    
    
                        Toast.makeText(getApplicationContext(), "检测到危险值 "+key+": "+badValue,
                                Toast.LENGTH_LONG).show();
                    }
                }
            }
        }
    }

    private String[] propsReader() {
    
    
        try {
    
    
            InputStream inputstream = Runtime.getRuntime().exec("getprop").getInputStream();
            if (inputstream == null) return null;
            String propVal = new Scanner(inputstream).useDelimiter("\\A").next();
            return propVal.split("\n");
        } catch (IOException | NoSuchElementException e) {
    
    
            return null;
        }
    }
}

The result of executing the above code on N5x

Check if a specific path has write permission

In Linux and Android systems, the root user has the highest authority and can perform some very low-level operations in the system, such as changing system settings, system files, program files, etc. Other regular users can only operate under their own permissions. By checking the read and write permissions of specific paths, you can determine whether the device is rooted.

The following Java code will use the mount command to check the read and write permissions of these paths. If they are readable and writable, the device may be rooted

            /system
            /system/bin
            /system/sbin
            /system/xbin
            /vendor/bin
            /sbin
            /etc
            /sys
            /proc
            /dev
package com.example.checkrw;

import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
import java.io.InputStream;
import java.util.NoSuchElementException;
import java.util.Scanner;

public class MainActivity extends AppCompatActivity {
    
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        this.checkForRWPaths();
    }


    private String[] pathsThatShouldNotBeWritable = {
    
    
            "/system",
            "/system/bin",
            "/system/sbin",
            "/system/xbin",
            "/vendor/bin",
            "/sbin",
            "/etc",
            "/sys",
            "/proc",
            "/dev"
    };

    private String[] mountReader() {
    
    
        try {
    
    
            InputStream inputstream = Runtime.getRuntime().exec("mount").getInputStream();
            if (inputstream == null) return null;
            String propVal = new Scanner(inputstream).useDelimiter("\\A").next();
            return propVal.split("\n");
        } catch (IOException | NoSuchElementException e) {
    
    
            Toast.makeText(getApplicationContext(), e.getMessage(),
                    Toast.LENGTH_LONG).show();
            return null;
        }
    }

    public void checkForRWPaths() {
    
    

        //Run the command "mount" to retrieve all mounted directories
        String[] lines = mountReader();

        if (lines == null){
    
    
            return;
        }

        int sdkVersion = android.os.Build.VERSION.SDK_INT;

        for (String line : lines) {
    
    

            // Split lines into parts
            String[] args = line.split(" ");

            if ((sdkVersion <= android.os.Build.VERSION_CODES.M && args.length < 4)
                    || (sdkVersion > android.os.Build.VERSION_CODES.M && args.length < 6)) {
    
    
                // If we don't have enough options per line, skip this and log an error
                Toast.makeText(getApplicationContext(), "Error formatting mount line: "+line+line,
                        Toast.LENGTH_LONG).show();
                continue;
            }

            String mountPoint;
            String mountOptions;

            /**
             * To check if the device is running Android version higher than Marshmallow or not
             */
            if (sdkVersion > android.os.Build.VERSION_CODES.M) {
    
    
                mountPoint = args[2];
                mountOptions = args[5];
            } else {
    
    
                mountPoint = args[1];
                mountOptions = args[3];
            }

            for(String pathToCheck: this.pathsThatShouldNotBeWritable) {
    
    
                if (mountPoint.equalsIgnoreCase(pathToCheck)) {
    
    

                    /**
                     * If the device is running an Android version above Marshmallow,
                     * need to remove parentheses from options parameter;
                     */
                    if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.M) {
    
    
                        mountOptions = mountOptions.replace("(", "");
                        mountOptions = mountOptions.replace(")", "");

                    }

                    // Split options out and compare against "rw" to avoid false positives
                    for (String option : mountOptions.split(",")){
    
    

                        if (option.equalsIgnoreCase("rw")){
    
    
                            Toast.makeText(getApplicationContext(), pathToCheck+" 路径以rw权限挂载! "+line,
                                    Toast.LENGTH_LONG).show();
                        }
                    }
                }
            }
        }
    }



}

The result of executing the above code on N5x

Detect test-keys

By detecting the value of the "ro.build.tags" parameter in its system properties, if the value is "test-keys", it means that the mobile phone system is built using the digital signature key of the test version, and this key It is commonly used by developers and is easy to be hacked, so there is a risk of being rooted. The officially released Android system uses "release-keys", which are digitally signed by the manufacturer and are more difficult to be attacked, so they are relatively safe. Therefore, if it is detected that "ro.build.tags" in the system properties of an Android device is "test-keys", it can be determined that the device may be rooted

For rooted N5x, the printed ro.build.tags value is test-keys

bullhead:/ $ getprop ro.build.tags
getprop ro.build.tags
test-keys

For Huawei mobile phones that have not been rooted, the printed value of ro.build.tags is release-keys

HWEVR:/ $ getprop ro.build.tags
getprop ro.build.tags
release-keys

Realize with java code

package com.example.checkbuildtags;

import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        if(detectTestKeys())
            Toast.makeText(getApplicationContext(), "Test Keys",
                    Toast.LENGTH_SHORT).show();
        else
            Toast.makeText(getApplicationContext(), "Relese Keys",
                    Toast.LENGTH_SHORT).show();
    }

    public boolean detectTestKeys() {
    
    
        String buildTags = android.os.Build.TAGS;
        return buildTags != null && buildTags.contains("test-keys");
    }

}

Execution result of rooted N5x

Execution results of Huawei phones that have not been rooted

Detect illegal apps

Filter out illegal applications by obtaining the list of applications installed in the system

The following command filters out the illegal application magisk

PS D:\Users\wyz\Desktop\Fastboot> ./adb shell pm list packages | findstr magisk
package:com.topjohnwu.magisk

Use java code to implement similar logic. Readers can add their own filter list in knownRootAppsPackages. However, this code cannot filter magisk. Readers are advised to use Runtime.getRuntime().exec to execute a shell command similar to the previous paragraph to filter magisk

package com.example.checkapp;

import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        detectPotentiallyDangerousApps();
    }

    static final String[] knownRootAppsPackages = {
    
    
            //add....
    };

    public void detectPotentiallyDangerousApps() {
    
    

        // Create a list of package names to iterate over from constants any others provided
        ArrayList<String> packages = new ArrayList<>();
        packages.addAll(Arrays.asList(this.knownRootAppsPackages));
        isAnyPackageFromListInstalled(packages);
    }

    private void isAnyPackageFromListInstalled(List<String> packages){
    
    
        PackageManager pm = getApplicationContext().getPackageManager();

        for (String packageName : packages) {
    
    
            try {
    
    
                // Root app detected
                pm.getPackageInfo(packageName, 0);
                Toast.makeText(getApplicationContext(), "检测到非法应用: "+packageName,
                        Toast.LENGTH_LONG).show();
            } catch (PackageManager.NameNotFoundException e) {
    
    
                Toast.makeText(getApplicationContext(), "没有检测到非法应用: "+packageName,
                        Toast.LENGTH_LONG).show();
            }
        }
    }
}

The above is a rough summary of various root detection methods by the author. Multiple detection methods should be superimposed to ensure that the detection success rate meets expectations. However, these are primary detection methods, and it is not ruled out that there are a lot of fish that slip through the net.

Guess you like

Origin blog.csdn.net/u010559109/article/details/129698203