2013年7月25日星期四

Android软件安全开发实践(下)


我们讨论了数据存储、网络通信、密码和认证策略等安全问题和解决方案,本期将继续从组件间通信、数据验证和保全保护等方面来实践Android软件安全开发之路。
组件间通信
组件间通信的安全问题是Android所独有的,也是目前软件中最常出现的一种问题。
我们先回顾一下组件间通信机制。Android有四类组件:activity、service、broadcast receiver和content provider。在同一个软件之中或不同软件之间,前三种组件使用Intent相互调用,使用ContentResolver对象访问content provider,共同实现软件的功能。使用Intent,可以显式或隐式地调用:
  • 显式(explicit):调用者知道要调用谁,通过组件名指定具体的被调用者;
  • 隐式(implicit):调用者不知道要调用谁,只知道执行的动作,由系统选择组件处理这个请求。
如下面的代码所示:

无论是显式还是隐式,如果要跨应用调用,还需要被调用的组件是对外暴露的。默认情况下,service、broadcast receiver和content provider是暴露的,申明了Intent-filter的actvity也是暴露的。
抽象地说,组件A要调用组件B,以期待B完成某个功能;它可以发送一些数据给组件B,也可以获得B执行后的返回结果。在这个模型中,问题出现在A和B之间不一定互相可信。
如果B是暴露的,任何软件都可以调用它,包括攻击者编写的软件。攻击者可能但并非总能成功:
  • 直接调用暴露的B,以获得其执行结果;
  • 构造特定的数据,并用于调用暴露的B,从而试图影响B的执行;
  • 调用暴露的B,并获取它执行完返回的结果。
如果A用的是隐式调用,任何软件都可以实现它的action从而响应调用。攻击者可能(但并非总能成功):
  • 构造伪造的组件C,响应A的Intent,以读取A要发给B的数据;
  • 构造伪造的组件C,响应A的Intent,弹出虚假的用户界面以展开进一步攻击(例如钓鱼);
  • 构造伪造的组件C,响应A的Intent,返回伪造的执行结果。
这样说可能比较抽象。下面我们对这两种情况分别讨论。
组件暴露的问题
看一个例子。在一个第三方深度定制的ROM中,预装了名为Cit.apk的软件,用于手机的硬件测试。它的AndroidManifest.xml局部如下:

可以看到,它申明一个名为.CitBroadcastReceiver的receiver,响应名为android.provider.Telephony.SECRET_CODE的action,并且指定了URI格式。
再来看这个receiver的代码片段(下面的代码是我反编译得到的,不一定与软件源码完全一致):

可以看到,当调用这个receiver,并且提供的URI中host字段为284时,会以root权限调用本地的bugreport工具,并将结果输出至m_logFileName指定的文件中。
默认情况下receiver是暴露的,因此这个receiver可以被其他软件调用,代码如下:

当这四行代码执行时,就会触发CitBroadcast-Receiver的那段代码。从上下文看,输出文件m_logFileName位于SD卡,任何软件都可以随意读写。因此,攻击者可以获得bugreport的输出结果,其中包含大量系统数据和用户数据。
请注意,在这个例子中,攻击者的软件不需要任何特殊权限,尤其是不需要root权限。这种由于组件暴露获得额外权限的攻击,被称之为permission re-delegation(权限重委派)。
怎么避免由于组件暴露产生的安全问题?有的组件必须暴露,例如入口activity,或者确实对外提供服务或跨软件协作;但也有的组件没必要暴露。接下来我们分别讨论。
不需要暴露的组件
再次回顾,默认情况下,service、broadcast receiver和content provider是暴露的,申明了Intent-filter的actvity也是暴露的。如果它们只被同一个软件中的代码调用,应该设置为不暴露。很容 易做到—在AndroidManifest.xml中为这个组件加上属性android:exported=”false”即可。
需要暴露的组件
如果组件需要对外暴露,应该通过自定义权限限制对它的调用。
首先,在实现了被调用组件的软件的Android-Manifest.xml中自定义一个权限:

