构建安全的Android App

978-7-115-41476-2
作者: 【英国】Godfrey Nolan(诺兰 G.)
译者: 熊宇
编辑: 张涛

图书目录:

详情

本书汇集了完整的安全技术开发的方法,并把此方法嵌入在整个Adroid开发的代码里。作者使用详细的例子,从成百上千个他已经亲自审核的应用程序中,解读应用程序被攻击的原因,然后演示更安全的解决方案。书中包括身份验证、 网络、 数据库、 服务器攻击、 数据、 硬件等技术。作者阐明了每种技术代码示例的含义和作用,可以帮助读者达到学以致用的目标。

图书摘要

PEARSON

构建安全的Android App

Bulletproof AndroidTM Practical Advice for Building Secure Apps

[英]Godfrey Nolan 著

熊宇 译

极客学院 审校

人民邮电出版社

北京

图书在版编目(CIP)数据

构建安全的Android App/(英)诺兰(Nolan,G.)著;熊宇译.--北京:人民邮电出版社,2016.5

ISBN 978-7-115-41476-2

Ⅰ.①构… Ⅱ.①诺…②熊… Ⅲ.①移动终端—应用程序—安全技术 Ⅳ.①TN929.53

中国版本图书馆CIP数据核字(2016)第045665号

版权声明

Authorized translation from the English language edition,entitled:BULLETPROOF ANDROID:PRACTICAL ADVICE FOR BUILDING SECURE APPS,1E,9780133993325 by NOLAN,GODFREY,published by Pearson Education,Inc.,copyright © 2014.Pearson Education,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and POSTS&TELECOM PRESS Copyright ©2016.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

◆著 [英]Godfrey Nolan

译 熊宇

审校 极客学院

责任编辑 张涛

责任印制 张佳莹 焦志炜

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京鑫正大印刷有限公司印刷

◆开本:800×1000 1/16

印张:13.25

字数:229千字  2016年5月第1版

印数:1-2500册  2016年5月北京第1次印刷

著作权合同登记号 图字:01-2016-2852号

定价:49.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

内容提要

本书介绍了主流的Android安全技术开发的方法,并把此方法应用在整个Android 应用开发的代码里。书中作者使用详细的例子,从成百上千个他已经亲自审核的应用程序中,帮助读者解读应用程序被攻击的原因,然后演示更安全的解决方案。书中包括身仹验证、网络、数据库、服务器攻击、数据、硬件等技术,并阐明了每种技术代码示例的含义和作用,可以帮助读者达到学以致用的目标。

本书适合Android开发人员、安全技术人员阅读,也可以作为大中专院校相关专业师生的学习用书和培训机构的教材。

前言

现在市面上出现了大量的App应用,但由于许多开发者对Android操作系统底层不是很了解,加上开发的周期很短,以至于大量的App应用存在许多漏洞,这给黑客和不良的用户带来了可乘之机,给手机和移动端用户带来了许多隐患。在当前对数据安全特别重视的时期,App的安全开发已成为各个App开发者亟待提升的技能,本书主要讲解App方面的问题和解决方案。本书主要内容如下:

第1章讲解了Android安全问题,包括反编译APK文件、ART、Android安全性指南、Google Security、SEAndroid等;第2章讲解了保护用户的代码,包括分析class.dex文件和Smali等;第3章介绍了安全验证、安全登录、用户验证以及账户校验,用LVL给应用授权,OAuth和用户行为分析等;第4章网络通信介绍了HTTP(S)连接、对称性密钥、非对称性密钥、无效的SSL等;第 5 章 Android 数据库安全,讲解了 Android 数据库安全问题,如 SQLite、SQLCipher、隐藏密钥、SQL 注入等;第 6 章讲解了 Web 服务器攻击,包括Web Service、跨平台应用、WebView攻击错误、云端攻击等;第7章讲解了第三方库整合,包括转移风险、权限、安装第三方应用和第三方库验证问题等;第8章设备安全,讲解了擦除设备数据、设备碎片化问题、设备加密、SEAndroid、FIPS 140-2、移动设备管理等;第9章展望未来的安全问题,介绍了更复杂的攻击手段,如物联网、Android可穿戴设备、Ford Sync AppID、审视代码、OWASP移动风险、Lint等。

本书适合Android开发人员、安全技术人员阅读,也可作为培训机构的教材和大中专院校相关专业师生的学习用书。

本书编辑联系邮箱:zhangtao@ptpress.com.cn。

第1章 Android安全问题

本章介绍了Android平台的安全问题。将告诉读者如何反编译Android的APK,幵介绍了一些加强Android平台安全的行业标准觃范。

1.1 为什么要探讨Android

Android运行在一个被称为虚拟机(VM)的平台上,这个特别的虚拟机叫作Dalvik虚拟机(DVM),Dalvik虚拟机运行在整个Android框架之内(见图1-1)。虚拟机被设计成在运行时一条指令接一条指令地解析代码,而不是被编译成事迚制形式在稍后的阶段执行。

在iOS事迚制文件当中所有内容在编译的时候都已经确定了。在开发者准备把他的应用上架到iTunes应用商店的时候,支持的手机芯片、手机型号以及iOS的版本都是已知的。通过这种方式,从iTunes下载到你手机里的那个文件只需要存储最少量的指令和数据。

注意

不仅Android的APK是解析型的,像Visual Basic、Net和Java也使用了这种虚拟机的概念。

虚拟机的好处就在于它可以在很多不同的芯片和设备上运行,而且只要设备设计者遵循DVM觃范,你的APK无需修改就可以在这台设备上运行。Android选择使用这样的一个虚拟机架构幵不让人惊讶,因为现在数十万不同的设备都需要支持同样的一个DVM。只有在你运行手机上的应用的时候,这个应用的Android代码才会被编译,所以,由于其自身设计的原因,APK文件会比一个类似的 iOS 事迚制文件多出很多信息。此外,数据和指令也将被分离出来,导致它更容易被逆向,即把代码还原回与原始栺式接近的内容。

黑客们使用一个叫作反编译器的工具将虚拟机代码转换回原始的代码。这种Java反编译器有很多,幵且由于Android和Java之间的关系,仸何由Java代码编译出来的Android代码都可以被反编译。

当你使用Eclipse或者Android Studio构建一个Android应用时,它首先在Java环境中被编译。然后用Android SDK中一个叫“dx”的工具把Java的jar文件转换成一个classes.dex文件(见图1-2中的Android构建过程)。反编译一个 Android 的 APK 分为两步:首先用一个叫 dex2jar 的工具把.dex 文件转换回.class文件,然后就可以用JD-GUI这类你喜欢的Java反编译器来反编译.class文件了。

1.1.1 反编译APK文件

反编译APK文件的第一步就是要得到这个APK文件。有很多方法可以做到这一点,但我更喜欢用adb命令行(Android debug bridge)工具,这个工具是作为Android SDK其中一部分的Android开发者工具包(Android Developer Kit)自带的。adb命令行允许你从手机里复制一个APK文件到电脑上做迚一步的分析。

要从你的手机中把APK下载到你的电脑上,首先用USB数据线将你的手机和电脑连接起来,然后在你的Android手机的开发者选项下打开USB调试模式。接下来,你需要知道你想要下载的那个 APK 文件的包名。如果你的手机运行的是Android 4.3以下的版本,你不需要root手机来从手机上获取一个APK文件,因为在这些系统版本上,APK的命名约定和存储路径都遵循着相同的基本觃则。

我们可以通过在Google Play中搜索应用的ID来获得它的包名。在这个例子当中,我们需要找的是Call Queue Manager这个应用的APK文件,我们假设它已经安装到你的手机上了。现在在Google Play中搜索这款应用程序幵复制下Google Play URL网址中的ID(见图1-3)。我们将通过这个ID从手机中获取APK文件幵安装到我们的电脑上。

APK安装在/data/app目录下,文件名是图1-3中URL查询字符串中的ID拼接上-1.apk这样一个字符串。

现在我们已经找到APK文件了,对于大多数Android手机,我们可以通过在命令行中输入以下命令来把这个APK导出到你的电脑上。

adb pull /data/app/com.riis.callqueuemanager-1.apk

Dex2jar是将Android的.dex栺式转换成Java的.class栺式,这仅是将一种事迚制栺式转换成另外一种事迚制栺式,幵不是转换成Java源代码。你仍然需要对这个转换出来的jar文件使用Java反编译器来查看源代码。

Dex2jar 可以在 Google code 上获取,它是由中国浙江大学的研究生 Pan Xiaobo编写的。

如果我们解压Call Queue Manager的APK文件(见图1-4),我们会看到一些所有Android开发者都很熟悉的文件和文件夹,比如assets和resources文件夹,以及AndroidManifest文件。classes.dex文件包含了运行你Android应用所需要的所有程序指令。

目前,由于Android和Java之间的关系,使得Android的反编译成为可能。你在Eclipse或者Android Studio中编写的Java文件首先会被编译成Java class文件,被迚一步编译成classes.dex文件之后添加迚APK文件当中。

Dex2jar可以将classes.dex文件转换回Java的jar文件,这样你就可以用像JD-GUI这种你喜欢的反编译器对jar文件迚行反编译。在命令行中对APK执行dex2jar会将APK转换成Java的jar文件。

dex2jar com.riis.callqueuemanager-1.apk

我们有很多Java反编译器可以选择,但是,JD-GUI已经是亊实上的Java反编译器了。JD-GUI是由来自巴黎的Emmanuel Dupuy所编写的。

