第八篇安全

Android内建的安全机制可以显著地减少了应用程序的安全问题。通过在默认的系统设置和文件权限设置的环境下建立应用,可避免为一系列的安全问题寻找解决方案。

一些帮助建立应用的核心安全的特性如下:

l  Android应用程序沙盒,将应用数据和代码的执行与其他程序隔离。

l  具有鲁棒性的常见安全功能的应用框架,例如加密,权限控制,安全IPC

l  使用改进的虚拟机等技术,减少了常见内存管理错误。

l  加密文件系统可以保护丢失或被盗走的设备数据。

l  用户权限控制限制访问系统关键信息和用户数据。

l  应用程序权限以单个应用为基础控制其数据。

尽管如此,熟悉Android安全特性仍然很重要;特别是,当使用外部存储、插件化等非Android推荐特性时,开发者将直面各种安全问题。遵守这些规范并将其作为优秀的代码风格,能够减少无意间给用户带来的安全问题。

8.1 拒绝服务

针对序列化对象而出现的拒绝服务主要是由于应用中使用了getSerializableExtra() 的API,由于应用开发者没有对传入的数据做异常判断,恶意应用可以通过传入畸形数据,导致应用本地拒绝服务。

已知的拒绝异常类型包括:ClassCaseException、NoClassDefFoundError

8.1.1 ClassCastException示例


Intent i = getIntent();
if(i.getAction().equals("serializable_action")){
    i.getSerializableExtra("serializable_key"); //未做异常判断
}





攻击应用代码如下:

Intent i = new Intent();
i.setAction("serializable_action");
i.setClassName("com.exp.serializable", "com.exp.serializable.MainActivity");
i.putExtra("serializable_key",XXX); //此处是传入畸形数据
startActivity(i);



比如XXX处传入BigInteger.valueOf(1)则发生转型异常错误java.lang.ClassCastException。

解决方式,使用Intent.getXXExtra时,主动加try-catch。

8.1.2NoClassDefFoundError示例

当传入一个本应用未定义的Serializable或Parcelable序列化对象时,接收Intent的目标组件在getSerializableExtra()、getParcelable()等会抛出类未定义的异常java.lang.NoClassDefFoundError。

这是因为当你给漏洞应用传入一个应用本身并没有的序列化类对象,在应用上下文中肯定是找不到这个类的。

示例:构建自定义的序列化类很简单:

public class DataSchema implements Serializable {
    private static final long serialVersionUID = -3601187837704976264L;
    public DataSchema() {
        super();
    }
}



对应的攻击代码中XXX处传入new DataSchema(),并且传入的key不管是否与漏洞应用相同,getXXXExtra都会抛出类未定义的异常。此问题,官方SDK至今没有处理。

因此,规范要求,不管是get什么extra,只要是getXXXExtra(),都加上try-catch捕获异常。

8.2 数据与文件存储

对于一个Android的应用程序来说,最为常见的安全问题是存放在设备上的数据能否被其他应用获取。在设备上存放数据基本方式有三种。

8.2.1 使用内部存储

默认情况下,在内部存储中创建的文件只有本应用可以访问。Android实现了这种机制,并且对于大多数应用程序都是有效的。我们应该避免在IPC文件中使用MODE_WORLD_WRITEABLE或者MODE_WORLD_READABLE模式,因为它们不为特殊程序提供限制数据访问的功能,它们也不对数据格式进行任何控制。如果想与其他应用的进程共享数据,可以使用Content Provider,它可以给其他应用提供了可读写权限。

如果想对敏感数据进行特别保护,可以使用应用程序无法直接获取的密钥来加密本地文件。例如,密钥可以存放在KeyStore而非设备上,使用用户密码进行保护。

8.2.2 使用外部存储

创建于外部存储的文件,比如SD卡,是全局可读写的。由于外部存储器可被用户移除并且能够被任何应用修改,因此不应使用外部存储保存应用的敏感信息。当处理来自外部存储的数据时,应用程序应该执行输入验证(参看输入验证章节)。

Android官方强烈建议应用在动态加载之前不要把可执行文件或class文件存储到外部存储中。如果一个应用从外部存储检索可执行文件,那么在动态加载之前它们应该进行签名与加密验证。

这是在涉及到插件化、热更新等框架业务时需要注意的。

8.2.3 使用内容提供者

内容提供者提供结构化存储机制,可以将内容限制为仅供自己的应用访问,也可以将内容导出以供其他应用访问。如果不打算向其他应用授予访问ContentProvider的权限,在应用清单中将其标记为android:exported=false;要允许其他应用访问存储的数据,将android:exported属性设置为"true"。

