Android Activity防劫持方案


最近,安全合规部门又对金融类、银行类app进行了大规模的多方面安全检查,其中有一项安全问题:Activity劫持。其实Android界面防劫持我们app这边也是做了的,但是为啥还会有这些问题呢?自我感觉就是绝不会有此类问题,于是我们向检测部门要了劫持工具,但是事实往往是打脸的。。。。。

那么什么是Activity劫持呢?简单的说就是我们APP正常的Activity界面被恶意攻替换上仿冒的恶意Activity界面进行攻击和非法用途。界面劫持攻击通常难被识别出来,其造成的后果必然会给用户带来严重损失。

举个例子来说,当用户打开安卓手机上的某一应用,进入到登陆页面,这时,恶意软件侦测到用户的这一动作,立即弹出一个与该应用界面相同的Activity,覆盖掉了合法的Activity,用户几乎无法察觉,该用户接下来输入用户名和密码的操作其实是在恶意软件的Activity上进行的,最终会发生什么就可想而知了。

那么应该怎么防护呢?目前是还没有什么专门针对 Activity 劫持的防护方法,因为,这种攻击是用户层面上的,现在还无法从代码层面上根除。但是,我们可以适当地在 APP 中给用户一些警示信息,如toast提示.“某某app正在后台运行”。

前面说到公司app被检测到Activity界面被劫持问题,其实我们项目也有代码逻辑处理劫持问题,但是现在看来还是不够完善的,项目中用的是监听app生命周期方法去做的,在app处于后台时,toast提醒用户,用到了两个库:

    //Gooogle官方获取App生命周期的监听器
    implementation "android.arch.lifecycle:extensions:1.1.1"
    annotationProcessor "android.arch.lifecycle:compiler:1.1.1"
    
接着需要实现LifecycleObserver
public class AppLifeCycleImpl implements LifecycleObserver {

    private final static String sDES = "MDroidS正在后台运行,请注意了!";
    
    public AppLifeCycleImpl() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    public void create() {
    }

    //App处于前台可见状态。
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void start() {
        UILog.e("AppLifeCycleImpl start");
    }

    //App重新进入前台
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    public void resume() {
        UILog.e("AppLifeCycleImpl resume");

    }

    //此后App进入不可见状态/后台
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void pause() {
        UILog.e("AppLifeCycleImpl pause");

    }

    //App进入后台或者熄灭屏幕
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void stop() {
        UILog.e("AppLifeCycleImpl stop");
        UIToast.showShort(sDES);

    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void onDestroy() {
        UILog.e("AppLifecycleObserver onDestroy");
    }
}

然后在自己的application中注册监这个Observer

    ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifeCycleImpl());
    
在这里会有个问题,当我们申请权限时或在个别手机上这个玩意也会吐司,我真是吐了,所以在这个基础上又做了一些操作,完善后的AppLifeCycleImpl:
public class AppLifeCycleImpl implements LifecycleObserver {

    private final static String sDES = "MDroidS正在后台运行,请注意了!";
    private boolean isBackground = false;

    public AppLifeCycleImpl() {
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    public void create() {
    }

    //App处于前台可见状态。
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    public void start() {
        UILog.e("AppLifeCycleImpl start");
    }

    //App重新进入前台
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    public void resume() {
        UILog.e("AppLifeCycleImpl resume");
        isBackground = false;

    }

    //此后App进入不可见状态/后台
    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void pause() {
        // 放在pause中,加快显示防劫持的提示信息
        UILog.e("AppLifeCycleImpl pause");
        // 增加appToBackground判断,兼容部分手机误判
        if (AppUtils.isAppForeground()) {
            return;
        }
        isBackground = true;
        UIToast.showShort(sDES);
    }

    //App进入后台或者熄灭屏幕
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    public void stop() {
        UILog.e("AppLifeCycleImpl stop");
        if (!isBackground) {
            isBackground = true;
            UIToast.showShort(sDES);
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    public void onDestroy() {
        UILog.e("AppLifecycleObserver onDestroy");
    }
}

其实就是加了一层判断和一个isBackground 标识,其中isAppForeground()方法在uitlcodex库里,引入即可:

  implementation'com.blankj:utilcodex:1.30.6'
  
然后就是这个玩意也被检测出页面劫持问题,其实大多数开发者想到的就是用这个去监听app生命周期,毕竟比较简单,但是现在这个不中用了,于是上网一搜寻找解决办法,网上的办法都是大同小异,多多少少都会有一些问题,最后吸取了一些精华整理出来的方案。

网上一些方案的需求又不符合我们项目逻辑,如:
package com.littlejerk.sample.util;

import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @author : HHotHeart
 * @date : 2021/8/23 22:07
 * @desc : 描述
 */
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();
    }
}

然后在Actiivty的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();
                    UIToast.showShort(AppLifeCycleImpl.sDES);
                    Looper.loop();
                }
            }
        }).start();
    }
    
这个方案实现的情况:
  • 用户主动退出 APP ( 返回键 、HOME 键)这种情况下我们不需要给用户弹出警告提示
  • APP 在锁屏再解锁的情况下我们不需要给用户弹出警告提示
  • 其他应用突然覆盖在我们 APP 上时给出合理的警告提示
实际检测中发现这个方案也检测不到发生页面劫持的情况,所以这个方法也是很鸡肋,再加上其实现的需求和我们项目也不一样。

其实相信大多数app对于进入后台的行为都会toast一下,如果有其它应用的页面覆盖到我们的app,这时自己的app能够及时感知到者行为并且及时通知用户,这样才是比较好地防范劫持问题。