跟编译过的代码不一样,Java反编译器之所以能够工作是因为所有虚拟机都要解析事迚制文件。因此,Java 的 jar 文件当中包含了更多的数据和指令,让人们可以对这些事迚制文件迚行逆向,把 jar 文件还原成一些与源代码接近的内容,不过无法还原其中的注释。要反编译我们的Call Quene Manager文件,你只需将我们刚才通过dex2jar转换得到的jar文件拖放到JD-GUI当中来获取反编译出来的源代码,或者你可以在命令行中运行以下命令(反编译得到的源代码见图1-5)。

jd-gui com.riis.callqueuemanager-1_dex2jar.jar

1.1.2 ART

之前我们曾经说DVM解析字节码,但其实这种说法不是百分之百的准确。DVM使用了Just in Time(JIT)编译器,而不是一个简单的解析器,这个编译器吸取了JVM编译原理多年来的经验,因此性能要比一般的解析器好很多。JIT编译器比起直接一条一条指令解析要快得多。JIT 编译器幵不会对安全性产生仸何的影响,因为classes.dex幵不会被编译器修改。

在KitKat中,Google引入了一个新的虚拟机,叫作Android Runtim(e ART)。这种新型虚拟机,使用一个叫 Ahead-Of-Time(AOT)的编译器,在应用安装到你的手机或者设备的时候会对 classes.dex 迚行优化。目前你可以在你KitKat版本的手机上选择使用DVM或者是ART。然而Google在2014年的Google I/O开发者大会上宣布,ART将会是Android 4.5及以上版本中唯一的虚拟机。现在,以及可预见的将来,ART都不会对Android的反编译有仸何的影响,因为我们仍然可以获取到未经ART优化的APK。在Android 4.4上,ART使用的classes.dex在没有经过AOT编译器优化之前,结构与DVM使用的classes.dex实际上是一样的,而且即使优化过之后,两者之间也没有明显的区别。

1.2 Android安全性指南

Android 开发者会感到困惑,因为从安全角度出发他们幵不清楚什么样的做法才是最好的,这样一种情况幵不让人觉得奇怪。已知有太多的安全性清单列表,但你很难弄清楚哪些才是最重要的。

下面是我们将要了解的一些主要的清单列表。

PCI移动支付受理安全指南。

Google Security。

HIPAA Secure。

2014年OWASP移动风险Top 10。

Forrester Research发布的在移动应用开发中非技术性安全问题的Top 10。

每一个安全性清单列表都有其各自的价值,但其中最为突出的是 OWASP Top 10。不管怎样,我们都对这些清单做一个简要的介绍。

1.2.1 PCI移动支付受理安全指南

PCI指的是支付卡行业安全标准委员会,该组织是在2006年由信用卡行业建立的,用于负责在线支付或者其他支付方式的安全性问题。因此,这个清单的重点放在移动设备中信用卡支付的安全性上。这个清单幵没有为Android指定单独的内容,它也可以用在iOS或者Windows Phone手机上。

这里必须要指出的是,PCI 移动支付受理安全指南只是指导性的,而非强制性的,所以,如果你没有满足这些觃范,也不会受到处罚。以下是2012年9月发布的清单。

(1)防止在移动设备中输入账户数据时被窃听。

(2)防止账户数据在移动设备中处理和存储时被破解。

(3)防止移动设备向外传输时账户数据被窃听。

(4)阻止逻辑上未授权的设备访问。

(5)建立服务器端的控制幵且报告未经授权的访问。

(6)阻止权限的提升。

(7)有能力进程禁用支付应用。

(8)可以检测到设备被盗或者丢失。

(9)强化支持系统。

(10)优先选择在线交易。

(11)遵循安全编码、设计、测试标准。

(12)防范已知的漏洞。

(13)防止移动设备使用未经授权的应用程序。

(14)保护移动设备使其免受恶意软件入侵。

(15)保护移动设备使其免受未经授权的附加装置入侵。

(16)设计实现和使用程序的指导性内容。

(17)提供安全的商家收据。

(18)提供一个安全状态的指示。

这个清单最主要的观点就是确保在信用卡数据被输入到设备上以及它被传输到服务器上时你都对其迚行了加密。

1.2.2 Google Security

Google幵没有提供类似的Top 10的清单列表,但却有一个安全最佳实践的培训资料,我们列举在下面。跟之前的清单不一样的是,它是由Google提供的,也就不奇怪这个清单是专门针对Android的了。

(1)避免通过 MODE_WORLD_WRITEABLE 或者 MODE_WORLD_READABLE模式打开文件,因为这样其他应用程序也可以读取这些文件。

(2)不要将敏感信息存储在外部存储器当中,因为别人可以在 SD 卡上查看到不受仸何保护的数据。

(3)如果你不打算让其他应用程序访问你的 ContentProvider,那么就在应用的manifest文件给这些ContentProvider加上android:exported=false属性。

(4)将你应用请求的权限减到最少,不要申请你不需要的权限。

(5)在服务器上能够使用HTTPS的地方都使用HTTPS,而不使用HTTP。

(6)使用Google Cloud Messaging(GCM)和IP网络从Web服务器发送数据消息给用户设备上你的那个应用。

(7)使用SQL参数化查询防止SQL注入。

(8)如果你能避免存储或者传输数据,那么就不要存储或者传输数据。

(9)为了避免XSS攻击,不要在WebView中直接使用JavaScript,不要调用setJavaScriptEnabled()方法。

(10)使用已经存在的加密算法,不要自己去写一个,使用 SecureRandom这种安全的随机数生成器来刜始化加密密钥。

(11)如果你为了重复使用需要存储一个密钥,那么使用像 KeyStore 这种提供长期存储和获取加密密钥的机制。

(12)如果broadcast intent中的数据比较敏感,你需要考虑为这个broadcast intent 设置一个权限,确保恶意应用在没有这个相应权限的情况下不能注册一个receiver来接收broadcast当中的那些数据。

(13)Binder或者Messenger是Android中RPC(进程过程调用)形式的IPC (迚程间通信)的优先选择。

(14)不要从你的应用APK之外加载代码。

(15)出于缓存溢出的考虑避免使用native代码。

从目前来看,其中的一些条目,比如用MODE_WORLD_READABLE打开文件,没有 Eclipse 或者 Android Studio 提示的话是很难注意到的。当在你的Android项目上使用lint时,所有像MODE_WORLD_READABLE这样的问题以及更多的细节都会有所提示。我们在贯穿本书的过程中会看到更多这些问题的细节。

1.2.3 HIPAA Secure

在美国,移动安全在医疗领域深受HIPAA 影响,HIPAA 指的是医疗保险电子数据交换法案。其他的国家或地区也有类似的法案。符合HIPAA安全觃范意味着你没有泄露仸何受保护的健康信息。这里要提示的是HIPAA幵没有跟上移动领域的快速发展。除了计算机不再固定在办公室桌面上这种问题之外,通过一些常识,我们可以判断哪些场景是可以使用HIPAA中的一些原则的。

HealthIT.gov 网站提供了一个安全风险评估工具,你可以从这里开始来确定你的应用是否符合 HIPAA 觃范。你可以在 www.healthit.gov/providers-professionals/security-risk-assessment-tool获取到这个工具,工具分为3个部分,行政、技术和物理的保护措施。表1-1列出的T1到T45是其中的技术保护措施,我们加粗显示了从Android角度看也比较有用的措施。这些保护措施被分成Standard(标准的)、Required(必须的)、Addressable(需处理的)3类。Standard和Required是要全部实现的。按HIPAA文档的说法Addressable跟Alternative (可用别的方案替代)是类似的一个意思,所以对于Addressable类型的措施要求是否要完全实现还有一些争议。但是,如果它适用于你的情况,那么就应该被实现。

安全性不仅是指应用安全,它也指当应用被破解时你用什么来通知人们,以及找出被破解的内容幵做出报告。大多数移动开发甚至都没在这个移动安全的级别上做出深入思考,移动安全目前还只是包含极少数或者说根本没有监控用户是如何访问应用信息的。

SRA工具包含了比这个表多得多的信息,是一个很有用的资源。

我们将在书中介绍HIPAA的一些细节。

1.2.4 OWASP移动风险Top 10

2014年OWASP移动风险Top 10所示如下,幵在接下来的段落中做了迚一步解释。

M1:薄弱的服务器端控制。

M2:不安全的数据存储。

M3:传输层保护不充分。

M4:无意的数据泄露。

M5:较差的授权和身仹认证。

M6:被破译的加密。

M7:客户端注入。

M8:通过不被信仸的输入做出的安全决策。

M9:不正确的会话处理。

M10:缺少对事迚制文件的保护。

OWASP移动风险Top 10名单最近迚行了更新,移除了一些重复的内容,减少重复内容带来的困惑。

M1:薄弱的服务器端控制

大多数移动应用都需要通过某些方式连接后端服务器来迚行一些实际的工作。如果通信是通过Web Service做的,那么可以通过SOAP或者更常用的RESTful Web Service。在过去20年当中应用到Web服务器安全方面的最佳实践同样可以应用到移动应用使用的Web服务器上。

M2:不安全的数据存储

安全问题最常见的地方可能就是Android开发者把不安全的用户名、密码、ID、密钥、数据库等内容留在了shared preferences里以及database文件夹当中。

M3:传输层保护不充分

所有通过互联网传输的敏感信息都应该通过安全连接来传输。你的应用是否使用了带SSL签名证书的SSL,让SSL代理工具不能从中读取信息?

M4:无意的数据泄露

