Skip to content

Files

Latest commit

63c79d8 · Dec 26, 2021

History

History
891 lines (618 loc) · 47.3 KB

File metadata and controls

891 lines (618 loc) · 47.3 KB

五、保护应用

在本章中,我们将介绍以下食谱:

  • 保护应用组件
  • 使用自定义权限保护组件
  • 保护内容提供商路径
  • 防御 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属性执行以下操作:

  • 活动:将您的应用外部可以成功调用startActivitystartActivityForResult的应用组件限制为具有所需权限的组件
  • 服务:将可以绑定(通过调用bindService())或启动(通过调用startService())服务的外部应用组件限制为具有指定权限的组件
  • 接收者:限制可以向具有指定权限的接收者发送广播意图的外部应用组件的数量
  • 提供商:限制对通过内容提供商可访问的数据的访问

每个组件 XML 元素的android:permission属性覆盖了<application>元素的android:permission属性。这意味着,如果您没有为组件指定任何必需的权限,并且在<application>元素中指定了一个权限,它将应用于其中包含的所有组件。虽然通过<application>元素指定权限并不是开发人员经常做的事情,因为它会影响组件对安卓系统本身的友好性(也就是说,如果您使用<application>元素覆盖活动所需的权限),home 启动器将无法启动您的活动。也就是说,如果你足够偏执,并且不需要任何未授权的交互来与你的应用或其组件发生,你应该使用<application>标签的android:permission属性。

类型

当您在组件上定义<intent-filter>元素时,它将自动导出,除非您明确设置exported="false"。然而,这似乎是一个鲜为人知的事实,因为许多开发人员无意中向其他应用开放了他们的内容提供商。因此,谷歌的回应是改变安卓 4.2 中<provider>的默认行为。如果将android:minSdkVersionandroid:targetSdkVersion设置为17,则<provider>上的exported属性将默认为false

另见

用自定义权限保护组件

安卓平台定义了一组默认权限,用于保护系统服务和应用组件的安全。很大程度上,这些权限在最普通的情况下起作用,但是当在应用之间共享定制的功能或组件时,通常需要对权限框架进行更定制的使用。这是通过定义自定义权限来实现的。

这个方法演示了如何定义自己的自定义权限。

怎么做…

我们开始吧!

  1. 在添加任何自定义权限之前,您需要为权限标签声明字符串资源。您可以通过在res/values/strings.xml :

    <string name="custom_permission_label">Custom Permission</string>.

    下的应用项目文件夹中编辑strings.xml文件来完成此操作

  2. 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属性的含义。

  3. 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>
  4. 您也可以通过将<uses-permission/>标签添加到应用的AndroidManifest.xml文件中来允许其他应用请求该权限:

    <uses-permission android:name="android.permission.CUSTOM_PERMISSION"/>

定义权限组

自定义权限可以进行逻辑分组,为请求给定权限的应用或需要特定权限的组件分配语义。权限分组是通过定义一个权限组并在定义这些组时将您的权限分配给它们来完成的,如前所述。以下是如何定义权限组:

  1. 如前所述,为权限组的标签添加字符串资源。这是通过在res/values/strings.xml文件中添加以下行来完成的:

    <string name="my_permissions_group_label">Personal Data Access</string>
  2. 然后,在应用的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"
    />
  3. 然后您将能够将您定义的权限分配给组,如下所示:

    <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权限在安装时自动授予,不会向用户标记。

另见

保护内容提供商路径

内容提供商可能是被利用最多的应用组件,因为他们通常持有对用户身份验证最关键的数据。他们经常持有大量关于用户及其对 SQL 注入攻击和信息泄露的亲和力的敏感数据。本演练将详细介绍一些措施,您可以采取这些措施来保护内容提供商的一般信息泄露,这些信息泄露是由内容提供商权限配置中的常见错误引起的。我们还将介绍如何保护数据库和内容提供者免受 SQL 注入攻击。

本食谱将讨论如何将某些配置添加到您的AndroidManifest.xml文件中,以保护对您的内容提供商的访问,一直到 URI 路径级别。它还讨论了滥用授权 URI 机制的一些安全风险,以免过多地将您的内容提供商路径暴露给未经授权或潜在的恶意应用。

统一资源标识符 ( URIs ) 用于内容提供商识别特定数据集,例如content://com.myprovider.android/email/inbox

怎么做...

保护任何组件的第一步是确保您已经正确注册了它的权限。保护内容提供商意味着不仅要为与内容提供商的一般交互提供许可,还要为相关的 URI 路径提供许可。

  1. 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]是其他应用必须拥有的权限,以便读取或写入任何内容提供商路径。在这个级别添加权限是一个非常好的步骤,可以确保在保护路径时不会留下任何机会。

  2. 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:writePermissionandroid:readPermission标记用于声明,每当外部应用想要执行任何与读相关的(query)或与写相关的(updateinsert)操作时,它们必须具有指定的权限才能执行。

    类型

    认为授予写访问权限也意味着授予读访问权限是一个常见的错误。但是,这不应该是默认行为。安卓愉快地遵循最佳实践,并要求读写权限分别声明。

    这是一个来自安卓谷歌浏览器应用的真实例子:

    <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>级别,应用是否需要在这两个级别注册所有权限?答案是否定的,路径级别的读、写和读/写权限优先。

  3. 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 使用pathPrefixpathPattern属性的权限。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 语句的任何组成部分的一部分。这使得他们能够从内容提供商那里获取比未经授权更多的信息。

