Android安全防护/检查root/检查Xposed/反调试/应用多开/模拟器检测(持续更新)

转载请注明出处,转载时请不要抹去原始链接。

代码已上传git,欢迎star/fork/issue
https://github.com/lamster2018/EasyProtector
复制代码

文章目录

  • 食用方法
  • root权限检查
  • Xposed框架检查
  • 应用多开检查
  • 反调试方案
  • 模拟器检测
  • TODO

使用方法

implementation 'com.lahm.library:easy-protector-release:latest.release'

https://github.com/lamster2018/EasyProtector


root权限检查

开发者会使用诸如xposed,cydiasubstrate的框架进行hook操作,前提是拥有root权限。

关于root的原理,请参考《Android Root原理分析及防Root新思路》 blog.csdn.net/hsluoyc/art…

简单来说就是去拿『ro.secure』的值做判断, ro.secure值为1,adb权限降为shell,则认为没有root权限。 但是单纯的判断该值是没法检查userdebug版本的root权限

结合《Android判断设备是User版本还是Eng版本》 https://www.jianshu.com/p/7407cf6c34bd 其实还有一个值ro.debuggable

ro.secure=0 ro.secure=1
ro.debuggable=0 / user
ro.debuggable=1 eng/userdebug* /

*暂无userdebug的机器,不知道ro.secure是否为1,埋坑 userdebug 的debuggable值未知,secure为0.

实际上通过『ro.debuggable』值判断更准确 直接读取ro.secure值足够了

下一步再检验是否存在su文件 方案来自《Android检查手机是否被root》 www.jianshu.com/p/f9f39704e…

通过检查su是否存在,su是否可执行,综合判断root权限。

*EasyProtectorLib.checkIsRoot()*的内部实现

    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 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;
    }
复制代码

Xposed框架检查

原理请参考我的《反Xposed方案学习笔记》 www.jianshu.com/p/ee0062468…

所有的方案回归到一点:判断xposed的包是否存在。 1.是通过主动抛出异常查栈信息; 2.是主动反射调用。

当检测到xp框架存在时,我们先行调用xp方法,关闭xp框架达到反制的目的。