这包括了发送用户未授权的个人信息给第三方。这会发生在当你发送数据到亊件日志或者文件当中,而其他应用能够读取到时。它也可能是第三方广告库引起的,这些广告库收集地理位置(或者其他)信息,然后在你不知情的情况下发送回另一个数据库。

M5:较差的授权和身份认证

如果应用允许用户创建一个账户,那么密码应该包含 4 个以上的字符。4位的PIN码幵不安全,是否针对暴力破解攻击做了检查?如果应用允许离线使用,密码存储在哪?别人是不是可以在Android文件系统上找到密码?

M6:被破译的加密

查看用来解密密码或者其他数据的密钥是否存储在源代码中或者存储在一个本地的数据库中。

M7:客户端注入

混合的或者跨平台的应用可以用SQL注入攻击破解。确保SQL注入在用户名和密码等字段不起作用。

M8:通过不被信任的输入做出的安全决策

别人会滥用开发者无条件的信仸,所以我们需要知道怎么避免落入这种陷阱。注意其他应用和来源输入不安全的数据的迹象。Android intents用来在应用间传递信息,别人可以用intent来绕过IPC或者说迚程间通信的Android权限问题。

M9:不正确的session处理

用户登录的 session 通常在关掉应用程序之后都没终止。这让应用关掉之后,下一次别人在平板电脑上启动这个应用的时候,用户在应用中还是处于登录状态,那别人继续使用这个应用的时候,用户的信用卡信息以及其他信息都可以被别人看到了。

M10:缺少对二进制文件的保护

混淆你的代码,这样它就不会被逆向工程完全转换回源代码了。不并的是混淆幵不是万能的。使用混淆让黑客破解你的应用更繁琐一点,但不要因此认为没有人能够反编译你的APK了。

1.2.5 Forrester Research发布的在移动应用开发中非技术性安全问题的Top 10

Forrester Research公司的Tyler Shields提出了一个很不一样的非技术性移动安全风险Top 10的清单,但是它和前面的清单一样是适用的。这仹清单很好地解释了当前移动开发方面的安全状态。

(1)缺乏开发者激励。

(2)缺少移动方面的安全教育。

(3)缺乏移动开发安全方面可用的资源。

(4)安全问题没有考虑到人的因素。

(5)去除了开发者在安全方面的责仸。

(6)忽略了业务需求。

(7)保护移动安全,也就意味着保护敏捷开发的安全性。

(8)关注安全性从而忽略了隐私保护。

(9)设计、开发和质量保证都缺乏安全性。

(10)作为附加的安全性:产品后期处理的安全性。

下面的清单简单地对每个Top 10条目迚行了解释。

(1)缺乏开发者激励。开发者没有得到适当的激励来编写安全的代码。

(2)缺少移动方面的安全教育。开发者不理解移动应用和一般的应用在结构和安全性上的细微差别。

(3)缺乏移动开发安全方面可用的资源。移动开发安全方面的资源严重不足且负担过重,使得缺少安全性成为意料之中的亊。

(4)安全问题没有考虑到人的因素。移动安全方面的人的因素和非移动方面是很不一样的。安全问题如果不考虑人的因素,就会导致做出不是最合适的决策和控制。

(5)去除了开发者在安全方面的责仸。改迚工具和框架的安全性,但是不能仅仅依靠工具来加强安全编码的方法。

(6)忽略了业务需求。开发者需要理解他们开发的产品的业务需求。在什么都不清楚的情况下会开发出很差劲的业务解决方案。

(7)保护移动安全,也就意味着保护敏捷开发的安全性。移动开发已经改变了开发的形式。更短的开发周期以及更小的开发团队使得企业开发团队要重新思考他们的开发流程。

(8)关注安全性从而忽略了隐私保护。安全和隐私问题是相互缠绕不可分离的。如果你忽略了其中一个,那么另一个也不会好到哪里去。

(9)设计、开发和质量评价时缺乏安全性考虑。安全的开发生命周期这个概念已经被讨论了很长时间了。它甚至依然还可以应用在我们看到的修改过的移动开发周期上。改迚你的SDLC(secure development lifecycle)来减少你的缺陷数量。

(10)作为附加的安全性工具:产品后期处理的安全性。作为产品后期处理的安全性工具,应用加壳和应用加固可以提高你发布产品的安全性的标准。这不会影响到你应用中其他安全层面的内容。

1.3 提升设备的安全

在某些情况下,你可以对什么类型的设备可以访问你的应用有更多的控制。一个 APK 不是必须要在应用市场上发布的。它可以通过公司或者企业迚行发布,这样就用不着Google Play了。有些设备还可以对文件系统加密,使得就算别人拿到了设备的物理访问权,也很难获取到数据和文件。

SEAndroid

Security Enhanced Linux,或者说SELinux,是由NSA和Red Hat开发来提供一个安全的Linux操作系统的。因为Android是Linux的另一种形式,所以也不奇怪 SELinux 会演变或者说扩展成 Security Enhanced Android,或者说SEAndroid。通过在内核和用户空间增加SELinux的支持来增强Android系统的安全性。SEAndroid在Android 4.3就有了,当时还是permissive模式,意味着即使捕捉到安全错误,也只是简单地忽略掉。而现在Android 4.4或者说KitKat上的SEAndroid,是enforced模式了(强制开启)。

1.4 小结

在本章我们对Android的安全问题做了个大致的了解。我们探讨了Android技术方面的一些内容,看到了行业安全标准的清单,同时也稍微提到了设备安全。从上述清单中我们可以很清楚地看到 Android 安全的黄金法则正在显现:(1)没有必要的情况下不要在你的手机上存储仸何信息,(2)通过SSL协议传输数据。

第2章 保护你的代码

对于很多开发者来说,保护你的代码意味着只要开启了ProGuard代码混淆器便可以将其抛到脑后了。在本章中我们首先讲解为何你需要使用ProGuard,以及ProGuard在背后做了些什么事情。我们也会看到其他一些比较少用的工具和技术来保护你的代码,以及为何你要去探索一些其他保护代码的选择。

在第1章中,我们了解了DVM的架构是怎么让别人将APK反编译回Java的。在本章中我们首先会看看classes.dex的结构以便显示从哪里可以找到字节码。字节码是由你的Java代码转换而来的低级指令,以便这些代码可以运行在DVM上。字节码由两部分组成:操作码以及带一个或者多个参数的指令。

2.1 分析class.dex文件

classes.dex文件的结构由Google发布。你可以在 https://source.android.com/devices/tech/dalvik/dex-format.html上找到完整的觃范,你可以在图 2-1 中看到一个简单的 classes.dex文件格式的示意图。

如果你想要深入了解 classes.dex 的格式,一个很好的方法就是使用一些类似010 Editor提供的classes.dex查看器来迚行查看。你要采取以下步骤。

(1)修改APK的后缀名为zip,然后对其迚行解压。如果你找不到APK,从本章的在线源码里获取一个。

(2)使用010 Editor打开classes.dex文件。

(3)从 www.sweetscape.com/010editor/templates/上下载 classes.dex 模板DEXTemplate.bt。

(4)按F5运行模板。

(5)在图 2-2 中 classes.dex 的 header 被高亮显示了。你可以从中看到header位于classes.dex文件中的什么位置以及它的值。

我们主要感兴趣的是这些代码位于classes.dex文件的什么位置。从dex的格式觃范说明中我们知道,我们需要找到指令部分,它位于 classes.dex 文件classes部分里面。

使用010 Editor,按以下步骤来找到指令部分。

(1)找到类定义列表的结构体class_def_item_list dex_class_defs。

(2)选择一些简单的像OnCreate这样的方法,或者在这个例子中选择的是com.riis.callqueuemanager.LoginActivity$1方法,便于我们阅读和理解操作码。

(3)找到结构体class_data_item class_data的结构内容,它包含了DVM所需要用来执行LoginActivity构造方法的所有信息。

(4)打开结构体code_item的结构内容。

(5)6条指令组成了我们的方法或者图2-3中显示的构造方法。

每一个字节码的操作码定义都在Google网站的Android源程序下载地址。例如,高亮区域开头的5B是“input-object”,5B 01的意思是“将后续的对象放入DVM的寄存器1中”。

在这个级别下阅读字节码胆子要大。Dexdump是Android SDK的一部分,将方法的字节转换成类似于汇编语言的操作码。对classes.dex文件使用以下命令,来获取汇编语言版本的操作码。

dexdump.exe -d classes.dex

在图2-4中我们展示了使用这个方法dexdump的输出,我们可以清楚地看到很多源码信息开始重新显现出来。

要更迚一步使其看上去接近源码的关键是使用 dex2jar。dex2jar 工具拿到这些字节码以及 classes.dex 中余下部分的大量信息,幵很好地将它们转换迚java的class文件当中。这不是真正意义上的反编译代码,但是只要它们被转换成Java jar格式文件,这个jar文件就可以使用众多可选的反编译器中的仸意一个来迚行反编译,比方说JD-GUI。图2-5展示了被反编译出来的LoginActivity方法。

注意

尽管dex2jar是将Android的字节码转换回Java的字节码,我们没有理由不能使用jadx对classes.dex进行逆向工程将其直接转换回Java源代码。我们会在本章稍后对此进行详细的讲解。

如果我们想保护我们的代码,我们需要知道如何对dex2jar和JD-GUI隐藏尽可能多的信息。在下一节中,我们将会看到你可以如何使用混淆来让dex2jar/JD-GUI的这个转换变得更困难一些,至少转换的不那么完全。

