安卓开发笔记Android功能代码

Android安全防护/检查root/检查Xposed/反调试/

2018-05-16  本文已影响137人  普通的程序员
转载请注明出处,转载时请不要抹去原始链接。

代码已上传git
https://github.com/lamster2018/EasyProtector

文章目录


使用方法

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

https://github.com/lamster2018/EasyProtector

demo

root权限检查

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

关于root的原理,请参考《Android Root原理分析及防Root新思路》
https://blog.csdn.net/hsluoyc/article/details/50560782

简单来说就是去拿『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》
https://www.jianshu.com/p/f9f39704e30c

通过检查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方案学习笔记》
https://www.jianshu.com/p/ee0062468251

所有的方案回归到一点:判断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;
    }

多开软件检测

多开软件的检验原理和方案来自,这里我只做了整合和测试。
《Android多开/分身检测》
https://blog.darkness463.top/2018/05/04/Android-Virtual-Check/

《Android虚拟机多开检测》
https://www.jianshu.com/p/216d65d9971e

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

*测试方案顺序如下1234,测试结果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<PackageInfo> 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;
    }

反调试方案

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

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

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/反调试学习笔记》
https://www.jianshu.com/p/082456acf89c

检查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模拟器》
https://www.jianshu.com/p/434b3075b5dd

现在的模拟器基本可以做到模拟手机号码,手机品牌,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 (basebandVersion == null | "".equals(basebandVersion)) ++suspectCount;
        //读渠道信息,针对一些基于vbox的模拟器
        String buildFlavor = CommandUtil.getSingleInstance().getProperty("ro.build.flavor");
        if (buildFlavor == null | "".equals(buildFlavor) | (buildFlavor != null && buildFlavor.contains("vbox")))
            ++suspectCount;
        //读处理器信息,这里经常会被处理
        String productBoard = CommandUtil.getSingleInstance().getProperty("ro.product.board");
        if (productBoard == null | "".equals(productBoard)) ++suspectCount;
        //读处理器平台,这里不常会处理
        String boardPlatform = CommandUtil.getSingleInstance().getProperty("ro.board.platform");
        if (boardPlatform == null | "".equals(boardPlatform)) ++suspectCount;
        //高通的cpu两者信息一般是一致的
        if (productBoard != null && boardPlatform != null && !productBoard.equals(boardPlatform))
            ++suspectCount;
        //一些模拟器读取不到进程租信息
        String filter = CommandUtil.getSingleInstance().exec("cat /proc/self/cgroup");
        if (filter == null || filter.length() == 0) ++suspectCount;

        return suspectCount > 2;
    }

以下是测试情况*

测试方案/模拟器 AS自带模拟器 Genymotion2.12.1 逍遥模拟器5.3.2 Appetize 夜神模拟器6.1.1
基带信息 O O O O O
渠道信息 O O X X O
处理器信息 O O X O O
进程组 X X O X O
检测结果 模拟器 模拟器 模拟器 模拟器 模拟器

*O代表该方案检测为模拟器,X代表检测不到
*Xamarin/Manymo因为网络原因暂未进行测试

TODO

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

上一篇 下一篇

猜你喜欢

热点阅读