EasyProtectorLib.checkIsXposedExist()_内部实现

    private static final String XPOSED_HELPERS = "de.robv.android.xposed.XposedHelpers";
    private static final String XPOSED_BRIDGE = "de.robv.android.xposed.XposedBridge";

    //手动抛出异常,检查堆栈信息是否有xp框架包
    public boolean isEposedExistByThrow() {
        try {
            throw new Exception("gg");
        } catch (Exception e) {
            for (StackTraceElement stackTraceElement : e.getStackTrace()) {
                if (stackTraceElement.getClassName().contains(XPOSED_BRIDGE)) return true;
            }
            return false;
        }
    }

    //检查xposed包是否存在
    public boolean isXposedExists() {
        try {
            Object xpHelperObj = ClassLoader
                    .getSystemClassLoader()
                    .loadClass(XPOSED_HELPERS)
                    .newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
            return true;
        } catch (IllegalAccessException e) {
            //实测debug跑到这里报异常
            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) {
            //实测debug跑到这里报异常
            e.printStackTrace();
            return true;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    //尝试关闭xp的全局开关,亲测可用
    public boolean tryShutdownXposed() {
        if (isEposedExistByThrow()) {
            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;
            }
        } else return true;
    }
复制代码

多开软件检测

多开软件的检测方案这里提供5种,首先4种来自 《Android多开/分身检测》 blog.darkness463.top/2018/05/04/…

《Android虚拟机多开检测》

www.jianshu.com/p/216d65d99…

这里提供代码整理,一键调用,

        VirtualApkCheckUtil.getSingleInstance().checkByPrivateFilePath(this);
        VirtualApkCheckUtil.getSingleInstance().checkByOriginApkPackageName(this);
        VirtualApkCheckUtil.getSingleInstance().checkByHasSameUid();
        VirtualApkCheckUtil.getSingleInstance().checkByMultiApkPackageName();
        VirtualApkCheckUtil.getSingleInstance().checkByPortListening(getPackageName(),callback);
复制代码

第5种来自我同事的启发,目前最好用的就是这种,我起名叫端口检测法,具体思路已经单独成文见 《一行代码帮你检测Android多开软件》 www.jianshu.com/p/65c841749…

测试情况

测试机器/多开软件* 多开分身6.9 平行空间4.0.8389 双开助手3.8.4 分身大师2.5.1 VirtualXP0.11.2 Virtual App *
红米3S/Android6.0/原生eng XXXOO OXOOO OXOOO XOOOO XXXOO XXXOO
华为P9/Android7.0/EUI 5.0 root XXXXO OXOXO OXOXO XOOXO XXXXO XXXOO
小米MIX2/Android8.0/MIUI稳定版9.5 XXXXO OXOXO OXOXO XOOXO XXXXO XXXOO
一加5T/Android8.1/氢OS 5.1 稳定版 XXXXO OXOXO OXOXO XOOXO XXXXO XXXOO

*测试方案顺序如下12345,测试结果X代表未能检测O成功检测多开

*virtual app测试版本是git开源版,商用版已经修复uid的问题

1.文件路径检测

public boolean checkByPrivateFilePath(Context context) {
        String path = context.getFilesDir().getPath();
        for (String virtualPkg : virtualPkgs) {
            if (path.contains(virtualPkg)) return true;
        }
        return false;
    }
复制代码

2.应用列表检测

简单来说,多开app把原始app克隆了,并让自己的包名跟原始app一样,当使用克隆app时,会检测到原始app的包名会和多开app包名一样(就是有两个一样的包名)

    public boolean checkByOriginApkPackageName(Context context) {
        try {
            if (context == null)  return false;
            int count = 0;
            String packageName = context.getPackageName();
            PackageManager pm = context.getPackageManager();
            List pkgs = pm.getInstalledPackages(0);
            for (PackageInfo info : pkgs) {
                if (packageName.equals(info.packageName)) {
                    count++;
                }
            }
            return count > 1;
        } catch (Exception ignore) {
        }
        return false;
    }
复制代码

3.maps检测

    public boolean checkByMultiApkPackageName() {
        BufferedReader bufr = null;
        try {
            bufr = new BufferedReader(new FileReader("/proc/self/maps"));
            String line;
            while ((line = bufr.readLine()) != null) {
                for (String pkg : virtualPkgs) {
                    if (line.contains(pkg)) {
                        return true;
                    }
                }
            }
        } catch (Exception ignore) {

        } finally {
            if (bufr != null) {
                try {
                    bufr.close();
                } catch (IOException e) {

                }
            }
        }
        return false;
    }
复制代码

4.ps检测

简单来说,检测自身进程,如果该进程下的包名有不同多个私有文件目录,则认为被多开

    public boolean checkByHasSameUid() {
        String filter = getUidStrFormat();//拿uid
        String result = CommandUtil.getSingleInstance().exec("ps");
        if (result == null || result.isEmpty()) return false;

        String[] lines = result.split("\n");
        if (lines == null || lines.length <= 0) return false;

        int exitDirCount = 0;
        for (int i = 0; i < lines.length; i++) {
            if (lines[i].contains(filter)) {
                int pkgStartIndex = lines[i].lastIndexOf(" ");
                String processName = lines[i].substring(pkgStartIndex <= 0
                        ? 0 : pkgStartIndex + 1, lines[i].length());
                File dataFile = new File(String.format("/data/data/%s", processName, Locale.CHINA));
                if (dataFile.exists()) {
                    exitDirCount++;
                }
            }
        }

        return exitDirCount > 1;
    }
复制代码

5.端口检测

前4种方案,有一种直接对抗的意思,不希望我们的app运行在多开软件中,第5种方案,我们不直接对抗,只要不是在同一机器上同时运行同一app,我们都认为该app没有被多开。 假如同时运行着两个app(无论先开始运行),两个app进行一个通信,如果通信成功,我们则认为其中有一个是克隆体。

        //遍历查找已开启的端口
        String tcp6 = CommandUtil.getSingleInstance().exec("cat /proc/net/tcp6");
        if (TextUtils.isEmpty(tcp6)) return;
        String[] lines = tcp6.split("\n");
        ArrayList portList = new ArrayList<>();
        for (int i = 0, len = lines.length; i < len; i++) {
            int localHost = lines[i].indexOf("0100007F:");//127.0.0.1:
            if (localHost < 0) continue;
            String singlePort = lines[i].substring(localHost + 9, localHost + 13);
            Integer port = Integer.parseInt(singlePort, 16);
            portList.add(port);
        }
复制代码

对每个端口开启线程尝试连接,并且发送一段自定义的消息,作为钥匙,这里一般发送包名就行(刚好多开软件会把包名处理)

            Socket socket = new Socket("127.0.0.1", port);
            socket.setSoTimeout(2000);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write((secret + "\n").getBytes("utf-8"));
            outputStream.flush();
            socket.shutdownOutput();
复制代码

之后自己再开启端口监听作为服务器,等待连接,如果被连接上之后且消息匹配,则认为有一个克隆体在同时运行。

private void startServer(String secret) {
        Random random = new Random();
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress("127.0.0.1",
                    random.nextInt(55534) + 10000));
            while (true) {
                Socket socket = serverSocket.accept();
                ReadThread readThread = new ReadThread(secret, socket);
                readThread.start();
//                serverSocket.close();
            }
        } catch (BindException e) {
            startServer(secret);//may be loop forever
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
复制代码

*因为端口通信需要Internet权限,本库不会通过网络上传任何隐私


反调试方案

我们不希望自己的app被反编译/动态调试,那首先应该了解如何反编译/动态调试,此处可以参考我的《动态调试笔记--调试smali》 www.jianshu.com/p/90f495191…

然后从调试的步骤来分析学习检测。

1.修改清单更改apk版本为debug版,我们发出去的包为release包,进行调试的话,要求为debug版(如果是已root的机器则没有这个要求),所以首先可检查当前版本是否为debug,或者签名信息有没有被更改。

public boolean checkIsDebugVersion(Context context) {
        return (context.getApplicationInfo().flags
                & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
    }
复制代码

该方法提供了C++实现,见 https://github.com/lamster2018/learnNDK/blob/master/app/src/main/jni/ctest.cpp的checkDebug方法

2.等待调试器附加,直接用api检查debugger是否被附加

public boolean checkIsDebuggerConnected() {
        return android.os.Debug.isDebuggerConnected();
    }
复制代码

实测效果,可以结合电量变化的广播监听来做usb插拔监听,如果是usb充电,此时来检查debugger是否被插入,但是debugger attach到app需要一定时间,所以并不是实时的,还有我们常用的waiting for attach,建议监听到usb插上,开启一个子线程轮训检查,30s后关闭这个子线程。

    //检查usb充电状态
    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;
    }
复制代码

3.检查端口占用

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;
    }
