在本章中,我们将介绍以下食谱:
- 保护应用组件
- 使用自定义权限保护组件
- 保护内容提供商路径
- 防御 SQL 注入攻击
- 应用签名验证(防篡改)
- 通过检测安装程序、模拟器和调试标志进行篡改保护
- 用 ProGuard 删除所有日志消息
- 使用 DexGuard 进行高级代码混淆
到目前为止,我们已经看到了如何设置和定制一个环境来发现和利用安卓应用中的漏洞。在本章中,我们将讨论几种保护技术,以使逆向工程人员和攻击者更加困难。
开发应用时的一个常见错误是无意中暴露了应用组件。我们将重点关注如何防止组件被其他应用公开和访问。我们还将看到如果需要共享数据,如何使用自定义权限来限制访问。
入侵或篡改检测是所有良好防御系统的基石,为此,我们将尝试检测攻击是否正在进行,以及我们的应用是否在受损状态下运行。
在这一章的最后,我们将介绍两种方法,让逆向工程的工作变得更加困难。我们将看到如何使用代码混淆和自定义 ProGuard 配置来从应用中移除所有日志消息并隐藏敏感的 API 调用。
第 7 章、安全联网中介绍了保护网络传输中的数据的主题,而第 9 章、加密和开发设备管理策略中介绍了如何通过加密来保证数据的安全。
通过正确使用AndroidManifest.xml
文件和在代码级别强制进行权限检查,可以保护应用组件。应用安全性的这两个因素使得权限框架非常灵活,并允许您以非常精细的方式限制访问组件的应用的数量。
您可以采取许多措施来锁定对组件的访问,但在此之前,您应该做的是确保您了解组件的用途、为什么需要保护它,以及如果恶意应用开始向您的应用发出攻击并访问其数据,您的用户将面临什么样的风险。这被称为基于风险的安全方法,建议您在配置您的AndroidManifest.xml
文件并向您的应用添加权限检查之前,首先诚实地回答这些问题。
在这份食谱中,我详细介绍了您可以采取的一些措施来保护通用组件,无论它们是活动、广播接收器、内容提供商还是服务。
首先,我们需要查看你的安卓应用AndroidManifest.xml
文件。android:exported
属性定义一个组件是否可以被其他应用调用。如果您的任何应用组件不需要被其他应用调用,或者需要明确屏蔽与安卓系统其他部分的组件(除了应用内部的组件)的交互,您应该向应用组件的 XML 元素添加以下属性:
<[component name] android:exported="false">
</[component name]>
这里的[component name]
可以是活动、提供者、服务或接收者。
通过AndroidManifest.xml
文件实施权限对于每个应用组件类型来说都意味着不同的事情。这是因为可以使用各种进程间通信 ( IPC ) 机制与它们进行交互。对于每个应用组件,android:permission
属性执行以下操作:
- 活动:将您的应用外部可以成功调用
startActivity
或startActivityForResult
的应用组件限制为具有所需权限的组件 - 服务:将可以绑定(通过调用
bindService()
)或启动(通过调用startService()
)服务的外部应用组件限制为具有指定权限的组件 - 接收者:限制可以向具有指定权限的接收者发送广播意图的外部应用组件的数量
- 提供商:限制对通过内容提供商可访问的数据的访问
每个组件 XML 元素的android:permission
属性覆盖了<application>
元素的android:permission
属性。这意味着,如果您没有为组件指定任何必需的权限,并且在<application>
元素中指定了一个权限,它将应用于其中包含的所有组件。虽然通过<application>
元素指定权限并不是开发人员经常做的事情,因为它会影响组件对安卓系统本身的友好性(也就是说,如果您使用<application>
元素覆盖活动所需的权限),home 启动器将无法启动您的活动。也就是说,如果你足够偏执,并且不需要任何未授权的交互来与你的应用或其组件发生,你应该使用<application>
标签的android:permission
属性。
当您在组件上定义<intent-filter>
元素时,它将自动导出,除非您明确设置exported="false"
。然而,这似乎是一个鲜为人知的事实,因为许多开发人员无意中向其他应用开放了他们的内容提供商。因此,谷歌的回应是改变安卓 4.2 中<provider>
的默认行为。如果将android:minSdkVersion
或android:targetSdkVersion
设置为17
,则<provider>
上的exported
属性将默认为false
。
- 安卓开发者参考指南中的
<service>
标签 - 安卓开发者参考指南中的
<receiver>
标签 - 安卓开发者参考指南中的
<activity>
标签 - 安卓开发者参考指南中的
<application>
标签 - 安卓开发者参考指南中的
AndroidManifest.xml
文件 - 安卓开发者参考指南中的
Context
类 - 安卓开发者参考指南中的
Activity
类
安卓平台定义了一组默认权限,用于保护系统服务和应用组件的安全。很大程度上,这些权限在最普通的情况下起作用,但是当在应用之间共享定制的功能或组件时,通常需要对权限框架进行更定制的使用。这是通过定义自定义权限来实现的。
这个方法演示了如何定义自己的自定义权限。
我们开始吧!
-
在添加任何自定义权限之前,您需要为权限标签声明字符串资源。您可以通过在
res/values/strings.xml
:<string name="custom_permission_label">Custom Permission</string>.
下的应用项目文件夹中编辑
strings.xml
文件来完成此操作 -
Adding normal protection-level custom permissions to your application can be done by adding the following lines to your
AndroidManifest.xml
file:<permission android:name="android.permission.CUSTOM_PERMISSION" android:protectionLevel="normal" android:description="My custom permission" android:label="@string/custom_permission_label">
我们将在如何工作… 部分介绍
android:protectionLevel
属性的含义。 -
Making use of this permission works the same as any other permission; you need to add it to the
android:permission
attribute of an application component. For an activity:<activity ... android:permission="android.permission.CUSTOM_PERMISSION"> </activity>
或者内容提供商:
<provider ... android:permission="android.permission.CUSTOM_PERMISSION"> </provider>
或者一项服务:
<service ... android:permission="android.permission.CUSTOM_PERMISSION"> </service>
或者接收器:
<receiver ... android:permission="android.permission.CUSTOM_PERMISSION"> </receiver>
-
您也可以通过将
<uses-permission/>
标签添加到应用的AndroidManifest.xml
文件中来允许其他应用请求该权限:<uses-permission android:name="android.permission.CUSTOM_PERMISSION"/>
自定义权限可以进行逻辑分组,为请求给定权限的应用或需要特定权限的组件分配语义。权限分组是通过定义一个权限组并在定义这些组时将您的权限分配给它们来完成的,如前所述。以下是如何定义权限组:
-
如前所述,为权限组的标签添加字符串资源。这是通过在
res/values/strings.xml
文件中添加以下行来完成的:<string name="my_permissions_group_label">Personal Data Access</string>
-
然后,在应用的
AndroidManifest.xml
文件中添加以下一行:<permission-group android:name="android.permissions.personal_data_access_group" android:label="@string/my_permissions_group_label" android:description="Permissions that allow access to personal data" />
-
然后您将能够将您定义的权限分配给组,如下所示:
<permission ... android:permissionGroup="android.permission.personal_data_acess_group" />
前面的演练演示了如何使用AndroidManifest.xml
文件的<permission>
元素定义自定义权限,以及如何使用清单的<permission-group>
元素定义权限组。在这里,我们分解并详细说明这些元素及其属性的细微差别。
<permission>
元素很容易理解。以下是属性的细分:
android:name
:定义权限的名称,即引用该权限的字符串值android:protectionLevel
:定义权限的保护级别,控制是否提示用户授予权限。我们在上一章中已经讨论过这一点,但这里概述一下保护级别:normal
:该权限用于定义非危险权限,这些权限不会被提示,可以自主授予dangerous
:该权限用于定义使用户面临相当大的财务、声誉和法律风险的权限signature
:该权限自动授予与定义它们的应用使用相同密钥签名的应用signatureOrSystem
:该权限会自动授予构成系统映像一部分的任何应用,或者使用与定义它们的应用相同的密钥进行签名
如果您只对在自己开发的应用之间共享组件感兴趣,请使用signature
权限。这方面的例子有一个免费的应用,它有一个解锁应用作为单独的付费下载,或者有几个可选插件的应用,它们希望共享功能。危险许可不会自动授予。安装时,可向用户显示 android:description
属性进行确认。如果您希望在其他应用可以访问您的应用数据时向用户进行标记,这将非常有用。normal
权限在安装时自动授予,不会向用户标记。
- 安卓开发者参考指南中的
<permission>
标签 - 安卓开发者参考指南中的
<uses-permission>
标签 - 安卓开发者参考指南中的
<permission-group>
标签 - 安卓开发者参考指南中的
Manifest.permission
类
内容提供商可能是被利用最多的应用组件,因为他们通常持有对用户身份验证最关键的数据。他们经常持有大量关于用户及其对 SQL 注入攻击和信息泄露的亲和力的敏感数据。本演练将详细介绍一些措施,您可以采取这些措施来保护内容提供商的一般信息泄露,这些信息泄露是由内容提供商权限配置中的常见错误引起的。我们还将介绍如何保护数据库和内容提供者免受 SQL 注入攻击。
本食谱将讨论如何将某些配置添加到您的AndroidManifest.xml
文件中,以保护对您的内容提供商的访问,一直到 URI 路径级别。它还讨论了滥用授权 URI 机制的一些安全风险,以免过多地将您的内容提供商路径暴露给未经授权或潜在的恶意应用。
统一资源标识符 ( URIs ) 用于内容提供商识别特定数据集,例如content://com.myprovider.android/email/inbox
。
保护任何组件的第一步是确保您已经正确注册了它的权限。保护内容提供商意味着不仅要为与内容提供商的一般交互提供许可,还要为相关的 URI 路径提供许可。
-
To secure your content provider with a permission that governs both read and write permissions for all of the paths related to your authority, you can add the following
provider
element of your android manifest:<provider android:enabled="true" android:exported="true" android:authorities="com.android.myAuthority" android:name="com.myapp.provider" android:permission="[permission name]"> </provider>
这里,
[permission name]
是其他应用必须拥有的权限,以便读取或写入任何内容提供商路径。在这个级别添加权限是一个非常好的步骤,可以确保在保护路径时不会留下任何机会。 -
Naturally, content providers will have a couple of content paths they want to serve content from. You can add read and write permissions to them as follows:
<provider android:writePermission="[write permission name]" android:readPermission="[read permission name]"> </provider>
前面的
android:writePermission
和android:readPermission
标记用于声明,每当外部应用想要执行任何与读相关的(query
)或与写相关的(update
和insert
)操作时,它们必须具有指定的权限才能执行。认为授予写访问权限也意味着授予读访问权限是一个常见的错误。但是,这不应该是默认行为。安卓愉快地遵循最佳实践,并要求读写权限分别声明。
这是一个来自安卓谷歌浏览器应用的真实例子:
<provider android:name="com.google.android.apps.chrome.ChromeBrowserProvider" android:readPermission="com.android.browser.permission.READ_HISTORY_BOOKMARKS" android:writePermission="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS" android:exported="true" ...
您也可以通过使用
AndroidManifest.xml
模式的<path-permission>
元素为您的每个路径添加更精细的权限;你是这样做的:<provider ...> <path-permission android:path="/[path name]" android:permission="[read/write permission name]" android:readPermission="[read permission name]" android:writePermission="[write permission name]"> </provider>
您可能想知道如果您同时使用这两个级别的权限会发生什么。在
<provider>
和<path-permission>
级别,应用是否需要在这两个级别注册所有权限?答案是否定的,路径级别的读、写和读/写权限优先。 -
Another thing worth mentioning is the grant URI mechanism. You can configure this at the provider level to apply to all paths, or at the path level, which will only affect the related paths. Although, why you would specify permissions at path level and grant URI at provider level a bit odd, since effectively, this would mean no permissions are set! It is fully recommended that developers not make use of the grant URI permission at the provider level at all, and rather use it per path. So, if and only if you need to make sure any application can query, insert, or update on a certain path while still having permissions protecting your other paths, you would do this as follows:
<provider ...> <grant-uri-permission android:path="[path name]" /> </provider>
您还可以指定一系列路径来授予 URI 使用
pathPrefix
或pathPattern
属性的权限。pathPrefix
将确保格兰特·URI 机制适用于以给定前缀开始的所有路径。pathPattern
将确保授权 URI 机制适用于与给定模式匹配的所有路径。例如:<grant-uri-permission android:path="[path name]" android:pathPrefix="unsecured"/>
这将对以“不安全”字符串开头的所有路径应用授予 URI 权限,例如:
content://com.myprovider.android/unsecuredstuff
content://com.myprovider.android/unsecuredsomemorestuff
content://com.myprovider.android/unsecured/files
content://com.myprovider.android/unsecured/files/music
对于前面的示例,如果查询、更新、插入或删除了这些路径中的任何一个,授予 URI 权限将会生效。
前一章讲述了针对内容提供者的一些常见攻击,其中之一就是臭名昭著的 SQL 注入攻击。这种攻击利用了这样一个事实,即对手能够提供 SQL 语句或与 SQL 相关的语法作为其选择参数、投影或有效 SQL 语句的任何组成部分的一部分。这使得他们能够从内容提供商那里获取比未经授权更多的信息。
确保对手无法向您的查询中注入未经请求的 SQL 语法的最佳方法是避免使用SQLiteDatabase.rawQuery()
而选择参数化语句。使用编译过的语句,如SQLiteStatement
,提供参数的绑定和转义,以抵御 SQL 注入攻击。此外,由于数据库不需要为每次执行解析语句,因此还有性能优势。SQLiteStatement
的一个替代方法是在SQLiteDatabase
上使用query
、insert
、update
和delete
方法,因为它们通过使用字符串数组来提供参数化语句。
当我们描述参数化语句时,我们描述的是一条带有问号的 SQL 语句,其中将插入或绑定值。下面是参数化 SQL insert
语句的一个例子:
INSERT VALUES INTO [table name] (?,?,?,?,...)
这里[table name]
是需要插入值的相关表的名称。
对于这个例子,我们使用了一个简单的 数据访问对象 ( DAO )模式,其中 RSS 项目的所有数据库操作都包含在RssItemDAO
类中:
-
When we instantiate
RssItemDAO
, we compile theinsertStatement
object with a parameterized SQLinsert
statement string. This needs to be done only once and can be re-used for multiple inserts:public class RssItemDAO { private SQLiteDatabase db; private SQLiteStatement insertStatement; private static String COL_TITLE = "title"; private static String TABLE_NAME = "RSS_ITEMS"; private static String INSERT_SQL = "insert into " + TABLE_NAME + " (content, link, title) values (?,?,?)"; public RssItemDAO(SQLiteDatabase db) { this.db = db; insertStatement = db.compileStatement(INSERT_SQL); }
INSERT_SQL
变量中标注的列的顺序很重要,因为它在绑定值时直接映射到索引。在上例中,content
映射到索引0
,link
映射到索引1
,而title
映射到索引2
。 -
Now, when we come to insert a new
RssItem
object to the database, we bind each of the properties in the order they appear in the statement:public long save(RssItem item) { insertStatement.bindString(1, item.getContent()); insertStatement.bindString(2, item.getLink()); insertStatement.bindString(3, item.getTitle()); return insertStatement.executeInsert(); }
请注意,我们调用
executeInsert
,这是一个返回新创建的行的标识的辅助方法。使用SQLiteStatement
语句就这么简单。 -
This shows how to use
SQLiteDatabase.query
to fetchRssItems
that match a given search term:public List<RssItem> fetchRssItemsByTitle(String searchTerm) { Cursor cursor = db.query(TABLE_NAME, null, COL_TITLE + "LIKE ?", new String[] { "%" + searchTerm + "%" }, null, null, null); // process cursor into list List<RssItem> rssItems = new ArrayList<RssItemDAO.RssItem>(); cursor.moveToFirst(); while (!cursor.isAfterLast()) { // maps cursor columns of RssItem properties RssItem item = cursorToRssItem(cursor); rssItems.add(item); cursor.moveToNext(); } return rssItems; }
我们使用
LIKE
和 SQL 通配符语法将文本的任何部分与标题列进行匹配。
- 的安卓开发者参考指南中的
SQLiteDatabase
类 - 安卓开发者参考指南中的
SQLiteStatment
类 - 位于的查询参数化备忘单 OWASP 社区页面
- http://www.sqlite.org/lang_expr.html 的 SQLite 表达式
安卓安全的基石之一是所有应用都必须经过数字签名。应用开发人员使用证书形式的私钥对应用进行签名。不需要使用证书颁发机构,事实上,使用自签名证书更常见。
证书通常定义有有效期,Google Play 商店要求有效期截止于 2033 年 10 月 22 日之后。这突出了一个事实,即我们的应用签名密钥在应用的整个生命周期中保持一致。主要原因之一是为了保护和防止 app 升级,除非旧的和升级的.apk
文件签名完全相同。
那么,如果这种验证已经发生,为什么还要增加签名一致性检查呢?
攻击者修改您应用的.apk
文件的部分过程会破坏数字签名。这意味着,如果他们想在安卓设备上安装.apk
文件,就需要使用不同的签名密钥。这可能有各种各样的动机,从软件盗版到恶意软件。一旦攻击者修改了您的应用,他们可能会通过许多替代应用商店中的一个或通过更直接的方式(如电子邮件、网站或论坛)进行分发。所以,这个食谱的动机是保护我们的应用、品牌和用户免受这种潜在风险的影响。幸运的是,在运行时,安卓应用可以查询PackageManager
找到应用签名。这个食谱展示了如何将当前的应用签名与你认为应该的签名进行比较。
这个方法使用 Keytool 命令行程序,并假设您已经创建了一个包含您的私有签名密钥的.keystore
文件。如果没有,您可以使用 Eclipse 中的安卓工具导出向导创建您的应用签名密钥,或者在终端窗口中使用带有以下命令的密钥工具程序:
keytool -genkey -v -keystore your_app.keystore
-alias alias_name -keyalg RSA -keysize 2048 -validity 10000
首先,你需要找到你的证书的 SHA1 签名/指纹。我们将把它硬编码到应用中,并在运行时与它进行比较。
-
Using Keytool from a terminal window, you can type the following:
keytool -list -v -keystore your_app.keystore
系统会提示您输入密钥库密码。
Keytool 现在将打印密钥库中包含的所有密钥的详细信息。找到您的应用密钥,在证书指纹标题下,您应该会看到十六进制格式的 SHA1。以下是使用示例密钥库
71:92:0A:C9:48:6E:08:7D:CB:CF:5C:7F:6F:EC:95:21:35:85:BC:C5
的证书的 SHA1 值示例: -
将您的 SHA1 散列从终端窗口复制到您的应用中,并在您的 Java
.class
文件中将其定义为静态字符串。 -
Remove the colons and you should end up with something like this:
private static String CERTIFICATE_SHA1 = "71920AC9486E087DCBCF5C7F6FEC95213585BCC5";
删除冒号的一个快速简单的方法是将散列复制并粘贴到以下网站,然后按下 validate 按钮:
-
现在,我们需要编写代码,在运行时获取
.apk
文件的当前签名:public static boolean validateAppSignature(Context context) { try { // get the signature form the package manager PackageInfo packageInfo = context.getPackageManager() .getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); Signature[] appSignatures = packageInfo.signatures; //this sample only checks the first certificate for (Signature signature : appSignatures) { byte[] signatureBytes = signature.toByteArray(); //calc sha1 in hex String currentSignature = calcSHA1(signatureBytes); //compare signatures return CERTIFICATE_SHA1.equalsIgnoreCase(currentSignature); } } catch (Exception e) { // if error assume failed to validate } return false; }
-
我们正在存储签名的 SHA1 散列;现在,由于我们有了证书,我们需要生成 SHA1 并转换为相同的格式(十六进制):
private static String calcSHA1(byte[] signature) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA1"); digest.update(signature); byte[] signatureHash = digest.digest(); return bytesToHex(signatureHash); } public static String bytesToHex(byte[] bytes) { final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8','9', 'A', 'B', 'C', 'D', 'E', 'F' }; char[] hexChars = new char[bytes.length * 2]; int v; for (int j = 0; j < bytes.length; j++) { v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); }
-
我们现在比较我们签名的证书的散列、我们硬编码到应用中的应用以及当前签名证书的散列。如果这些相等,我们可以确信 app 没有再次被签名:
CERTIFICATE_SHA1.equalsIgnoreCase(currentSignature);
如果一切正常,并且.apk
运行的是我们已经签署的版本,validateAppSignature()
方法将返回true
。但是,如果有人编辑了.apk
文件并再次签名,currentSignature
将与CERTIFICATE_SHA1
不匹配。所以,validateAppSignature()
将返回 false。
请记住,要么确保哈希以大写形式存储,要么使用String.equalsIgnoreCase()
方法进行比较。
这种技术应该被认为足以挫败当前的自动化应用重新打包程序。然而,值得理解的是其局限性。由于签名证书的散列被硬编码在.apk
文件中,熟练的逆向工程师可以剖析.apk
文件并用新证书的散列替换 SHA1。这允许verifyAppSignature
呼叫通过。此外,对verifyAppSignature
的方法调用可以完全删除。这两种选择都需要时间和逆向工程技能。
我们在谈论签名时不能不提到 bug 8219321,也就是 2013 年美国黑帽峰会上 Bluebox 安全发布的主密钥漏洞。这个 bug 已经被谷歌和原始设备制造商修补过了。在http://www.saurik.com/id/17可以找到对此的完整分解和分析。
当然这完全是主观,真的要看你的申请。显而易见且简单的解决方案是在启动时检查篡改,如果检测到,退出应用,并向用户发送一条消息解释原因。此外,你可能想知道妥协。因此,向您的服务器发送通知是合适的。或者,如果您没有服务器,并且正在使用谷歌分析等分析包,您可以创建一个自定义“篡改”事件并报告它。
为了阻止软件盗版,你可以禁用高级应用功能。对于游戏来说,禁用多人游戏或删除游戏进度/高分将是一个有效的威慑。
- 本章后面的用 DexGuard 进行高级代码混淆的方法,它为篡改保护提供了一个有用的补充,使逆向工程更难发现、理解和重要地删除篡改检查
- 安卓开发者网站的签署你的应用页面
- https://gist.github.com/scottyab/b849701972d57cf9562e签名校验码要点
- 安卓开发者参考指南中的
Signature
类 - 安卓开发者参考指南中的
PackageManager
类 - *漏洞利用(&修复)安卓“主密钥”*博客文章描述了在http://www.saurik.com/id/17的主密钥漏洞利用
- 位于的 Keytool 甲骨文文档
在这个配方中,我们将查看三个额外的检查,它们可能指示篡改、泄露或敌对的环境。这些都是设计的一旦你准备好释放就会被激活。
这些篡改检查可以位于你的应用中的任何地方,但是允许它们在单独的类或父类的多个位置被调用是最有意义的。
-
检测谷歌游戏商店是否是安装程序:
public static boolean checkGooglePlayStore(Context context) { String installerPackageName = context.getPackageManager() .getInstallerPackageName(context.getPackageName()); return installerPackageName != null && installerPackageName.startsWith("com.google.android"); }
-
检测它是否在模拟器上运行:
public static boolean isEmulator() { try { Class systemPropertyClazz = Class .forName("android.os.SystemProperties"); boolean kernelQemu = getProperty(systemPropertyClazz, "ro.kernel.qemu").length() > 0; boolean hardwareGoldfish = getProperty(systemPropertyClazz, "ro.hardware").equals("goldfish"); boolean modelSdk = getProperty(systemPropertyClazz, "ro.product.model").equals("sdk"); if (kernelQemu || hardwareGoldfish || modelSdk) { return true; } } catch (Exception e) { // error assumes emulator } return false; } private static String getProperty(Class clazz, String propertyName) throws Exception { return (String) clazz.getMethod("get", new Class[] { String.class }) .invoke(clazz, new Object[] { propertyName }); }
-
检测应用是否启用了
debuggable
标志,这应该只在开发期间启用:public static boolean isDebuggable(Context context){ return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; }
检测安装程序是否是谷歌游戏商店是一个简单的检查,安装程序应用的包名与谷歌游戏商店的包名相匹配。具体来说,它检查安装程序的包是否以com.google.android
开头。如果你只是通过谷歌商店分发,这是一个有用的检查。
Java 反射 API 使得在运行时检查类、方法和字段成为可能;在这种情况下,允许我们重写会阻止普通代码编译的访问修饰符。仿真器检查使用反射来访问隐藏的系统类android.os.SystemProperties
。一句警告:使用隐藏的 API 可能会有风险,因为它们可能会在安卓版本之间改变。
当debuggable
启用时,可以通过安卓调试桥连接,并进行详细的动态分析。debuggable
变量是AndroidManifest.xml
文件中<application>
元素的一个简单属性。为了执行动态分析,改变属性可能是最容易和最有针对性的方法之一。在第 3 步中,我们看到了如何检查应用信息对象上的debuggable
标志的值。
查看*应用签名验证(防篡改)*配方,了解在检测到篡改时如何处理的建议。一旦发布到 Play 商店,在检测到应用正在模拟器上运行或正在调试时,可以合理地假设该应用正在被分析和/或攻击。因此,在这些情况下,采取更激进的行动来挫败攻击者是合理的,例如擦除应用数据或共享首选项。但是,如果您要删除用户数据,请确保在您的许可协议中注明这一点,以避免任何潜在的法律问题。
- 使用 DexGuard 方法的高级代码混淆,为篡改保护提供了一个有用的补充,使逆向工程更难发现、理解和重要地删除这些篡改检查
SystemProperties.java
类来自的安卓源代码- 安卓开发者参考指南中的
PackageManager
类 - 安卓开发者参考指南中的
ApplicationInfo
类
ProGuard 是一个开源的 Java 代码混淆器,随 Android SDK 一起提供。对于那些不熟悉混淆器的人来说,他们会从代码中移除执行时不需要的任何信息,例如未使用的代码和调试信息。此外,标识符是从一个容易阅读、描述性和可维护的代码重命名为一个优化的、更短的、非常难阅读的代码。以前,对象/方法调用可能看起来像这样:SecurityManager.encrypt(String text);
,但经过模糊处理后,它可能看起来像:a.b(String c);
。如你所见,它没有给出它的目的的线索。
ProGuard 还通过删除未使用的方法、字段和属性来减少代码量,并通过使用机器优化的代码使其执行速度更快。这是移动环境的理想选择,因为这种优化可以大幅减少导出的.apk
文件的大小。这在仅使用第三方库的子集时特别有用。
还有其他可用的 Java 混淆器,但由于 ProGuard 是 Android SDK 的一部分,许多第三方开发库都包含自定义的 ProGuard 配置,以确保它们能够正常运行。
首先,我们将在安卓应用上启用 ProGuard:
-
If you're developing your application using Eclipse with the Android ADT plugin, you'll need to locate your workspace and navigate to the folder containing your application code. Once you've found it, you should see a text file called
project.properties
:要启用 ProGuard,您需要确保以下行未被注释:
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
这假设你有安卓软件开发工具包的默认文件夹结构,因为之前的配置包括一个静态路径,即
/tools/proguard/proguard-android.txt
。如果您没有正确的文件夹结构,或者您没有为 Eclipse 使用安卓开发者工具包插件,您可以获取proguard-android.txt
文件,并将其放在应用工作文件夹上方的一个文件夹中。在这种情况下,您可以按如下方式配置此目录:proguard.config=proguard-android.txt:proguard-project.txt
-
安卓工作室的配置要求在你的
buildType
版本中有以下几行到你的 Gradle 构建文件中:android { ... buildTypes { release { runProguard true proguardFile file('../proguard-project.txt) proguardFile getDefaultProguardFile('proguard-android.txt') } } }
-
保留对
proGuard-android.txt
文件的引用很重要,因为它包含安卓特有的排除,没有它们,应用可能无法运行。这是proguard-android.txt
文件的摘录,指示 ProGuard 在活动中保留可用于 XML 属性onClick
:-keepclassmembers class * extends android.app.Activity { public void *(android.view.View); }
的方法
为项目启用 ProGuard 后,有两个简单的步骤可以确保删除所有日志消息。
-
为了让 ProGuard 能够成功找到所有的日志语句,我们必须使用一个包装类来包装安卓日志:
public class LogWrap { public static final String TAG = "MyAppTag"; public static void e(final Object obj, final Throwable cause) { Log.e(TAG, String.valueOf(obj)); Log.e(TAG, convertThrowableStackToString(cause)); } public static void e(final Object obj) { Log.e(TAG, String.valueOf(obj)); } public static void w(final Object obj, final Throwable cause) { Log.w(TAG, String.valueOf(obj)); Log.w(TAG, convertThrowableStackToString(cause)); } public static void w(final Object obj) { Log.w(TAG, String.valueOf(obj)); } public static void i(final Object obj) { Log.i(TAG, String.valueOf(obj)); } public static void d(final Object obj) { Log.d(TAG, String.valueOf(obj)); } public static void v(final Object obj) { Log.v(TAG, String.valueOf(obj)); } public static String convertThrowableStackToString(final Throwable thr) { StringWriter b = new StringWriter(); thr.printStackTrace(new PrintWriter(b)); return b.toString(); } }
-
在您的应用代码中,使用
LogWrap
代替标准的android.util.Log
。例如:try{ … } catch (IOException e) { LogWrap.e("Error opening file.", e); }
-
将以下自定义 ProGuard 配置插入项目的
proguard-project.txt
文件:-assumenosideeffects class android.util.Log { public static boolean isLoggable(java.lang.String, int); public static int v(...); public static int i(...); public static int w(...); public static int d(...); public static int e(...); }
-
通过将优化配置文件添加到项目中来启用 ProGuard 优化:
proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt
-
在发布模式下构建应用以应用 ProGuard:
-
在 Eclipse 中使用安卓工具导出向导
-
In a terminal window at the root of your project, type the following commands:
蚂蚁 :
ant release
为度:
gradle assembleRelease
-
当您在发布模式下构建应用时,构建系统将在取消注释时检查proguard.config
属性,并在将应用打包到应用(.apk
)之前使用 ProGuard 处理应用的字节码。
当 ProGuard 正在处理字节码时, assumeNoeffects
属性允许它完全删除这些代码行——在这种情况下,所有相关的方法都来自android.util.Log
。使用优化配置和日志包装器,我们让 ProGuard 安全地识别对各种android.util.Log
方法的所有调用。启用优化的另一个好处是优化代码增强了混淆因素,使其更难阅读。
让我们仔细看看 ProGuard 的一些输出和限制。
以下是将 ProGuard 应用到安卓.apk
的输出文件:
mapping.txt
:顾名思义,这包含了混淆类、字段名和原始名称之间的映射,对于使用伴随工具 ReTrace 来消除混淆应用产生的堆栈跟踪/错误报告是必不可少的Seeds.txt
:这里列出了没有混淆的类和成员Usage.txt
:列出了从.apk
文件中剥离的代码Dump.txt
:描述.apk
文件中所有类文件的内部结构
还值得注意的是,每次构建的输出文件都会被 ProGuard 覆盖。对于每个应用版本来说,保存一份mappings.txt
文件是非常重要的;否则,无法转换堆栈跟踪。
用 ProGuard 混淆应用会增加逆向工程、理解和利用应用所需的时间和技能水平。然而,逆转仍然是可能的;因此,它当然不应该是保护应用的唯一部分,而是整个安全方法的一部分。
- 使用 DexGuard 的高级代码混淆方法,它谈到了 ProGuard 的兄弟 DexGuard,用于更深层次的安卓专用混淆
- 安卓开发者网站上的 ProGuard 工具网页位于http://developer.android.com/tools/help/proguard.html
- 位于http://proguard.sourceforge.net/index.htm的 ProGuard 官方网站
- 处的 ProGuard 示例配置 http://ProGuard . SourceForge . net/index . html # manual/examples . html
DexGuard 是由 Eric Lafortune (开发 ProGuard 的人)编写的商业优化器和混淆器工具。它被用来代替 ProGuard。DexGuard 不是针对 Java,而是专门针对安卓资源和 Dalvik 字节码。与 ProGuard 一样,开发人员的主要优势之一是源代码保持可维护性和可测试性,而编译后的输出经过优化和强化。
总的来说,使用 DexGuard 更安全,因为它针对安卓进行了优化,并提供了额外的安全功能。在这个配方中,我们将在前一个配方的签名验证检查的基础上实现其中的两个特性,API 隐藏和字符串加密:
- API 隐藏:这使用反射来伪装对敏感 API 和代码的调用。它非常适合隐藏攻击者想要攻击的关键区域。例如,许可证检查检测将成为软件盗版者的目标,因此这是一个需要加强努力的领域。当反编译时,基于反射的调用更难破译。
- 字符串加密:这将对源代码中的字符串进行加密,以对逆向工程师隐藏它们。这对于代码中定义的 API 键和其他常量特别有用。
我们使用 API 隐藏将特定的方法调用转换为基于反射的调用。这对于我们想要对攻击者隐藏的敏感方法特别有用,在这种情况下,就是验证签名方法。反射调用由存储为字符串的类和方法签名组成。我们可以通过使用补充的字符串加密特性来加密这些反射字符串,从而进一步增强它。这提供了一种保护应用敏感区域的可靠方法,例如篡改检测、许可证检查和加密/解密。
DexGuard 需要开发者许可,可在http://www.saikoa.com/dexguard获得。
假设安卓软件开发工具包工具(版本 22。或更高版本)和 DexGuard 已被下载并提取到可访问的目录中。示例将使用/ Users/user1/dev/lib/DexGuard/
并基于 DexGuard 版。在这里,我们将介绍在 Eclipse 中安装 DexGuard,并将其集成到 Ant 和 Gradle 构建系统中。安装后,您的应用将受益于比 ProGuard 更高的安全级别。但是,我们将启用一些定制的配置来保护应用的敏感区域:
-
将插件 JAR 文件(
com.saikoa.dexguard.eclipse.adt_22.0.0.v5_3_14.jar
)从 DexGuard 的/eclipse
目录复制到您的 Eclipse 安装的/dropins
目录。 -
当您启动/重启 Eclipse 时,将自动安装 DexGuard 插件。
-
If all has been successful, when you right-click on an Android project, you should notice a new option in the Android tools menu:
导出优化和模糊应用包(DexGuard)
-
您的项目现在将像往常一样被编译并内置到
.apk
文件中;然而,在幕后,DexGuard 将用于优化和模糊应用。
启用蚂蚁很简单。在安卓项目的local.properties
配置文件中指定 DexGuard 目录。
-
如果你没有文件,创建一个。为此,添加以下行:
dexguard.dir=/Users/user1/dev/lib/DexGuard/
-
将
Custom_rules.xml
从 DexGuard 目录ant
复制到你的安卓项目的根目录。
要为 Gradle 构建系统启用 DexGuard,请修改项目的 build.gradle
文件:
buildscript {
repositories {
flatDir { dirs '/=/Users/user1/dev/lib/DexGuard/lib' }
}
dependencies {
classpath 'com.android.tools.build:gradle:0.5.1'
classpath ':dexguard:'
}
}
apply plugin: 'dexguard'
android {
.....
buildTypes {
release {
proguardFile plugin.getDefaultDexGuardFile('dexguard-release.pro')
proguardFile 'dexguard-project.txt'
}
}
}
设置完成后,我们可以启用并配置 API 隐藏和字符串加密:
-
在你的安卓项目根目录下,新建一个名为
dexguard-project.txt
的文件。 -
配置 DexGuard 以加密敏感字符串。在这个例子中,我们使用了一个通用的模式,在一个接口中包含不可变的常量和在前面的方法中使用的证书散列,因为这些常量在反编译后可以很容易地被读取,甚至当用 ProGuard 混淆时。
-
Encrypt a specific string in the
Constants
interface:-encryptstrings interface com.packt.android.security.Constants { public static final java.lang.String CERTIFICATE_SHA1; }
或者,您可以加密接口或类中的所有字符串。这里有一个加密所有在
MainActivity.java
中定义的字符串的例子:-encryptstrings class com.packt.android.security.MainActivity
-
为了应对*应用签名验证(防篡改)*方法中提到的限制,我们将演示一个相关的方法,此外,隐藏对
verifyAppSignature
方法的方法调用会使攻击者很难弄清楚篡改检测发生在哪里:-accessthroughreflection class com.packt.android.security.Tamper { boolean verifyAppSignature (Context); } -accessthroughreflection class android.content.pm.PackageManager { int checkSignatures(int, int); int checkSignatures(java.lang.String, java.lang.String); android.content.pm.PackageInfo getPackageInfo(java.lang.String, int); } -accessthroughreflection class android.content.pm.Signature { byte[] toByteArray(); char[] toChars(); java.lang.String toCharsString(); }
-
最后一步是在发布模式下构建/导出,以确保对生成的
.apk
文件应用 DexGuard 保护:- Eclipse :右击您的项目,然后选择安卓工具 | 导出优化和模糊的应用包……(dex guard)
- 蚂蚁:在项目根目录的终端窗口运行
ant release
命令 - 渐变:在项目根目录的终端窗口运行
gradle releaseCompile
命令
下面是与 ProGuard 的正面比较:
| |阿帕尔德
|
DexGuard
| | --- | --- | --- | | 收缩 | X | X | | 最佳化 | X | X | | 名称混淆 | X | X | | 字符串加密 | | X | | 类别加密 | | X | | 反射 | | X | | 资产加密 | | X | | 资源 XML 混淆 | | X | | 转换为达尔维克 | | X | | 包装 | | X | | 签署 | | X | | 篡改检测 | | X |
篡改检测是人们长期以来的最爱,它使用了一个实用程序库,并采用了与本章中其他食谱相同的一些原理。它是有利的,因为它非常容易实现,因为它只是一行代码。
从 ProGuard 升级到 DexGuard 是无缝的,因为为 ProGuard 定义的任何自定义配置都是完全兼容的。这种兼容性的另一个好处是现有的 ProGuard 支持和专业知识社区。
- http://www.saikoa.com/dexguard 的官方 DexGuard 网站