接下来我们以Activity的生命周期作文章,我们都知道Activity的跳转必然会涉及到生命周期的回调,如 A跳转到B生命周期方法回调:
  • A页面回调onPause();
  • B页面回调onCreate()、onResume(),然后回调A的onStop(),如果B页面是透明Activity,则不会回调A的onStop();
  • A页面如果跳转到其它app,则app内部肯定不会新建activity,即不会回调onCreate();
  • app内部activity之间的切换应该是流畅的(耗时会ANR),产生ANR情况大都是500ms后的了;
基于上述activity的回调和需求分析,我们可以设计这样的方案:在 Activity 生命周期走到 onPause 时,延时发送一个事件,该事件会触发一个 oast 提醒用户已离开本应用。然后在 onCreate、onResume 中移除延时事件。

上面分析得也差不多了,总得来说有两部分,一是发送延时通知和取消通知toast的工具类,二是监听activity生命周期,然后在合适的周期回调方法中去发送和取消通知。

通知发送和取消工具类如下:

package com.littlejerk.sample.util;

import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;

import com.littlejerk.library.manager.toast.UIToast;

/**
 * @author : HHotHeart
 * @date : 2021/8/24 20:43
 * @desc : 界面防劫持工具类,通过延时通知的发送和取消实现
 */
public class HijackingPrevent {

    public final static String sDES = "MDroidS正在后台运行,请注意了!";

    /**
     * 退出APP的标识
     */
    private boolean isExit = false;
    /**
     * 延时事件
     */
    private Runnable runnable;
    /**
     * 延时事件发送和取消
     */
    private Handler handler;

    /**
     * 创建单例
     */
    private HijackingPrevent() {
        handler = new Handler(Looper.getMainLooper());
        runnable = new Runnable() {
            @Override
            public void run() {
                if (isExit()) {
                    isExit = false;
                    UIToast.showShort(sDES);
                }
            }
        };
    }

    /**
     * 获取单例
     *
     * @return
     */
    public static HijackingPrevent getInstance() {
        return Holder.S_HIJACKING_PROVENT;
    }

    /**
     * Holder初始化单例
     */
    private static class Holder {
        private static final HijackingPrevent S_HIJACKING_PROVENT = new HijackingPrevent();
    }

    /**
     * 退出activity时,延时通知
     */
    public synchronized void delayNotify(Activity activity) {
        // 不需要通知,则返回
        if (!isNeedNotify(activity)) {
            return;
        }
        setExit(true);
        // 先移除已有的
        handler.removeCallbacks(runnable);
        handler.postDelayed(runnable, 500);

    }

    /**
     * 进入当前app activity时,移除通知
     */
    public synchronized void removeNotify() {
        if (isExit()) {
            setExit(false);
            handler.removeCallbacks(runnable);
        }
    }

    /**
     * 判断是否需要通知Toast
     */
    public synchronized boolean isNeedNotify(Activity activity) {
        if (activity == null) {
            return false;
        }
        String actName = activity.getClass().getName();
        if (TextUtils.isEmpty(actName)) {
            return false;
        }
        //除了申请权限的activity,其它都需要延时通知
        return !actName.contains("UtilsTransActivity");
    }

    /**
     * 是否退出app
     *
     * @return
     */
    public boolean isExit() {
        return isExit;
    }

    /**
     * 设置app退出与否标识
     *
     * @param isExit
     */
    public void setExit(boolean isExit) {
        this.isExit = isExit;
    }
}

对于监听activity生命周期方法,我们可以实现Application.ActivityLifecycleCallbacks接口,然后注册这个回调,至于何时发送这个通知,什么时候取消通知,前面也说的比较清楚。

public class ActivityLifeCycleImpl implements Application.ActivityLifecycleCallbacks {
    private static final String TAG = "ActivityLifeCycleImpl";
    @Override
    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
        UILog.e(TAG,"onActivityCreated" + activity.getClass().getName());
        // 移除通知
        HijackingPrevent.getInstance().removeNotify();

    }

    @Override
    public void onActivityStarted(@NonNull Activity activity) {

    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {
        UILog.e(TAG,"onActivityResumed");
        // 移除通知
        HijackingPrevent.getInstance().removeNotify();
    }

    @Override
    public void onActivityPaused(@NonNull Activity activity) {
        UILog.e(TAG,"onActivityPaused");
        // 延时通知
        HijackingPrevent.getInstance().delayNotify(activity);
    }

    @Override
    public void onActivityStopped(@NonNull Activity activity) {
        UILog.e(TAG,"onActivityStopped");
    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {

    }
}

在你的application的onCreate()方法中注册:

   registerActivityLifecycleCallbacks(new ActivityLifeCycleImpl());

其实到这里,方案实现的也差不多了,在这里我们可以观察到HijackingPrevent中对UtilsTransActivity做了拦截处理,UtilsTransActivity是PermissionUtils权限申请的页面,然后,在申请权限request()后调用取消通知的方法,如:
        PermissionUtils.permission(Manifest.permission.CAMERA)
                .callback(new PermissionUtils.SingleCallback() {
                    @Override
                    public void callback(boolean isAllGranted,
                                         @NonNull List<String> granted,
                                         @NonNull List<String> deniedForever,
                                         @NonNull List<String> denied) {

                    }
                }).request();

        HijackingPrevent.getInstance().removeNotify();

这样做的目的就是为了在申请权限的时候不用toast提醒用户的需求,具体看业务人员的要求。有时候一筹莫展时,出去冲浪一下,肯定会有意想不到的收获,站在巨人肩膀上才能看得更远。

最后的方案比较好地解决了合规检查界面防劫持问题,自己也尝试用这个劫持工具去测试其它银行app,发现大多数地都没有toast提示用户app进入后台。新时代农民工应学会思考,融会贯通。

APP合规检查系列文章:
Android 组件导出风险及防范
Android 申请权限前简单封装弹框阐述申请理由工具类,应付app合规检查

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黄小梁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值