在创建要导出以供其他应用使用的ContentProvider时,可以在清单中指定允许读取和写入的单一权限,也可以针对读取和写入操作分别指定权限。要求仅对需要完成相应任务的应用授予权限。

如果要使用内容提供者仅在公司内部的应用之间共享数据,最好将android:protectionLevel属性设置为"signature"保护级别。签名权限不需要用户确认,因此,这种方式不仅能提升用户体验,而且在相关应用如能使用相同的密钥进行签名来访问数据时,还能更好地控制对数据的访问。

内容提供者还可以通过以下方式提供更细化的访问权限:在manifest清单中使用<path-permission>还能进一步限制到具体path的数据的权限。

示例:provider可以只开放部分URI的权限

<provider android:name=".PrivProvider"
    android:authorities="cn.wei.flowingflying.propermission.PrivProvider"
    android:readPermission="wei.permission.READ_CONTENTPROVIDER"
    android:exported="true" >
    <path-permission android:pathPrefix="/hello" android:readPermission="READ_HELLO_CONTENTPROVIDER" />
</provider>



访问内容提供者时,请使用参数化的查询方法(例如query()、update()和delete()),以免产生SQL注入风险。

8.2.4 使用URI规范传入的file路径

为了避免目录遍历漏洞,对于外部接口传入的file路径采用标准的URI规范进行整理和判断。

void openFile(UriparamUri, String paramString) throws FileNotFoundException {

    String decodedUriString =Uri.decode(paramUri.toString());

    File file = new File(IMAGE_DIRECTORY,Uri.parse(decodedUriString).getLastPathSegment());


    if(file.getCanonicalPath().indexOf(localFile.getCanonicalPath()) != 0) {
        thrownew IllegalArgumentException();

    }

}



8.3 使用权限

由于Android 通过沙盒机制管理各个应用,因此应用必须以明确的方式共享资源和数据。权限,就是其中重要的一环。

8.3.1 减少权限的请求

我们应该尽量减少应用请求的权限。如果不具备对敏感数据的访问权限,就能降低不慎误用这类权限的风险,并可提高用户的信任,同时让应用不那么容易受到攻击者的攻击。一般来说,如果应用无需某项权限也能正常运行,就不要请求该权限。比如:

可以不将数据存储在外部存储设备(需要请求权限),而将其存储在内部存储空间。

8.3.2 创建权限

在满足安全性要求的前提下尽可能少定义权限,如果必须创建新权限,请尽量考虑创建"signature"保护级别的权限。签名级别权限的内容对调用者完全透明开放,而且只有拥有了目标应用的开发者签名的应用才可访问这些内容。

如果创建了"dangerous"保护级别的权限,则可能发生不可控的数据访问。无论如何,我们都要注意:

l  该权限必须包含一个字符串,向用户清楚明确地说明他们需要做出的权限需求;

l  该权限的字符串需译成多种不同语言;

l  用户可能会因为权限含糊不清或存在风险而选择不安装应用;

l  应用可能会在权限创建程序尚未安装的情况下请求权限。

8.4 使用安全网络

网络通信涉及传输对用户而言可能比较私密的数据,因此本质上就存在安全风险。因此,应该采取各种最佳做法来确保用户的数据安全。

8.4.1 使用IP网络

Android网络运行机制与其他Linux环境差别不大,关键是确保对敏感数据使用合适的协议,来保证网络流量的安全,主要涉及两方面:

一方面,对通信数据的私密信、完整性和合法性,应用综合的加解密技术进行保证;另一方面,对数据传输过程的身份验证,应用相关的证书技术进行保证;

一般来说,涉及用户数据的通信,都应该使用成熟的安全通信协议来保证以上两点,特别是IP通信中采用HTTPS协议。成熟的网络通信框架都提供了响应的安全通信API,而SDK也提供了HttpsURLConnection来保证网络流量安全。

因此,规范要求:

1,  在服务器支持HTTPS的情况下一律使用HTTPS(而非 HTTP),因为移动设备经常会连接到不安全的网络(如公共WLAN热点);

2, 切勿相信通过 HTTP 或其他非安全协议下载的数据,如有必要,进行充足的校验。如文件的指纹散列值、apk的签名等。

8.4.2 使用电话网络

在移动开发中,可能涉及使用电话网络收发短信的业务。

短信协议主要是为用户间电信通信设计的,并不适合传输应用数据,并且短信在网络上和设备上均为经过加密,也没有经过严格的身份验证;更进一步,许多设备对短信应用进行了严格管理,可能拦截屏蔽其认为来自恶意方的短信,因为短信可能存在欺骗性内容或可能在网络传输时被拦截。

