Android 安全之 Activity 劫持防护

文本讲解 Android 中 Activity 劫持防护的具体方法,公司开发的的项目在安全检查中出现 Activity 被劫持的问题。在网上有很多关于 Activity 劫持防护方式实践过都存在问题,自己完善了一些方法希望和大家一起分享。

什么是 Activity 劫持

Android 为了提高用户的用户体验,对于不同的应用程序之间的切换,基本上是无缝。举一个例子,用户打开安卓手机上的某一应用例如支付宝,进入到登陆页面,这时恶意软件检测到用户的这一动作,立即弹出一个与支付宝界面相同的 Activity,覆盖掉了合法的 Activity,用户几乎无法察觉,该用户接下来输入用户名和密码的操作其实是在恶意软件的 Activity上进行的,接下来会发生什么就可想而知了。具体关于 Activity 劫持原理可以参考如下这篇文章:https://blog.csdn.net/nailsoul/article/details/11767243

阿里聚安全
阿里聚安全旗下产品安全组件 SDK 具有安全签名、安全加密、安全存储、模拟器检测、反调试、反注入、反 Activity 劫持等功能。 开发者只需要简单集成安全组件 SDK 就可以有效解决上述登录窗口被木马病毒劫持的问题,从而帮助用户和企业减少损失。

防护手段

目前,还没有什么专门针对 Activity 劫持的防护方法,因为,这种攻击是用户层面上的,目前还无法从代码层面上根除。但是,我们可以适当地在 APP 中给用户一些警示信息,提示用户其登陆界面以被覆盖。在网上查了很多解决方法如下:

  • 在 Acitivity 的 onStop 方法中 调用封装的 AntiHijackingUtil 类(检测系统程序白名单)检测程序是否被系统程序覆盖。
  • 在前面建立的正常Activity的登陆界面(也就是 MainActivity)中重写 onKeyDown 方法和 onPause 方法,判断程序进入后台是否是用户自身造成的(触摸返回键或 HOME 键)这样一来,当其被覆盖时,就能够弹出警示信息。

AntiHijackingUtil 类代码如下:

/**
 * Description: Activity反劫持检测工具
 * author: zs
 * Date: 2018/7/8 16:31
 */
public class AntiHijackingUtil {
    public static final String TAG = "AntiHijackingUtil";

    /**
     * 检测当前Activity是否安全
     */
    public static boolean checkActivity(Context context) {
        PackageManager pm = context.getPackageManager();
        // 查询所有已经安装的应用程序
        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) {// 这个排序必须有.
            if ((app.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
                safePackages.add(app.packageName);
            }
        }
        // 得到所有的系统程序包名放进白名单里面.
        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) {// 获取系统api版本号,如果是5x系统就用这个方法获取当前运行的包名
            runningActivityPackageName = getCurrentPkgName(context);
        } else {
            runningActivityPackageName =
                    activityManager.getRunningTasks(1).get(0).topActivity.getPackageName();
        }
        // 如果是4x及以下,用这个方法.
        if (runningActivityPackageName != null) {
            // 有些情况下在5x的手机中可能获取不到当前运行的包名,所以要非空判断。
            if (runningActivityPackageName.equals(context.getPackageName())) {
                return true;
            }
            // 白名单比对
            for (String safePack : safePackages) {
                if (safePack.equals(runningActivityPackageName)) {
                    return true;
                }
            }
        }
        return false;
    }

    private static String getCurrentPkgName(Context context) {
        // 5x系统以后利用反射获取当前栈顶activity的包名.
        ActivityManager.RunningAppProcessInfo currentInfo = null;
        Field field = null;
        int START_TASK_TO_FRONT = 2;
        String pkgName = null;
        try {
            // 通过反射获取进程状态字段.
            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);
            //表示前台运行进程.
            if (app.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                Integer state = null;
                try {
                    state = field.getInt(app);// 反射调用字段值的方法,获取该进程的状态.
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 根据这个判断条件从前台中获取当前切换的进程对象
                if (state != null && state == START_TASK_TO_FRONT) {
                    currentInfo = app;
                    break;
                }
            }
        }
        if (currentInfo != null) {
            pkgName = currentInfo.processName;
        }
        return pkgName;
    }

    /**
     * 判断当前是否在桌面
     *
     * @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());
    }

    /**
     * 获得属于桌面的应用的应用包名称
     *
     * @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;
    }

    /**
     * 判断当前是否在锁屏再解锁状态
     *
     * @param context 上下文
     */
    public static boolean isReflectScreen(Context context) {
        KeyguardManager mKeyguardManager =
                (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
        return mKeyguardManager.inKeyguardRestrictedInputMode();
    }
}

但是这两种方法都存在问题,Android onKeyDown 方法目前根本无法监听到 HOME 键,你们可以用代码试一下,我这边验证过了。AntiHijackingUtil 类只检测了系统程序,发现在锁屏状态下代码无法检查也提示警告。效果如下在点击 HOME 键和锁屏在解锁的情况下依然提示应用警告。

Activity 劫持防护失败

防护方法改进

Activity 劫持防护我们想达到的预期目标如下:

  • 用户主动退出 APP ( 返回键 、HOME 键)这种情况下我们不需要给用户弹出警告提示
  • APP 在锁屏再解锁的情况下我们不需要给用户弹出警告提示
  • 其他应用突然覆盖在我们 APP 上时给出合理的警告提示

APP 返回桌面、锁屏再解锁情况检测代码

 /**
     * 判断当前是否在桌面
     *
     * @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());
    }

    /**
     * 获得属于桌面的应用的应用包名称
     *
     * @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;
    }

    /**
     * 判断当前是否在锁屏再解锁状态
     *
     * @param context 上下文
     */
    public static boolean isReflectScreen(Context context) {
        KeyguardManager mKeyguardManager =
                (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
        return mKeyguardManager.inKeyguardRestrictedInputMode();
    }

并且在 onStop 方法中检测是否需要弹出警告提醒

   @Override
    protected void onStop() {
        super.onStop();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 白名单
                boolean safe = AntiHijackingUtil.checkActivity(getApplicationContext());
                // 系统桌面
                boolean isHome = AntiHijackingUtil.isHome(getApplicationContext());
                // 锁屏操作
                boolean isReflectScreen = AntiHijackingUtil.isReflectScreen(getApplicationContext());
                // 判断程序是否当前显示
                if (!safe && !isHome && !isReflectScreen) {
                    Looper.prepare();
                    Toast.makeText(getApplicationContext(), R.string.activity_safe_warning,
                            Toast.LENGTH_LONG).show();
                    Looper.loop();
                }
            }
        }).start();

    }

感觉问题解决了,我们通过点击通知栏打开其他应用来模拟恶意软件覆盖咱们的 APP来看一下应用运行效果吧


Activity 劫持防护成功

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容