Android安全开发初步(一)

不看不知道,原来经常写的代码存在很多的安全问题,安全这一块着实不简单(我是这么认为的),很多原理其实是看不太懂,只怪自己水平不够,买的几本Android进阶书还没看….哎~~意识到了时间的宝贵

Handler内存泄漏

在使用Handler的时候,我们经常会写下面的一段代码,但是这样会导致严重的内存泄漏问题

1
2
3
4
5
6
7
8
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
}

问题分析

当Android应用程序启动时,framework会为该应用程序的主线程创建一个Looper对象。这个Looper对象包含一个简单的消息队列Message Queue,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序framework事件,例如Activity生命周期方法调用、button点击等,这些消息都会被添加到消息队列中并被逐个处理。
另外,主线程的Looper对象会伴随该应用程序的整个生命周期。

然后,当主线程里,实例化一个Handler对象后,它就会自动与主线程Looper的消息队列关联起来。所有发送到消息队列的消息Message都会拥有一个对Handler的引用,所以当Looper来处理消息时,会据此回调[Handler#handleMessage(Message)]
当使用内部类(包括匿名类)来创建Handler的时候,Handler对象也会隐式地持有一个外部类对象(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)
这里补充一点java知识:

在java里,非静态内部类匿名类 都会潜在的引用(持有)它们所属的外部类。但是,静态内部类却不会。
个人理解:要不然你内部类怎么可以使用外部类的变量?还有.this这种形式

所以:
只要有未处理的消息,那么消息会引用handler(通常在后台线程中),非静态的handler又会引用外部类,即Activity,导致Activity无法被回收,造成泄漏。
同理,如果你用匿名Runnable来实现消息的发送,它属于非静态匿名类,同样会引用外部类。

一般情况下,我们是等耗时操作完成后进行发送消息,进行处理,这时候Activity应该要正常销毁的(也可能在等待过程中用户退出了Activity),上面的代码会造成其他的对象持有Activity的引用导致Activity无法被GC回收,导致内存泄漏。

解决方法

通过程序逻辑来进行保护

  1. 在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
  2. 如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
1
2
3
4
5
public void onDestroy() {
super.onDestroy();
//清除消息队列,也防止了内存泄漏
mHandler.removeCallbacksAndMessages(null);
}

handler是线程通讯工具类。用于传递消息。它有两个队列:
1.消息队列
2.线程队列
消息队列使用sendMessage和HandleMessage的组合来发送和处理消息。
线程队列类似一段代码,或者说一个方法的委托,用户传递方法。使用post,postDelayed 添加委托,使用 removeCallbacks移除委托。
这里看到两张图不错:
handler类结构
handler消息处理

更多可参考:
http://www.jianshu.com/p/e8f3c9e0b873

将Handler声明为静态类

如果我们改为静态类,那么它不会持有外部的引用,Activity可以被GC回收了,就这样:

1
2
3
4
5
6
static class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}

但是又出现了一个问题,应该注意到了,不会持有 Activity 的引用那么怎么可能操作 Activity 中的对象呢?我们可以加一个Activity 的弱引用 (WeakReference)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static class MyHandler extends Handler {
WeakReference<Activity > mActivityReference;

//构造函数
MyHandler(Activity activity) {
mActivityReference= new WeakReference<Activity>(activity);
}

@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
mImageView.setImageBitmap(mBitmap);
}
}
}

知识补充

什么是WeakReference?

WeakReference弱引用,与强引用(即我们常说的引用)相对,它的特点是,GC在回收时会忽略掉弱引用,即就算有弱引用指向某对象,但只要该对象没有被强引用指向(实际上多数时候还要求没有软引用,但此处软引用的概念可以忽略),该对象就会在被GC检查到时回收掉。对于上面的代码,用户在关闭Activity之后,就算后台线程还没结束,但由于仅有一条来自Handler的弱引用指向Activity,所以GC仍然会在检查的时候把Activity回收掉。这样,内存泄露的问题就不会出现了。

关于静态内部类

静态内部类只能访问外部类的静态成员。
静态内部类的对象可以直接生成:Outer.Inner in=new Outer.Inner();而不需要通过生成外部类对象来生成。

静态内部类和非静态内部类一样,都是在被调用时才会被加载
所以可以这么理解:调用外部类的静态变量,静态方法可以让外部类得到加载,不过这里静态内部类没有被加载

其他地方ViewHolder内部类 定义为静态的,是一种好习惯。

其他

很多人认为,静态变量能不用就不用,一旦静态生命周期必然很长,大部分情况下Handler用来更新UI,既然Activity被关闭了,那么Handler也没必要存在了,这种情况下也许采用第一种方法比较更好。
但是也有说静态内部类和静态变量还是有区别的,这里并不会有什么大问题….
静态内部类编译的时候是跟外部类同一级别的,使用static就有了限制,占用资源也就小了

