Android 应用之安全开发

大佬:“这个 APP 破解下,可以兼容客户已出货的产品”
我:“这个不合适吧”
大佬:“这个客户对我们很重要”
我:“好吧”
然后,就是通过反编译某 APP ,分析蓝牙交互协议,在新的 APP 中去兼容已出货的设备,达到无缝对接。 –这种场景在开发中还是比较经常碰到的。

一、引言

随着移动互联网向社会生活的各个领域渗透,APP 的使用越来越广泛。但 Android 系统由于其开源的属性,市场上针对开源代码定制的 ROM 参差不齐(特别中国区域),在系统层面的安全防范和易损性都不一样,Android 应用市场对 APP 的审核相对 iOS 来说也比较宽泛,市场上一些主流的 APP 虽然多少都做了一些安全防范,但由于大部分 APP 不涉及资金安全,所以对安全的重视程度不够;而且由于安全是门系统学科,绝大部分 APP 层的开发人员缺乏对 APP 安全意识及措施,导致被有心者有机可乘。

Android 开发是当前最火的话题之一,但很少开发者会讨论这个领域的安全问题,除了专业从业者,但移动应用安全隐患也给发展带来了挑战。

  • 开发团队通常将精力集中在产品设计、功能实现、用户体验和系统效率等方面,而很少考虑安全问题;
  • 与一切都是集中管理的 iOS 相比,Android 提供了一种开放的环境,在获得了灵活性、可以满足各种定制需求的同时,也损失了部分安全性;
  • Android 提供的安全机制比较复杂,开发者需要理解它们,并对常见的攻击思路和攻击方法有所了解,才能有效地保护软件;
  • 目前很少出现对特定移动软件安全漏洞的大规模针对性攻击,在真实的攻击出现之前,许多人对此并不重视。

声明,我不是专业的安全人员,从事的跟安全工作也没有什么关系,本文从自已平时涉及的项目出发,对客户端的代码质量、代码篡改、不安全的数据储存、不安全的通信、不安全的认证、加密不足等安全问题作了说明,从普通开发者角度尽量去提高自已的 APP 安全,以降低代码安全漏洞,减少代码被利用的可能性,避免信任危机、经济损失和法律风险。

二、移动应用面临的安全问题



2.1 病毒

Android 病毒就是手机木马,主要是一些恶意的应用程序。手机木马有的独立存在,有的则伪装成图片文件的方式附在正版 APP 上,隐蔽性极强,部分病毒还会出现变种,并且一代比一代更强大。

这些病毒有一些通用的特征:

  • 母包 + 恶意子包的运行机制
  • 通过技术手段防止用户通过正常途径卸载
  • 以窃取用户账户资金为目的
  • 以短信作为指令通道

2.2 关键信息泄露(反编译)

虽然 Java 代码一般要做混淆,但是 Android 的几大组件的创建方式是依赖注入的方式,因此不能被混淆,而且目前常用的一些反编译工具比如 Apktool 等能够毫不费劲的还原 Java 里的明文信息,Native 里的库信息也可以通过 objdump 或 IDA 获取。因此一旦 Java 或 Native 代码里存在明文敏感信息,基本上是毫无安全而言的。

2.3 APP 重打包

即反编译后重新加入恶意的代码逻辑,从新打包一个 APK 文件。重打包的目的一般都是上面提到和病毒结合,对正版 APK 进行解包,插入恶意病毒后重新打包并发布,因此伪装性很强。截住 APP 重打包就一定程度上防止了病毒的传播。

2.4 进程被劫持

一般通过进程注入或者调试进程的方式来 Hook 进程,改变程序运行的逻辑和顺序,获取程序运行的内存信息,也就是用户所有的行为都被监控起来,这也是盗取帐号密码最常用的一种方式。

当然 Hook 行为不一定完全是恶意的,比如有些安全软件会利用 Hook 的功能做主动防御。一般来说,Hook 需要获取 root 权限或者跟被 Hook 进程相同的权限,因此如果你的手机没有被 root,而且是正版 APK 的话,被注入还是很困难的。 

