安全风险 - 检测Android设备系统是否已Root

在很多app中都禁止 root 后的手机使用相关app功能,这种场景在金融app、银行app更为常见一些;当然针对 root 后的手机,我们也可以做出风险提示,告知用户当前设备已 root ,谨防风险!

最近在安全检测中提出了一项 风险bug:当设备已处于 root 状态时,未提示用户风险!

那么我们首先要做的就是 Android 设备的 Root 检测,然后根据业务方的述求,给出风险提示或者禁用app

基础认知

Android 设备被 root 后,会多出 su 文件,同时也可能获取超级用户权限,就有可能存在 Superuser.apk 文件 ,所以我们主要从以下几个方面去判断设备被 root

  • 检查系统中是否存在 su 文件
  • 检查系统是否可执行 su 文件
  • 检查系统中是否 /system/app/Superuser.apk 文件(当 root 后会将 Superuser.apk 文件放于 /system/app/中)

细节分析

看了几篇 Blog 后,主要还是借鉴了 Android如何判断系统是否已经被Root + EasyProtector框架 ,希望可以更好的兼容处理方案

判断系统内是否包含 su

java 复制代码
 /**
  * 是否存在su命令,并且有执行权限
  *
  * @return 存在su命令,并且有执行权限返回true
  */
 public static boolean isSuEnable() {
     File file = null;
     String[] paths = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/", "/su/bin/"};
     try {
         for (String path : paths) {
             file = new File(path + "su");
             if (file.exists() && file.canExecute()) {
                 Log.i(TAG, "find su in : " + path);
                 return true;
             }
         }
     } catch (Exception x) {
         x.printStackTrace();
     }
     return false;
 }

判断系统内是否包含 busybox

BusyBox 是一个集成了多个常用 Linux 命令和工具的软件,它的主要用途是提供一个基础但全面的 Linux 操作系统环境,适用于各种嵌入式系统和资源受限的环境

java 复制代码
 /**
  * 是否存在busybox命令,并且有执行权限
  *
  * @return 存在busybox命令,并且有执行权限返回true
  */
 public static boolean isSuEnable() {
     File file = null;
     String[] paths = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/", "/su/bin/"};
     try {
         for (String path : paths) {
             file = new File(path + "busybox");
             if (file.exists() && file.canExecute()) {
                 Log.i(TAG, "find su in : " + path);
                 return true;
             }
         }
     } catch (Exception x) {
         x.printStackTrace();
     }
     return false;
 }

检测系统内是否安装了Superuser.apk之类的App

java 复制代码
 public static boolean checkSuperuserApk(){
     try {
         File file = new File("/system/app/Superuser.apk");
         if (file.exists()) {
             Log.i(LOG_TAG,"/system/app/Superuser.apk exist");
             return true;
         }
     } catch (Exception e) { }
     return false;
 }

判断 ro.debuggable 属性和 ro.secure 属性

默认手机出厂后 ro.debuggable 属性应该为0,ro.secure应该为1;意思就是系统版本要为 user 版本

java 复制代码
  private int getroDebugProp() {
     int debugProp;
     String roDebugObj = CommandUtil.getSingleInstance().getProperty("ro.debuggable");
     if (roDebugObj == null) debugProp = 1;
     else {
         if ("0".equals(roDebugObj)) debugProp = 0;
         else debugProp = 1;
     }
     return debugProp;
 }
 
 private int getroSecureProp() {
     int secureProp;
     String roSecureObj = CommandUtil.getSingleInstance().getProperty("ro.secure");
     if (roSecureObj == null) secureProp = 1;
     else {
         if ("0".equals(roSecureObj)) secureProp = 0;
         else secureProp = 1;
     }
     return secureProp;
 }

检测系统是否为测试版

Tips

  • 这种验证方式比较依赖在设备中通过命令进行验证,并不是很适合在软件中直接判断root场景
  • 若是非官方发布版,很可能是完全root的版本,存在使用风险

在系统 adb shell 中执行

shell 复制代码
# cat /system/build.prop | grep ro.build.tags
ro.build.tags=release-keys

还有一种检测方式是检测系统挂载目录权限,主要是检测 Android 沙盒目录文件或文件夹读取权限(在 Android 系统中,有些目录是普通用户不能访问的,例如 /data/system/etc 等;比如微信沙盒目录下的文件或文件夹权限是否正常)


合并实践

有兴趣的话也可以把 CommandUtilgetProperty方法SecurityCheckUtilroot 相关方法 合并到 RootTool 中,因为我还用到了 EasyProtector框架 的模拟器检测功能,故此处就先不进行二次封装了

封装 RootTool

java 复制代码
import android.util.Log;

import java.io.File;

public class RootTool {
    private static final String TAG = "root";