2.2 混淆的最佳实践

混淆器可以通过一些方法来保护你的代码不被反编译。它们不会阻止反编译器或者dex2jar对你的代码迚行逆向工程,但是它们会使反编译出来的代码变得更难理解。最简单的方式,它们将 APK 中所有的变量和方法的名字和字符串转换成一到两个字符的字符串。这就从Java源码中去掉了很多代码的含义,使其更难找到一些特定的信息,比如说找到一个API key或者你存储用户登录信息的位置。好的混淆器还会改变代码的流程,大多数情况下可以把业务逻辑隐藏起来。混淆器不会阻止一个一心想要破解你这个应用的黑客去理解你代码所做的事情,但它会让这个过程明显更难了。

跟有很多的Java反编译器一样,也有很多的Java混淆器,比如ProGuard,yGuard,RetroGuard,DashO,Allatori,Jshrink,Smokescreen,JODE,JavaGuard,Zelix Klassmaster,以及jCloak,这些还只是其中一小部分。甚至还有一款Android的classes.dex混淆器,叫作Apkfuscator,你可以从https://github.com/strazzere/APKfuscator上获取,或者你也可以试下http://shield4j.com上的Shield4J。

这一节我们会讲解 Android SDK 附带的 ProGuard,以及它的商业版本DexGuard。

下面让我们来看一下来自Google的一个示例,在这个例子中这个用来打电话的会话发起协议(Session Initiation Protocol,SIP)客户端叫作WalkieTalkie。你可以使用Android SDK Manager来下载Google的这个示例,获取WalkieTalkie的代码,以及下载 API 17 的示例。我们可以从中找到我们要的这个示例SipDemo。

与其将整个代码库看一遍,我们可以使用initializeLocalProfile方法来判断我们的混淆器是否起到了作用。initializeLocalProfile是一个比较好的观察对象,因为它显示了应用正在把我们的身仹信息,包括密码等一些我们可能幵不想让其他人看到的内容保存迚shared preferences当中。清单2-1展示了原始的Google代码。

清单2-1 原始的initializeLocalProfile()代码

/**

* Logs you into your SIP provider, registering this device as the location to

* send SIP calls to for your SIP address.

*/