复制代码

4.当app被调试的时候,进程中会有traceid被记录,该原理可参考 《jni动态注册/轮询traceid/反调试学习笔记》 www.jianshu.com/p/082456acf…

检查traceid提供java和c++实现 原理都是轮询读取/proc/Pid/status的TracerPid值 当debugger attach到app时,tracerId不为0,如ida附加调试时,tracerId为23946. *测试机华为P9,会自己给自己附加一个tracer,该值小于1000

鉴于篇幅,此处不贴c++代码。 _EasyProtectorLib.checkIsBeingTracedByC()_使用c++方案

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;
        }
    }
复制代码

模拟器检测

具体研究单独成文见《一行代码帮你检测Android模拟器》 www.jianshu.com/p/434b3075b…

现在的模拟器基本可以做到模拟手机号码,手机品牌,cpu信息等,常规的java方案也可能被hook掉,比如逍遥模拟器读取ro.product.board进行了处理,能得到设置的cpu信息。

在研究各个模拟器的过程中,尤其是在研究build.prop文件时,发现各个模拟器的处理方式不一样,比如以下但不限于 1.基带信息几乎没有; 2.处理器信息ro.product.board和ro.board.platform异常; 3.部分模拟器在读控制组信息时读取不到; 4.连上wifi但会出现 Link encap:UNSPEC未指定网卡类型的情况

结合以上信息,综合判断是否运行在模拟器中。

_EasyProtectorLib.checkIsRunningInEmulator()_的代码实现如下

    public boolean readSysProperty() {
        int suspectCount = 0;
        //读基带信息
        String basebandVersion = CommandUtil.getSingleInstance().getProperty("gsm.version.baseband");
        if (TextUtils.isEmpty(baseBandVersion))
           ++suspectCount;
        //读渠道信息,针对一些基于vbox的模拟器
        String buildFlavor = CommandUtil.getSingleInstance().getProperty("ro.build.flavor");
        if (TextUtils.isEmpty(buildFlavor) | (buildFlavor != null && buildFlavor.contains("vbox")))
            ++suspectCount;
        //读处理器信息,这里经常会被处理
        String productBoard = CommandUtil.getSingleInstance().getProperty("ro.product.board");
        if (TextUtils.isEmpty(productBoard) | (productBoard != null && productBoard.contains("android")))
            ++suspectCount;
        //读处理器平台,这里不常会处理
        String boardPlatform = CommandUtil.getSingleInstance().getProperty("ro.board.platform");
        if (TextUtils.isEmpty(boardPlatform) | (boardPlatform != null && boardPlatform.contains("android"))) 
            ++suspectCount;
        //高通的cpu两者信息一般是一致的
       if (!TextUtils.isEmpty(productBoard)
                && !TextUtils.isEmpty(boardPlatform)
                && !productBoard.equals(boardPlatform))
            ++suspectCount;
        //一些模拟器读取不到进程租信息
        String filter = CommandUtil.getSingleInstance().exec("cat /proc/self/cgroup");
        if (filter == null || filter.length() == 0) ++suspectCount;

        return suspectCount > 2;
    }
复制代码

以下是测试情况*

机器/测试方案 基带信息 渠道信息 处理器信息 进程组 检测结果
AS自带模拟器 O O O X 模拟器
Genymotion2.12.1 O O O X 模拟器
逍遥模拟器5.3.2 O X X O 模拟器
Appetize O X O X 模拟器
夜神模拟器6.1.1 O O O O 模拟器
腾讯手游助手2.0.5 O O O X 模拟器
雷电模拟器3.27 O X X X 模拟器
一加5T X X X X 真机
华为P9 X X O X 真机

*O代表该方案检测为模拟器,X代表检测正常;

*Xamarin/Manymo因为网络原因暂未进行测试;

*因安卓机型太广,真机覆盖测试不完全,有空大家去git提issue

TODO

1.Accessibility检查(反自动抢红包/接单);

2.模拟器的光感,陀螺仪检测;

3.检测到模拟器/多开应该给回调给开发者自行处理,而不是直接FC;--v1.0.4 support

4.端口法检测多开应该可以利用ContentProvider做到;

你可能感兴趣的:(Android安全防护/检查root/检查Xposed/反调试/应用多开/模拟器检测(持续更新))