确保对手无法向您的查询中注入未经请求的 SQL 语法的最佳方法是避免使用SQLiteDatabase.rawQuery()而选择参数化语句。使用编译过的语句,如SQLiteStatement,提供参数的绑定和转义,以抵御 SQL 注入攻击。此外,由于数据库不需要为每次执行解析语句,因此还有性能优势。SQLiteStatement的一个替代方法是在SQLiteDatabase上使用queryinsertupdatedelete方法,因为它们通过使用字符串数组来提供参数化语句。

当我们描述参数化语句时,我们描述的是一条带有问号的 SQL 语句,其中将插入或绑定值。下面是参数化 SQL insert语句的一个例子:

INSERT VALUES INTO [table name] (?,?,?,?,...)

这里[table name]是需要插入值的相关表的名称。

怎么做...

对于这个例子,我们使用了一个简单的 数据访问对象 ( DAO )模式,其中 RSS 项目的所有数据库操作都包含在RssItemDAO类中:

  1. When we instantiate RssItemDAO, we compile the insertStatement object with a parameterized SQL insert 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映射到索引0link映射到索引1,而title映射到索引2

  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语句就这么简单。

  3. This shows how to use SQLiteDatabase.query to fetch RssItems 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 通配符语法将文本的任何部分与标题列进行匹配。

另见

应用签名验证(防篡改)

安卓安全的基石之一是所有应用都必须经过数字签名。应用开发人员使用证书形式的私钥对应用进行签名。不需要使用证书颁发机构,事实上,使用自签名证书更常见。

证书通常定义有有效期,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 签名/指纹。我们将把它硬编码到应用中,并在运行时与它进行比较。

  1. 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 值示例:

    How to do it...

  2. 将您的 SHA1 散列从终端窗口复制到您的应用中,并在您的 Java .class文件中将其定义为静态字符串。

  3. Remove the colons and you should end up with something like this:

    private static String CERTIFICATE_SHA1 = "71920AC9486E087DCBCF5C7F6FEC95213585BCC5";

    删除冒号的一个快速简单的方法是将散列复制并粘贴到以下网站,然后按下 validate 按钮:

    http://www.string-functions.com/hex-string.aspx

  4. 现在,我们需要编写代码,在运行时获取.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;
    }
  5. 我们正在存储签名的 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);
    }
  6. 我们现在比较我们签名的证书的散列、我们硬编码到应用中的应用以及当前签名证书的散列。如果这些相等,我们可以确信 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可以找到对此的完整分解和分析。

响应篡改检测

当然这完全是主观,真的要看你的申请。显而易见且简单的解决方案是在启动时检查篡改,如果检测到,退出应用,并向用户发送一条消息解释原因。此外,你可能想知道妥协。因此,向您的服务器发送通知是合适的。或者,如果您没有服务器,并且正在使用谷歌分析等分析包,您可以创建一个自定义“篡改”事件并报告它。

为了阻止软件盗版,你可以禁用高级应用功能。对于游戏来说,禁用多人游戏或删除游戏进度/高分将是一个有效的威慑。

另见

通过检测安装程序、仿真器和调试标志进行篡改保护

在这个配方中,我们将查看三个额外的检查,它们可能指示篡改、泄露或敌对的环境。这些都是设计的一旦你准备好释放就会被激活。

怎么做...

这些篡改检查可以位于你的应用中的任何地方,但是允许它们在单独的类或父类的多个位置被调用是最有意义的。

  1. 检测谷歌游戏商店是否是安装程序:

      public static boolean checkGooglePlayStore(Context context) {
        String installerPackageName = context.getPackageManager()
            .getInstallerPackageName(context.getPackageName());
        return installerPackageName != null
            && installerPackageName.startsWith("com.google.android");
      }
  2. 检测它是否在模拟器上运行:

    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 });
    }
  3. 检测应用是否启用了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 商店,在检测到应用正在模拟器上运行或正在调试时,可以合理地假设该应用正在被分析和/或攻击。因此,在这些情况下,采取更激进的行动来挫败攻击者是合理的,例如擦除应用数据或共享首选项。但是,如果您要删除用户数据,请确保在您的许可协议中注明这一点,以避免任何潜在的法律问题。

另见

用 ProGuard 删除所有日志消息