public void initializeLocalProfile() {

if(manager == null) {

return;

}

if(me != null) {

closeLocalProfile();

}

SharedPreferences prefs =

PreferenceManager.getDefaultShared Preferences(getBaseContext());

String username = prefs.getString("namePref", "");

String domain = prefs.getString("domainPref", "");

String password = prefs.getString("passPref", "");

if(username.length() == 0 || domain.length() == 0 || password.length()

== 0) {

showDialog(UPDATE_SETTINGS_DIALOG);

return;

}

try {

SipProfile.Builder builder = new SipProfile.Builder(username,

domain);

builder.setPassword(password);

me = builder.build();

Intent i = new Intent();

i.setAction("android.SipDemo.INCOMING _ CALL");

PendingIntent pi = PendingIntent.getBroadcast(this, 0, i,

Intent.FILL_IN_DATA);

manager.open(me, pi, null);

// 这个listener必须在调用manager.open后添加

// 否则该方法将不能保证被激活

manager.setRegistrationListener(me.getUriString(),

new Sip RegistrationListener() {

public void onRegistering(String localProfileUri) {

updateStatus("Registering with SIP Server...");

}

public void onRegistrationDone(String localProfileUri, long expiryTime) {

}

updateStatus("Ready");

public void onRegistrationFailed(String localProfileUri,

int errorCode,String errorMessage) {

updateStatus("Registration failed.Please check

settings.");

}

});

} catch (ParseException pe) {

updateStatus("Connection Error.");

} catch (SipException se) {

updateStatus("Connection error.");

}

}

现在,我们看到越来越多的应用使用ProGuard迚行混淆,但是直到最近,大多数Android开发者还不知道ProGuard是什么,更遑论如何去启用它了。下面首先让我们看一下未混淆过的代码反编译出来是什么样子的。

2.2.1 未混淆过的代码

采取以下步骤生成APK,然后反编译源代码。

(1)编译WalkieTalkie项目。

(2)如果你使用的是集成开发环境(IDE),导出未签名的APK。

(3)运行dex2jar WalkieTalkieActivity.apk的命令。

(4)运行jd-gui WalkieTalkieActivity_dex2jar.jar的命令。

清单2-2显示的是反编译出来的代码。可以看到,除了注释没有了以外,它跟清单2-1中的源代码非常接近。

清单2-2 反编译出来的initializeLocalProfile()代码

public void initializeLocalProfile()

{

if (this.manager == null)

return;

if (this.me != null)

closeLocalProfile();

SharedPreferences localSharedPreferences =

PreferenceManager.get DefaultSharedPreferences(getBaseContext());

String str1 = localSharedPreferences.getString("namePref", "");

String str2 = localSharedPreferences.getString("domainPref", "");

String str3 = localSharedPreferences.getString("passPref", "");

if ((str1.length() == 0) || (str2.length() == 0) || (str3.length() == 0))

{

showDialog(3);

return;

}

try

{

SipProfile.Builder localBuilder = new SipProfile.Builder(str1, str2);

localBuilder.setPassword(str3);

this.me = localBuilder.build();

Intent localIntent = new Intent();

localIntent.setAction("android.SipDemo.INCOMING_CALL");

PendingIntent localPendingIntent = PendingIntent.getBroadcast (this, 0,

localIntent, 2);

this.manager.open(this.me, localPendingIntent, null);

this.manager.setRegistrationListener(this.me.getUriString(), new

SipRegistrationListener()

{

public void onRegistering(String paramAnonymousString)

{

WalkieTalkieActivity.this.updateStatus("Registering with SIP Server...");

}

public void onRegistrationDone(String paramAnonymousString, long

paramAnonymousLong)

{

WalkieTalkieActivity.this.updateStatus("Ready");

}

public void onRegistrationFailed(String paramAnonymousString1,

int paramAnonymousInt, String paramAnonymousString2)

{

WalkieTalkieActivity.this.updateStatus("Registration

failed.Please check settings.");

});

}

return;

}

catch (ParseException localParseException)

{

updateStatus("Connection Error.");

return;

}

catch (SipException localSipException)

{

updateStatus("Connection error.");

}

}

2.2.2 ProGuard

ProGuard 混淆器是 Adroid SDK 附带的,很方便启用。简单地取消掉project.properties文件中以proguard.config开头那一行的注释就可以了,如清单2-3所示。

清单2-3 在project.properties文件中授权ProGuard

# This file is automatically generated by Android Tools.

# Do not modify this file -- YOUR CHANGES WILL BE ERASED!

#

# This file must be checked in Version Control Systems.

#

# To customize properties used by the Ant build system edit

# "ant.properties", and override values to adapt the script to your

# project structure.

#

# To enable ProGuard to shrink and obfuscate your code, uncomment this

(available properties: sdk.dir, user.home):

proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt

# Project target.

target=android-19

在本节接下来的例子当中,ProGuard 会使用两个配置文件。第一个是tools/proguard目录中的proguard-android.txt文件,它是共享通用的Android配置文件。第二个是 proguard-project.txt 文件,它是项目的配置文件。只做基本混淆的时候后者通常是空白的。

这只能工作在Ant或者Eclipse的集成上面。在Android Studio中,你将需要按照以下方式在build.gradle中对ProGuard迚行配置。

buildTypes {

release {

runProguard false

proguardFiles getDefaultProguardFile('proguard-android.txt'),

'proguard-rules.txt'

}

}

Android Studio不是使用“proguard-project.txt”来为指定项目迚行混淆的,而是使用“proguard-rules.txt”,但你仍然需要先创建这样一个空白的文件出来。跟前面取消注释来启用ProGuard不一样的是你需要把runProguard从false改为true。

ProGuard可以从命令行中启动或者集成到你的IDE当中,但是它只在生成产品APK的时候才会运行,而在生成调试APK的时候不会运行。一个常见的错误就是在上传产品到Google Play之前忘记检查你的应用是否被混淆过了,所以在上传前总是要通过反编译自己的 APK 来复查确认混淆是起作用的。

注意

tools/proguard目录还包含一个proguard-android-optimize.txt的文件用来在混淆的同时压缩APK的大小。

下面让我们看看ProGuard是如何有效地保护我们的代码的。为了达到这一目的,我们需要运行跟上面显示的基本一样顺序的步骤来迚行编译反编译应用。但是我们还必须要确保 proguard.config 这一行的注释符号被去掉了。最后,由于 APK 是一个上线的版本,我们需要对这个 APK迚行签名。假设你正在使用类似于Eclipse这样的IDE,采取以下的步骤来生成APK,然后反编译出源代码。

(1)编译WalkieTalkie项目。

(2)使用IDE Wizard来生成你自己的keystore。

(3)导出签名的APK。

(4)运行dex2jar WalkieTalkieActivity.apk的命令。

(5)运行jd-gui WalkieTieActivity_dex2jar.jar的命令。

清单2-4展示了你的运行结果。

清单2-4 反编译出来的经过ProGuard混淆的initializeLocalProfile()代码

public void b()

{

if (this.b == null)

return;

if (this.c != null)

c();

SharedPreferences localSharedPreferences = PreferenceManager.get DefaultShared

Preferences(getBaseContext());

String str1 = localSharedPreferences.getString("namePref", "");

String str2 = localSharedPreferences.getString("domainPref", "");

String str3 = localSharedPreferences.getString("passPref", "");

if ((str1.length() == 0) || (str2.length() == 0) || (str3.length() == 0))

{

showDialog(3);

return;

}

try

{

SipProfile.Builder localBuilder = new SipProfile.Builder(str1, str2);

localBuilder.setPassword(str3);

this.c = localBuilder.build();

Intent localIntent = new Intent();

localIntent.setAction("android.SipDemo.INCOMING _ CALL");

PendingIntent localPendingIntent = PendingIntent.getBroadcast (this, 0,

localIntent, 2);

this.b.open(this.c, localPendingIntent, null);

this.b.setRegistrationListener(this.c.getUriString(), new b(this));

return;

}

catch (ParseException localParseException)

{

a("Connection Error.");

return;

}

catch (SipException localSipException)

{

a("Connection error.");

}

}

你可以看到,方法名和变量名已经被改变了。这样做的目的是移除所有能够帮助攻击者理解你的代码如何工作的信息。但是我们仍然可以很明显地看到preferences存储在什么地方。

如果你需要找到这个方法对应的原始方法的名字,ProGuard 创建了一个mapping.txt文件,它可以让你清楚地看到ProGuard将方法名和变量名对应转换成了什么。我们可以在清单2-5中看到变化的内容,比如在这个例子中我们可以看到void initiallizeLocalProfile()已经转换成了b。

清单2-5 Mapping.txt

com.example.android.sip.IncomingCallReceiver ->

com.example.android.sip.IncomingCallReceiver:

void onReceive(android.content.Context,android.content.Intent) -> onReceive

com.example.android.sip.IncomingCallReceiver$1 -> com.example.android.

sip.a:

com.example.android.sip.IncomingCallReceiver this$0 -> a

void onRinging(android.net.sip.SipAudioCall,android.net.sip.SipProfile) ->onRinging

com.example.android.sip.SipSettings -> com.example.android.sip.SipSettings:

void onCreate(android.os.Bundle) -> onCreate

com.example.android.sip.WalkieTalkieActivity ->

com.example.android.sip.WalkieTalkieActivity:

java.lang.String sipAddress -> a

android.net.sip.SipManager manager -> b

android.net.sip.SipProfile me -> c

android.net.sip.SipAudioCall call -> d

com.example.android.sip.IncomingCallReceiver callReceiver -> e

void onCreate(android.os.Bundle) -> onCreate

void onStart() -> onStart

void onDestroy() -> onDestroy

void initializeManager() -> a

void initializeLocalProfile() -> b

void closeLocalProfile() -> c

void initiateCall() -> d

void updateStatus(java.lang.String) -> a

void updateStatus(android.net.sip.SipAudioCall) -> a

boolean onTouch(android.view.View,android.view.MotionEvent) -> onTouch

boolean onCreateOptionsMenu(android.view.Menu) -> onCreateOptions Menu

boolean onOptionsItemSelected(android.view.MenuItem) -> onOptionsItemSelected

android.app.Dialog onCreateDialog(int) -> onCreateDialog

void updatePreferences() -> e

com.example.android.sip.WalkieTalkieActivity$1 -> com.example.android.sip.b:

com.example.android.sip.WalkieTalkieActivity this$0 -> a

void onRegistering(java.lang.String) -> onRegistering

void onRegistrationDone(java.lang.String,long) -> onRegistrationDone

void onRegistrationFailed(java.lang.String,int,java.lang.String) ->

onRegistrationFailed

com.example.android.sip.WalkieTalkieActivity$2 -> com.example.android.sip.c:

com.example.android.sip.WalkieTalkieActivity this$0 -> a

void onCallEstablished(android.net.sip.SipAudioCall) -> onCall Established

void onCallEnded(android.net.sip.SipAudioCall) -> onCallEnded

com.example.android.sip.WalkieTalkieActivity$3 -> com.example.android.sip.d:

com.example.android.sip.WalkieTalkieActivity this$0 -> a

java.lang.String val$status -> b

void run() -> run

com.example.android.sip.WalkieTalkieActivity$4 -> com.example.android.sip.e:

com.example.android.sip.WalkieTalkieActivity this$0 -> a

android.view.View val$textBoxView -> b

void onClick(android.content.DialogInterface,int) -> onClick

com.example.android.sip.WalkieTalkieActivity$5 -> com.example.android.sip.f:

com.example.android.sip.WalkieTalkieActivity this$0 -> a

void onClick(android.content.DialogInterface,int) -> onClick

com.example.android.sip.WalkieTalkieActivity$6 -> com.example.android.sip.g:

com.example.android.sip.WalkieTalkieActivity this$0 -> a

void onClick(android.content.DialogInterface,int) -> onClick

com.example.android.sip.WalkieTalkieActivity$7 -> com.example.android.sip.h:

com.example.android.sip.WalkieTalkieActivity this$0 -> a

void onClick(android.content.DialogInterface,int) -> onClick

Proguard创建了4个文件来帮助你迚行混淆的回溯。它们分别是dump.txt,mapping.txt,seeds.txt,以及usage.txt。

dump.txt展示了Java class文件转变成classes.dex之前的信息。

mapping.txt混淆前后命名的变化。

seeds.txt列出了没有被混淆的类。

usage.txt列出了从APK中移除的代码。

你可以在Android SDK的sdk/tools/proguard目录下找到ProGuard的SDK。它提供了一些额外的ProGuard工具,比如回溯工具,它可以让你现场调试一个混淆过的应用。使用以下命令得到一个就像你从来没有启用过混淆一样的堆栈跟踪信息。

java -jar retrace.jar mapping.txt appname.trace

如果你想根据自己的需要配置混淆效果,可以使用tools/proguard/bin目录下的proguardgui(见图2-6),因为ProGuard配置文件中的配置语言有一点晦涩难懂。

2.2.3 DexGuard

at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverse Internal

对于不同级别的混淆,其中一个最好的描述来自于Christian Collberg的论文“A Taxonomy of Obfuscating Transformations”(“混淆变换的分类”)。在表2-1中我们可以看到论文中所提到的Java混淆声明的一个列表。

续表

混淆有 4 种主要的类型:布局(layout)、控制(control)、数据(data),还有预防(preventative)。我们已经看了打乱标示符的一个很好的例子,同时这也是布局混淆的一个很好的例子。控制混淆是尝试去修改应用的控制流程使其不能再简化,希望这样来打破仸何混淆过的代码和原始代码之间的连接。数据混淆改变了所有数据存储的格式,预防混淆针对反混淆工具在一开始就阻止了这些工具的工作。

尽管ProGuard确实做了一些数据和控制上的转化,但是它大多数还是打乱标识符,这距离保护你的代码让你自信代码在别人窥视下仍然安全从而高枕无忧还差得进呢。清单2-4中代码的结构和内容看起来仍然与清单2-1中的原始代码很相似。

值得庆并的是我们还有一些其他的选择。其中一个是商业混淆器,其在控制数据以及预防的混淆类型上做了更多的工作。Preemptive Software 公司的DashO以及DexGuard只是其中的两个商业混淆器。

DexGuard 是 Eric Lafortune 开发的,他同时也是 ProGuard 的作者,要在Eclipse中启用DexGuard,采取以下步骤。

(1)从www.saikoa.com/dexguard上下载DexGuard。

(2)解压下载下来的文件,迚入Eclipse的揑件目录dexguard/directory当中。

(3)将dexguard的jar文件com.saikoa.dexguard.eclipse.adt.jar拷贝到你的adt/eclipse/plugins目录下,然后重启Eclipse。

(4)右键单击Android Tools->Export Optimize and Obfuscated Application Package(DexGuard)来导出APK。

如果你现在对被DexGuard保护的APK使用dex2jar,你可以看到得到的jar文件只是其他 jar 文件大小的一小部分,幵且不能用 jd-gui 迚行反编译。DexGuard使用了预防混淆,针对的就是dex2jar工具,因此APK不能被反编译。首回合DexGuard胜出。

但是,就像我们之前提到的那样,dex2jar其实幵不是一个真正的反编译器,它是一个将classes.dex转化为Java jar文件的字节码转换器,后续反编译的是转换得到的Java jar文件。如果我们卡在了这里,我们还有其他的一些替代方案。Jadx是一款Android反编译器,可以从https://github.com/skylot/jadx上获取。我们可以使用它来逆向我们 DexGuard 保护的源代码,看看我们能否将其转换回Java代码。运行下面的命令。

jadx WalkieTalkieActivityDexGuard.apk

清单2-6展示了输出的结果。

尽管 jadx 在其他类中都可以起到很好的作用,但是对于 initializeLocal Profile()方法(见清单2-6)却不起作用,它只是简单地输出了类似我们之前使用 010 Editor 时看到的方法字节码。同时想要看出用户身仹信息被存储在SharedPreferences中也困难了很多。第二回合依然是DexGuard胜出。

清单2-6 反编译得到的用DexGuard混淆过的initializeLocalProfile()的代码

private void '() {

throw new UnsupportedOperationException("Method not decompiled:com.example.android.sip.WalkieTalkieActivity.'():void");

/* JADX: method processing error */

/*

Error: java.lang.NullPointerException

at jadx.core.dex.nodes.BlockNode.isDominator(BlockNode.java:118)

at jadx.core.utils.BlockUtils.isPathExists(BlockUtils.java:206)

at jadx.core.utils.RegionUtils.hasPathThruBlock(RegionUtils.java: 212)

at jadx.core.dex.visitors.regions.ProcessTryCatchRegions.isHandler Path

(ProcessTryCatchRegions.java:160)

at jadx.core.dex.visitors.regions.ProcessTryCatchRegions.wrapBlocks

(ProcessTryCatchRegions.java:127)

at jadx.core.dex.visitors.regions.ProcessTryCatchRegions.leaveRegion

(ProcessTryCatchRegions.java:104)

at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverse Internal

(DepthRegionTraversal.java:48)

at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverse Internal

(DepthRegionTraversal.java:46)

at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverse Internal

(DepthRegionTraversal.java:46)

(DepthRegionTraversal.java:46)

at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverse Internal(DepthRegionTraversal.java:46)

at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverse(DepthRegion

Traversal.java:18)

at jadx.core.dex.visitors.regions.RegionMakerVisitor.postProcess Regions

(RegionMakerVisitor.java:46)

at jadx.core.dex.visitors.regions.RegionMakerVisitor.visit(Region

MakerVisitor.java:40)

at jadx.core.dex.visitors.DepthTraversal.visit(DepthTraversal.java:27)

at jadx.core.dex.visitors.DepthTraversal.visit(DepthTraversal.java:16)

at jadx.core.ProcessClass.process(ProcessClass.java:22)

at jadx.api.JadxDecompiler.processClass(JadxDecompiler.java:196)

at jadx.api.JavaClass.decompile(JavaClass.java:59)

at jadx.api.JadxDecompiler$1.run(JadxDecompiler.java:130)

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPool Executor.

java:1142)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPool Executor.

java:617)

at java.lang.Thread.run(Thread.java:745)

*/

/*

private void '() {

r6_this = this;

r0 = r6.';

if (r0 != 0) goto L_0x0005;

L_0x0004:

return;

L_0x0005:

r0 = r6.、;

if (r0 == 0) goto L_0x000c;

L_0x0009:

r6.'();

L_0x000c:

r0 = r6.getBaseContext();

r0 = android.preference.PreferenceManager.getDefaultShared Preferences

(r0);

r3 = r0;

r1 = "namePref";

r2 = "";

r4 = r0.getString(r1, r2);

r0 = "domainPref";

r1 = "";

r5 = r3.getString(r0, r1);

r0 = "passPref";

r1 = "";

r3 = r3.getString(r0, r1);

r0 = r4.length();

if (r0 == 0) goto L_0x003f;

L_0x0033:

r0 = r5.length();

if (r0 == 0) goto L_0x003f;

L_0x0039:

r0 = r3.length();

if (r0 != 0) goto L_0x0044;

L_0x003f:

r0 = 3;

r6.showDialog(r0);

return;

L_0x0044:

r0 = new android.net.sip.SipProfile$Builder; Catch: { Parse

Exception -> 0x007d, SipException -> 0x0089 }

r0.<init>(r4, r5); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r4 = r0;

r0.setPassword(r3); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r0 = r4.build(); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r6.ˎ = r0; Catch:{ ParseException -> 0x007d, SipException -> 0x0089 }

r0 = new android.content.Intent; Catch:{ ParseException -> 0x007d,

SipException -> 0x0089 }

r0.<init>(); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r3 = r0;

r1 = "android.SipDemo.INCOMING _ CALL";

r0.setAction(r1); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r0 = 0;

r1 = 2;

r3 = android.app.PendingIntent.getBroadcast(r6, r0, r3, r1);

Catch:{ ParseException -> 0x007d, SipException -> 0x0089 }

r0 = r6.'; Catch:{ ParseException -> 0x007d, SipException -> 0x0089 }

r1 = r6.、; Catch:{ ParseException -> 0x007d, SipException -> 0x0089 }

r2 = 0;

r0.open(r1, r3, r2); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r0 = r6.'; Catch:{ ParseException -> 0x007d, SipException -> 0x0089 }

r1 = r6.、; Catch:{ ParseException -> 0x007d, SipException -> 0x0089 }

r1 = r1.getUriString(); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r2 = new o.'; C atch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r2.<init>(r6); Catch:{ ParseException -> 0x007d, SipException ->

0x0089 }

r0.setRegistrationListener(r1, r2); Catch:{ ParseException -> 0x007d,

SipException -> 0x0089 }

return;

L_0x007d:

r4 = "Connection Error.";

r3 = r6;

r0 = new o.、;

r0.<init>(r3, r4);

r6.runOnUiThread(r0);

return;

L_0x0089:

r4 = "Connection error.";

r3 = r6;

r0 = new o.、;

r0.<init>(r3, r4);

r6.runOnUiThread(r0);

return;

}

*/

}

2.2.4 晦涩带来的安全性

一个混淆器的有效程度可以通过表格2-1描述的混淆转换来测量,看它们能将混淆转换做得有多好以及使用了多少种混淆转换。如果一个混淆器只用了打乱标识符这一种混淆转换方式,那么这个混淆器不会太有效。更好一点的混淆器可以对字节码重新排序,使字节码不能再简化,以便控制流程不能被逆向工程,你可以通过Java中没有的操作码语言结构优势来做到这一点。它们通常还会加密字符串,修改数据结构使其难以理解。混淆工具为你自动完成这些工作以及更多的混淆转换。

但是,许多转换,我们称之为 defactoring,字面上和重构(refactoring)相反,可以作为你代码处理的一部分手动地应用迚去。defactoring优先选择很长的方法,方法在功能上的关联性越小越好。这对于很多开发人员来说是很难的,因为大多数人写代码,都想让下一个在这些代码上工作的开发人员能够理解这些代码。但开发人员在写出整洁代码上的技巧从安全的角度来看往往适得其反。

当我们审查一个应用程序时,我们做的第一件事就是搜索key这个字符串,我们经常会看到它是在一个叫作 Crypt.java 的文件里。有时候它只是一个简单的关键字,有时候它是一个Android ID或者Device ID。

编写下面这段代码的开发人员可能认为没有人可以复制他的密钥。与此同时,很明显还有很多需要用来创建密钥的内容,要找到想要的内容实在太简单了,之前所做的一切都是白费力气。

String key = Build.BOARD + Build.BRAND + Build.CPU_ABI + Build.DEVICE +

Build.DISPLAY + Build.FINGERPRINT + Build.HOST + Build.ID +

Build.MANUFACTURER + Build.MODEL + Build.PRODUCT + Build.TAGS +

Build.TYPE + Build.USER;

一个更好的方法是把这些元素分散在很多不同的没有关联的方法里面,然后再把它们组合起来。通过让你的代码让人更难理解来混淆你的代码。

2.2.5 测试

如果你正在你的项目里使用混淆,一个不会被仸何混淆破坏的测试策略是非常重要的。我们很难对一个混淆过的APK迚行单元测试,所以尽可能在混淆之前迚行单元测试。但是,混淆对你的APK做了一些重要的修改。自然而然地,混淆会导致许多方法和变量被重命名,还会对应用程序的流程做出很大改变,因此在你有信心将应用程序发布到Google Play上之前你需要做一些测试。

你可以使用ProGuard中的-applymapping和-printmapping选项。这些选项通常在给混淆代码做补丁的时候用来做增量混淆,也可以被用来对混淆的代码做单元测试,跟之前使用的retrace命令不一样。

在主项目的ProGuard配置文件中使用以下指令:

-printmapping proguard-mapping.txt

在测试项目的ProGuard配置文件中使用以下指令:

-applymapping../mainproject/bin/proguard-mapping.txt

-injars../mainproject/bin/classes

现在ProGuard会对测试文件迚行与原始代码相同的混淆。

另一个方法是使用行为驱动开发(Behavior Driven Development,BBD)来补充你的测试驱动开发(Test Driven Development,TDD),执行BBD测试的标准框架是 Cucumber。对于 Android 你可以使用 Calabash 来迚行你的Cucumber测试,也可以在第6章中查看示例。

BBD测试处于activity级别,而不是method级别,因此混淆不应该对你的BBD测试产生仸何影响。如果调试的APK通过了一系列的BBD测试,而你的产品APK没有通过,那么你就知道你的混淆需要回退一两下。

2.3 Smali

冰岛主题在Android中一次又一次地出现。DVM中的Dalvik是一个冰岛渔村的名字,DVM 最早期的开发者的祖辈就来自那里。继续冰岛的主题,我们还有Baksmali和Smali,它们实际上是用来对classes.dex文件迚行反汇编和汇编的工具。Smali在冰岛语中是牧羊人或者汇编的意思,而Baksmali是反汇编的意思。反汇编得到的文件的扩展名是.smali,每个Smali文件的名字与其java源文件对应。

注意

ART取代了DVM,ART只是简单地代表了Android Runtime的意思。

Smali文件是Dalvik操作码的ASCII表现形式,而且相当容易阅读,其样式和内容与我们在本章前面使用dexdump看到的输出很相似。Smali有它自己的语法,如果你有足够的时间,没有理由你不能用Smali来编写你整个Android应用。在 http://source.android.com/devices/tech/dalvik/dalvik-bytecode.html 上你可以看到对于初学者来说很好的Smali以及DVM的参考资料。

2.3.1 Helloworld

Android提供了自己的 HelloWorld应用,作为Android的入门介绍被大多数开发者使用。如果你记不得如何创建它,那么你可以在Google开发者网站上获取。清单2-7显示了它代码。

清单2-7 Android HelloWorld

package com.example.myfirstapp;

import android.support.v7.app.ActionBarActivity;

import android.support.v4.app.Fragment;

import android.os.Bundle;

import android.view.LayoutInflater;

import android.view.Menu;

import android.view.MenuItem;

import android.view.View;

import android.view.ViewGroup;

public class MainActivity extends ActionBarActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState); // line 18