    /**
     * 是否存在su命令,并且有执行权限
     *
     * @return 存在su命令,并且有执行权限返回true
     */
    public static boolean isSuEnable() {
        File file = null;
        String[] paths = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/", "/su/bin/"};
        try {
            for (String path : paths) {
                file = new File(path + "su");
                if (file.exists() && file.canExecute()) {
                    Log.i(TAG, "find su in : " + path);
                    return true;
                }
            }
        } catch (Exception x) {
            x.printStackTrace();
        }
        return false;
    }

    /**
     * 是否存在busybox命令,并且有执行权限
     *
     * @return 存在busybox命令,并且有执行权限返回true
     */
    public static boolean isSuBusyEnable() {
        File file = null;
        String[] paths = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/", "/su/bin/"};
        try {
            for (String path : paths) {
                file = new File(path + "busybox");
                if (file.exists() && file.canExecute()) {
                    Log.i(TAG, "find su in : " + path);
                    return true;
                }
            }
        } catch (Exception x) {
            x.printStackTrace();
        }
        return false;
    }

    /**
     * 检测系统内是否安装了Superuser.apk之类的App
     */
    public static boolean checkSuperuserApk() {
        try {
            File file = new File("/system/app/Superuser.apk");
            if (file.exists()) {
                Log.i(TAG, "/system/app/Superuser.apk exist");
                return true;
            }
        } catch (Exception e) {
        }
        return false;
    }

    /**
    *  检测系统是否为测试版:若是非官方发布版,很可能是完全root的版本,存在使用风险
    * */
    public static boolean checkDeviceDebuggable(){
        String buildTags = android.os.Build.TAGS;
        if (buildTags != null && buildTags.contains("test-keys")) {
            Log.i(TAG,"buildTags="+buildTags);
            return true;
        }
        return false;
    }

}

EasyProtector Root检测剥离

为了方便朋友们进行二次封装,在后面我会将核心方法进行图示标明

CommandUtil

java 复制代码
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;

/**
 * Project Name:EasyProtector
 * Package Name:com.lahm.library
 * Created by lahm on 2018/6/8 16:23 .
 */
public class CommandUtil {
    private CommandUtil() {
    }

    private static class SingletonHolder {
        private static final CommandUtil INSTANCE = new CommandUtil();
    }

    public static final CommandUtil getSingleInstance() {
        return SingletonHolder.INSTANCE;
    }

    public String getProperty(String propName) {
        String value = null;
        Object roSecureObj;
        try {
            roSecureObj = Class.forName("android.os.SystemProperties")
                    .getMethod("get", String.class)
                    .invoke(null, propName);
            if (roSecureObj != null) value = (String) roSecureObj;
        } catch (Exception e) {
            value = null;
        } finally {
            return value;
        }
    }

