×

Android App 安全策略

96
肖丹晨
2018.01.30 21:34* 字数 4358

前言
对于一款成熟的App,在某个时间点一定会开始涉及或者说要开始考虑安全问题。本着一贯的风格,把自己最近一段时间以来了解和搜集的安全方面的知识整理归纳下,一是方便内部培训,再就是分享出来,希望对大家的实际开发工作能有所帮助。
有兴趣的同学可以加入学习小组QQ群: 193765960做进一步的讨论。

版权归作者所有,转发请注明出处:https://www.jianshu.com/u/d43d948bef39

1. 代码保护

1.1 代码混淆

1.1.1 ProGuard

在Android Studio当中混淆APK,借助SDK中自带的Proguard工具,只需要修改build.gradle中的一行配置即可。如下所示:

release {
    minifyEnabled true
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}

其中minifyEnabled用于设置是否启用混淆,proguardFiles用于选定混淆配置文件。build.gradle中minifyEnabled的值,这里我们只需要把值改成true,打出来的APK包就会是混淆过的了。

使用ProGuard混淆的应用,通过apktool还是可以看到Manifest和res资源,使用dex2jar也可以看到混淆后的源码,虽然大部分代码已经混淆了。还是可以看个大概,而且通过smail的修改,重新进行逆向apk。
对于一般的应用足够,而对于应用中用了很多开源项目的,在业务层进行混淆也应该足够了。

参考文章:
Android安全攻防战,反编译与混淆技术完全解析(上)

1.1.2 DexGuard

DexGuard是收费的,是在Proguard基础上,加入了更多的保护措施。使用DexGuard混淆后,生成的apk文件,就无法正常使用apktool反编译了。
DexGuard:https://www.guardsquare.com/dexguard

即使做了混淆处理。面对那群让人心生敬畏的黑客和逆向工作者,我们还是太脆弱了。
那群鸟人,即使你的代码搞成.so他们也能给你反编译了,更别说他们还有大把的手段直接从内存下手。肿么办?凉拌。因为安全本来就是一个相对的概念,没有绝对的安全,就看谁的成本更大,手段更高超而已。

1.2 代码加固

除了常规的apk混淆之外,我更推荐大家进一步使用一款专业的加固工具对APK进行加固,如果公司肯花点钱,搞个收费版的效果更佳。市场上加固工具也有很多,哪位同学比较熟悉各种工具之间效能对比的话,还希望能够给代价讲讲,我再这里不对说了。

1.3 日志清理

黑客为了解析我们的代码逻辑,从日志下手对他们来说是非常常见而且有效的手段。日志中往往隐藏了对黑客来说非常重要的关键词和代码逻辑。
所以为了减少信息的泄漏或者说增加代码被解析的难读,release版本的日志屏蔽是非常重要的。
成熟的做法是APP构建自己的日志框架,在打release包时,能够方便的屏蔽掉所有的日志信息。
关于日志框架(LogUtil类等等),网上一大堆,读者自行百度吧。

2. 存储安全

数据存储安全是APP安全策略的重中之重,敏感数据像用户名、密码、秘钥、令牌以及通讯录等等的保护都是需要重点考虑的。
常见的问题:

  • 将隐私数据明文存储在外部存储中。
  • 将系统数据明文保存在外部存储中。
  • 将软件运行时依赖的数据存储在外部存储中。
  • 将软件安装包或二进制代码存储在外部存储中。
  • 全局可读写的内部存储文件。

下面介绍了将数据保存在设备上的三种基本方法。

2.1 数据存储

2.1.1 内部存储

默认情况下,您在内部存储空间中创建的文件仅供您的应用访问。这项保护措施由 Android 实现,而且这对于大多数应用来说足够了。