因此,规范要求:

一方面,切勿使用未经身份验证或时效限制的短信数据来执行敏感命令;

另一方面,由于短信可以被其他拥有READ_SMS权限的应用读取,因此,强烈建议使用安全的IP网络进行消息推送。

8.5 执行输入验证

无论应用是在哪种平台上运行,输入验证功能不完善都是影响应用的最常见安全问题。Android 为此提供了许多平台级对策,可降低应用出现输入验证问题的可能性,因此,尽量使用这些功能。一些典型的应用场景包括:

l  从文件读取、通过网络接收或从 IPC 接收的任何数据,进行缓存区控制,避免一次性读取数据造成缓存区溢出;如避免对未经验证的文件流执行readLine()操作;

l  使用基于字符串的动态语言(如 JavaScript 和 SQL)也可能因为转义字符和脚本注入而出现输入验证问题;

l  使用提交到 SQL 数据库或内容提供程序的查询中的数据,也可能出现 SQL 注入问题。最好的预防措施是使用参数化查询(请参阅上文数据库和内容提供者的相关内容)。将权限限制为只读或只写,也可以降低SQL注入引发破坏的可能性;

l  如果无法使用上述或其他安全功能,考虑使用结构合理的数据格式,并验证数据是否符合预期的格式;

l  最后,黑名单或替换字符是一种有效的策略,但这些技术在实际操作中很容易遗漏或出错,因此应尽量避免使用,如有必要,使用白名单策略。

8.6 处理用户数据

首先,应该尽量避免访问用户敏感数据,也避免存储和传输这些数据。如果有必要,请充分考虑一下几点:

l  评估应用逻辑能否使用经过哈希或者不可逆的数据处理进行实现;例如,我们通常不会直接使用用户的明文密码,而是采用多重处理后的哈希值,以避免传输或存储过程中泄露数据的可能性;

l  分析自己的应用是否会在无意中将用户信息泄露给其他方,如广告使用的第三方组件或应用使用的第三方服务;如果允许第三方访问数据,务必做好权限保护;

l  如果必须访问敏感数据,请判断这些信息是必须传输至服务器,还是可以在客户端上执行相应操作。建议在客户端上运行所有需要使用敏感数据的代码,以避免传输用户数据;

l  如果需要GUI,创建一个较长的具有唯一性的编号并加以存储,勿使用可能与个人信息关联的电话标识符,如电话号码或 IMEI。

l  如果应用会访问密码或用户名等个人信息,部分司法辖区可能会要求您提供隐私权政策,以说明您如何使用或存储这类数据。因此,遵循安全最佳做法(即尽可能减少对用户数据的访问)也有助于简化合规工作。

8.7 使用WebView

由于WebView使用的网络内容可能包含 HTML 和 JavaScript,不当的使用可能引入常见的网络安全问题,例如跨站脚本攻击(JavaScript 注入)。Android内置了多种机制,可将WebView的功能限制为应用所需的最低功能,以缩小这些潜在问题的影响范围,主要会涉及的问题如下:

l  启用JS脚本功能。不直接使用WebView中的 JavaScript,请勿调用setJavaScriptEnabled()。默认情况下,WebView不会执行JavaScript,因此不可能出现跨站脚本攻击这样的安全问题。

l  JS交互接口。addJavaScriptInterface()允许JavaScript调用JAVA对象,因此在使用时请格外小心。如果要使用,请仅将addJavaScriptInterface()应用于所有输入内容都可信的网页,否则不受信任的JavaScript可能会调用应用中的Android方法。一般情况下,要求仅将addJavaScriptInterface()用于本应用内含的JavaScript文件。

l  WebView的缓存。如果应用通过WebView访问敏感数据,需要使用clearCache()方法来删除本地存储的所有文件。也可以使用请求头头(例如no-cache)来指示应用不应缓存特定内容。

l  只加载受信任的URI。在Android 4.4(API 19)之前平台上运行的设备使用的webkit版本存在多个安全问题。如果应用在这些设备上运行,确认WebView对象只加载值得信任的资源,避免应用在SSL通信中不会暴露给潜在漏洞。

8.8 处理凭据

尽量避免将用户名和密码存储在设备上。您可以使用用户提供的用户名和密码进行初始身份验证,然后使用针对特定服务的短时效授权令牌。

8.9 加解密