还有就是,貌似 Android 对弱引用的支持并不太好,所以还是尽量少用

参考

java静态内部类加载
https://my.oschina.net/rengwuxian/blog/181449
http://www.jianshu.com/p/cb9b4b71a820

代码混淆

一般情况下我们使用ProGuard工具来提供代码混淆
PS:因为特殊原因这里是在Eclipse下进行混淆,现在基本都是AS了,那个以后再来补充,其实都差不多,AS下更加简单吧..

ProGuard是什么

ProGuard是一个工具,用来混淆和优化Java代码。
工作方式:移除无效的代码,将代码中的类名、函数名替换为晦涩难懂的名字。
注意,它只能混淆Java代码,Android工程中Native代码,资源文件(图片、xml),它是无法混淆的。
玩过Android逆向的知道,经过混淆后的类反编译出来名都是单个字母,完全看不懂

如何开启混淆

修改Android工程根目录下的project.properties文件,把proguard.config=….这一行前面的注释“#”去掉。
这一行指定了系统默认的proguard配置文件,位于Android SDK/tools/proguard目录下。
当然,你也可以自己编写配置文件,但不建议这样做,因此系统默认的配置已经涵盖了许多通用的细节,如果你还有额外的配置,可以添加在 proguard-project.txt 文件中。
注意: 只有在生成release版本的apk时,混淆配置才会起作用,debug版本的apk不会进行混淆。

那些需要手动配置

系统默认的配置已经涵盖了大部分的内容,但是如果你的工程中有如下内容,则需要手动添加配置到proguard-project.txt文件中。

  • 只在 AndroidManifest.xml 引用的类
  • 通过JNI回调方式被调用的函数
  • 运行时动态调用的函数或者成员变量
  • 反射用到的类
  • WebView中JavaScript调用的方法
  • Layout文件引用到的自定义View
  • 一些引入的第三方库(一般都会有混淆说明的) 这里推荐两个开源项目,里面收集了一些第三方库的混淆规则

当然,如果你不确定哪些需要手动配置,可以以默认的配置生成程序,当运行中发现ClassNotFoundException异常时,即可找到哪个类不该被混淆。

手动配置的规则

手动添加的配置,一般以“-keep”开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//不混淆Test的构造函数
-keepclassmembers class
com.example.Test {
public <init>(int,int);
}

//不混淆package com.example下的所有类/接口
-keep class com.example.** { * ; }

//不混淆com.example.Test类:
-keep class com.example.Test { * ; }
/*如果希望不混淆某个接口,则把上述命令中的class替换为interface即可。*/

//不混淆特定的函数
-keepclassmembers class
com.example.Test {
public void setTestString(java.lang.String);
}

//不混淆com.Test类的子类
-keep public class * extends com.example.Test
//不混淆com.example.TestInterface的实现
-keep class * implements com.example.TestInterface {
public static final com.example.TestInterface$Creator *;
}

//排除第三方依赖android-support-v4为例,AS中已自动处理无需手动添加
-libraryjars
libs/android-support-v4.jar
-dontwarn
android.support.v4.**{*;}
-keep class android.support.v4.**{*;}
-keep interface android.support.v4.**{*;}
/*注意: 需要添加dontwarn,因为默认情况下proguard会检查每一个引用是否正确,但是第三方库里往往有些不会用到的类,没有正确引用,所以如果不配置的话,系统会报错。*/

参考

http://ticktick.blog.51cto.com/823160/1413066
https://segmentfault.com/a/1190000004461614

Activity安全

有些说实话还没完全看懂,是太心急了么?逼不得已…

exported

不准备对外公开的activity一定要设置为非公开,以防止被人非法调用(反编译下很容易就能找到,至于非法调用用于什么坏事我的安全意识还想不出来,反正关掉就是了,也许是用来做钓鱼页面)
同时,一定要注意的是, 非公开的Activity不能设置intent-filter
因为,如果假设在同一机器上,有另外一个app有同样的intent-filter的话, 调用该Activity的intent会唤醒Android的选择画面, 让你选择使用那个app接受该intent。这样就会事实上绕过了非公开的设置。
默认值:如果包含有intent-filter 默认值为true; 没有intent-filter默认值为false

1
2
3
4
<activity
android:name=".PrivateActivity"
android:label="@string/app_name"
android:exported="false" />

如果设置了导出权限,都可能被系统或者第三方的应用程序直接调出并使用。 组件导出可能导致登录界面被绕过、信息泄露、数据库SQL注入、DOS、恶意调用等风险。