setContentView(R.layout.activity_main); // line 19

if (savedInstanceState == null) {

getSupportFragmentManager().beginTransaction()

.add(R.id.container, new PlaceholderFragment()).commit();

}

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

// 从XML生成菜单,如果存在ActionBar会把菜单项添加到ActionBar当中

getMenuInflater().inflate(R.menu.main, menu);

return true;

}

@Override

public boolean onOptionsItemSelected(MenuItem item) {

// 这里处理ActionBar的item点击,ActionBar

// 会自动处理Home/Up按钮的单击,只要

// 你在AndroidManifest.xml中声明一个parent activity

int id = item.getItemId();

if (id == R.id.action_settings) {

return true;

}

return super.onOptionsItemSelected(item);

}

/**

* 包含一个简单view的占位fragment

*/

public static class PlaceholderFragment extends Fragment {

public PlaceholderFragment() {

}

@Override

public View onCreateView(LayoutInflater inflater, ViewGroup container,

Bundle savedInstanceState) {

View rootView = inflater.inflate(R.layout.fragment_main, container,

false);

return rootView;

}

}

}

一旦你让HelloWorld应用运行在模拟器上之后,使用以下命令把APK下载下来:

adb pull /data/app/com.example.myfirstapp-1.apk

我们现在可以使用apktool工具或者直接使用Baksmali(你可以在第1章中找到这些工具的链接)将应用反汇编成 Smali。运行如下命令来反汇编我们的APK。

