在很多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
等;比如微信沙盒目录下的文件或文件夹权限是否正常)
合并实践
有兴趣的话也可以把 CommandUtil
的 getProperty方法
和 SecurityCheckUtil
的 root
相关方法 合并到 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,请谨防安全风险!") }
封装建议(可忽略)
有兴趣的话,可以将下方这些图示方法 copy
到 RootTool
,这样调用时仅使用 RootTool
即可
CommandUtil 中 getProperty
反射方法
SecurityCheckUtil 中 root
核心方法