接下来,为被调用组件添加这个权限限制,即在AndroidManifest.xml中为这个组件添加android:permission属性:

另一种方法是在组件的实现代码中使用Context.checkCallingPermission()检查调用者是否拥有这个权限。
最后,要调用这个暴露的组件,调用者所在的软件应该申明使用这个权限,即在AndroidManifest.xml中添加相应的use-permission申明。
进一步地,还可以将这种组件暴露的需求分为两种情况。
  • 如果这个组件只打算给自己开发的其他软件使用,而不希望暴露给第三方软件,在定义权限时,protectionLevel字段应该选择signature。 这种设置要求权限使用者(即调用者)与权限定义者(即被调用者)必须由相同的证书进行签名,因此第三方无法使用该权限,也就无法调用该组件。
  • 如果这个组件要暴露给第三方,则protection-Level应使用normal或dangerous。此时,任何软件都可以使用该权限,只在安装时会 通知用户。考虑到用户一般会忽略权限提示,此时自定义权限实际失去了保护效果,我们依然要仔细审查该组件的代码,避免出现能力泄露或权限重委派等问题。
此外,对content provider,可以更细粒度地为读取数据和写入数据设置不同的权限,对应的manifest标签分别为android:readPermission和android:writePermission。
隐式调用的问题
隐式调用的主要问题是被劫持,或Intent携带的数据被读取。
为了劫持调用,攻击者可以实现一个恶意的组件,申明相同的Intent-filter。在多个组件都可以响应同一个Intent的情况下,如果是调用 activity,系统会弹出界面要求用户对多个软件做出选择,攻击者可以模仿真实软件的图标和名称吸引用户点击;如果是调用service,系统会随机 选择一个service;如果是调用receiver,系统会逐一地将Intent发给这些receiver。
劫持了调用后,攻击者可以(但并非总能成功):
  • 启动一个虚假的软件界面,展开钓鱼攻击(例如要求用户输入账户密码)
  • 读取Intent携带的数据
  • 拦截broadcast的进一步发送,导致真正的receiver无法收到消息,从而拒绝服务
  • 如果是用startActivityForResult()调用了虚假的activity,可以返回恶意或虚假的结果给调用者
  • 执行其他恶意代码
限于篇幅,对这些情形我们不提供示例代码。来看一下怎么解决这类问题。
不需要隐式调用
除了基于Intent类中已有ACTIONs的隐式调用,绝大部分隐式调用都属于这两种情况:同一软件中不同组件的调用;同一开发者不同软件间的调用。
这两种情况下,事实上,开发时都已可以确定要调用的组件是哪个。因此可以避免隐式调用,改为基于组件名的显式调用。
需要隐式调用
发送broadcast除了使用sendBroadcast(Intent),还有一个方法是sendBroadcast(Intent, String),它的第二个参数可以指定接受者需要的权限。
如果是调用activity或者service,目前我所知,还没有简单的方法实现接收者的权限限制。在Android文档中提出可以自定义Binder和AIDL实现通信双方的互相验证,但真正实现并不容易,也不为官方所推荐。
数据验证
无论是客户端还是服务器,在处理外部获得的数据之前,都应先判断和验证数据的有效性。这里主要指是否包含畸形的数据。
在 Web开发中,服务器需要对用户提交的数据进行有效性验证,否则很容易出现众所周知的SQL注入等攻击。在移动开发中也不例外。虽然客户端与服务器在底层 通信协议上对用户是透明、不可见的,但开发者不应因此就假设双方传输的数据永远会和预先设计的一致。类似的,在读取用户从UI元素输入的输入、读取存储在 本地的数据后,使用前也应进行有效性验证。
软件版权保护
攻击者对Android软件进行逆向分析,除了寻找其中的安全漏洞,还可以直接攻击软件本身。
  • 破解软件的收费机制、License验证或功能限制;
  • 修改软件代码,去掉广告库,或者修改广告库(一般是改变推介ID字段),或者增加广告库,然后重新打包并分发;
  • 重新打包,植入恶意代码并分发;
  • 逆向分析,学习软件特色功能的实现方法,或者获得可复用的代码;
