Android安全防禦/檢查root/檢查Xposed/反調試/應用多開/模擬器檢測(持續更新)

轉載請註明出處,轉載時請不要抹去原始連接。

代碼已上傳git,歡迎star/fork/issue
https://github.com/lamster2018/EasyProtector
複製代碼

文章目錄

  • 食用方法
  • root權限檢查
  • Xposed框架檢查
  • 應用多開檢查
  • 反調試方案
  • 模擬器檢測
  • TODO

使用方法

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

https://github.com/lamster2018/EasyProtector java

demo


root權限檢查

開發者會使用諸如xposed,cydiasubstrate的框架進行hook操做,前提是擁有root權限。android

關於root的原理,請參考《Android Root原理分析及防Root新思路》 blog.csdn.net/hsluoyc/art…c++

簡單來講就是去拿『ro.secure』的值作判斷, ro.secure值爲1,adb權限降爲shell,則認爲沒有root權限。 可是單純的判斷該值是無法檢查userdebug版本的root權限git

結合《Android判斷設備是User版本仍是Eng版本》 https://www.jianshu.com/p/7407cf6c34bd 其實還有一個值ro.debuggablegithub

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

*暫無userdebug的機器,不知道ro.secure是否爲1,埋坑 userdebug 的debuggable值未知,secure爲0.shell

實際上經過『ro.debuggable』值判斷更準確 直接讀取ro.secure值足夠了api

下一步再檢驗是否存在su文件 方案來自《Android檢查手機是否被root》 www.jianshu.com/p/f9f39704e…bash

經過檢查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<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;
    }
複製代碼

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<Integer> 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作到;

相關文章
相關標籤/搜索