最近,安全合规部门又对金融类、银行类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 : appListapp = (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()方法调用:

@Overrideprotected void onStop() {super.onStop();new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic 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";@Overridepublic void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {UILog.e(TAG,"onActivityCreated" + activity.getClass().getName());// 移除通知HijackingPrevent.getInstance().removeNotify();}@Overridepublic void onActivityStarted(@NonNull Activity activity) {}@Overridepublic void onActivityResumed(@NonNull Activity activity) {UILog.e(TAG,"onActivityResumed");// 移除通知HijackingPrevent.getInstance().removeNotify();}@Overridepublic void onActivityPaused(@NonNull Activity activity) {UILog.e(TAG,"onActivityPaused");// 延时通知HijackingPrevent.getInstance().delayNotify(activity);}@Overridepublic void onActivityStopped(@NonNull Activity activity) {UILog.e(TAG,"onActivityStopped");}@Overridepublic void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}@Overridepublic void onActivityDestroyed(@NonNull Activity activity) {}
}

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

registerActivityLifecycleCallbacks(new ActivityLifeCycleImpl());

其实到这里,方案实现的也差不多了,在这里我们可以观察到HijackingPrevent中对UtilsTransActivity做了拦截处理,UtilsTransActivity是PermissionUtils权限申请的页面,然后,在申请权限request()后调用取消通知的方法,如:

        PermissionUtils.permission(Manifest.permission.CAMERA).callback(new PermissionUtils.SingleCallback() {@Overridepublic 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合规检查


Android Activity防劫持方案相关推荐

  1. Android 界面防劫持

    目录 一.什么是页面劫持 二.页面劫持常用攻击手段 三.如何防范页面劫持 3.1 用户方面 3.2 开发者方面 四.参考 一.什么是页面劫持 界面劫持是指在Android系统中,恶意软件通过监控目标软 ...

  2. APP界面防劫持,处于后台时弹窗提示

    当程序处于后台时弹窗提醒,防止其他APP界面劫持 1. 实现对APP所有Activity生命周期的监控 顶层activity中onStope方法被执行,则认为程序处于后台.由于Activity被销毁或 ...

  3. Android 系统(70)---Android刘海屏适配方案

    Android刘海屏适配方案 什么是刘海屏 随着iPhone X发布,国内一些厂商也推出了刘海屏手机,即将发布的Android p也提供了对刘海屏的支持.so,我们的app也要提前做好适配. 屏幕的正 ...

  4. Android推送集成方案总结

    Android推送集成方案总结 刚做完推送集成方案,记录下坑. 这里记录的特性和使用时针对写blog时采用的sdk的,具体使用流程和限制还请参考官方给出的sdk. 1.推送规则 小米手机用小米推送: ...

  5. 网站app被劫持怎么办?HTTPDNS阿里云域名防劫持, DNSPod 移动解析服务 D+

    网站app被劫持怎么办?HTTPDNS阿里云域名防劫持, DNSPod 移动解析服务 D+ HTTPDNS_移动开发_域名解析_域名防劫持-阿里云 https://www.aliyun.com/pro ...

  6. APP风控SDK之Android APP防作弊SDK解决方案

     推荐阅读 ​Android APP防作弊SDK解决方案 APP防代理抓包 APP防Fiddler抓包 APP防Burp Suite抓包 移动安全和Web安全 kali渗透测试环境搭建 Web安全|d ...

  7. h5 神策埋点_神策Android全埋点方案分析

    神策Android全埋点方案 原理简单分析: Activity生命周期通过监听Application.ActivityLifecycleCallbacks,fragment的生命周期 及一些点击事件则 ...

  8. android如何避免钓鱼页面,Android应用钓鱼劫持风险的检测与防范

    Android应用钓鱼劫持风险的检测与防范 Detection and Prevention of the Phishing Risk of Android Application DOI: 10.1 ...

  9. Android 开发之多种方案PDF阅读

    Android 开发之多种方案PDF阅读 最近开发中涉及到阅读港股公告,但是HK股票的公告都是坑,居然是pdf的,所以没办法,就要研究安卓pdf阅读,期间踩了点坑-- 安卓的webview与ios 的 ...

  10. android获取activity截图,Android Activity 不能被截屏的解决方法

    Android Activity 不能被截屏的解决方法 在Activity 添加即可 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECU ...

最新文章

  1. Windows Server 2008 R2 Beta VHD镜像文件发布
  2. 求积问题[体会Python至简之道]
  3. 云安全,到底是什么一回事?
  4. 欢迎使用Markdown
  5. 一块V100运行上千个智能体、数千个环境,这个曲率引擎框架实现RL百倍提速
  6. 算法学习--Day5
  7. Java经典实例:比较浮点数
  8. Web service 超过了最大请求长度错误解决
  9. pacf和acf_如何通过Wordpress API,ACF和Express.js使Wordpress更加令人兴奋
  10. 生活中java继承例子_简单继承例子:java
  11. Postfix邮件服务系统
  12. Java学习笔记——IO
  13. Seaweedfs安装配置使用及mount挂载
  14. 彩色电视制式与色度解码电路
  15. STM32的延迟函数1us、1ms
  16. 2021-05-13
  17. 自驾游app开发的前景和优势
  18. 函数签名function signature是什么意思
  19. WEB测试项目实战——2.产品需求与设计评审
  20. 计算机中正斜杠/与反斜杠\的区别

热门文章

  1. 遥感计算机的分类原理,遥感图像的计算机分类
  2. 计算机操作系统|汤小丹|第四版|习题答案(一)
  3. m3 pcb开孔 螺丝_螺丝过孔工艺孔底孔尺寸参照表
  4. 队列的实现(C语言版)
  5. matlab算系统阻抗,基于MatlabRF系统阻抗匹配设计.pdf
  6. python 调用bat失败_死机、卡顿、蓝屏,Python部门的老江湖告诉我的一些超级变态代码...
  7. 微信小程序动画效果集合
  8. 全局唯一序列号生成器-支持分布式
  9. mysql实体监听器_监听器模式(Listener)
  10. 大数据十大核心原理(互联网上整理)