    public String exec(String command) {
        BufferedOutputStream bufferedOutputStream = null;
        BufferedInputStream bufferedInputStream = null;
        Process process = null;
        try {
            process = Runtime.getRuntime().exec("sh");
            bufferedOutputStream = new BufferedOutputStream(process.getOutputStream());

            bufferedInputStream = new BufferedInputStream(process.getInputStream());
            bufferedOutputStream.write(command.getBytes());
            bufferedOutputStream.write('\n');
            bufferedOutputStream.flush();
            bufferedOutputStream.close();

            process.waitFor();

            String outputStr = getStrFromBufferInputSteam(bufferedInputStream);
            return outputStr;
        } catch (Exception e) {
            return null;
        } finally {
            if (bufferedOutputStream != null) {
                try {
                    bufferedOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedInputStream != null) {
                try {
                    bufferedInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (process != null) {
                process.destroy();
            }
        }
    }

    private static String getStrFromBufferInputSteam(BufferedInputStream bufferedInputStream) {
        if (null == bufferedInputStream) {
            return "";
        }
        int BUFFER_SIZE = 512;
        byte[] buffer = new byte[BUFFER_SIZE];
        StringBuilder result = new StringBuilder();
        try {
            while (true) {
                int read = bufferedInputStream.read(buffer);
                if (read > 0) {
                    result.append(new String(buffer, 0, read));
                }
                if (read < BUFFER_SIZE) {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result.toString();
    }
}

SecurityCheckUtil

java 复制代码
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.BatteryManager;
import android.os.Process;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * Project Name:EasyProtector
 * Package Name:com.lahm.library
 * Created by lahm on 2018/5/14 下午10:31 .
 */
public class SecurityCheckUtil {

    private static class SingletonHolder {
        private static final SecurityCheckUtil singleInstance = new SecurityCheckUtil();
    }

    private SecurityCheckUtil() {
    }

    public static final SecurityCheckUtil getSingleInstance() {
        return SingletonHolder.singleInstance;
    }

    /**
     * 获取签名信息
     *
     * @param context
     * @return
     */
    public String getSignature(Context context) {
        try {
            PackageInfo packageInfo = context.
                    getPackageManager()
                    .getPackageInfo(context.getPackageName(),
                            PackageManager.GET_SIGNATURES);
            // 通过返回的包信息获得签名数组
            Signature[] signatures = packageInfo.signatures;
            // 循环遍历签名数组拼接应用签名
            StringBuilder builder = new StringBuilder();
            for (Signature signature : signatures) {
                builder.append(signature.toCharsString());
            }
            // 得到应用签名
            return builder.toString();
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 检测app是否为debug版本
     *
     * @param context
     * @return
     */
    public boolean checkIsDebugVersion(Context context) {
        return (context.getApplicationInfo().flags
                & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
    }

    /**
     * java法检测是否连上调试器
     *
     * @return
     */
    public boolean checkIsDebuggerConnected() {
        return android.os.Debug.isDebuggerConnected();
    }

    /**
     * usb充电辅助判断
     *
     * @param context
     * @return
     */
    public boolean checkIsUsbCharging(Context context) {
        IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent batteryStatus = context.registerReceiver(null, filter);
        if (batteryStatus == null) return false;
        int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
        return chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
    }

    /**
     * 拿清单值
     *
     * @param context
     * @param name
     * @return
     */
    public String getApplicationMetaValue(Context context, String name) {
        ApplicationInfo appInfo = context.getApplicationInfo();
        return appInfo.metaData.getString(name);
    }

    /**
     * 检测本地端口是否被占用
     *
     * @param port
     * @return
     */
    public boolean isLocalPortUsing(int port) {
        boolean flag = true;
        try {
            flag = isPortUsing("127.0.0.1", port);
        } catch (Exception e) {
        }
        return flag;
    }

    /**
     * 检测任一端口是否被占用
     *
     * @param host
     * @param port
     * @return
     * @throws UnknownHostException
     */
    public boolean isPortUsing(String host, int port) throws UnknownHostException {
        boolean flag = false;
        InetAddress theAddress = InetAddress.getByName(host);
        try {
            Socket socket = new Socket(theAddress, port);
            flag = true;
        } catch (IOException e) {
        }
        return flag;
    }

    /**
     * 检查root权限
     *
     * @return
     */
    public boolean isRoot() {
        int secureProp = getroSecureProp();
        if (secureProp == 0)//eng/userdebug版本,自带root权限
            return true;
        else return isSUExist();//user版本,继续查su文件
    }

    private int getroSecureProp() {
        int secureProp;
        String roSecureObj = CommandUtil.getSingleInstance().getProperty("ro.secure");
        if (roSecureObj == null) secureProp = 1;
        else {
            if ("0".equals(roSecureObj)) secureProp = 0;
            else secureProp = 1;
        }
        return secureProp;
    }

    private int getroDebugProp() {
        int debugProp;
        String roDebugObj = CommandUtil.getSingleInstance().getProperty("ro.debuggable");
        if (roDebugObj == null) debugProp = 1;
        else {
            if ("0".equals(roDebugObj)) debugProp = 0;
            else debugProp = 1;
        }
        return debugProp;
    }

    private boolean isSUExist() {
        File file = null;
        String[] paths = {"/sbin/su",
                "/system/bin/su",
                "/system/xbin/su",
                "/data/local/xbin/su",
                "/data/local/bin/su",
                "/system/sd/xbin/su",
                "/system/bin/failsafe/su",
                "/data/local/su"};
        for (String path : paths) {
            file = new File(path);
            if (file.exists()) return true;
        }
        return false;
    }

    private static final String XPOSED_HELPERS = "de.robv.android.xposed.XposedHelpers";
    private static final String XPOSED_BRIDGE = "de.robv.android.xposed.XposedBridge";

    /**
     * 通过检查是否已经加载了XP类来检测
     *
     * @return
     */
    @Deprecated
    public boolean isXposedExists() {
        try {
            Object xpHelperObj = ClassLoader
                    .getSystemClassLoader()
                    .loadClass(XPOSED_HELPERS)
                    .newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
            return true;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            return true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return false;
        }

        try {
            Object xpBridgeObj = ClassLoader
                    .getSystemClassLoader()
                    .loadClass(XPOSED_BRIDGE)
                    .newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
            return true;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            return true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 通过主动抛出异常,检查堆栈信息来判断是否存在XP框架
     *
     * @return
     */
    public boolean isXposedExistByThrow() {
        try {
            throw new Exception("gg");
        } catch (Exception e) {
            for (StackTraceElement stackTraceElement : e.getStackTrace()) {
                if (stackTraceElement.getClassName().contains(XPOSED_BRIDGE)) return true;
            }
            return false;
        }
    }

    /**
     * 尝试关闭XP框架
     * 先通过isXposedExistByThrow判断有没有XP框架
     * 有的话先hookXP框架的全局变量disableHooks
     * <p>
     * 漏洞在,如果XP框架先hook了isXposedExistByThrow的返回值,那么后续就没法走了
     * 现在直接先hookXP框架的全局变量disableHooks
     *
     * @return 是否关闭成功的结果
     */
    public boolean tryShutdownXposed() {
        Field xpdisabledHooks = null;
        try {
            xpdisabledHooks = ClassLoader.getSystemClassLoader()
                    .loadClass(XPOSED_BRIDGE)
                    .getDeclaredField("disableHooks");
            xpdisabledHooks.setAccessible(true);
            xpdisabledHooks.set(null, Boolean.TRUE);
            return true;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            return false;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return false;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 检测有么有加载so库
     *
     * @param paramString
     * @return
     */
    public boolean hasReadProcMaps(String paramString) {
        try {
            Object localObject = new HashSet();
            BufferedReader localBufferedReader =
                    new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/maps"));
            for (; ; ) {
                String str = localBufferedReader.readLine();
                if (str == null) {
                    break;
                }
                if ((str.endsWith(".so")) || (str.endsWith(".jar"))) {
                    ((Set) localObject).add(str.substring(str.lastIndexOf(" ") + 1));
                }
            }
            localBufferedReader.close();
            localObject = ((Set) localObject).iterator();
            while (((Iterator) localObject).hasNext()) {
                boolean bool = ((String) ((Iterator) localObject).next()).contains(paramString);
                if (bool) {
                    return true;
                }
            }
        } catch (Exception fuck) {
        }
        return false;
    }

    /**
     * java读取/proc/uid/status文件里TracerPid的方式来检测是否被调试
     *
     * @return
     */
    public boolean readProcStatus() {
        try {
            BufferedReader localBufferedReader =
                    new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/status"));
            String tracerPid = "";
            for (; ; ) {
                String str = localBufferedReader.readLine();
                if (str.contains("TracerPid")) {
                    tracerPid = str.substring(str.indexOf(":") + 1, str.length()).trim();
                    break;
                }
                if (str == null) {
                    break;
                }
            }
            localBufferedReader.close();
            if ("0".equals(tracerPid)) return false;
            else return true;
        } catch (Exception fuck) {
            return false;
        }
    }

    /**
     * 获取当前进程名
     *
     * @return
     */
    public String getCurrentProcessName() {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("/proc/self/cmdline");
            byte[] buffer = new byte[256];// 修改长度为256,在做中大精简版时发现包名长度大于32读取到的包名会少字符,导致常驻进程下的初始化操作有问题
            int len = 0;
            int b;
            while ((b = fis.read()) > 0 && len < buffer.length) {
                buffer[len++] = (byte) b;
            }
            if (len > 0) {
                String s = new String(buffer, 0, len, "UTF-8");
                return s;
            }
        } catch (Exception e) {

        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (Exception e) {

                }
            }
        }
        return null;
    }
}

调用实践

kotlin 复制代码
 if (RootTool.checkDeviceDebuggable() || RootTool.checkSuperuserApk() || RootTool.isSuBusyEnable() || RootTool.isSuEnable()||SecurityCheckUtil.getSingleInstance().isRoot) {
     //根据需要进行风险提示等相关业务
     ToastUtils.showToast("您当前设备可能已root,请谨防安全风险!")
 }

封装建议(可忽略)

有兴趣的话,可以将下方这些图示方法 copyRootTool ,这样调用时仅使用 RootTool 即可

CommandUtilgetProperty 反射方法

SecurityCheckUtilroot 核心方法

相关推荐
西瓜本瓜@2 小时前
在Android中如何使用Protobuf上传协议
android·java·开发语言·git·学习·android-studio
似霰6 小时前
安卓adb shell串口基础指令
android·adb
fatiaozhang95278 小时前
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
android·adb·电视盒子·魔百盒刷机·魔百盒固件
CYRUS_STUDIO9 小时前
Android APP 热修复原理
android·app·hotfix
鸿蒙布道师9 小时前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师9 小时前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
大耳猫9 小时前
【解决】Android Gradle Sync 报错 Could not read workspace metadata
android·gradle·android studio
ta叫我小白10 小时前
实现 Android 图片信息获取和 EXIF 坐标解析
android·exif·经纬度
dpxiaolong11 小时前
RK3588平台用v4l工具调试USB摄像头实践(亮度,饱和度,对比度,色相等)
android·windows
tangweiguo0305198712 小时前
Android 混合开发实战:统一 View 与 Compose 的浅色/深色主题方案
android