2.5 数据在传输过程中遭劫持

传输过程最常见的劫持就是中间人攻击。很多安全要求较高的应用程序要求所有的业务请求都是通过 Https,但是 Https 的中间人攻击也逐渐多了起来,而且在实际使用中,证书交换和验证在一些山寨手机或者非主流 ROM 上面存在一些问题,让 Https 的使用碰到阻碍。

2.6 键盘输入安全隐患

支付密码一般是通过键盘输入的,键盘输入的安全直接影响了密码的安全。键盘的安全隐患来自三个方面:

  • 使用第三方输入法,则所有的点击事件在技术上都可以被三方输入法截取,如果不小心使用了一些不合法的输入法,或者输入法把采集的信息上传并且泄露,后果是不堪设想的。
  • 截屏,该方法需要手机具有 root 权限,才能跑起截屏软件
  • getevent,通过读取系统驱动层 dev/input/event1 中的信息,获取手机触屏的位置坐标,在结合键盘的布局,就能算出来事件跟具体数字的映射关系,这也是目前比较常用的攻击方式。

2.7 Webview 漏洞

由于现在 Hybrid APP 的盛行(混合开发),Webview 在 APP 的使用也是越来越多,Android 系统 Webview 存在一些漏洞,造成 js 提权。最为著名的就是传说中 js 注入漏洞和 webkit xss 漏洞。

三、基础安全

3.1 Manifest 文件安全

禁止 PermissionGroup 的属性为空

PermissionGroup 可以对 permission 进行一个逻辑上的分组。如果 PermissionGroup 的属性为空,会导致权限定义无效,且其他 APP 无法使用该权限。

开发建议:设置 PermissionGroup 属性值或者不使用 PermissionGroup。

protectionLevel 属性设置

由于对 APP 的自定义 permission 的 protectionLevel 属性设置不当,会导致组件(如:content provider)数据泄露危险。最好的权限设置应为 signature 或signatureOrSystem ,进而避免被第三方应用利用。
开发建议:注意使用 signature 或 signatureOrSystem 防止其他 APP 注册或接受该 APP 的消息,提高安全性。

合理设置 sharedUserId 权限

通过 sharedUserId,可以让拥有同一个 User Id 的多个 APP 运行在同一个进程中,互相访问任意资源。将 sharedUserId 设置为 android.uid.system,可以把 APP 放到系统进程中,APP 将获得极大的权限。如果 APP 同时有 master key 漏洞,容易导致被 root。
开发建议:合理设置软件权限。

设置 allowBackup 为 false

当这个标志被设置成 true 或不设置该标志位时,应用程序数据可以备份和恢复,adb 调试备份允许恶意攻击者复制应用程序数据。
开发建议:设置 AndroidManifest.xml 的 android:allowBackup 标志为 false。

禁止 Debuggable 为 true

在 AndroidManifest.xml 中定义 Debuggable 项,如果该项被打开,APP 存在被恶意程序调试的风险,可能导致泄露敏感信息等问题。
开发建议:显示的设置 AndroidManifest.xml 的 debuggable 标志为 false。

3.2 组件安全

合理设置导出Activity、activity-alias、service、receiver

Activity、activity-alias、service、receiver 组件对外暴露会导致数据泄露和恶意的攻击。
开发建议:最小化组件暴露。对不会参与跨应用调用的组件添加 android:exported=false 属性。

设置组件访问权限。对跨应用间调用的组件或者公开的 receiver、service、activity 和 activity-alias 设置权限,同时将权限的 protectionLevel 设置为 signature 或 signatureOrSystem。组件传输数据验证。对组件之间,特别是跨应用的组件之间的数据传入与返回做验证和增加异常处理,防止恶意调试数据传入,更要防止敏感数据返回。

使用显式 Intent 调用 bindService()