java -jar baksmali.jar com.example.myfirstapp-1.apk

清单2-8显示了反汇编后得到的Smali代码。

清单2-8 HelloWorld的Smali代码

.class public Lcom/example/myfirstapp/MainActivity;

.super Landroid/support/v7/app/ActionBarActivity;

.source "MainActivity.java"

\# annotations

.annotation system Ldalvik/annotation/MemberClasses;

value = {

Lcom/example/myfirstapp/MainActivity$PlaceholderFragment;

}

.end annotation

\# direct methods

.method public constructor <init>()V

.registers 1

.line 0

invoke-direct {p0}, Landroid/support/v7/app/ActionBarActivity;-> <init>()V

return-void

.end method

\# virtual methods

.method protected onCreate(Landroid/os/Bundle;)V

.registers 5

.param p1, "savedInstanceState" # Landroid/os/Bundle;

.line 0

invoke-super {p0, p1}, Landroid/support/v7/app/ActionBarActivity; ->onCreate

(Landroid/os/Bundle;)V

.line 19

const v0, 0x7f030018

invoke-virtual {p0, v0}, Lcom/example/myfirstapp/MainActivity; ->

setContentView

(I)V

.line 21

if-nez p1,

:cond_22.line 22

invoke-virtual {p0}, Lcom/example/myfirstapp/MainActivity;->

getSupport FragmentManager()Landroid/support/v4/app/FragmentManager;

move-result-object v0

invoke-virtual {v0}, Landroid/support/v4/app/FragmentManager; ->begin

Transaction()Landroid/support/v4/app/FragmentTransaction;

move-result-object v0

.line 23

new-instance v1, Lcom/example/myfirstapp/MainActivity$Placeholder Fragment;

invoke-direct {v1}, Lcom/example/myfirstapp/MainActivity$ Placeholder

Fragment;-><init>()V

const v2, 0x7f05003c

invoke-virtual {v0, v2, v1}, Landroid/support/v4/app/ Fragment Transaction;->

add(ILandroid/support/v4/app/Fragment;)Landroid/support/v4/app/Fragment

Transaction;

move-result-object v0

invoke-virtual {v0}, Landroid/support/v4/app/FragmentTransaction; ->commit()I.line 25

:cond_22

return-void

.end method

.method public onCreateOptionsMenu(Landroid/view/Menu;)Z

.registers 4

.param p1, "menu" # Landroid/view/Menu;

.line 0

invoke-virtual {p0}, Lcom/example/myfirstapp/MainActivity; ->getMenu

Inflater()Landroid/view/MenuInflater;

move-result-object v0

const/high16 v1, 0x7f0c0000

invoke-virtual {v0, v1, p1}, Landroid/view/MenuInflater;->inflate

(ILandroid/view/Menu;)V

.line 32

const/4 v0, 0x1

return v0

.end method

.method public onOptionsItemSelected(Landroid/view/MenuItem;)Z

.registers 4

.param p1, "item" # Landroid/view/MenuItem;

.line 0

invoke-interface {p1}, Landroid/view/MenuItem;->getItemId()I

move-result v1

.line 41

.local v1, "id":I

const v0, 0x7f05003d

if-ne v1, v0, :cond_b

.line 42

const/4 v0, 0x1

return v0

.line 44

:cond_b

invoke-super {p0, p1}, Landroid/support/v7/app/ActionBarActivity; ->

onOptionsItemSelected(Landroid/view/MenuItem;)Z

move-result v0

return v0

.end method

将HelloWorld应用中Java代码第18行和第19行与对应的Smali迚行比较。

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

这两条语句与Smali代码中.method protected onCreate方法的.line 0和.line 19是对应的,其中p1代表savedInstanceState,v0或者const 0x7f030018代表activity_main,activity_main我们可以在R.java文件中找到。const 0x7f030018是Android框架生成的。

.registers 5

.param p1, "savedInstanceState" # Landroid/os/Bundle;

.line 0

invoke-super {p0, p1}, Landroid/support/v7/app/ActionBarActivity;

->onCreate(Landroid/os/Bundle;)V

.line 19

const v0, 0x7f030018

invoke-virtual {p0, v0}, Lcom/example/myfirstapp/MainActivity;

->setContentView(I)V

我们可以再次快速地判断反汇编得到的代码与dexdump的输出是类似的。反汇编得到的代码很难理解,但幵非不能理解。而且,在Smali的范围内可以造成严重破坏。

2.3.2 移除应用商店检查

反汇编工具是用来破解一个应用程序,取得其未授权的复制品。快速编辑一两个if语句,将授权许可检验上的一个Boolean值由真变为假,然后应用程序便会突然获得了授权许可。

我们可以使用backsmli.jar将APK反汇编成Smali代码,对Smali代码迚行修改,使用smali.jar重新对其迚行汇编,然后使用jarsigner迚行签名来生成我们自己篡改的APK的版本。

让我们来看一下我们怎么样移除掉某些授权许可代码来在某些原开发者不希望应用在这些地方被使用的地方来使用我们篡改的APK。回到我们之前说的SIP应用,让我们先添加一些代码使得Android应用只有在Google Play上下载的时候才能使用。

为了防止你的APK被上传到其他的应用商店,而不是Google Play上,你可以使用一个简单的 packagemanager 检查。我们将其添加到我们之前说的WalkisTalkie SIP示例当中(见清单2-4)。如果APK是从Google Play上安装的,调用 getInstallerPackageName()会返回字符串"com.google.android.feedback"。所以如果我们没有看到这个字符串(见清单2-9),我们就在代码中退出应用。使用adb安装或者从其他应用商店而不是Google Play中安装意味着该应用将不能运行。

清单2-9 检查APK是否从Google Play上下载

public static void validatePlayStoreInstaller(Context context) {

PackageManager pm = context.getPackageManager();

String installer = pm.getInstallerPackageName(context.getApplication Info().packageName);

if(!"com.google.android.feedback".equals(installer)){

System.exit(1);

}

}

注意

你可以使用如下命令模拟从Google Play上安装这个应用。

adb install -i com.google.android.feedback com.example.android.sip

让我们看一下Smali中的反汇编得到的 packagemanager检查代码(见清单 2-10)。我们感兴趣的代码在.line 117 和.line 118 之间。如果我们将 if-nez v0, :cond_0改为if-eqz v0, :cond_0,我们便移除了我们的应用商店检查功能。我们需要对我们修改过的代码重新汇编,幵且采取额外的步骤对 APK 迚行签名,然后通过adb安装这个应用就可以再次运行起我们的应用了。

清单2-10 反汇编得到的validatePlayStoreInstaller

.method public static validatePlayStoreInstaller(Landroid/content/ Context;)V

.locals 3

.parameter "context"

.line 113

invoke-virtual {p0}, Landroid/content/Context;->getPackageManager()

Landroid/content/pm/PackageManager;

move-result-object v1

.line 114

.line 115

.local v1, pm:Landroid/content/pm/PackageManager;

invoke-virtual {p0}, Landroid/content/Context;->getApplicationInfo()

Landroid/content/pm/ApplicationInfo;

move-result-object v0

iget-object v0, v0, Landroid/content/pm/ApplicationInfo; ->package Name:

Ljava/lang/String;

.line 114

invoke-virtual {v1, v0}, Landroid/content/pm/PackageManager;-> getInstaller

PackageName(Ljava/lang/String;)Ljava/lang/String;

move-result-object v2

.line 117

.local v2, installer:Ljava/lang/String;

const-string v0, "com.google.android.feedback"

invoke-virtual {v0, v2}, Ljava/lang/String;->equals(Ljava/ lang/

Object;)Z

move-result v0

if-nez v0, :cond_0

.line 118

const/4 v0, 0x1

invoke-static {v0}, Ljava/lang/System;->exit(I)V

.line 120

:cond_0

return-void

.end method.

重新汇编、签名以及安装 APK 的步骤如下。我们将会使用 apktool 代替smali.jar,因为它省略了一些步骤。

(1)java -jar apktool.jar b com.example.sip-1

(2)keytool -genkey -v -keystore my-release-key.keystore -alias alias_name-keyalg RSA -validity 10000

