安全风险 - 检测Android设备系统是否已Root
创始人
2024-11-11 15:10:42
0

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

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

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

    • 基础认知
    • 细节分析
      • 判断系统内是否包含 su
      • 判断系统内是否包含 busybox
      • 检测系统内是否安装了Superuser.apk之类的App
      • 判断 ro.debuggable 属性和 ro.secure 属性
      • 检测系统是否为测试版
    • 合并实践
      • 封装 RootTool
      • EasyProtector Root检测剥离
      • 调用实践
      • 封装建议(可忽略)

基础认知

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      * 

* 漏洞在,如果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 核心方法

在这里插入图片描述

相关内容

热门资讯

2分钟德州!(pokenow)... 2分钟德州!(pokenow)发牌规律性总结,(欢乐棋牌)就是真的有挂,wepoke教程(有挂透视)...
4分钟模拟器!(鱼扑克app俱... 4分钟模拟器!(鱼扑克app俱乐部)最新辅助,(扑克王)一直真的有挂,AI教程(有挂模拟器)确实是有...
3分钟苹果版本!(epoker... 3分钟苹果版本!(epoker)真的有挂,(gg扑克)一贯真的有挂,力荐教程(有挂绝活儿)1、打开德...
一分钟ai辅助!(cloudp... 一分钟ai辅助!(cloudpoker云扑克)智能ai,(智星德州菠萝)原来真的有挂,规律教程(有挂...
5分钟自建房!(impoker... 5分钟自建房!(impoker德州)ai辅助,(轰趴十三水)果然真的有挂,我来教教你(有挂妙计)1、...
2分钟ai辅助!(红龙软件德州... 2分钟ai辅助!(红龙软件德州扑克)辅助工具苹果,(poker master安卓版)好像真的有挂,必...
5分钟机制!(fishpoke... 5分钟机制!(fishpoker俱乐部)有透视辅助吗,(poker world)总是真的有挂,切实教...
一分钟开挂!(来玩app德州扑... 一分钟开挂!(来玩app德州扑克)机制,(红龙软件德州扑克)原来真的有挂,揭秘教程(有挂资料)确实是...
2分钟机器人!(EV扑克)智能... 2分钟机器人!(EV扑克)智能ai辅助,(cloudpoker云扑克)好像真的有挂,微扑克教程(有挂...
4分钟机制!(聚星扑克)ai辅... 4分钟机制!(聚星扑克)ai辅助器苹果版,(nzt德州)的确真的有挂,细节方法(有挂黑科技)确实是有...