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

avatar
作者
筋斗云
阅读量:0

在很多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

 /**   * 是否存在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 操作系统环境,适用于各种嵌入式系统和资源受限的环境

 /**   * 是否存在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

 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 版本

  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 中执行

# cat /system/build.prop | grep ro.build.tags ro.build.tags=release-keys 

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


合并实践

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

封装 RootTool

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

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

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;     } } 

调用实践

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

封装建议(可忽略)

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

CommandUtilgetProperty 反射方法

在这里插入图片描述

SecurityCheckUtilroot 核心方法

在这里插入图片描述

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!