创建隐式 Intent 时,Android 系统通过将 Intent 的内容与在设备上其他应用的清单文件中声明的 Intent 过滤器进行比较,从而找到要启动的相应组件。如果 Intent 与 Intent 过滤器匹配,则系统将启动该组件,并将其传递给对象。如果多个 Intent 过滤器兼容,则系统会显示一个对话框,支持用户选取要使用的应用。
为了确保应用的安全性,启动 Service 时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务将响应 Intent,且用户无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),系统会抛出异常。

开发建议:为了确保应用的安全性,启动 Service 时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务将响应 Intent,且用户无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),系统会抛出异常。
影响范围
全部。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),系统会抛出异常。

合理处理 Intent Scheme URL

Intent Scheme URI 是一种特殊的 URL 格式,用来通过 Web 页面启动已安装应用的 Activity 组件,大多数主流浏览器都支持此功能。
Android Browser 的攻击手段—— Intent Scheme URLs 攻击。这种攻击方式利用了浏览器保护措施的不足,通过浏览器作为桥梁间接实现 Intend-Based 攻击。相比于普通 Intend-Based 攻击,这种方式极具隐蔽性,
如果在 APP 中,没有检查获取到的 load_url 的值,攻击者可以构造钓鱼网站,诱导用户点击加载,就可以盗取用户信息。所以,对 Intent URI 的处理不当时,就会导致基于 Intent 的攻击。
如果浏览器支持 Intent Scheme URI 语法,一般会分三个步骤进行处理:

  • 利用 parseUri 解析 uri,获取原始的 intent 对象;
  • 对 intent 对象设置过滤规则;
  • 通过 startActivityIfNeeded 或者 startActivity 发送 intent;其中步骤2 起关键作用,过滤规则缺失或者存在缺陷都会导致 Intent Schem URL 攻击。

关键点
Intent.parseUri 函数,通过扫描出所有调用了 Intent.parseUri 方法的路径,并检测是否使用如下的策略。
比较安全的使用 Intent Scheme URI 方法是:
如果使用了 Intent.parseUri 函数,获取的 intent 必须严格过滤,intent 至少包含 addCategory(android.intent.category.BROWSABLE),setComponent(null),setSelector(null)3 个策略。
所以,在检的时候只要根据 Intent.parseUri 函数返回的 Intent 对象有没有按照以下方式实现即可做出判断:

// convert intent scheme URL to intent object
Intent intent = Intent.parseUri(uri);

// forbid launching activities without BROWSABLE category
intent.addCategory(android.intent.category.BROWSABLE);

// forbid explicit call
intent.setComponent(null);

// forbid intent with selector intent  intent.setSelector(null);
// start the activity by the intent
context.startActivityIfNeeded(intent, -1)

开发建议:如果使用了 Intent.parseUri 函数,获取的 intent 必须严格过滤,intent 至少包含 addCategory(android.intent.category.BROWSABLE),setComponent(null),setSelector(null)3 个策略。除了以上做法,最佳处理不要信任任何来自网页端的任何 intent,为了安全起见,使用网页传过来的 intent 时,要进行过滤和检查。

本地拒绝服务

Android 系统提供了 Activity、Service 和 Broadcast Receiver 等组件,并提供了 Intent 机制来协助应用间的交互与通讯,Intent 负责对应用中一次操作的动作、动作涉及数据、附加数据进行描述,Android 系统则根据此 Intent 的描述,负责找到对应的组件,将 Intent 传递给调用的组件,并完成组件的调用。Android 应用本地拒绝服务漏洞源于程序没有对 Intent.GetXXXExtra() 获取的异常或者畸形数据处理时没有进行异常捕获,从而导致攻击者可通过向受害者应用发送此类空数据、异常或者畸形数据来达到使该应用 Crash 的目的,简单的说就是攻击者通过 Intent 发送空数据、异常或畸形数据给受害者应用,导致其崩溃。
对导出的组件传递一个不存在的序列化对象,若没有 try…catch 捕获异常就会崩溃

ComponentName cn = new ComponentName(com.test, com.test.TargetActivity)

Intent i = new Intent()
i.setComponentName(cn)
i.putExtra(key, new CustomSeriable())
startActivity(i)

public class DataSchema implements Serializable {
    public DataSchema() {
        super();
    }
}