要为敏感数据提供额外的保护,您可以选择使用该应用无法直接访问的密钥来对本地文件进行加密。例如,您可以将密钥存储在 [KeyStore](https://developer.android.com/reference/java/security/KeyStore.html) 中,并使用未存储在相应设备上的用户密码加以保护。不过,如果攻击者获得超级用户权限,就可以在用户输入密码时进行监控,数据也就失去了这层保护屏障;但是,这种方式可以保护丢失设备上的数据,而无需进行文件系统加密

2.1.2 外部存储

外部存储设备(例如 SD 卡)上创建的文件不受任何读取和写入权限的限制。对于外部存储设备中的内容,不仅用户可以将其移除,而且任何应用都可以对其进行修改,因此最好不要使用外部存储设备来存储敏感信息。

就像处理来源不受信任的数据一样,您应对外部存储设备中的数据执行输入验证

2.1.3 content provider

如果您不打算向其他应用授予访问您的 ContentProvider的权限,请在应用清单中将其标记为 [android:exported=false];要允许其他应用访问存储的数据,请将 [android:exported]属性设置为 "true"

在创建要导出以供其他应用使用的 ContentProvider时,您可以在清单中指定允许读取和写入的单一权限,也可以针对读取和写入操作分别指定权限。

如果您要使用内容提供程序仅在自己的应用之间共享数据,最好将 [android:protectionLevel]属性设置为 "signature" 保护级别。

访问内容提供程序时,请使用参数化的查询方法(例如 query()update()delete()),以免产生来源不受信任的 SQL 注入风险。请注意,如果以组合用户数据的方式构建 selection 参数,然后再将其提交至参数化方法,则使用参数化方法可能不够安全

2.2 数字签名

数字签名技术是Android APP的安全基石之一,APP开发者使用私钥,以证书的形式对APP进行签名。

对关键的代码或者数据进行数字签名验证,可以有效的降低apk被篡改带来的风险。

2.3 秘钥保护

将重要的数据比如秘钥等存储在.so中,以APP的数字签名进行提取之前的验证条件,可以较好地降低敏感数据暴露的风险。
另外,如果需要使用动态的秘钥,则可以采用RSA非对称加密,从服务器获取动态秘钥;
获取动态秘钥后,使用AES对数据进行秘钥加密。

3. 组件安全

保护APP组件的途径有两条,其一是正确地使用AndroidManifest.xml文件,其二是在代码级别上强制进行权限检查。

参考书籍:
Android安全技术解密与规范 周圣涛著

3.1 原则

3.1.1 最小化组件暴露

检查AndroidManifest.xml中对组件的导出设置,如果组件不允许被其他APP调用,则android:exported的属性值需要设置为false。需要注意的是,如果APP的minsdkVersion的值设置<=16或组件设置了intent-filter,则android:exported的属性值默认是为true的。

3.1.2 设置组件访问权限

如果你的组件导出了,为了降低被任意APP滥用或攻击,你需要考虑是否需要对调用方做过滤和筛选。
如果需要过滤的话,比较好的做法是对组件声明android:permission
对permission可根据需要进一步设置android:protectedLevel,特别是当设置为signature级别表明只允许相同数字签名的APP访问组件。

3.1.3 组件传输数据验证

对组件之间,特别是跨应用的组件之间的数据传入和返回做数据验证,防止恶意数据传入,更要防止敏感数据的返回。
对于导出的组件,最好做intent的校验:
数据格式或数据结构校验;
异常处理:防止crash攻击。

3.1.4 暴露组件的代码检查

Android SDK 提供了很多API,我们能够利用这些API在程序运行时检查、执行、授予和撤销权限。

3.2 Activity组件安全

使用activity的风险和选择取决于需求对activity的定义。这里我们基于activity的使用方式将其分为4中类型:

  • 私有activity:只能有APP内部使用的activity,最安全的activity。
  • 公共activity:任意APP都能启动此类activity。
  • 伙伴activity:合作伙伴APP才能启动的activity,授权。
  • 内部activity:只有内部APP才能启动,全家桶。

3.2.1 私有activity

私有activity不能由其他应用程序启动,因此它是最安全的。当一个activity只在本APP内部使用时,只要将activity声明为显式intent调用方式,那么就不需要担心其他应用程序调用开启。
然而,有一个风险是第三方APP可以读取一个开启activity的intent。因此,为了避免第三方应用程序读取Intent来复制intent,我们可以在intent中放置一些extra做判断,避免第三方应用程序调用。

一个私有activity必须做到的关键几点如下:
1)不声明taskAffinity
2)不声明launchMode
3)设置exported属性为false
4)保证intent发送时的安全性,确定intent是来自本应用程序(签名验证和包名验证)
5)在确保是本应用程序发送Intent的时候,可以防止一些敏感信息
6)启动activity的时候不设置FLAG_ACTIVITY_NEW_TASK
7)使用显示的intent和指定的类的方式来调用一个activity
8)敏感信息放在extra中发送
9)在onActivityResult的时候需要对发挥的data小心处理

