在很多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
等;比如微信沙盒目录下的文件或文件夹权限是否正常)
合并实践
有兴趣的话也可以把 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
核心方法