ProGuard 是一个开源的 Java 代码混淆器,随 Android SDK 一起提供。对于那些不熟悉混淆器的人来说,他们会从代码中移除执行时不需要的任何信息,例如未使用的代码和调试信息。此外,标识符是从一个容易阅读、描述性和可维护的代码重命名为一个优化的、更短的、非常难阅读的代码。以前,对象/方法调用可能看起来像这样:SecurityManager.encrypt(String text);,但经过模糊处理后,它可能看起来像:a.b(String c);。如你所见,它没有给出它的目的的线索。

ProGuard 还通过删除未使用的方法、字段和属性来减少代码量,并通过使用机器优化的代码使其执行速度更快。这是移动环境的理想选择,因为这种优化可以大幅减少导出的.apk文件的大小。这在仅使用第三方库的子集时特别有用。

还有其他可用的 Java 混淆器,但由于 ProGuard 是 Android SDK 的一部分,许多第三方开发库都包含自定义的 ProGuard 配置,以确保它们能够正常运行。

做好准备

首先,我们将在安卓应用上启用 ProGuard:

  1. 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:

    Getting ready

    要启用 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
  2. 安卓工作室的配置要求在你的buildType版本中有以下几行到你的 Gradle 构建文件中:

    android {
    ...
        buildTypes {
            release {
                runProguard true
                proguardFile file('../proguard-project.txt)
                proguardFile getDefaultProguardFile('proguard-android.txt')
            }
        }
    }
  3. 保留对proGuard-android.txt文件的引用很重要,因为它包含安卓特有的排除,没有它们,应用可能无法运行。这是proguard-android.txt文件的摘录,指示 ProGuard 在活动中保留可用于 XML 属性onClick :

    -keepclassmembers class * extends android.app.Activity {
       public void *(android.view.View);
    }

    的方法

怎么做...

为项目启用 ProGuard 后,有两个简单的步骤可以确保删除所有日志消息。

  1. 为了让 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();
      }
    }
  2. 在您的应用代码中,使用LogWrap代替标准的android.util.Log。例如:

    try{
      …
     } catch (IOException e) {
      LogWrap.e("Error opening file.", e);
    }
  3. 将以下自定义 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(...);
    }
  4. 通过将优化配置文件添加到项目中来启用 ProGuard 优化:

    proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt
  5. 在发布模式下构建应用以应用 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 输出

以下是将 ProGuard 应用到安卓.apk的输出文件:

  • mapping.txt:顾名思义,这包含了混淆类、字段名和原始名称之间的映射,对于使用伴随工具 ReTrace 来消除混淆应用产生的堆栈跟踪/错误报告是必不可少的
  • Seeds.txt:这里列出了没有混淆的类和成员
  • Usage.txt:列出了从.apk文件中剥离的代码
  • Dump.txt:描述.apk文件中所有类文件的内部结构

类型

还值得注意的是,每次构建的输出文件都会被 ProGuard 覆盖。对于每个应用版本来说,保存一份mappings.txt文件是非常重要的;否则,无法转换堆栈跟踪。

限制

用 ProGuard 混淆应用会增加逆向工程、理解和利用应用所需的时间和技能水平。然而,逆转仍然是可能的;因此,它当然不应该是保护应用的唯一部分,而是整个安全方法的一部分。

另见

使用 DexGuard 进行高级代码混淆

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 更高的安全级别。但是,我们将启用一些定制的配置来保护应用的敏感区域:

安装 DexGuard Eclipse 插件

  1. 将插件 JAR 文件(com.saikoa.dexguard.eclipse.adt_22.0.0.v5_3_14.jar)从 DexGuard 的/eclipse目录复制到您的 Eclipse 安装的/dropins目录。

  2. 当您启动/重启 Eclipse 时,将自动安装 DexGuard 插件。

  3. 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)

  4. 您的项目现在将像往常一样被编译并内置到.apk文件中;然而,在幕后,DexGuard 将用于优化和模糊应用。

为蚂蚁构建系统启用索引守卫

启用蚂蚁很简单。在安卓项目的local.properties配置文件中指定 DexGuard 目录。

  1. 如果你没有文件,创建一个。为此,添加以下行:

    dexguard.dir=/Users/user1/dev/lib/DexGuard/
  2. Custom_rules.xml从 DexGuard 目录ant复制到你的安卓项目的根目录。

为梯度构建系统启用 DexGuard

要为 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 隐藏和字符串加密:

  1. 在你的安卓项目根目录下,新建一个名为dexguard-project.txt的文件。

  2. 配置 DexGuard 以加密敏感字符串。在这个例子中,我们使用了一个通用的模式,在一个接口中包含不可变的常量和在前面的方法中使用的证书散列,因为这些常量在反编译后可以很容易地被读取,甚至当用 ProGuard 混淆时。

  3. 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
  4. 为了应对*应用签名验证(防篡改)*方法中提到的限制,我们将演示一个相关的方法,此外,隐藏对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();
    }
  5. 最后一步是在发布模式下构建/导出,以确保对生成的.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 支持和专业知识社区。

另见