在AndroidManifest.xml中的声明如下:

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

在java代码中的操作如下:

private final static int REQUEST_CODE = 1;

@Override
public void onUseAactivityClick(View view){
  //显示intent调用,直接写类名
  Intent intent = new Intent(this, PrivateActivity.class);
  
  //设置包名
  intent.setPackage(getPackageName());
  intent.putExtra("PARAM","敏感数据");
  startActivityForResult(intent,REQUEST_CODE);
}

@Override
public void onActivityResult(int requestCode, int resultCode, intent data){
  super.onActivityResult(requestCode,resultCode,data);

  //判断result的状态
  if(resultCode != RESULT_OK){
    return;
  }

  switch(requestCode){
  case REQUEST_CODE:
    //注意返回数据的处理
    String result = data.getStringExtra("RESULT");
    ...
  break;
  }
}

3.2.2 公共activity

我能要知道,一旦一个activity声明为公共activity,那么,任何一个APP都可以发送一个intent来启动它,他的安全性也必须要更加注意了。

一个公共activity必须做到的关键几点如下:
1)设置exported属性为true
2)接收到intent的时候要小心处理
3)finish的时候,不要再返回intent中放置敏感信息

在AndroidManifest.xml中的声明如下:

<activity
        android:name=".PublicActivity"
        android:exported="true"
        android:label="@string/app_name" >
        <!--定义一个action-->
        <intent-filter>
            <action android:name="com.example.PUBLIC_ACTIVITY_ACTION"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
</activity>

在java代码中的操作如下:

public calss PublicActivity extends Activity{
  @Override
  public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    //判断确保param来源的正确性,发送之前可以适当做一些加密处理
    String param = getIntent().getStringExtra("PARAM");
  }

  public void onReturnResultClick(View view){
    Intent intent = new Intent();
    //finish时不要在intent中放置敏感信息,放置被截取
    intent.putExtra("RESULT","非敏感信息");
    setResult(RESULT_OK,intent);
    finish();
  }
}

3.2.3 伙伴activity

伙伴activity,顾名思义是只能由特定的应用程序使用的activity,即得到授权的APP,共享其信息和方法。这样的activity会存在一个风险,第三方应用程序可以读取启动这个activity的intent信息。因此,尽量不要在intent中放置一些敏感信息,也有必要做一些操作不让第三方应用读取到intent。

一个伙伴activity必须做到的关键几点如下:
1)不声明taskAffinity
2)不声明launchMode
3)设置exported属性为true
4)不添加intent-filter
5)使用白名单机制验证签名
6)处理partner activity来的intent的时候要小心注意
7)只返回给partner activity一些公开信息

在AndroidManifest.xml中的声明如下:

<activity
        android:name=".PartnerActivity"
        android:exported="true"
        android:label="@string/app_name" />

在activity中做签名验证的操作如下:

public calss PartnerActivity extends Activity{
  //伙伴应用的签名
  private static final String PARTNER_SIGNATURE = "0de0f9c90dd0ed0e8d0edd...";

  //伙伴应用的包名
  private static final String PARTNER_SIGNATURE = "com.xxxx.yyyyyy";

  //检查是否是伙伴应用
  private static boolean checkPartner(Context context, String pkgname){
    //包名不正确
    if(!TextUtils.equals(pkgname,PARTNER_SIGNATURE )){
      return false;
    }

    //签名不正确
    if(!getSignature(pkgname).eauals(PARTNER_SIGNATURE ))){
      return false;
    }
  }
  @Override
  public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    //判断确保param来源的正确性,发送之前可以适当做一些加密处理
    String param = getIntent().getStringExtra("PARAM");
  }

  public void onReturnResultClick(View view){
    Intent intent = new Intent();
    //finish时不要在intent中放置敏感信息,放置被截取
    intent.putExtra("RESULT","非敏感信息");
    setResult(RESULT_OK,intent);
    finish();
  }
}

