Android 安全之 Activity 劫持防護(hù)

文本講解 Android 中 Activity 劫持防護(hù)的具體方法,公司開(kāi)發(fā)的的項(xiàng)目在安全檢查中出現(xiàn) Activity 被劫持的問(wèn)題。在網(wǎng)上有很多關(guān)于 Activity 劫持防護(hù)方式實(shí)踐過(guò)都存在問(wèn)題,自己完善了一些方法希望和大家一起分享。

什么是 Activity 劫持

Android 為了提高用戶(hù)的用戶(hù)體驗(yàn),對(duì)于不同的應(yīng)用程序之間的切換,基本上是無(wú)縫。舉一個(gè)例子,用戶(hù)打開(kāi)安卓手機(jī)上的某一應(yīng)用例如支付寶,進(jìn)入到登陸頁(yè)面,這時(shí)惡意軟件檢測(cè)到用戶(hù)的這一動(dòng)作,立即彈出一個(gè)與支付寶界面相同的 Activity,覆蓋掉了合法的 Activity,用戶(hù)幾乎無(wú)法察覺(jué),該用戶(hù)接下來(lái)輸入用戶(hù)名和密碼的操作其實(shí)是在惡意軟件的 Activity上進(jìn)行的,接下來(lái)會(huì)發(fā)生什么就可想而知了。具體關(guān)于 Activity 劫持原理可以參考如下這篇文章:https://blog.csdn.net/nailsoul/article/details/11767243

阿里聚安全
阿里聚安全旗下產(chǎn)品安全組件 SDK 具有安全簽名、安全加密、安全存儲(chǔ)、模擬器檢測(cè)、反調(diào)試、反注入、反 Activity 劫持等功能。 開(kāi)發(fā)者只需要簡(jiǎn)單集成安全組件 SDK 就可以有效解決上述登錄窗口被木馬病毒劫持的問(wèn)題,從而幫助用戶(hù)和企業(yè)減少損失。

防護(hù)手段

目前,還沒(méi)有什么專(zhuān)門(mén)針對(duì) Activity 劫持的防護(hù)方法,因?yàn)?,這種攻擊是用戶(hù)層面上的,目前還無(wú)法從代碼層面上根除。但是,我們可以適當(dāng)?shù)卦?APP 中給用戶(hù)一些警示信息,提示用戶(hù)其登陸界面以被覆蓋。在網(wǎng)上查了很多解決方法如下:

  • 在 Acitivity 的 onStop 方法中 調(diào)用封裝的 AntiHijackingUtil 類(lèi)(檢測(cè)系統(tǒng)程序白名單)檢測(cè)程序是否被系統(tǒng)程序覆蓋。
  • 在前面建立的正常Activity的登陸界面(也就是 MainActivity)中重寫(xiě) onKeyDown 方法和 onPause 方法,判斷程序進(jìn)入后臺(tái)是否是用戶(hù)自身造成的(觸摸返回鍵或 HOME 鍵)這樣一來(lái),當(dāng)其被覆蓋時(shí),就能夠彈出警示信息。

AntiHijackingUtil 類(lèi)代碼如下:

/**
 * Description: Activity反劫持檢測(cè)工具
 * author: zs
 * Date: 2018/7/8 16:31
 */
public class AntiHijackingUtil {
    public static final String TAG = "AntiHijackingUtil";

    /**
     * 檢測(cè)當(dāng)前Activity是否安全
     */
    public static boolean checkActivity(Context context) {
        PackageManager pm = context.getPackageManager();
        // 查詢(xún)所有已經(jīng)安裝的應(yīng)用程序
        List<ApplicationInfo> listAppcations =
                pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);
        Collections.sort(listAppcations, new ApplicationInfo.DisplayNameComparator(pm));// 排序

