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 : 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防劫持方案相关推荐
- Android 界面防劫持
目录 一.什么是页面劫持 二.页面劫持常用攻击手段 三.如何防范页面劫持 3.1 用户方面 3.2 开发者方面 四.参考 一.什么是页面劫持 界面劫持是指在Android系统中,恶意软件通过监控目标软 ...
- APP界面防劫持,处于后台时弹窗提示
当程序处于后台时弹窗提醒,防止其他APP界面劫持 1. 实现对APP所有Activity生命周期的监控 顶层activity中onStope方法被执行,则认为程序处于后台.由于Activity被销毁或 ...
- Android 系统(70)---Android刘海屏适配方案
Android刘海屏适配方案 什么是刘海屏 随着iPhone X发布,国内一些厂商也推出了刘海屏手机,即将发布的Android p也提供了对刘海屏的支持.so,我们的app也要提前做好适配. 屏幕的正 ...
- Android推送集成方案总结
Android推送集成方案总结 刚做完推送集成方案,记录下坑. 这里记录的特性和使用时针对写blog时采用的sdk的,具体使用流程和限制还请参考官方给出的sdk. 1.推送规则 小米手机用小米推送: ...
- 网站app被劫持怎么办?HTTPDNS阿里云域名防劫持, DNSPod 移动解析服务 D+
网站app被劫持怎么办?HTTPDNS阿里云域名防劫持, DNSPod 移动解析服务 D+ HTTPDNS_移动开发_域名解析_域名防劫持-阿里云 https://www.aliyun.com/pro ...
- APP风控SDK之Android APP防作弊SDK解决方案
推荐阅读 Android APP防作弊SDK解决方案 APP防代理抓包 APP防Fiddler抓包 APP防Burp Suite抓包 移动安全和Web安全 kali渗透测试环境搭建 Web安全|d ...
- h5 神策埋点_神策Android全埋点方案分析
神策Android全埋点方案 原理简单分析: Activity生命周期通过监听Application.ActivityLifecycleCallbacks,fragment的生命周期 及一些点击事件则 ...
- android如何避免钓鱼页面,Android应用钓鱼劫持风险的检测与防范
Android应用钓鱼劫持风险的检测与防范 Detection and Prevention of the Phishing Risk of Android Application DOI: 10.1 ...
- Android 开发之多种方案PDF阅读
Android 开发之多种方案PDF阅读 最近开发中涉及到阅读港股公告,但是HK股票的公告都是坑,居然是pdf的,所以没办法,就要研究安卓pdf阅读,期间踩了点坑-- 安卓的webview与ios 的 ...
- android获取activity截图,Android Activity 不能被截屏的解决方法
Android Activity 不能被截屏的解决方法 在Activity 添加即可 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECU ...
最新文章
- Windows Server 2008 R2 Beta VHD镜像文件发布
- 求积问题[体会Python至简之道]
- 云安全,到底是什么一回事?
- 欢迎使用Markdown
- 一块V100运行上千个智能体、数千个环境,这个曲率引擎框架实现RL百倍提速
- 算法学习--Day5
- Java经典实例:比较浮点数
- Web service 超过了最大请求长度错误解决
- pacf和acf_如何通过Wordpress API,ACF和Express.js使Wordpress更加令人兴奋
- 生活中java继承例子_简单继承例子:java
- Postfix邮件服务系统
- Java学习笔记——IO
- Seaweedfs安装配置使用及mount挂载
- 彩色电视制式与色度解码电路
- STM32的延迟函数1us、1ms
- 2021-05-13
- 自驾游app开发的前景和优势
- 函数签名function signature是什么意思
- WEB测试项目实战——2.产品需求与设计评审
- 计算机中正斜杠/与反斜杠\的区别
热门文章
- 遥感计算机的分类原理,遥感图像的计算机分类
- 计算机操作系统|汤小丹|第四版|习题答案(一)
- m3 pcb开孔 螺丝_螺丝过孔工艺孔底孔尺寸参照表
- 队列的实现(C语言版)
- matlab算系统阻抗,基于MatlabRF系统阻抗匹配设计.pdf
- python 调用bat失败_死机、卡顿、蓝屏,Python部门的老江湖告诉我的一些超级变态代码...
- 微信小程序动画效果集合
- 全局唯一序列号生成器-支持分布式
- mysql实体监听器_监听器模式(Listener)
- 大数据十大核心原理(互联网上整理)