NullPointerException 异常导致的拒绝服务
源于程序没有对 getAction() 等获取到的数据进行空指针判断,从而导致了空指针异常导致应用崩溃
风险代码:

Intent i = new Intent();

if (i.getAction().equals(TestForNullPointerException)) {
    Log.d(TAG, Test for Android Refuse Service Bug);
}

ClassCastException 异常导致的拒绝服务
源于程序没有对 getSerializableExtra() 等获取到的数据进行类型判断而进行强制类型转换,从而导致类型转换异常导致拒绝服务漏洞
风险代码:

Intent i = getIntent();
String test = (String) i.getSerializableExtra(serializable\_key);

IndexOutOfBoundsException 异常导致拒绝服务漏洞

源于程序没有对 getIntegerArrayListExtra() 等获取到的数据数组元素大小判断,导致数组访问越界而造成拒绝服务漏洞
风险代码:

Intent intent = getIntent();

ArrayList<Integer> intArray = intent.getIntegerArrayListExtra(user\_id);
if (intArray != null) {
    for (int i = 0; i < 10; i++) {
        intArray.get(i);
    }
}

ClassNotFoundException 异常导致的拒绝服务漏洞

Intent i = getIntent();
getSerializableExtra(key);

开发建议:将比不要导出的组建设置为不导出

在处理 Intent 数据时,进行捕获异常,通过 getXXXExtra() 获取的数据时进行以下判断,以及用 try catch 方式捕获所有异常,防止出现拒绝服务漏洞,包括:空指针异常、类型转换异常、数组越界访问异常、类未定义异常、其他异常

Try{
    ....
    xxx.getXXXExtra()
    ....
}Catch Exception{
    **   **  **// 为空即可**
}
删除 Debug 和 Test 信息

一些 APP 在正式发布前,为了方便调试 APP,都会在 APP 里集成一些调试或测试界面。这些测试界面可能包含敏感的信息。

四、反调试 & 反篡改

从 Android APP 的结构来说,dex 文件是最重要、最需要保护的,因为 dex 中存放了代码的信息,开发者通过使用 dex2jar 和 jd-gui 简单几步就可以查看到源码。

Andriod 应用程序使用 Java 开发,可通过反编译的方式获取对应的源码

  • APK 包其实就是个 ZIP 包,用 WinRAR 解开获得 classes dex classes.dex
  • 使用 dex2jar 将程序转换成 jar 文件
  • 使用 jad 对 jar 文件进行反编译

ProGuard 是一个免费的 Java 类文件的压缩,优化,混肴器
新建个 Android 工程之后,proguard.cfg 文件会在工程的根目录下自动创建文件定义了混淆器是怎样优化和混淆你的代码

目前开发常用方法:

  • 使用混淆保护,对 APK 代码进行基础的防护;
  • 加壳,dump 出 dex 对于大多数人来说依然是一件非常困难的事;
  • 反二次打包,可以通过在原生层验证签名来实现(其代码在 Java 层);
  • 处理编译后的二进制 AndroidManifest.xml 文件,添加无效的参数,使反编译得到错误的清单文件;
  • 第三方平台:爱加密(加壳技术,对 dex 文件做了一层保护壳,),360 加固等。

五、数据安全

5.1 SQLite 安全

SQLite sql 注入漏洞

SQLite 做为 android 平台的数据库,对于数据库查询,如果开发者采用字符串链接方式构造 SQL 语句,就会产生 SQL 注入。

开发建议:

  • provider 不需要导出,请将 export 属性设置为 false
  • 若导出仅为内部通信使用,则设置 protectionLevel=signature
  • 不直接使用传入的查询语句用于 projection 和 selection,使用由 query 绑定的参数 selectionArgs
  • 完备的 SQL 注入语句检测逻辑
Databases 任意读写漏洞

APP 在使用 openOrCreateDatabase 创建数据库时,将数据库设置了全局的可读权限,攻击者恶意读取数据库内容,获取敏感信息。在设置数据库属性时如果设置全局可写,攻击者可能会篡改、伪造内容,可以能会进行诈骗等行为,造成用户财产损失。