Android 不仅提供数据隔离机制、支持完整文件系统加密并提供安全通信通道,还提供成熟的加解密模块来保护数据。

l  https通信使用HttpsURLConnection或SSLSocket,这也是许多网络框架的封装基础。

l  使用安全随机数生成器SecureRandom初始化任意加密密钥KeyGenerator。如果使用的密钥不是安全随机数生成器生成的,那么会显著降低算法的强度,容易导致出现离线攻击。

l  如果需要存储密钥以供重复使用,请使用KeyStore等可以长期存储和检索加密密钥的机制。

l  使用现有公开成熟加密算法,而不是自行开发私有算法,如Cipher类中提供的AES或RSA实现中的算法。

8.10 进程间通信

我们的应用普遍存在进程间通信的需求,要求避免使用传统的Linux技术来实现IPC,比如Socket和文件共享;要求使用Androi针对IPC提供的系统功能,例如使用Service的Intent、Binder或Messenger,以及BroadcastReceiver。Android IPC机制让我们可以验证连接至IPC的应用的身份,并为每种IPC机制设置安全策略。

许多安全元素在各种IPC机制之间是共享的。常见的场景包括:

l  如果IPC机制并不打算让其他应用使用,在该组件的清单元素(例如<service>元素)中将android:exported属性设置为"false"。

l  如果IPC预期供其他应用访问,您可以使用<permission>元素应用安全策略。如果IPC是在公司的不同应用(以同一密钥登录)之间使用,建议您在android:protectionLevel中使用"signature"级别权限。

8.10.1 使用Service

Service通常用于提供其他应用要使用的功能,默认情况下,服务不会被导出,而且无法由任何其他应用调用。不过,如果将任何intent过滤条件添加到服务声明中,那么默认就会导出该服务。

因此,要求明确声明android:exported属性,以确保其行为符合要求。同时还可以使用android:permission属性来保护服务。这样,其他应用只有在自己的清单中声明相应的<uses-permission>元素,才能启动、停止或绑定到服务。

本应用服务可以先调用checkCallingPermission(),然后再实现该调用,从而保护针对该服务、拥有相应权限的各个IPC调用。通常情况下,如果确实是涉及私有数据的服务,建议在清单中使用声明式权限,因为权限的方式使得其安全性不容易被忽略。

8.10.2 使用Binder和Messenger接口

使用Binder或Messenger是Android中RPC式IPC的首选机制。它们提供了定义完善的AIDL接口,使我们可以聚焦于业务流程,如果必要,也可让端点互相进行权限验证,类似上述checkCallingPermission()。

8.10.3 使用BroadcastReceiver

BroadcastReceiver会处理Intent发起的异步请求,要求注意以下安全要点。

l  声明exported属性。默认情况下,接收器会被导出,而且可以由任何其他应用调用,类似于Service,要求显示地声明android:exported属性。

l  设置收发权限。如果BroadcastReceiver预期供其他应用使用,应该在应用清单中向接收器设置安全权限。这样可防止没有相应权限的应用向BroadcastReceiver发送intent。

8.11 动态加载代码

Android官方表示,强烈建议不要从应用APK外部加载代码。这样做不仅会明显加大应用因代码注入或代码篡改产生问题的可能性,还会增加版本管理和应用测试的难度。这最终会导致无法验证应用的行为,因此,某些环境中可能会禁止采用此做法。

因此,尽管插件化和热更新技术在国内表现得比较热门,但是其风险性值得关注。

如果必要适用动态加载,要求:

l  动态加载的代码需要拥有与应用本身相同的安全策略和权限水平;

l  外部代码必须来自可验证的信任来源;

不安全的位置包括:通过未加密的协议从网络上下载,外部存储设备。这类代码可能遭到篡改,从而执行某些恶意操作

8.12 日志管理

务必做好日志打印管理。在Android中,日志是共享资源,拥有READ_LOGS权限的所有应用均可访问。即使日志数据是临时数据并会在重新启动时清空,不当记录用户信息也可能在无意之中将用户数据泄露给其他应用。

务必做好日志缓存文件的管理。一方面,应当利用受保护的目录缓存日志,如data下的File或Cache目录;另一方面,日志必须经过充分加密,避免传输过程中的信息泄露。

8.13 使用NDK开发

Android 使用 Linux 内核构建而成。如果要使用原生代码进行开发,熟悉一下Linux开发安全最佳做法会非常有用。由于笔者经验水平有限,本文中没有介绍Linux安全做法,不过可以查阅非常受欢迎的《Secure Programming for Linux and Unix HOWTO》,网址为http://www.dwheeler.com/secure-programs