在很多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
*/publicstaticbooleanisSuEnable(){File file =null;String[] paths ={"/system/bin/","/system/xbin/","/system/sbin/","/sbin/","/vendor/bin/","/su/bin/"};try{for(String path : paths){
file =newFile(path +"su");if(file.exists()&& file.canExecute()){Log.i(TAG,"find su in : "+ path);returntrue;}}}catch(Exception x){
x.printStackTrace();}returnfalse;}
判断系统内是否包含 busybox
BusyBox
是一个集成了多个常用
Linux
命令和工具的软件,它的主要用途是提供一个基础但全面的
Linux
操作系统环境,适用于各种嵌入式系统和资源受限的环境
/**
* 是否存在busybox命令,并且有执行权限
*
* @return 存在busybox命令,并且有执行权限返回true
*/publicstaticbooleanisSuEnable(){File file =null;String[] paths ={"/system/bin/","/system/xbin/","/system/sbin/","/sbin/","/vendor/bin/","/su/bin/"};try{for(String path : paths){
file =newFile(path +"busybox");if(file.exists()&& file.canExecute()){Log.i(TAG,"find su in : "+ path);returntrue;}}}catch(Exception x){
x.printStackTrace();}returnfalse;}
检测系统内是否安装了Superuser.apk之类的App
publicstaticbooleancheckSuperuserApk(){try{File file =newFile("/system/app/Superuser.apk");if(file.exists()){Log.i(LOG_TAG,"/system/app/Superuser.apk exist");returntrue;}}catch(Exception e){}returnfalse;}
判断 ro.debuggable 属性和 ro.secure 属性
默认手机出厂后
ro.debuggable
属性应该为0,
ro.secure
应该为1;意思就是系统版本要为
user
版本
privateintgetroDebugProp(){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;}privateintgetroSecureProp(){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.tagsro.build.tags=release-keys
还有一种检测方式是
检测系统挂载目录权限
,主要是检测
Android
沙盒目录文件或文件夹读取权限(在
Android
系统中,有些目录是普通用户不能访问的,例如
/data
、
/system
、
/etc
等;比如微信沙盒目录下的文件或文件夹权限是否正常)
合并实践
有兴趣的话也可以把
CommandUtil
的
getProperty方法
和
SecurityCheckUtil
的
root
相关方法 合并到
RootTool
中,因为我还用到了 EasyProtector框架 的模拟器检测功能,故此处就先不进行二次封装了
封装 RootTool
importandroid.util.Log;importjava.io.File;publicclassRootTool{privatestaticfinalStringTAG="root";/**
* 是否存在su命令,并且有执行权限
*
* @return 存在su命令,并且有执行权限返回true
*/publicstaticbooleanisSuEnable(){File file =null;String[] paths ={"/system/bin/","/system/xbin/","/system/sbin/","/sbin/","/vendor/bin/","/su/bin/"};try{for(String path : paths){
file =newFile(path +"su");if(file.exists()&& file.canExecute()){Log.i(TAG,"find su in : "+ path);returntrue;}}}catch(Exception x){
x.printStackTrace();}returnfalse;}/**
* 是否存在busybox命令,并且有执行权限
*
* @return 存在busybox命令,并且有执行权限返回true
*/publicstaticbooleanisSuBusyEnable(){File file =null;String[] paths ={"/system/bin/","/system/xbin/","/system/sbin/","/sbin/","/vendor/bin/","/su/bin/"};try{for(String path : paths){
file =newFile(path +"busybox");if(file.exists()&& file.canExecute()){Log.i(TAG,"find su in : "+ path);returntrue;}}}catch(Exception x){
x.printStackTrace();}returnfalse;}/**
* 检测系统内是否安装了Superuser.apk之类的App
*/publicstaticbooleancheckSuperuserApk(){try{File file =newFile("/system/app/Superuser.apk");if(file.exists()){Log.i(TAG,"/system/app/Superuser.apk exist");returntrue;}}catch(Exception e){}returnfalse;}/**
* 检测系统是否为测试版:若是非官方发布版,很可能是完全root的版本,存在使用风险
* */publicstaticbooleancheckDeviceDebuggable(){String buildTags =android.os.Build.TAGS;if(buildTags !=null&& buildTags.contains("test-keys")){Log.i(TAG,"buildTags="+buildTags);returntrue;}returnfalse;}}
EasyProtector Root检测剥离
为了方便朋友们进行二次封装,在后面我会将核心方法进行图示标明
CommandUtil
importjava.io.BufferedInputStream;importjava.io.BufferedOutputStream;importjava.io.IOException;/**
* Project Name:EasyProtector
* Package Name:com.lahm.library
* Created by lahm on 2018/6/8 16:23 .
*/publicclassCommandUtil{privateCommandUtil(){}privatestaticclassSingletonHolder{privatestaticfinalCommandUtilINSTANCE=newCommandUtil();}publicstaticfinalCommandUtilgetSingleInstance(){returnSingletonHolder.INSTANCE;}publicStringgetProperty(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;}}publicStringexec(String command){BufferedOutputStream bufferedOutputStream =null;BufferedInputStream bufferedInputStream =null;Process process =null;try{
process =Runtime.getRuntime().exec("sh");
bufferedOutputStream =newBufferedOutputStream(process.getOutputStream());
bufferedInputStream =newBufferedInputStream(process.getInputStream());
bufferedOutputStream.write(command.getBytes());
bufferedOutputStream.write('\n');
bufferedOutputStream.flush();
bufferedOutputStream.close();
process.waitFor();String outputStr =getStrFromBufferInputSteam(bufferedInputStream);return outputStr;}catch(Exception e){returnnull;}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();}}}privatestaticStringgetStrFromBufferInputSteam(BufferedInputStream bufferedInputStream){if(null== bufferedInputStream){return"";}intBUFFER_SIZE=512;byte[] buffer =newbyte[BUFFER_SIZE];StringBuilder result =newStringBuilder();try{while(true){int read = bufferedInputStream.read(buffer);if(read >0){
result.append(newString(buffer,0, read));}if(read <BUFFER_SIZE){break;}}}catch(Exception e){
e.printStackTrace();}return result.toString();}}
SecurityCheckUtil
importandroid.content.Context;importandroid.content.Intent;importandroid.content.IntentFilter;importandroid.content.pm.ApplicationInfo;importandroid.content.pm.PackageInfo;importandroid.content.pm.PackageManager;importandroid.content.pm.Signature;importandroid.os.BatteryManager;importandroid.os.Process;importjava.io.BufferedReader;importjava.io.File;importjava.io.FileInputStream;importjava.io.FileReader;importjava.io.IOException;importjava.lang.reflect.Field;importjava.net.InetAddress;importjava.net.Socket;importjava.net.UnknownHostException;importjava.util.HashSet;importjava.util.Iterator;importjava.util.Set;/**
* Project Name:EasyProtector
* Package Name:com.lahm.library
* Created by lahm on 2018/5/14 下午10:31 .
*/publicclassSecurityCheckUtil{privatestaticclassSingletonHolder{privatestaticfinalSecurityCheckUtil singleInstance =newSecurityCheckUtil();}privateSecurityCheckUtil(){}publicstaticfinalSecurityCheckUtilgetSingleInstance(){returnSingletonHolder.singleInstance;}/**
* 获取签名信息
*
* @param context
* @return
*/publicStringgetSignature(Context context){try{PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),PackageManager.GET_SIGNATURES);// 通过返回的包信息获得签名数组Signature[] signatures = packageInfo.signatures;// 循环遍历签名数组拼接应用签名StringBuilder builder =newStringBuilder();for(Signature signature : signatures){
builder.append(signature.toCharsString());}// 得到应用签名return builder.toString();}catch(PackageManager.NameNotFoundException e){
e.printStackTrace();}return"";}/**
* 检测app是否为debug版本
*
* @param context
* @return
*/publicbooleancheckIsDebugVersion(Context context){return(context.getApplicationInfo().flags
&ApplicationInfo.FLAG_DEBUGGABLE)!=0;}/**
* java法检测是否连上调试器
*
* @return
*/publicbooleancheckIsDebuggerConnected(){returnandroid.os.Debug.isDebuggerConnected();}/**
* usb充电辅助判断
*
* @param context
* @return
*/publicbooleancheckIsUsbCharging(Context context){IntentFilter filter =newIntentFilter(Intent.ACTION_BATTERY_CHANGED);Intent batteryStatus = context.registerReceiver(null, filter);if(batteryStatus ==null)returnfalse;int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED,-1);return chargePlug ==BatteryManager.BATTERY_PLUGGED_USB;}/**
* 拿清单值
*
* @param context
* @param name
* @return
*/publicStringgetApplicationMetaValue(Context context,String name){ApplicationInfo appInfo = context.getApplicationInfo();return appInfo.metaData.getString(name);}/**
* 检测本地端口是否被占用
*
* @param port
* @return
*/publicbooleanisLocalPortUsing(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
*/publicbooleanisPortUsing(String host,int port)throwsUnknownHostException{boolean flag =false;InetAddress theAddress =InetAddress.getByName(host);try{Socket socket =newSocket(theAddress, port);
flag =true;}catch(IOException e){}return flag;}/**
* 检查root权限
*
* @return
*/publicbooleanisRoot(){int secureProp =getroSecureProp();if(secureProp ==0)//eng/userdebug版本,自带root权限returntrue;elsereturnisSUExist();//user版本,继续查su文件}privateintgetroSecureProp(){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;}privateintgetroDebugProp(){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;}privatebooleanisSUExist(){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 =newFile(path);if(file.exists())returntrue;}returnfalse;}privatestaticfinalStringXPOSED_HELPERS="de.robv.android.xposed.XposedHelpers";privatestaticfinalStringXPOSED_BRIDGE="de.robv.android.xposed.XposedBridge";/**
* 通过检查是否已经加载了XP类来检测
*
* @return
*/@DeprecatedpublicbooleanisXposedExists(){try{Object xpHelperObj =ClassLoader.getSystemClassLoader().loadClass(XPOSED_HELPERS).newInstance();}catch(InstantiationException e){
e.printStackTrace();returntrue;}catch(IllegalAccessException e){
e.printStackTrace();returntrue;}catch(ClassNotFoundException e){
e.printStackTrace();returnfalse;}try{Object xpBridgeObj =ClassLoader.getSystemClassLoader().loadClass(XPOSED_BRIDGE).newInstance();}catch(InstantiationException e){
e.printStackTrace();returntrue;}catch(IllegalAccessException e){
e.printStackTrace();returntrue;}catch(ClassNotFoundException e){
e.printStackTrace();returnfalse;}returntrue;}/**
* 通过主动抛出异常,检查堆栈信息来判断是否存在XP框架
*
* @return
*/publicbooleanisXposedExistByThrow(){try{thrownewException("gg");}catch(Exception e){for(StackTraceElement stackTraceElement : e.getStackTrace()){if(stackTraceElement.getClassName().contains(XPOSED_BRIDGE))returntrue;}returnfalse;}}/**
* 尝试关闭XP框架
* 先通过isXposedExistByThrow判断有没有XP框架
* 有的话先hookXP框架的全局变量disableHooks
* <p>
* 漏洞在,如果XP框架先hook了isXposedExistByThrow的返回值,那么后续就没法走了
* 现在直接先hookXP框架的全局变量disableHooks
*
* @return 是否关闭成功的结果
*/publicbooleantryShutdownXposed(){Field xpdisabledHooks =null;try{
xpdisabledHooks =ClassLoader.getSystemClassLoader().loadClass(XPOSED_BRIDGE).getDeclaredField("disableHooks");
xpdisabledHooks.setAccessible(true);
xpdisabledHooks.set(null,Boolean.TRUE);returntrue;}catch(NoSuchFieldException e){
e.printStackTrace();returnfalse;}catch(ClassNotFoundException e){
e.printStackTrace();returnfalse;}catch(IllegalAccessException e){
e.printStackTrace();returnfalse;}}/**
* 检测有么有加载so库
*
* @param paramString
* @return
*/publicbooleanhasReadProcMaps(String paramString){try{Object localObject =newHashSet();BufferedReader localBufferedReader =newBufferedReader(newFileReader("/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){returntrue;}}}catch(Exception fuck){}returnfalse;}/**
* java读取/proc/uid/status文件里TracerPid的方式来检测是否被调试
*
* @return
*/publicbooleanreadProcStatus(){try{BufferedReader localBufferedReader =newBufferedReader(newFileReader("/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))returnfalse;elsereturntrue;}catch(Exception fuck){returnfalse;}}/**
* 获取当前进程名
*
* @return
*/publicStringgetCurrentProcessName(){FileInputStream fis =null;try{
fis =newFileInputStream("/proc/self/cmdline");byte[] buffer =newbyte[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 =newString(buffer,0, len,"UTF-8");return s;}}catch(Exception e){}finally{if(fis !=null){try{
fis.close();}catch(Exception e){}}}returnnull;}}
调用实践
if(RootTool.checkDeviceDebuggable()|| RootTool.checkSuperuserApk()|| RootTool.isSuBusyEnable()|| RootTool.isSuEnable()||SecurityCheckUtil.getSingleInstance().isRoot){//根据需要进行风险提示等相关业务
ToastUtils.showToast("您当前设备可能已root,请谨防安全风险!")}
封装建议(可忽略)
有兴趣的话,可以将下方这些图示方法
copy
到
RootTool
,这样调用时仅使用
RootTool
即可
CommandUtil 中
getProperty
反射方法
SecurityCheckUtil 中
root
核心方法
版权归原作者 远方那座山 所有, 如有侵权,请联系我们删除。