开发建议

  • 用 MODE_PRIVATE 模式创建数据库
  • 使用 sqlcipher 等工具加密数据库
  • 避免在数据库中存储明文和敏感信息

5.2 剪贴板敏感信息泄露风险

由于 Android 剪贴板的内容向任何权限的 APP 开放,很容易就被嗅探泄密。同一部手机中安装的其他 APP,甚至是一些权限不高的 APP,都可以通过剪贴板功能获取剪贴板中的敏感信息。

风险代码:

    clipBtn = (Button) findViewById(R.id.btn\_clip);

        clipBtn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD\_SERVICE);
                ClipData clip1 = ClipData.newPlainText(label,password=123456);
                clipboard.setPrimaryClip(clip1);
            }
        });

开发建议:避免使用剪贴板敏文存储敏感信息或进行加密

5.3 密钥硬编码风险

在代码中禁止硬编码私钥等敏感信息,攻击者反编译代码,即可拿到。

5.4 Intent敏感数据泄露

APP 创建 Intent 传递数据到其他 Activity,如果创建的 Activity 不是在同一个 Task 中打开,就很可能被其他的 Activity 劫持读取到 Intent 内容,跨 Task 的Activity 通过 Intent 传递敏感信息是不安全的。

开发建议:尽量避免使用包含 FLAG_ACTIVITY_NEW_TASK 标志的 Intent 来传递敏感信息。

5.5 PendingIntent 误用风险

使用 pendingIntent 时候,如果使用了一个空 Intent,会导致恶意用户劫持 Intent 的内容。禁止使用空 intent 去构造 pendingIntent。
开发建议:禁止使用空 intent 去构造 pendingIntent。

5.6 数据或程序(DEX、SO)加载、删除检查

程序在加载外部 dex、so 文件是否判断文件来源、是否存放可信区域;程序删除文件是否可篡改文件路劲

是否加载公共区域程序,如 sdcard、/data/local/tmp/、应用自创建但其他应用有读写权限的目录上
是否从网络下载,检测方法包括:阅读代码、监听网路请求、见识存储区域文件读写、查看安装包
升级包是否存在公共区域存储。

5.7 文件全局读写漏洞

在使用 getDir、getSharedPreferences(SharedPreference) 或 openFileOutput 时,如果设置了全局的可读权限,攻击者恶意读取文件内容,获取敏感信息。在设置文件属性时如果设置全局可写,攻击者可能会篡改、伪造内容,可能会进行诈骗等行为,造成用户财产损失。其中 getSharedPreferences 如果设置全局写权限,则当攻击 APP 跟被攻击 APP 具有相同的 Android:sharedUserId 属性时和签名时,攻击 APP 则可以访问到内部存储文件进行写入操作。

开发建议

使用 MODE_PRIVATE 模式创建内部存储文件
加密存储敏感数据
避免在文件中存储明文敏感信息
避免滥用 Android:sharedUserId 属性

如果两个 APP Android:sharedUserId 属性相同,切使用的签名也相同,则这两个 APP 可以互相访问内部存储文件数据

5.8 日志泄露风险

在 APP 的开发过程中,为了方便调试,通常会使用 log 函数输出一些关键流程的信息,这些信息中通常会包含敏感内容,如执行流程、明文的用户名密码等,这会让攻击者更加容易的了解 APP 内部结构方便破解和攻击,甚至直接获取到有价值的敏感信息。
开发建议: 禁止打印敏感信息

六、Webview 安全

6.1 Webview组件安全

WebView 组件中的接口函数 addJavascriptInterface 存在远程代码执行漏洞,远程攻击者利用此漏洞能实现本地 Java 和 js 的交互,可对 Android 移动终端进行网页挂马从而控制受影响设备。

6.2 Webview API接口是否进行白名单限制

Webview 接口避免使用第三方程序恶意使用发送短信,拨打电话,删除文件
修复方案: 白名单进行限制,功能仅限于该应用的功能范围之内