3.2.4 内部activity(全家桶)

内部activity是禁止由内部APP之外的其他APP使用的activity。一般都是设置一个signature的权限,如系统内部使用的应用程序,都具有相同的签名。

这样的activity也会存在一个风险,第三方应用可以读取启动这个activity的intent信息。因此,尽量不要在intent中放置一些敏感的信息,也有必要做一些操作不让第三方应用读取到intent。

一个内部activity必须做到的关键几点如下:
1)定义activity的权限为signature
2)不声明taskAffinity
3)不声明launchMode
4)设置exported属性为true
5)不添加intent-filter
6)验证签名
7)通过intent传输的数据要小心

在AndroidManifest.xml中的声明如下:

......
<!--自定义一个signature的permission-->
<permission
    android:name="com.xxxx.activity.inhouseactivity.MY_PERMISSION"
    android:protectionLevel="signature"/>

......
<!--声明内部activity-->
<activity
    android:name=".InHouseActivity"
    android:exported="true"
    android:permission="com.xxxx.activity.inhouseactivity.MY_PERMISSION" />

在activity中的操作如下:

public class InHouseActivity extends Activity{
  //启动权限
  private static final String MY_PERMISSION = "com.xxxx.activity.inhouseactivity.MY_PERMISSION";

  @Override
  public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    //检查permission
    if(!TextUtils.equals(MY_PERMISSION,getPermission(this))){
        //权限不正确
        finish();
        return;
    }
    ......
  }

  public void onReturnResultClick(View view){
    Intent intent = new Intent();
    intent.putExtra("RESULT","敏感信息");
    setResult(RESULT_OK,intent);
    finish();
  }
}

3.3 Broadcast Receiver组件安全

Broadcast Receiver(广播接收器)是专注于接收广播通知信息,并做出对应处理的组件。APP可以拥有任意数量的广播接收器以对所有它感兴趣的通知信息予以响应。

对于broadcast receiver的创建和接收,需要根据不同的需求与风险做不同的选择决策。根据广播的发送接收流程和广播的使用方式,broadcast也可以分为三个不同的级别:

  • 私有广播接收器:此类广播只能被APP内部所接受,所以是最安全的广播。
  • 公共广播接收器:此类广播没有指定具体的接收者,能被任意的APP接收。
  • 内部广播接收器:广播只能被一些特定的APP所接收。

broadcast receiver根据注册声明方式可以分为两种:静态广播和动态广播。

  • 静态广播:在Androidmanifest.xml中注册。
  • 动态广播:在android代码中注册和注销。

Google官方也意识到了广播的安全性问题,在supportv4包中提供了一个类LocalBroadcastManager,主要负责程序内部广播的注册与发送。使用LocalBroadcastManager有如下好处:

  • 发送的广播只会在APP内传播,不会泄露给其他APP,确保隐私数据不会泄露。
  • 其他APP也无法向自己的APP发送该广播。
  • 比系统全局广播更加高效。

但是,它只适用于在代码中注册发送广播,在Androidmanifest中注册的广播接收则不适用。

3.3.1 私有Broadcast Receiver

私有广播是最安全的广播,其发出来的广播只能有APP内部能够接收。动态广播无法注册为私有广播,所以私有广播只存在于静态广播中。

私有广播的几个关键点如下:
1)不添加intent-filter
2)exported属性为false
3)广播处理完毕后要终止掉广播

私有广播的声明(Androidmanifest):

<!--私有广播-->
<receiver
    android:name=".PrivateReceiver"
    android:exported="false"/>