        List<String> safePackages = new ArrayList<>();
        for (ApplicationInfo app : listAppcations) {// 這個(gè)排序必須有.
            if ((app.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
                safePackages.add(app.packageName);
            }
        }
        // 得到所有的系統(tǒng)程序包名放進(jìn)白名單里面.
        ActivityManager activityManager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        String runningActivityPackageName;
        int sdkVersion;
        try {
            sdkVersion = Integer.valueOf(android.os.Build.VERSION.SDK);
        } catch (NumberFormatException e) {
            sdkVersion = 0;
        }
        if (sdkVersion >= 21) {// 獲取系統(tǒng)api版本號(hào),如果是5x系統(tǒng)就用這個(gè)方法獲取當(dāng)前運(yùn)行的包名
            runningActivityPackageName = getCurrentPkgName(context);
        } else {
            runningActivityPackageName =
                    activityManager.getRunningTasks(1).get(0).topActivity.getPackageName();
        }
        // 如果是4x及以下,用這個(gè)方法.
        if (runningActivityPackageName != null) {
            // 有些情況下在5x的手機(jī)中可能獲取不到當(dāng)前運(yùn)行的包名,所以要非空判斷。
            if (runningActivityPackageName.equals(context.getPackageName())) {
                return true;
            }
            // 白名單比對(duì)
            for (String safePack : safePackages) {
                if (safePack.equals(runningActivityPackageName)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static String getCurrentPkgName(Context context) {
        // 5x系統(tǒng)以后利用反射獲取當(dāng)前棧頂activity的包名.
        ActivityManager.RunningAppProcessInfo currentInfo = null;
        Field field = null;
        int START_TASK_TO_FRONT = 2;
        String pkgName = null;
        try {
            // 通過(guò)反射獲取進(jìn)程狀態(tài)字段.
            field = ActivityManager.RunningAppProcessInfo.class.getDeclaredField("processState");
        } catch (Exception e) {
            e.printStackTrace();
        }
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List appList = am.getRunningAppProcesses();
        ActivityManager.RunningAppProcessInfo app;
        for (int i = 0; i < appList.size(); i++) {
            //ActivityManager.RunningAppProcessInfo app : appList
            app = (ActivityManager.RunningAppProcessInfo) appList.get(i);
            //表示前臺(tái)運(yùn)行進(jìn)程.
            if (app.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                Integer state = null;
                try {
                    state = field.getInt(app);// 反射調(diào)用字段值的方法,獲取該進(jìn)程的狀態(tài).
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 根據(jù)這個(gè)判斷條件從前臺(tái)中獲取當(dāng)前切換的進(jìn)程對(duì)象
                if (state != null && state == START_TASK_TO_FRONT) {
                    currentInfo = app;
                    break;
                }
            }
        }
        if (currentInfo != null) {
            pkgName = currentInfo.processName;
        }
        return pkgName;
    }

    /**
     * 判斷當(dāng)前是否在桌面
     *
     * @param context 上下文
     */
    public static boolean isHome(Context context) {
        ActivityManager mActivityManager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
        return getHomes(context).contains(rti.get(0).topActivity.getPackageName());
    }

    /**
     * 獲得屬于桌面的應(yīng)用的應(yīng)用包名稱(chēng)
     *
     * @return 返回包含所有包名的字符串列表
     */
    private static List<String> getHomes(Context context) {
        List<String> names = new ArrayList<String>();
        PackageManager packageManager = context.getPackageManager();
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_HOME);
        List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
                PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo ri : resolveInfo) {
            names.add(ri.activityInfo.packageName);
        }
        return names;
    }

    /**
     * 判斷當(dāng)前是否在鎖屏再解鎖狀態(tài)
     *
     * @param context 上下文
     */
    public static boolean isReflectScreen(Context context) {
        KeyguardManager mKeyguardManager =
                (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
        return mKeyguardManager.inKeyguardRestrictedInputMode();
    }
}

但是這兩種方法都存在問(wèn)題,Android onKeyDown 方法目前根本無(wú)法監(jiān)聽(tīng)到 HOME 鍵,你們可以用代碼試一下,我這邊驗(yàn)證過(guò)了。AntiHijackingUtil 類(lèi)只檢測(cè)了系統(tǒng)程序,發(fā)現(xiàn)在鎖屏狀態(tài)下代碼無(wú)法檢查也提示警告。效果如下在點(diǎn)擊 HOME 鍵和鎖屏在解鎖的情況下依然提示應(yīng)用警告。

Activity 劫持防護(hù)失敗

防護(hù)方法改進(jìn)

Activity 劫持防護(hù)我們想達(dá)到的預(yù)期目標(biāo)如下:

  • 用戶(hù)主動(dòng)退出 APP ( 返回鍵 、HOME 鍵)這種情況下我們不需要給用戶(hù)彈出警告提示
  • APP 在鎖屏再解鎖的情況下我們不需要給用戶(hù)彈出警告提示
  • 其他應(yīng)用突然覆蓋在我們 APP 上時(shí)給出合理的警告提示

APP 返回桌面、鎖屏再解鎖情況檢測(cè)代碼

 /**
     * 判斷當(dāng)前是否在桌面
     *
     * @param context 上下文
     */
    public static boolean isHome(Context context) {
        ActivityManager mActivityManager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> rti = mActivityManager.getRunningTasks(1);
        return getHomes(context).contains(rti.get(0).topActivity.getPackageName());
    }

    /**
     * 獲得屬于桌面的應(yīng)用的應(yīng)用包名稱(chēng)
     *
     * @return 返回包含所有包名的字符串列表
     */
    private static List<String> getHomes(Context context) {
        List<String> names = new ArrayList<String>();
        PackageManager packageManager = context.getPackageManager();
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_HOME);
        List<ResolveInfo> resolveInfo = packageManager.queryIntentActivities(intent,
                PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo ri : resolveInfo) {
            names.add(ri.activityInfo.packageName);
        }
        return names;
    }

    /**
     * 判斷當(dāng)前是否在鎖屏再解鎖狀態(tài)
     *
     * @param context 上下文
     */
    public static boolean isReflectScreen(Context context) {
        KeyguardManager mKeyguardManager =
                (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
        return mKeyguardManager.inKeyguardRestrictedInputMode();
    }

并且在 onStop 方法中檢測(cè)是否需要彈出警告提醒

   @Override
    protected void onStop() {
        super.onStop();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 白名單
                boolean safe = AntiHijackingUtil.checkActivity(getApplicationContext());
                // 系統(tǒng)桌面
                boolean isHome = AntiHijackingUtil.isHome(getApplicationContext());
                // 鎖屏操作
                boolean isReflectScreen = AntiHijackingUtil.isReflectScreen(getApplicationContext());
                // 判斷程序是否當(dāng)前顯示
                if (!safe && !isHome && !isReflectScreen) {
                    Looper.prepare();
                    Toast.makeText(getApplicationContext(), R.string.activity_safe_warning,
                            Toast.LENGTH_LONG).show();
                    Looper.loop();
                }
            }
        }).start();

    }

感覺(jué)問(wèn)題解決了,我們通過(guò)點(diǎn)擊通知欄打開(kāi)其他應(yīng)用來(lái)模擬惡意軟件覆蓋咱們的 APP來(lái)看一下應(yīng)用運(yùn)行效果吧


Activity 劫持防護(hù)成功

代碼全部同步到 GitHub:https://github.com/christian-zs/AntihijackUtil

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容