6.3 WebView 钓鱼漏洞

钓鱼这个事一直都安全界最常用的攻击,也是最难通过技术手段解决的一类问题,而且钓鱼的手段也是千奇百怪,要防钓鱼除了让用户提高安全意识,不点击来路不明的链接外,
技术层面可以做到如下两点:

  • 检查 WebView 加载目标URL是否存在钓鱼欺骗等安全风险
  • 对 Webview 关闭脚本环境
  • WebView 跨域漏洞

主要是由于 JS 的 XmlHttpRequest 可以读取本地文件,从而读取到 APP data 数据库目录下的 webviewCookiesChromium.db , 这个 db 通常是系统存放 cookie 的地方,相当于变相的为读取 cookie 开了权限。

七、弱加密风险检测

7.1 禁止使用弱加密算法

安全性要求高的应用程序必须避免使用不安全的或者强度弱的加密算法,现代计算机的计算能力使得攻击者通过暴力破解可以攻破强度弱的算法。例如,数据加密标准算法 DES(密钥默认是 56 位长度、算法半公开、迭代次数少)是极度不安全的,使用类似 EFF(Electronic Frontier Foundaton)Deep Crack 的计算机在一天内可以暴力破解由 DES 加密的消息。

开发建议:建议使用安全性更高的 AES 加密算法

7.2 不安全的密钥长度风险

在使用 RSA 加密时,密钥长度小于 512bit,小于 512 bit 的密钥很容易被破解,计算出密钥。
风险代码:

public static KeyPair getRSAKey() throws NoSuchAlgorithmException {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA);
        keyGen.initialize(512);
        KeyPair key = keyGen.generateKeyPair();
        return key;
      }

开发建议:使用 RSA 加密时,建议密钥长度大于 1024bit

7.3 AES/DES弱加密风险(ECB)

AES 的 ECB 加密模式容易遭到字典攻击,安全性不够。
风险代码:

    SecretKeySpec key = new SecretKeySpec(keyBytes, AES);
    Cipher cipher = Cipher.getInstance(AES/ECB/PKCS7Padding, BC);
    cipher.init(Cipher.ENCRYPT\_MODE, key);

开发建议:避免使用 ECB 模式,建议使用 CBC。

7.4 IVParameterSpec不安全初始化向量

使用 IVParameterSpec 函数,如果使用了固定的初始化向量,那么密码文本可预测性高得多,容易受到字典攻击等。
风险代码:

byte[] iv = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 };

IvParameterSpec ips = new IvParameterSpec(iv)

复制代码开发建议
IVParameterSpec 初始化时,不使用常量 vector。

7.5 KeyStore 弱密码风险

keytool 是一个 Java 数据证书的管理工具,Keytool 将密钥(key,私钥和公钥配对)和证书(certificates)存在一个称为 keystore 的文件中,并通过密码保护 keystore 中的密钥。如果密码设置过于简单,例如:123456、android 等,则会导致 keystore 文件的私钥泄露,从而导致一系列的信息泄露风险。
开发建议:提高 keystore 保护密码的强度

八、其他风险

8.1 谨慎使用高风险函数

在程序需要执行系统命令等函数,需要谨慎使用,严格控制命令来源,防止黑客替换命令攻击。
开发建议:严格按照要求使用

8.2 重要函数逻辑安全

程序中重要的逻辑函数建议使用 NDK 技术通过 c/c++ 代码实现。

因为 APK 本身未进行专业加固保护,存在被 baksmali/apktool/dex2jar 直接反编译获取程序 Java 代码的风险,建议程序的重要函数使用 Android ndk 技术通过 c/c++ 实现,将重要函数编译到 so 库中,能够提高重要函数的逻辑安全强度。

8.3 发布版本需加固

发布的软件,应对 APP 进行加固,防止攻击者获取 APP 代码、业务逻辑、API 接口等,对业务和公司声誉造成一定影响,防止 APP 被破解二次打包,导致损失。
开发建议:APP 加固


Refer

  • 《移动互联网应用软件安全通用技术规范》