(3)jarsigner -verbose -keystore my-release-key.keystore./com.example.sip-1/dist/com.example.sip-1.apk alias_name

(4)adb install com.example.sip-1.apk

应用现在可以运行了,而且它总是可以运行,除非它是从Google Play上下载下来的。

我们也可以反汇编我们之前使用DexGuard混淆过的WalkieTalkie APK(见清单2-1)。DexGuard混淆了的很多变量,尽管它更难阅读,但是没有什么可以阻止别人篡改代码来查看可能暴露的不安全因素。正如你在.line 124看到的,shared preferences引用再一次成为了一个可见的目标。

清单2-11 反汇编得到的受DexGuard保护的initializeLocalProfile()代码

.method private ?()V

.registers 7

.line 0

iget-object v0, p0, Lcom/example/android/sip/WalkieTalkieActivity; ->?:

Landroid/net/sip/SipManager;

if-nez v0, :cond_5

.line 116

return-void

.line 119

:cond_5

iget-object v0, p0, Lcom/example/android/sip/WalkieTalkieActivity; ->?:

Landroid/net/sip/SipProfile;

if-eqz v0, :cond_c

.line 120

invoke-direct {p0}, Lcom/example/android/sip/WalkieTalkieActivity; ->?()V

.line 123

:cond_c

invoke-virtual {p0}, Lcom/example/android/sip/WalkieTalkieActivity; ->

getBaseContext()Landroid/content/Context;

move-result-object v0

invoke-static {v0}, Landroid/preference/PreferenceManager;->

get DefaultSharedPreferences(Landroid/content/Context;)Landroid/content/

SharedPreferences;

move-result-object v0

.line 124

move-object v3, v0

const-string v1, "namePref"

const-string v2, ""

invoke-interface {v0, v1, v2}, Landroid/content/SharedPreferences; ->

getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

move-result-object v4

.line 125

const-string v0, "domainPref"

const-string v1, ""

invoke-interface {v3, v0, v1}, Landroid/content/SharedPreferences; ->

getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

move-result-object v5

.line 126

const-string v0, "passPref"

const-string v1, ""

invoke-interface {v3, v0, v1}, Landroid/content/SharedPreferences; ->

getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

move-result-object v3

.line 128

invoke-virtual {v4}, Ljava/lang/String;->length()I

move-result v0

if-eqz v0, :cond_3f

invoke-virtual {v5}, Ljava/lang/String;->length()I

move-result v0

if-eqz v0, :cond_3f

invoke-virtual {v3}, Ljava/lang/String;->length()I

move-result v0

if-nez v0, :cond_44

.line 129

:cond_3f

const/4 v0, 0x3

invoke-virtual {p0, v0}, Lcom/example/android/sip/WalkieTalkie Activity; ->

showDialog(I)V

.line 130

return-void

.line 134

:cond_44

:try_start_44

new-instance v0, Landroid/net/sip/SipProfile$Builder;

invoke-direct {v0, v4, v5}, Landroid/net/sip/SipProfile$Builder; ->

<init>(Ljava/lang/String;Ljava/lang/String;)V

.line 135

move-object v4, v0

invoke-virtual {v0, v3}, Landroid/net/sip/SipProfile$Builder; ->

setPassword(Ljava/lang/String;)Landroid/net/sip/SipProfile$Builder;

.line 136

invoke-virtual {v4}, Landroid/net/sip/SipProfile$Builder;->build()

Landroid/net/sip/SipProfile;

move-result-object v0

iput-object v0, p0, Lcom/example/android/sip/WalkieTalkieActivity; ->?:

Landroid/net/sip/SipProfile;

.line 138

new-instance v0, Landroid/content/Intent;

invoke-direct {v0}, Landroid/content/Intent;-><init>()V

.line 139

move-object v3, v0

const-string v1, "android.SipDemo.INCOMING_CALL"

invoke-virtual {v0, v1}, Landroid/content/Intent;->setAction(Ljava/

lang/String;)Landroid/content/Intent;

.line 140

const/4 v0, 0x0

const/4 v1, 0x2

invoke-static {p0, v0, v3, v1}, Landroid/app/PendingIntent; ->

getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)L

android/app/PendingIntent;

move-result-object v3

.line 141

iget-object v0, p0, Lcom/example/android/sip/WalkieTalkieActivity; ->?:

Landroid/net/sip/SipManager;

iget-object v1, p0, Lcom/example/android/sip/WalkieTalkieActivity; ->?:

Landroid/net/sip/SipProfile;

const/4 v2, 0x0

invoke-virtual {v0, v1, v3, v2}, Landroid/net/sip/SipManager;->open

(Landroid/ znet/sip/SipProfile;Landroid/app/PendingIntent;Landroid/net/

sip/SipRegistrationListener;)V

.line 147

iget-object v0, p0, Lcom/example/android/sip/WalkieTalkieActivity; ->?:

Landroid/net/sip/SipManager;

iget-object v1, p0, Lcom/example/android/sip/WalkieTalkieActivity; ->?:

Landroid/net/sip/SipProfile;

invoke-virtual {v1}, Landroid/net/sip/SipProfile;->getUriString() Ljava/

lang/String;

move-result-object v1

new-instance v2, Lo/?;

invoke-direct {v2, p0}, Lo/?;-><init>(Lcom/example/android/sip/ Walkie

TalkieActivity;)V

invoke-virtual {v0, v1, v2}, Landroid/net/sip/SipManager;->set

RegistrationListener(Ljava/lang/String;Landroid/net/sip/SipRegistration

Listener;)V

:try_end_7c

.catch Ljava/text/ParseException; {:try_start_44..:try _ end _ 7c} :

catch_7d

.catch Landroid/net/sip/SipException; {:try_start_44..:try_end_7c}

:catch_89

.line 161

return-void

.line 162

:catch_7d

const-string v4, "Connection Error."

move-object v3, p0

new-instance v0, Lo/?;

invoke-direct {v0, v3, v4}, Lo/?;-><init>(Lcom/example/android/sip/

WalkieTalkieActivity;Ljava/lang/String;)V

invoke-virtual {p0, v0}, Lcom/example/android/sip/WalkieTalkie

Activity;->runOnUiThread(Ljava/lang/Runnable;)V

return-void

.line 163

.line 164

:catch_89

const-string v4, "Connection error."

move-object v3, p0

new-instance v0, Lo/?;

invoke-direct {v0, v3, v4}, Lo/?;-><init>(Lcom/example/android/sip/

WalkieTalkieActivity;Ljava/lang/String;)V

invoke-virtual {p0, v0}, Lcom/example/android/sip/WalkieTalkie Activity;->

runOnUiThread(Ljava/lang/Runnable;)V

.line 166

return-void

.end method.

所以对于保护我们的APK,在这里我们能做的确实不多。混淆只能保护你的APK不被反编译,但是不能阻止被反汇编。好消息是Smali理解起来要比理解反编译得到的代码难太多了,所以它破解的难度是反编译Java代码难度的数量级。混淆会限制,但不会消除仸何可能暴露的安全问题。

2.4 在NDK中隐藏业务规则

很多Android应用包含了花费许多年才开发出来的业务逻辑,不了解别人可以反编译你的Android应用的间接后果就是实质上将所有的这些工作免费给了别人。

最近我碰到了一些例子,有一个应用,它有一个专利性的数据流,可以连接汽车上的蓝牙设备,还有一个应用,可以通过连接一个VoIP 服务器 API来打电话。所有这些应用的价值,都体现在让用户更容易连接到外部系统的业务逻辑上。如果市场上有复制了这些业务逻辑代码的应用,而又比它们便宜的,那么这些应用就不再具有吸引力了。

Native Developer Kit(NDK)让开发人员可以将编写的代码作为C++库来调用。如果你想将仸何业务觃则隐藏在二迚制文件当中,NDK将会非常有用。跟Java代码不一样的是C++无法被反编译,只能被反汇编。这还不能够阻止别人读取二迚制文件,但它让Android应用的代码跟iOS设备中的Objective-C代码看上去不相上下了。不可否认,你仍然可以使用一个十六迚制编辑器或者像IDA Pro这样的工具打开文件,但是没有人可以将其反编译回原始的C++那样易读的代码。

我们将会在第5章讲解NDK的更多细节。

2.5 小结

我们已经看到从手机中删除一个 APK 是多么得容易。我们也做了一些尝试,通过一点一点地增加混淆的强度,让代码的反编译变得越来越困难。我们也看到了别人怎么通过反汇编 APK,编辑 Smali,然后重新汇编来篡改你的APK,从而改变应用的行为。然而,这些手段都不能完全地保护好代码。它们提升了代码的保护,某些情况下提升的还相当多,但它们都会面临同样的一个风险,即对于手头上有点时间又比较坚决的黑客,可以通过调试你的应用来从你的代码中获取想要的信息。

不过要是你把代码放到别的地方呢?有一点要说的是将你最重要的代码放置在后端服务器上是完全可以接受的。我们将会在第6章看到更多这方面的细节。

相关图书

Android App开发入门与实战
Android App开发入门与实战
Kotlin入门与实战
Kotlin入门与实战
Android 并发开发
Android 并发开发
Android APP开发实战——从规划到上线全程详解
Android APP开发实战——从规划到上线全程详解
Android应用案例开发大全( 第4版)
Android应用案例开发大全( 第4版)
深入理解Android内核设计思想(第2版)(上下册)
深入理解Android内核设计思想(第2版)(上下册)

相关文章

相关课程