私有广播的定义(Java):

public class PrivateReceiver extends BraodcastReceiver{
    @Override
    public void onReceive(Context context,Intent intent){
        String param = intent.getStringExtra("PARAM");
        setResultCode(Activity.RESULT_OK);
        //处理一些重要敏感信息
        setResultData("敏感信息");
        //终止掉广播,不需要再继续接收广播了
        abortBroadcast();
    }
}
  • 在发送广播的时候注意在创建intent时,使用具体类名做声明即可,免除intent被拦截的风险。代码如下:
//普通广播
public void onSendNormalClick(View view){
    Intent intent = new Intent(this,PrivateReceiver.class);
    intent.putExtra("PARAM","敏感信息");
    sendBroadcast(intent);
}

//顺序广播
public void onSendOrderedClick(View view){
    Intent intent = new Intent(this,PrivateReceiver.class);
    intent.putExtra("PARAM","敏感信息");
    sendOrderedBroadcast(intent,null,mResultReceiver,null,0,null,null);
}

3.3.2 公共Broadcast Receiver

公共广播,没有确定的接收方,可以被一些未确定的APP接收。所以,使用公共广播的时候必须要注意此类广播被一些恶意应用接收造成数据泄露。

公共广播的关键点如下:
1)exported属性为true。
2)获取intent的时候小心处理。
3)return result的时候别放置敏感信息。

公共广播应该是通用的,所以能够被静态广播接收器和动态广播接收器所接收。

public class PublicReceiver extends BroadcastReceiver{
    private static final String MY_BRROADCAST_PUBLIC="com.xxx.MY_BRROADCAST_PUBLIC";

    @Override
    public void onReceive(Context context,Intent intent){
        //判断action
        if(MY_BRROADCAST_PUBLIC.equals(intent.getAction())){
            String param = intent.getStringExtra("PARAM");
            //数据小心处理
        }
        setResultCode(Activity.RESULT_OK);
        //存放非敏感信息
        setResultData("非敏感信息");
        //终止掉广播
        abortBroadcast();
   }
}

静态公共广播声明(Androidmanifest):

<!--静态公共广播接收器-->
<receiver
    android:name=".PublicReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="com.xxx.MY_BRROADCAST_PUBLIC"/>
    </intent-filter>
</receiver>

动态公共广播声明(java):
动态的公共广播我们在service中声明,代码如下:

public class DynamicReceiverService extends Service{
    private static final String MY_BROADCAST_PUBLIC = "com.xxx.MY_BRROADCAST_PUBLIC";
    private PublicReceiver mReceiver;

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

    @Override
    public void onCreat(){
        super.onCreate();
        //动态注册公共广播接收器
        mReceiver = new PublicReceiver();
        InterFilter filter = new IntentFilter();
        filter.addAction(MY_BROADCAST_PUBLIC);
        filter.setPriority(100);//优先考虑动态广播而不是静态广播
        registerReceiver(mReceiver,filter);
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
        //注销广播接收器
        unregisterReceiver(mReceiver);
        mReceiver = null;
    }
}

未完待续

4. 网络安全

4.1 中间人攻击和网络劫持

使用Https和ssl证书校验。

5. AndroidManifest.xml

需要特别注意的点:

  • 权限检查:
    a) 最小化系统权限
    b) 自定义权限进行级别保护设置
  • 调试标记检查:debuggable="false"
  • 程序数据任意备份检查:allowBackup="false"
  • Webview:
    a) 防远程代码调用:
removeJavascriptInterface(“accessibility”); 
removeJavascriptInterface(“accessibilityTraversal”); 
removeJavascriptInterface(“searchBoxJavaBridge_”);

b) 自动保存用户名密码检查: webview.getsettings().setsavePassword(false)

以上资料描述上可能存在错误,也不够详细,在后期会逐渐完善。

参考资料:

  1. http://www.droidsec.cn
  2. https://developer.android.com/training/best-security.html
  3. http://www.jsondream.com/2016/11/12/chat-about-Internet-Security-Architecture.html
行走的代码
Web note ad 1