可以采取多种措施来增加破解、修改和逆向分析的难度,减少被攻击的可能。这些措施包括:
  • 使用代码混淆工具,例如SDK带的ProGuard以及其他Java混淆器。
  • 采用NDK开发核心模块。
  • 使用官方或第三方的软件保护方案,例如SDK的android.drm包、Google Play的软件许可(Application Licensing)支持。
  • 去掉开发时的调试代码,关闭调试开关,删除多余的Log代码。
然而,采取了这些措施并不等于就万无一失了。例如,用NDK开发的Native代码,也可以使用IDA Pro及其插件来反汇编、反编译和调试;用Google的DRM方案,也有AntiLVL这样的破解工具。理论上,现有防御手段都可能找到方法继续攻击, 其价值只是提高攻击难度和成本。
总结
已出现和将要出现的威胁
到 目前为止,在学术研究以外,针对Android软件漏洞的攻击只出现一起—劫持国外多个社交网站客户端登陆会话的黑客工具FaceNiff。但随着攻击者 制造传播恶意代码的成本增加和收益降低,以及移动终端隐私数据逐渐成为地下产业链的交易资源,针对Android流行软件漏洞的攻击在未来几年之内几乎一 定会出现并爆发。
统一的安全模型
限于篇幅,本文只介绍了几种常见且简单的安全问题,还存在许多我们知道的、还不知道的漏洞。如何找到和解决这些问题?
回顾已介绍的内容,我们可以发现它们有类似的安全模型:通信双方的信任问题。
  • 在数据存储中,读写数据的代码和存储在本地的数据互相不可信。
  • 在数据通信中,发送者和接受者互相不可信。
  • 在登录认证中,发起认证请求的用户的和接受认证请求的服务器互相不可信。
  • 在组件间通信中,发起Intent的组件与接收Intent的组件互相不可信。
  • 在数据验证中,处理数据的模块不能相信产生数据的源。
面对将来的问题,我们也可以尝试抽象出这种模型,区分互相不可信的实体,然后在不可信、不安全的基础上,尽可能地实现相对的可信和安全。
进一步学习和行动
Android 的开发文档Best Practices: Designing for Security和源码文档Tech Info: Security分别从开发和系统实现的角度介绍了系统的安全机制。另外,viaForensics提供了名为42+ Best Practices: Secure mobile development for iOS and Android的在线教程,更详细地介绍了移动软件面临的安全威胁,并给出了安全开发实践策略。
社区方面,从Android安全开发的角 度,Stack-Overflow并不一定是很好的选择—其中一些最佳回答没有考虑安全,直接使用可能产生问题。Google Group的anroid-security-discuss讨论组则更为专业和准确。OWASP成立了一个Mobile Security工作组,目前已发布Top Ten Mobile Risks等多份白皮书,并举办了AppSec会议。这个工作组的效率虽然不高,但产出质量非常棒。
学术方面,2011和2012年的四大会议及其work-shop上均有移动软件漏洞挖掘和攻击阻止的论文出现,从它们的related works部分可以综合快速地了解学术界的思路。
目前的移动开发还没有形成如此成熟的体系,这也许与其轻快敏捷的互联网产品开发风格有关。但我相信,真正实效的移动软件安全开发,最终依然会融合到需求分析、系统设计、开发实现、测试验证、部署运维等每一个环节,从而与PC平台的SDL殊途同归。
作者肖梓航,安天实验室高级研究员,主要方向是移动反病毒和移动软件安全,发起或参与了多个移动安全开源项目。

没有评论:

发表评论