最近,安全合规部门又对金融类、银行类app进行了大规模的多方面安全检查,其中有一项安全问题:Activity劫持。其实Android界面防劫持我们app这边也是做了的,但是为啥还会有这些问题呢?自我感觉就是绝不会有此类问题,于是我们向检测部门要了劫持工具,但是事实往往是打脸的。。。。。
那么什么是Activity劫持呢?简单的说就是我们APP正常的Activity界面被恶意攻替换上仿冒的恶意Activity界面进行攻击和非法用途。界面劫持攻击通常难被识别出来,其造成的后果必然会给用户带来严重损失。
举个例子来说,当用户打开安卓手机上的某一应用,进入到登陆页面,这时,恶意软件侦测到用户的这一动作,立即弹出一个与该应用界面相同的Activity,覆盖掉了合法的Activity,用户几乎无法察觉,该用户接下来输入用户名和密码的操作其实是在恶意软件的Activity上进行的,最终会发生什么就可想而知了。
那么应该怎么防护呢?目前是还没有什么专门针对 Activity 劫持的防护方法,因为,这种攻击是用户层面上的,现在还无法从代码层面上根除。但是,我们可以适当地在 APP 中给用户一些警示信息,如toast提示.“某某app正在后台运行”。
前面说到公司app被检测到Activity界面被劫持问题,其实我们项目也有代码逻辑处理劫持问题,但是现在看来还是不够完善的,项目中用的是监听app生命周期方法去做的,在app处于后台时,toast提醒用户,用到了两个库:
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() {
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void start() {
UILog.e("AppLifeCycleImpl start");
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void resume() {
UILog.e("AppLifeCycleImpl resume");
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void pause() {
UILog.e("AppLifeCycleImpl pause");
}
@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() {
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void start() {
UILog.e("AppLifeCycleImpl start");
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void resume() {
UILog.e("AppLifeCycleImpl resume");
isBackground = false;
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void pause() {
UILog.e("AppLifeCycleImpl pause");
if (AppUtils.isAppForeground()) {
return;
}
isBackground = true;
UIToast.showShort(sDES);
}
@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;
public class AntiHijackingUtil {
public static final String TAG = "AntiHijackingUtil";
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) {
runningActivityPackageName = getCurrentPkgName(context);
} else {
runningActivityPackageName =
activityManager.getRunningTasks(1).get(0).topActivity.getPackageName();
}
if (runningActivityPackageName != null) {
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) {
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++) {
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;
}
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());
}
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;
}
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;
public class HijackingPrevent {
public final static String sDES = "MDroidS正在后台运行,请注意了!";
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);
}
}
};
}
public static HijackingPrevent getInstance() {
return Holder.S_HIJACKING_PROVENT;
}
private static class Holder {
private static final HijackingPrevent S_HIJACKING_PROVENT = new HijackingPrevent();
}
public synchronized void delayNotify(Activity activity) {
if (!isNeedNotify(activity)) {
return;
}
setExit(true);
handler.removeCallbacks(runnable);
handler.postDelayed(runnable, 500);
}
public synchronized void removeNotify() {
if (isExit()) {
setExit(false);
handler.removeCallbacks(runnable);
}
}
public synchronized boolean isNeedNotify(Activity activity) {
if (activity == null) {
return false;
}
String actName = activity.getClass().getName();
if (TextUtils.isEmpty(actName)) {
return false;
}
return !actName.contains("UtilsTransActivity");
}
public boolean isExit() {
return 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进入后台。新时代农民工应学会思考,融会贯通。
|