主要作用是:是否支持其它应用调用当前组件。

更多请参考:android:exported 属性详解

合理的使用exported

我们接下来谈谈在开发中如何更合理设置exported。
Activity被调用的场景分为3种:封闭式半封闭式开放式

  • 封闭式
    被调用的Activity与调用的Context必须在同一个App,其他任何App不能调用
    这种是我们最常见的Activity,有2种情况:

    1. 没有intent-filter情况
      可以不设置exported或者设置exported为false

      1
      2
      3
      4
      5
      6
      7
      8
      <activity
      android:name=".SecondActivity"
      android:label="@string/app_name" />
      <!-- 或 -->
      <activity
      android:name=".SecondActivity"
      android:label="@string/app_name"
      android:exported="false"/>
    2. 没有intent-filter情况
      必须设置exported为false

      1
      2
      3
      4
      <activity
      android:name=".SecondActivity"
      android:label="@string/app_name"
      android:exported="false"/>
  • 半封闭式
    被调用的Activity只能被部分其他App调用,如同一个公司的2个App之间
    这种场景下,除了满足封闭式设置外,还必须把调用App和被调用App设置相同的uid,即在2个App的AndroidManifest.xml添加相同的android:sharedUserId,如

    1
    2
    3
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:sharedUserId="com.example.categorytest">
  • 开放式
    可以被任何App调用 这种场景主要是对外接口,如微信、微博的分享接口。大多数情况下,这样的Activity都会有intent-filter,因此也没必要显式地把exported设为true,不设是可以的,当然不能设为false。
    但如果你没有intent-filter,那就必须显式地把exported设为true。 当然,对于三方app接口的intent-filter设置还有一些要求,如在隐式intent调用必须添加android.intent.category.DEFAULT

补充:
关于主Activity,应用程序需要包含至少一个Activity组件来支持MAIN操作和LAUNCHER种类,即为主Activity
暴露的Activity组件不包括主Activity,如果你把主Activity设置exported为false了,那你的应用就甭想运行了,正常的应用来说。

参考:
http://blog.csdn.net/gorgle/article/details/51420586
http://www.itdadao.com/articles/c15a12377p0.html

不要指定taskAffinity

Android中的activity全都归属于task管理 , 简单说来task是一种stack(堆栈)的数据结构, 先入后出。
一般来说, 如果不指明归属于什么task, 同一个app内部的所有Activity都会存续在一个task中,task的名字就是app的packageName。
因为在同一个andorid设备中,不会有两个同packageName的app存在,所以能保证Activity不被攻击。

1
2
3
4
<activity android:name=".Activity1"
android:taskAffinity="com.winuxxan.task"
android:label="@string/app_name">
</activity>

恶意软件中的Activity如果也声明为同样的taskAffinity,那他的Activity就会启动到你的task中,就会有机会拿到你的intent(我们一般会在intent中对数据进行加密处理)

那么taskAffinity到底什么用呢?
它的作用是描述了不同Activity之间的亲密关系。拥有相同的taskAffinity的Activity是亲密的,它们之间在相互跳转时,会位于同一个task中,而不会新建一个task!
简单说就是意味着这activity更喜欢哪个TESK

一个新的activity,默认地启动到调用startActivity()方法的activity的task中。它和调用者放到同样的back stack中。然而,如果传递给startActivity()的intent包含FLAG_ACTIVITY_NEW_TASK标志,系统将会需找一个不同的task来容纳新的activity。通常,它是一个新的task。然而,不是必须都是如此的。如果已经存在一个和新的activity具有相同的affinity的task,新activity会启动到该task中。如果没有,它会启动一个新的task。

当一个activity它的allowTaskReparenting属性设置为true
这种情况,activity可以从它启动的task移到和它有相同affinity的task,当该task来到前台的时候。

不要指定LaunchMode(默认standard模式)

Android中Activity的LaunchMode分成以下四种

  • Standard
    这种方式打开的Activity不会被当作rootActivity,会生成一个新的Activity的instance(实例),会和打开者在同一个task内
  • singleTop
    和standard基本一样,唯一的区别在于如果当前task第一个Activity就是该Activity的话,就不会生成新的instance
  • singleTask
    系统会创建一个新task(如果没有启动应用)和一个activity新实例在新task根部,然后,如果activity实例已经存在单独的task中,系统会调用已经存在activity的 onNewIntent()方法,而不是存在新实例,仅有一个activity实例同时存在。
  • singleInstance
    和singleTask相似,除了系统不会让其他的activities运行在所有持有的task实例中,这个activity是独立的,并且task中的成员只有它,任何其他activities运行这个activity都将打开一个独立的task。

