Android Activity防劫持方案
生活随笔
收集整理的這篇文章主要介紹了
Android Activity防劫持方案
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
最近,安全合規(guī)部門又對金融類、銀行類app進(jìn)行了大規(guī)模的多方面安全檢查,其中有一項(xiàng)安全問題:Activity劫持。其實(shí)Android界面防劫持我們app這邊也是做了的,但是為啥還會(huì)有這些問題呢?自我感覺就是絕不會(huì)有此類問題,于是我們向檢測部門要了劫持工具,但是事實(shí)往往是打臉的。。。。。
那么什么是Activity劫持呢?簡單的說就是我們APP正常的Activity界面被惡意攻替換上仿冒的惡意Activity界面進(jìn)行攻擊和非法用途。界面劫持攻擊通常難被識(shí)別出來,其造成的后果必然會(huì)給用戶帶來嚴(yán)重?fù)p失。
舉個(gè)例子來說,當(dāng)用戶打開安卓手機(jī)上的某一應(yīng)用,進(jìn)入到登陸頁面,這時(shí),惡意軟件偵測到用戶的這一動(dòng)作,立即彈出一個(gè)與該應(yīng)用界面相同的Activity,覆蓋掉了合法的Activity,用戶幾乎無法察覺,該用戶接下來輸入用戶名和密碼的操作其實(shí)是在惡意軟件的Activity上進(jìn)行的,最終會(huì)發(fā)生什么就可想而知了。
那么應(yīng)該怎么防護(hù)呢?目前是還沒有什么專門針對 Activity 劫持的防護(hù)方法,因?yàn)?#xff0c;這種攻擊是用戶層面上的,現(xiàn)在還無法從代碼層面上根除。但是,我們可以適當(dāng)?shù)卦?APP 中給用戶一些警示信息,如toast提示.“某某app正在后臺(tái)運(yùn)行”。
前面說到公司app被檢測到Activity界面被劫持問題,其實(shí)我們項(xiàng)目也有代碼邏輯處理劫持問題,但是現(xiàn)在看來還是不夠完善的,項(xiàng)目中用的是監(jiān)聽app生命周期方法去做的,在app處于后臺(tái)時(shí),toast提醒用戶,用到了兩個(gè)庫: //Gooogle官方獲取App生命周期的監(jiān)聽器implementation "android.arch.lifecycle:extensions:1.1.1"annotationProcessor "android.arch.lifecycle:compiler:1.1.1" 接著需要實(shí)現(xiàn)LifecycleObserver public class AppLifeCycleImpl implements LifecycleObserver {private final static String sDES = "MDroidS正在后臺(tái)運(yùn)行,請注意了!";public AppLifeCycleImpl() {}@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)public void create() {}//App處于前臺(tái)可見狀態(tài)。@OnLifecycleEvent(Lifecycle.Event.ON_START)public void start() {UILog.e("AppLifeCycleImpl start");}//App重新進(jìn)入前臺(tái)@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)public void resume() {UILog.e("AppLifeCycleImpl resume");}//此后App進(jìn)入不可見狀態(tài)/后臺(tái)@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)public void pause() {UILog.e("AppLifeCycleImpl pause");}//App進(jìn)入后臺(tái)或者熄滅屏幕@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中注冊監(jiān)這個(gè)Observer ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifeCycleImpl()); 在這里會(huì)有個(gè)問題,當(dāng)我們申請權(quán)限時(shí)或在個(gè)別手機(jī)上這個(gè)玩意也會(huì)吐司,我真是吐了,所以在這個(gè)基礎(chǔ)上又做了一些操作,完善后的AppLifeCycleImpl: public class AppLifeCycleImpl implements LifecycleObserver {private final static String sDES = "MDroidS正在后臺(tái)運(yùn)行,請注意了!";private boolean isBackground = false;public AppLifeCycleImpl() {}@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)public void create() {}//App處于前臺(tái)可見狀態(tài)。@OnLifecycleEvent(Lifecycle.Event.ON_START)public void start() {UILog.e("AppLifeCycleImpl start");}//App重新進(jìn)入前臺(tái)@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)public void resume() {UILog.e("AppLifeCycleImpl resume");isBackground = false;}//此后App進(jìn)入不可見狀態(tài)/后臺(tái)@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)public void pause() {// 放在pause中,加快顯示防劫持的提示信息UILog.e("AppLifeCycleImpl pause");// 增加appToBackground判斷,兼容部分手機(jī)誤判if (AppUtils.isAppForeground()) {return;}isBackground = true;UIToast.showShort(sDES);}//App進(jìn)入后臺(tái)或者熄滅屏幕@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");} } 其實(shí)就是加了一層判斷和一個(gè)isBackground 標(biāo)識(shí),其中isAppForeground()方法在uitlcodex庫里,引入即可: implementation'com.blankj:utilcodex:1.30.6' 然后就是這個(gè)玩意也被檢測出頁面劫持問題,其實(shí)大多數(shù)開發(fā)者想到的就是用這個(gè)去監(jiān)聽app生命周期,畢竟比較簡單,但是現(xiàn)在這個(gè)不中用了,于是上網(wǎng)一搜尋找解決辦法,網(wǎng)上的辦法都是大同小異,多多少少都會(huì)有一些問題,最后吸取了一些精華整理出來的方案。
網(wǎng)上一些方案的需求又不符合我們項(xiàng)目邏輯,如: 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";/*** 檢測當(dāng)前Activity是否安全*/public static boolean checkActivity(Context context) {PackageManager pm = context.getPackageManager();// 查詢所有已經(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;}// 白名單比對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 {// 通過反射獲取進(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 : appListapp = (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)程對象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)用包名稱** @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();} } 然后在Actiivty的onStop()方法調(diào)用: @Overrideprotected void onStop() {super.onStop();new Thread(new Runnable() {@Overridepublic 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();UIToast.showShort(AppLifeCycleImpl.sDES);Looper.loop();}}}).start();} 這個(gè)方案實(shí)現(xiàn)的情況:
- 用戶主動(dòng)退出 APP ( 返回鍵 、HOME 鍵)這種情況下我們不需要給用戶彈出警告提示
- APP 在鎖屏再解鎖的情況下我們不需要給用戶彈出警告提示
- 其他應(yīng)用突然覆蓋在我們 APP 上時(shí)給出合理的警告提示
其實(shí)相信大多數(shù)app對于進(jìn)入后臺(tái)的行為都會(huì)toast一下,如果有其它應(yīng)用的頁面覆蓋到我們的app,這時(shí)自己的app能夠及時(shí)感知到者行為并且及時(shí)通知用戶,這樣才是比較好地防范劫持問題。
接下來我們以Activity的生命周期作文章,我們都知道Activity的跳轉(zhuǎn)必然會(huì)涉及到生命周期的回調(diào),如 A跳轉(zhuǎn)到B生命周期方法回調(diào):
- A頁面回調(diào)onPause();
- B頁面回調(diào)onCreate()、onResume(),然后回調(diào)A的onStop(),如果B頁面是透明Activity,則不會(huì)回調(diào)A的onStop();
- A頁面如果跳轉(zhuǎn)到其它app,則app內(nèi)部肯定不會(huì)新建activity,即不會(huì)回調(diào)onCreate();
- app內(nèi)部activity之間的切換應(yīng)該是流暢的(耗時(shí)會(huì)ANR),產(chǎn)生ANR情況大都是500ms后的了;
上面分析得也差不多了,總得來說有兩部分,一是發(fā)送延時(shí)通知和取消通知toast的工具類,二是監(jiān)聽activity生命周期,然后在合適的周期回調(diào)方法中去發(fā)送和取消通知。
通知發(fā)送和取消工具類如下:
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 : 界面防劫持工具類,通過延時(shí)通知的發(fā)送和取消實(shí)現(xiàn)*/ public class HijackingPrevent {public final static String sDES = "MDroidS正在后臺(tái)運(yùn)行,請注意了!";/*** 退出APP的標(biāo)識(shí)*/private boolean isExit = false;/*** 延時(shí)事件*/private Runnable runnable;/*** 延時(shí)事件發(fā)送和取消*/private Handler handler;/*** 創(chuàng)建單例*/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時(shí),延時(shí)通知*/public synchronized void delayNotify(Activity activity) {// 不需要通知,則返回if (!isNeedNotify(activity)) {return;}setExit(true);// 先移除已有的handler.removeCallbacks(runnable);handler.postDelayed(runnable, 500);}/*** 進(jìn)入當(dāng)前app activity時(shí),移除通知*/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;}//除了申請權(quán)限的activity,其它都需要延時(shí)通知return !actName.contains("UtilsTransActivity");}/*** 是否退出app** @return*/public boolean isExit() {return isExit;}/*** 設(shè)置app退出與否標(biāo)識(shí)** @param isExit*/public void setExit(boolean isExit) {this.isExit = isExit;} } 對于監(jiān)聽activity生命周期方法,我們可以實(shí)現(xiàn)Application.ActivityLifecycleCallbacks接口,然后注冊這個(gè)回調(diào),至于何時(shí)發(fā)送這個(gè)通知,什么時(shí)候取消通知,前面也說的比較清楚。 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");// 延時(shí)通知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()); 其實(shí)到這里,方案實(shí)現(xiàn)的也差不多了,在這里我們可以觀察到HijackingPrevent中對UtilsTransActivity做了攔截處理,UtilsTransActivity是PermissionUtils權(quán)限申請的頁面,然后,在申請權(quán)限r(nóng)equest()后調(diào)用取消通知的方法,如: 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(); 這樣做的目的就是為了在申請權(quán)限的時(shí)候不用toast提醒用戶的需求,具體看業(yè)務(wù)人員的要求。有時(shí)候一籌莫展時(shí),出去沖浪一下,肯定會(huì)有意想不到的收獲,站在巨人肩膀上才能看得更遠(yuǎn)。
最后的方案比較好地解決了合規(guī)檢查界面防劫持問題,自己也嘗試用這個(gè)劫持工具去測試其它銀行app,發(fā)現(xiàn)大多數(shù)地都沒有toast提示用戶app進(jìn)入后臺(tái)。新時(shí)代農(nóng)民工應(yīng)學(xué)會(huì)思考,融會(huì)貫通。
APP合規(guī)檢查系列文章:
Android 組件導(dǎo)出風(fēng)險(xiǎn)及防范
Android 申請權(quán)限前簡單封裝彈框闡述申請理由工具類,應(yīng)付app合規(guī)檢查
總結(jié)
以上是生活随笔為你收集整理的Android Activity防劫持方案的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 简单分析暴风影音的最新0DAY菜鸟版
- 下一篇: htc816t Android go,移