所有发送给root Activity(根Activiy)的intent都会在android中留下履历(入侵关键,大概)。所以一般来说严禁用singleTask或者singleInstance来启动画面。
然而,即使用了standard来打开画面,也可能会出问题,比如如果调用者的Activity是用singleInstance模式打开,即使用standard模式打开被调用Activity,因为调用者的Activity task是不能有其他task的, 所以android会被迫生成一个新的task,并且把被调用者塞进去,最后被调用者就成了rootActivity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<!-- root Activity以”singleInstance”模式启动 -->
<!-- 不设置taskAffinity-->
<activity
android:name=".PrivateUserActivity"
android:label="@string/app_name"
android:launchMode="singleInstance" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 非公開Activity -->
<!-- 启动模式为”standard” -->
<!-- 不设置taskAffinity-->
<activity
android:name=".PrivateActivity"
android:label="@string/app_name"
android:exported="false" />
</application>

FLAG_ACTIVITY_NEW_TASK

发给Activity的intent不要设定为FLAG_ACTIVITY_NEW_TASK
就算上面的Activity的lauchMode设置完善了, 在打开intent的时候还是能指定打开模式。
比如在intent中指明用FLAG_ACTIVITY_NEW_TASK模式的话,发现该activity不存在的话,就会强制新建一个task。如果同时设置了FLAG_ACTIVITY_MULTIPLE_TASK+FLAG_ACTIVITY_NEW_TASK,就无论如何都会生成新的task,该Activity就会变成rootActiviy,并且intent会被留成履历

Intent中数据的加密

这个前面提到过,Activity中数据的传递都依靠intent, 很容易被攻击, 所以 就算同一个app内部传递数据, 最好还是要加密, 加密算法很多。

明确ActivityName发送Intent

避免被恶意软件所截取,主要是注意在不同APP直接的数据发送

1
2
3
4
5
6
7
8
9
10
Intent intent = new Intent();
intent.setClassName(
"org.jssec.android.activity.publicactivity",
"org.jssec.android.activity.publicactivity.PublicActivity");
startActivity(intent);
//另一种方式
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.otherapp",
"com.example.otherapp.MainActivity2"));
startActivity(intent);

不是指明了packageName和ActivityName就能避免所有的问题,如果有一个恶意软件故意做成和你发送目标同PackageName, 同ActivityName, 此时的intent就会被截取.
对于这两种方式经查看源码发现 Intent 的setClass() 方法的实现正是使用ComponentName 类:

1
2
3
4
public Intent setClass(Context packageContext,Class<?> cls){
mComponent=new ComponentName(packageContext,cls);
return this;
}

接收intent时明确对方的身份

一个好方法是比对对方的app的hashcode。
当前,前提是调用者要用startActivityForResult(),因为只有这个方法,被调用者才能得到调用者的packageName

intent数据泄漏到LogCat

1
2
3
4
5
6
7
8
9
//Intent中发送的数据就会被自动写入LogCat
Uri uri = Uri.parse("mailto:[email protected]");
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
startActivity(intent);
//这样写就能避免
Uri uri = Uri.parse("mailto:");
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra(Intent.EXTRA_EMAIL, new String[] {"[email protected]"});
startActivity(intent);

其他

注意下这个权限<uses-permission android:name="android.permission.GET_TASKS" />
有了这个权限就能取出这台手机上所有task上所有根Activity接受到的intent,大概
所以:所有根Activity中的intent都能被所有app共享

补充

关于上面介绍的那些每一个其实都大有学问呐,不过都没深入进去追究,目前水平也是有限。
这里看到了LaunchMode,再补充下它的应用场景吧:

singleTop适合接收通知启动的内容显示页面。
例如,某个新闻客户端的新闻内容页面,如果收到10个新闻推送,每次都打开一个新闻内容页面是很烦人的。
singleTask适合作为程序入口点。
例如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
singleInstance适合需要与程序分离开的页面。
例如闹铃提醒,将闹铃提醒与闹铃设置分离。
singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。

TaskAffinity对LaunchMode的影响:
不指定TaskAffinity,singleTask会在默认的task 中执行,这个符合预期,一般也都是这么用的,不需要指定。
不指定TaskAffinity,singleInstance之后启动的页面不能放倒singleInstance所在那个task中,会放倒默认的task中,不过一般singleInstance也不适合作为程序中间页。

http://blog.csdn.net/xiaodongrush/article/details/28597855

参考

http://blog.csdn.net/thestoryoftony/article/details/9370427
http://droidyue.com/blog/2015/08/16/dive-into-android-activity-launchmode/index.html

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~