安卓安全秘籍-全-

安卓安全秘籍(全)

原文:zh.annas-archive.org/md5/BE954DA99BB76B6D22AFE08F5BDE9A0C

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

安卓系统迅速成为了最受欢迎的移动操作系统之一,不仅受到用户的喜爱,各种开发公司和开发者也同样青睐。当然,这也使其成为了恶意对手的热门攻击目标。

自 2005 年以来,安卓系统一直在公共领域广为人知,其功能和复杂性都实现了巨大的增长。如今的移动智能手机通常包含了关于用户的高度敏感信息,以及访问他们的电子邮件、短信和社交、专业网络服务的权限。与任何软件一样,这种能力和复杂性的提升也带来了安全风险的提升;软件越强大、越复杂,管理起来就越困难,适应这个充满挑战的世界也就越困难。

这尤其适用于移动智能手机上的软件。这些充满个人和敏感信息的热点提出了一个有趣的安全背景,在其中解决问题。从某种角度看,移动智能手机的安全背景很难与网络上的服务器或“云”中的服务器相提并论,因为它们本质上是不移动的。它们不容易被移动或盗取;我们可以实施软件和物理安全措施,以防止未经授权的访问。我们还可以持续监控它们,并自主快速响应安全事件。然而,对于我们装在口袋、手提包里,以及遗忘在出租车上的设备来说,游戏规则就大不相同了!

安卓用户和开发者需要不断关注他们的移动安全风险,正因为这种需求,移动安全与风险评估专家和安全工程师的需求量很大。本书旨在为初露头角的安卓安全评估专家平滑学习曲线,同时作为经验丰富的安卓安全专业人士解决常见安卓安全问题的工具。

本书涵盖内容

第一章,安卓开发工具,介绍了我们如何设置并运行开发者用来在安卓平台上开发安卓应用和本地级别组件的工具。这一章也作为对那些对安卓还不太熟悉,并想了解设置常见开发环境和工具需要些什么的人的介绍。

第二章,应用安全参与,向我们介绍了安卓操作系统提供的,专门用于保护应用程序的组件。本章涵盖了手动检查以及使用一些与保护应用程序及其与操作系统的交互相关的安全工具和服务。

第三章,Android Security Assessment Tools,介绍了安卓安全专家用于评估应用程序给用户带来的技术风险的流行以及新兴的安全工具和框架。在这里,你将学习如何设置、运行并扩展后续章节中将使用的黑客攻击和逆向工程工具。

第四章,利用应用程序,涵盖了针对安卓应用程序的案例利用技术。本章内容涵盖了所有类型的安卓应用程序组件,并详细介绍了如何从源代码和跨应用程序的语境中检查它们的安全风险。它还介绍了第三章中介绍的工具的更高级用法,即安卓安全评估工具。

第五章,保护应用程序,被设计为与第四章,利用应用程序完全相反。这一章不是纯粹讨论应用程序的缺陷,而是讲述应用程序的修复方法。它引导读者了解开发者可以使用的实用技术,以保护应用程序免受第四章中详细描述的某些攻击。

第六章,逆向工程应用程序,帮助读者学习如何破解应用程序,并教授他们安卓逆向工程师用于检查和分析应用程序的技术。你将详细了解 Dex 文件格式,以及如何将 Dex 字节码解释为有用的表示,使逆向工程更加容易。该章节还涵盖了逆向工程师在应用程序和本地组件在安卓操作系统上运行时,可以使用的新的动态分析方法。

第七章,安全网络,帮助读者深入了解应用程序开发人员可以遵循的实用方法,以保护数据在网络中传输时的安全。使用这些技术,你将能够为安全套接字层(SSL)通信添加更强的验证。

第八章, 原生利用与分析,致力于涵盖专注于 Android 平台的本地环境的安全评估和测试技术。读者将学会寻找可用于 root 手机和提升 Android 系统权限的安全漏洞,以及对本地服务执行低级别攻击,包括内存破坏和竞争条件利用。

第九章, 加密和开发设备管理策略,重点介绍如何正确使用加密技术,避免一些常见的反模式,以保持应用程序中的数据安全。它推荐了几个健壮且节省时间的第三方库,以快速且安全地增强应用程序的安全性。最后,我们将介绍如何使用 Android 设备管理 API 来实现和执行企业安全策略。

阅读本书所需的条件

尽管这本书对软件有一些要求,但书中的许多演练在开始使用它们来参与讨论的主题之前,都会讨论下载和安装所需的软件。

以下是开始演练前你可能需要安装的软件列表:

  • Android 软件开发工具包(SDK)

  • Android 原生开发工具包(NDK)

  • GNU C/C++编译器(GCC)

  • GNU 调试器(GDB)

  • Python,最好是 2.7 版本,但 3.0 版本应该也可以正常工作

  • Virtual box

  • Ettercap(适用于 Windows 或 Linux/Unix 系统)

  • Dex2Jar

  • Objdump

  • Radamsa

  • JD-GUI

  • Java 开发工具包(JDK)

  • drozer,一个 Android 安全评估框架

  • OpenSSL命令行工具

  • keytool命令行工具

本书的目标读者

本书有的章节致力于利用 Android 应用程序,有的章节专注于加强它们的安全性,旨在展示硬币的两面,即攻击者和防御者。

安全研究人员、分析师和渗透测试人员将享受有关如何利用 Android 应用的具体细节。对于渴望了解更多安全知识的应用程序开发人员,他们将获得如何保护应用程序免受攻击的实际建议。

约定

在这本书中,你会发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理程序将如下所示:"您必须使用–t开关指定上一步选择的系统映像 ID。"

代码块设置如下:

from drozer import android
from drozer.modules import common, Module
class AttackSurface(Module,common.Filters, common.PackageManager):

当我们希望引起您对代码块中某个特定部分的注意时,相关的行或项目会以粗体显示,如下所示:

from drozer import android
from drozer.modules import common, Module
class AttackSurface(Module,common.Filters, common.PackageManager):

任何命令行输入或输出都会按照以下方式编写:

sudo aptitude update //If you have aptitude installed

新术语重要词汇会用粗体显示。你在屏幕上、菜单中或对话框里看到的单词,例如,会在文本中像这样出现:"接受许可后,你可以通过点击安装来收集你的文档和 API"。

注意

警告或重要提示会像这样出现在一个框里。

提示

提示和技巧会像这样出现。

读者反馈

我们非常欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或可能不喜欢的地方。读者的反馈对我们来说非常重要,它帮助我们开发出对你真正有用的书籍。

如果要给我们发送一般性反馈,只需将电子邮件发送至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果你有一个有专业知识的主题,并且有兴趣撰写或参与书籍编写,请查看我们在www.packtpub.com/authors的作者指南。

客户支持

既然你现在拥有了 Packt 的一本书,我们有很多方法可以帮助你最大限度地利用你的购买。

下载示例代码

你可以从你在www.packtpub.com的账户下载你所购买的所有 Packt 书籍的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给你。

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果你在我们的书中发现了一个错误——可能是文本或代码中的错误——如果你能报告给我们,我们将不胜感激。这样做,你可以避免其他读者的困扰,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问www.packtpub.com/submit-errata报告,选择你的书,点击勘误提交表单链接,并输入你的勘误详情。一旦你的勘误被验证,你的提交将被接受,勘误将在我们网站的相应位置上传,或者添加到现有勘误列表中。任何现有的勘误可以通过在www.packtpub.com/support选择你的标题来查看。

侵犯版权

互联网上版权材料的侵犯是所有媒体面临的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上发现我们作品的任何非法副本,无论何种形式,请立即提供位置地址或网站名称,以便我们可以寻求补救。

如果发现任何疑似盗版材料,请通过<copyright@packtpub.com>联系我们,并提供相关链接。

我们感谢您帮助保护我们的作者,以及我们向您提供有价值内容的能力。

咨询问题

如果您在阅读本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:Android 开发工具

在本章中,我们将介绍以下内容:

  • 安装 Android 开发工具(ADT)

  • 安装 Java 开发工具包(JDK)

  • 更新 API 源

  • ADT 的替代安装方法

  • 安装本地开发工具包(NDK)

  • 模拟 Android

  • 创建 Android 虚拟设备(AVDs)

  • 使用 Android 调试桥(ADB)与 AVD 进行交互

  • 在 AVD 上复制文件

  • 通过 ADB 在 AVD 上安装应用程序

引言

曾经有位非常聪明的人说过,“你应该亲近你的朋友,但更要亲近你的敌人”。作为一名安全专业人士意味着要关注开发者正在做什么,已经做了什么,以及可能做什么。因为他们的决定极大地影响着安全环境;毕竟,如果没有人编写糟糕的软件,没有人会去利用它!

鉴于这本书面向任何对分析、黑客或开发 Android 平台感兴趣的人,了解你的敌人的概念同样适用于你!Android 开发者需要跟上 Android 黑客的活动,以希望在他们给用户带来负面影响之前捕捉到安全漏洞。反之,Android 黑客也需要跟上 Android 开发者的步伐。

接下来的章节将指导你获取最新和最棒的开发及黑客工具,并让你直接与 Android 安全架构进行交互,既包括破解应用程序也包括保护它们。

本章重点介绍如何安装并运行Android 开发工具ADT),并讨论如何解决安装问题以及保持工具更新。如果你认为自己已经非常熟悉 Android 开发环境和工具链,可以自由跳过这一章。

不再赘述,让我们直接讨论如何获取并安装最新的 Android 开发工具。

安装 Android 开发工具(ADT)

考虑到已经有许多版本的 Android 框架部署在移动平台上,以及支持它的各种不同手机,Android 开发者需要能够访问到 Android 平台上许多设备和操作系统特定的应用程序编程接口(APIs)。

我们不仅仅讨论 Android API,还包括手机特定的 API。每个手机制造商都希望通过提供专有的 API 和服务来吸引开发者,例如 HTC OpenSense APIs。ADT 统一了这些 API 的访问方式;提供了所有必要的工具来调试、开发和部署你的 Android 应用;并且让你轻松下载并保持更新。

如何操作...

以下步骤将指导你完成下载 ADT 并让它们运行起来的过程:

  1. 你需要访问developer.android.com,并导航到 ADT 下载页面,或者直接访问developer.android.com/sdk/index.html#download。你应该能看到如下截图所示的页面:如何操作...

  2. 到达那里后,点击下载 SDK,应出现以下屏幕:如何操作...

  3. 当然,在下载之前,你需要接受许可协议,并选择适当的 CPU 类型。如果你不确定如何检查 CPU 类型,可以选择注册大小。

    在 Windows 上,你需要完成以下步骤:

    1. 点击开始

    2. 我的电脑上右键点击。

    3. 选择属性

    4. 应弹出包含计算机系统特定信息的窗口。你寻找的信息应在系统部分,标记为系统类型

    如何操作...

    在 Ubuntu、Debian 或基于 Unix 的发行版上检查系统类型,执行以下步骤:

    1. Ctrl + Alt + T 打开终端,或者直接通过图形界面启动它。

    2. 执行以下命令:

       uname -a
      
      
    3. 或者,你可以使用 lscpu,它应该会显示类似于以下截图的内容:

    如何操作...

  4. 当你对许可协议感到满意,并选择了正确的系统类型后,在 ADT 下载页面上点击下载。下载 ZIP 文件后,在 Windows 上应该看起来像以下截图:如何操作...

在基于 Linux 或 Unix 的发行版上,该归档文件具有相同的结构。

安装 Java 开发工具包(JDK)

Android 使用定制的 Java 运行时版本来支持其应用程序。这意味着,在我们可以使用 Eclipse 开发 Android 应用程序之前,实际上需要安装 Java 运行时和开发工具。这些工具包含在Java 开发工具包JDK)中。

如何操作...

在 Windows 上安装 JDK 的步骤如下:

  1. 从 Oracle 的下载页面获取 JDK 的副本,www.oracle.com/technetwork/java/javase/downloads/index.html。点击DOWNLOAD。以下截图显示了下载页面:如何操作...

  2. 确保选择适合你的系统类型的适当版本;查看前面的演练以了解如何检查系统类型。以下截图突出了 Oracle Java JDK 支持的 Windows 系统类型:如何操作...

  3. 下载 JDK 后,运行jdk-[版本]-[平台版本].exe文件。例如,您可能有一个名为jdk-7u21-windows-i586.exe的 EXE 文件。现在您需要做的就是按照提示操作,直到所有设置安装完成。一旦启动安装向导,它看起来应该像下面的截图:如何操作…

一旦安装向导完成其工作,您应该在C:\Program Files\Java\jdk[版本]下看到全新安装的 JDK 和 JRE,并且现在应该能够启动 Eclipse。

还有更多…

在 Ubuntu Linux 上安装 Java 运行时和开发工具相对简单。考虑到 Ubuntu 有一个成熟的包和仓库管理器,您只需要通过终端窗口执行几个简单的命令来利用它。您需要执行以下步骤:

  1. 打开一个终端,可以通过您的 Unity、KDE 或 Gnome 桌面搜索终端应用程序,或者按Ctrl + Alt + T

  2. 在安装之前,您可能需要更新您的软件包列表,除非您几分钟前已经做过。您可以通过执行以下任一命令来完成此操作:

    sudo aptitude update   //If you have aptitude installed 
    
    

    或者:

    sudo apt-get update
    
    

    您应该会看到您的终端打印出从您的仓库执行的所有下载,如下面的截图所示:

    还有更多…

  3. 完成后,执行以下命令:

    sudo apt-get install openjdk-[version]-jdk apt-get 
    
    

    如果您已正确添加到sudoers文件,您需要输入密码。或者,如果您有 root 用户的密码,可以通过执行以下命令借用 root 权限来完成此操作:

    su root
    
    

    下面的截图显示了这一点:

    还有更多…

一旦您的 JDK 安装正确,您应该能够启动 Eclipse 并开始进行 Android 开发。当您启动 Eclipse 时,您应该会看到以下截图:

还有更多…

成功安装后,您的 Eclipse 安装中的工具栏应该看起来类似于下面截图中的样子:

还有更多…

更新 API 源

SDK 管理器和相关工具与 ADT 包捆绑在一起;它们提供了对最新和最稳定的 API、Android 模拟器镜像以及各种调试和应用程序测试工具的访问。以下演练将指导您如何更新您的 API 和其他与 Android 开发相关的资源。

如何操作...

更新 ADT 的 API 如下操作:

  1. 导航到 SDK 管理器。如果您从 Windows 进行所有操作,您应该在ADT-bundle文件夹的根目录中找到名为SDK Manager.exe的文件。Ubuntu 用户可以在[ADT-bundle 路径]/sdk/tools/android找到它。

  2. 您需要做的就是启动 SDK 管理器。它应该启动并开始检索可用的 API 和文档包的新列表。如何操作…

  3. 你需要确保选择了 工具 软件包;当然,你也可以选择其他附加软件包。一个好的建议是下载最后两个版本。Android 向后兼容性很好,所以你不必太担心较旧的 API 和文档,除非你正在使用它们来支持非常旧的 Android 设备。

  4. 你需要表明你接受许可协议。你可以为每个正在安装的对象这样做,或者点击 全部接受

  5. 接受完许可协议后,你可以通过点击 安装 来收集你的文档和 API,如下截图所示:如何操作...

ADT 的替代安装方法

如果前面介绍安装 Eclipse 和 ADT 插件的方法由于某些原因不奏效,你可以选择走老路,下载你自己的 Eclipse 副本并手动通过 Eclipse 安装 ADT 插件。

如何操作...

下载并安装 ADT 的步骤如下:

  1. www.eclipse.org/downloads/ 下载 Eclipse—Helios 或更高版本。请确保选择适合你操作系统的版本。你应该会看到一个类似以下截图的页面:如何操作...

  2. 从 Android 网站 developer.android.com/sdk/installing/installing-adt.html 下载适合你平台版本的 ADT 套件。以下截图显示该网站页面的部分内容:如何操作...

  3. 确保你已经安装了 Java JDK。

  4. 如果你的 JDK 安装没有问题,运行在第 1 步下载的 Eclipse 安装程序。

  5. 一旦 Eclipse 安装完毕并准备就绪,就可以安装你的 ADT 插件了。

  6. 打开 Eclipse 并点击菜单栏中的 帮助 按钮。如何操作...

  7. 点击 安装新软件...

  8. 将会弹出 可用软件 对话框。你需要点击 添加…如何操作...

  9. 将会显示 添加仓库 对话框。你需要点击 归档... 按钮。

  10. 应该会弹出一个文件浏览器。此时,你需要导航到在前面的步骤中下载的 ADT ZIP 文件。如何操作...

  11. 找到 ADT 文件后,点击 打开

  12. 然后点击 确定

  13. 将会显示 .zip 压缩文件中的可用软件包。点击 全选 然后点击 下一步如何操作...

  14. 现在你需要接受许可协议;当然,你有权不接受。阅读它总是一个好主意。如果你满意,选择 我接受许可协议的条款 选项,然后点击 完成如何操作...

  15. 软件安装现在开始。你可能会收到一个警告,指出内容未签名,无法验证真实性。点击确定如何操作...

  16. 重启 Eclipse。

Android SDK、设备模拟器和支持 Eclipse 的功能现在应该已经准备就绪。查看你的 Eclipse 工具栏,它应该有一些新图标。

安装本地开发工具包(NDK)

如果你想在 Android 设备上进行任何低级利用或开发,你需要确保自己能够在 Android 平台上进行较低级别的应用程序编写。低级别指的是使用诸如 C/C++等语言进行开发,这些语言使用的编译器是为了适应嵌入式平台及其各种细节而构建的。

Java 和本地/低级编程语言之间有什么区别?这个话题本身就足以写成一本书。但仅就表面层次的差异来说,Java 代码是在虚拟机中运行的,之前会进行编译和静态分析——即源代码在运行前会被检查。对于 Android Java,这个虚拟机被称为 Dalvik——稍后会详细介绍。Android 的本地开发组件按照其源代码的精确描述,在随 Android 设备附带的类 Linux 嵌入式操作系统上运行。除了编译器扩展和优化之外,没有额外的解释和检查层次,使本地代码得以运行。

Android 团队提供的工具链和文档,使得 Android 开发者能够轻松地进行本地开发,这些工具和文档统称为本地开发工具包NDK)。NDK 包含了 Android 开发者编译其 C/C++代码所需的所有工具,并支持 ARM、MIPS 和 x86 嵌入式平台。它还包括一些帮助本地开发者分析和调试本地应用程序的工具。本教程将介绍如何在你的机器上安装并运行 NDK。

在开始之前,你需要查看developer.android.com/tools/sdk/ndk/index.html#Reqs上的系统要求列表,以确保你的机器可以顺利运行。

如何操作...

在你的机器上获取 NDK 就像下载它并确保它能运行一样简单。我们可以使用以下步骤:

  1. 下载 NDK 非常简单。前往developer.android.com/tools/sdk/ndk/index.html获取最新版本,并确保选择适合你系统类型的适当版本。如何操作...

  2. 将 NDK 解压到方便的位置。

模拟 Android

Android SDK 带有一个相当不错的工具,名为模拟器,它允许你模拟 Android 设备。该模拟器包含一些最受欢迎的手机,并允许你创建自己的模拟手机。使用这个工具,你可以刷新新内核,摆弄平台,当然,还可以调试应用程序和测试你的 Android 恶意软件和应用漏洞。在整本书中,我们将大量使用这个工具,所以,重要的是你要了解 Android 模拟器。

模拟器使用起来非常直观。当你想要启动一个设备时,你需要做的就是从 SDK 文件夹或直接从 Eclipse 打开Android 虚拟设备AVD)工具。然后,你可以设置一个新的设备,为其配备自己的内存卡、CPU 和屏幕大小以及其他自定义功能,或者你可以从列表中选择一个预配置的设备。在本节中,我将详细介绍这些内容。

只是一个简短的免责声明:以下屏幕截图是在 Windows 7 机器上拍摄的,但 AVD 管理器和设备模拟器在 Windows 和 Linux 平台上工作完全相同,所以 Linux 用户也将能够跟随操作指南。

如何操作...

要在 Eclipse 中模拟一个设备,请按照以下步骤操作:

  1. 点击工具栏上的 AVD 管理器图标。如何操作...

  2. AVD 会弹出。你可以选择一个预配置的特色设备,或者根据你自己的标准设置一个设备。对于这个指南,让我们坚持配置我们自己的设备。

  3. 点击新建…如何操作...

  4. 创建新的 Android 虚拟设备 (AVD) 对话框应该会弹出。你需要为新的虚拟设备填写一些指标并为其命名。这里你可以输入任何你感觉合适的内容,因为这个指南只是为了让你模拟你的第一个设备。

  5. 完成后,点击确定。新的设备应该会出现在 AVD 对话框中。

  6. 点击你刚刚创建的设备,然后点击启动…如何操作...

在这一点上,AVD 会提示你选择屏幕尺寸选项;默认值还不错。完成后点击启动,几秒钟后你的新 AVD 将开始启动。

如何操作...

创建 Android 虚拟设备 (AVD)

你们中的一些人可能出于某些原因更喜欢从命令行界面使用 AVD。也许你有一些很棒的脚本,你想编写它们来设置一些出色的 AVD。本指南详细介绍了如何创建 AVD 并直接从命令行启动它们。

如何操作…

在创建你自己的 AVD 之前,你需要为它指定一些属性;最重要的一个是将要使用的系统镜像。为此,执行以下步骤:

  1. 你可以使用以下命令找到可供你使用的系统镜像列表:

    [path-to-sdk-install]/tools/android list targets
    
    

    或者在 Windows 终端中使用以下命令:

    C:\[path-to-sdk-install]\tools\android list targets
    
    

    例如,在命令提示符中输入以下内容:

    C:\Users\kmakan\Documents\adt-bundle-windows-x86-20130219\sdk\tools\android list targets
    
    

    这个命令将列出你系统上可用的系统镜像。如果你想要更多,需要通过 SDK 管理器安装它们。在这份列表中你需要找的信息是目标 ID,因为下一步你需要它们来识别系统镜像。

  2. 使用以下命令创建 AVD:

    [path-to-sdk-install]/tools/android create avd –n [name of your new AVD] –t [system image target id]
    
    

    你需要为新创建的 AVD 决定一个名称,通过–n开关来指定。你需要在上一步骤中选择系统镜像 ID,使用–t开关来指定。如果一切顺利,你应该刚刚创建了一台全新的虚拟机。

  3. 你可以使用以下命令启动全新的 AVD:

    [path-to-sdk-install]/tools/emulator –avd [avd name]
    
    

    在这里,[avd name]是你在上一步中决定的 AVD 名称。如果一切顺利,你新创建的 AVD 应该会立即启动。

还有更多…

你可能想要了解更多关于命令的信息。关于模拟器,它能够模拟具有不同配置的设备。

模拟内存卡或外部存储

当你创建虚拟设备时,可以使用–c选项指定它也模拟一些外部存储,如下面的命令所示:

android create –avd –n [avd name] –t [image id] –c [size][K|M]

例如,请看以下命令:

android create –avd –n virtdroid –t 1 –c 128

显然,你需要提供新模拟内存卡的大小。你还需要指定单位,通过指定K表示千字节或M表示兆字节。

分区大小

另一件你可能想要做的事情是指定希望分配给内部存储分区的空间大小。你可以通过使用-partition-size开关来实现,如下面的命令所示,当你调用模拟器时指定:

emulator –avd [name] –partition-size [size in MBs]

你还需要为分区提供大小。默认情况下,测量单位是兆字节(MB)。

另请参阅

在模拟器方面,还有许多其他选项可以利用。如果你有兴趣了解更多,请查看以下链接提供的文档:

使用 Android 调试桥(ADB)与 AVD 交互

与模拟的 Android 设备交互是开发者和 Android 安全工程师/审计师最重要的技能之一。Android 调试桥ADB)提供了与 Android 设备本地级别组件交互所需的功能。它允许开发者和安全工程师读取文件系统的内容,并与包管理器、应用程序管理器、内核驱动接口和初始化脚本等进行交互,仅举几例。

如何操作...

使用 ADB 与虚拟设备交互的方法如下:

  1. 你需要首先启动一个 AVD,或者,如果你愿意,只需通过 USB 将你的 Android 设备连接到任何安装了 SDK 的机器上。你可以使用以下命令启动 AVD:

    emulator –avd [name]
    
    
  2. 我们可以使用以下命令列出所有连接的 Android 设备(针对 Windows 机器):

    C;\\[path-to-sdk-install]\platform-tools\adb devices
    
    

    或者,如果你使用的是 Linux 机器,使用以下命令:

    [path-to-sdk-install]/platform-tools/adb devices
    
    

    这个命令会给你列出连接的设备列表,基本上就是你可以通过 ADB 连接的所有设备。你需要留意列表中的设备名称。当你使用 ADB 启动与它们的连接时,你需要识别这些设备。

  3. 你可以使用以下命令向你的 Android 设备启动 shell 连接:

    /sdk/platform-tools/abd shell –s [specific device]
    
    

    或者,如果你恰好知道你想连接的 Android 设备是唯一的模拟设备,你可以使用以下命令:

    /sdk/platform-tools/adb shell –e
    
    

    或者,如果设备是唯一通过 USB 连接的设备,你可以使用以下命令:

    /sdk/platform-tools/adb shell –d
    
    

    开关 –d-e-p 同样适用于其他 ADB 命令,不仅仅是 shell。如果这操作顺利,你应该会看到类似以下命令的提示字符串——用于标识正在使用的命令 shell 的字符串:

    root@android$
    
    

现在你应该拥有一个功能齐全的 shell,其中包含一些传统的 Unix/Linux 命令和工具。在你的指尖尝试搜索文件系统,了解所有内容的位置。

还有更多…

既然你已经连接了一个设备,你需要了解一些关于导航 Android 文件系统和使用命令的知识。以下是一个帮助你开始的小列表:

  • ls {path}: 这将列出指定路径目录的内容

  • cat {file}: 这将在屏幕上打印文本文件的内容

  • cd {path}: 这将改变当前工作目录到指定路径的目录

  • cd ../: 这将改变当前工作目录到刚好高一级的目录

  • pwd: 这将打印当前工作目录

  • id: 这将检查你的用户 ID

另请参阅

在 AVD 上复制文件

在你即将进行的 Android 平台冒险中,你可能会想要在某些时刻从 Android 设备(无论它们是不是模拟器)上复制东西。复制文件相当简单。你需要的是以下内容:

  • 你希望连接的设备

  • 你想要复制到/从设备上复制的文件

  • 你想要放置这个文件的位置

如何操作…

要使用 ADB 访问你 Android 设备上的文件,你需要执行以下操作:

  1. 实际上这个过程相当简单。你需要从命令行界面执行以下命令:

    adb {options} pull [path to copy from] [local path to copy to]
    
    
  2. 要将文件复制到 AVD 上,你可以使用以下命令:

    adb {options} push [local path to copy from] [path to copy to on avd]
    
    

通过 ADB 向 AVD 安装应用程序

有时你可能需要将本地文件系统上的应用安装包APKs)安装到你自己拥有的模拟器或设备上。通常基于 Android 的安全工具在 Play 商店中无法获取——因为它们可能会让不守规矩的用户面临过多风险,或者被恶意软件滥用——需要手动安装。此外,你可能还需要开发和 Android 原生二进制文件来演示和验证漏洞利用。

如何操作...

使用 ADB 安装 APK 可以通过以下方式完成:

  1. 你需要知道 APK 在你本地机器上的具体位置,找到后,你可以像下面这个命令一样,用path来替换它:

    adb {options} install [path to apk]
    
    
  2. 你还可以使用针对特定设备的命令来缩小你想安装的目标设备。你可以使用以下命令:

    adb {-e | -d | -p } install [path to apk]
    
    

第二章:与应用程序安全互动

在本章中,我们将涵盖以下内容:

  • 检查应用程序证书和签名

  • 签名 Android 应用程序

  • 验证应用程序签名

  • 检查 AndroidManifest.xml 文件

  • 通过 ADB 与活动管理器交互

  • 通过 ADB 提取应用程序资源

引言

在本章中,我们将通过直接与它们互动,了解一些 Android 安全架构组件的实际应用,特别是那些专注于保护应用程序的部分。"在你亲自动手之前,你永远不会真正理解任何事。" 这正是本章试图激发的理念;实际动手拆卸一些安全机制,剖析它们,真正了解它们的作用。

我们在这里只介绍最基本的内容,这些技巧和窍门能让你在需要逆向工程应用程序或对 Android 应用程序进行彻底的手动安全评估时,或者你只是纯粹对应用程序安全感兴趣并想了解更多时,获取所需的信息。

检查应用程序证书和签名

应用证书是开发者用来声明他们对发布到应用市场的应用程序的信任。这是通过声明他们的身份并将其与他们的应用程序以加密方式相关联来完成的。应用签名确保没有应用程序可以通过提供一种简单有效的机制来冒充另一个应用程序,从而确定并强制执行 Android 应用程序的完整性。所有应用程序在安装前必须使用证书进行签名,这是一个要求。

Android 应用程序签名是对 JAR 签名的重新利用。它通过对应用程序内容应用加密哈希函数来工作。我们很快将看到 APK 文件中确切哪些内容被哈希。然后,这些哈希值与声明开发者身份的证书一起分发,将其与开发者的公钥以及实际上他的私钥相关联,因为它们在语义上是相关的。证书通常使用开发者的私钥进行加密,这意味着它是一个自签名证书。没有可信赖的第三方来证明开发者实际上拥有给定的公钥。这个过程产生了一个签名,并且应该与这个公钥一起分发或发布。

一个应用程序的签名是唯一的,找到应用程序的证书和签名是一项关键技能。你可能在设备上寻找恶意软件签名,或者你可能想要列出共享给定公钥的所有应用程序。

准备工作

在开始之前,你需要在你的机器上安装以下软件:

  • Java JDK:这可以在 Unix/Linux 发行版或 Microsoft Windows 系统上安装,如前一章所示

  • Android SDK:如前一章所示,可以在你的 Linux Debian 或 Microsoft Windows 系统上安装。

  • WinZip(针对 Windows):可以在www.winzip.com下载;如果你运行的是 Windows 7,则不需要显式安装 WinZip

  • 解压(针对 Debian/Ubuntu Linux 系统):可以通过在终端输入以下命令进行安装:

    sudo apt-get install unzip
    
    

假设我们还没有一个明确想要查看证书的应用程序,并且你想要完全复制这里所展示的内容,从模拟器中提取一个应用会方便得多。此食谱还详细介绍了如何设置模拟器以执行此操作。

按照这里的方式进行模拟器设置,确保你可以访问完全相同的程序和模拟系统,最终,拥有相同的证书,这样容易检查你是否走在正确的轨道上。在模拟 Android 设备之前,需要确保 Android SDK 工具已更新,包括最新的 API 级别和模拟器镜像。如果你不确定如何升级你的 Android SDK,请参考上一章。

因此,首先让我们通过执行以下步骤来启动一个Android Virtual DeviceAVD):

  1. 打开命令行界面并执行以下命令:

    [path-to-your-sdk-install]/android create avd –n [your avd name]  –t [system image target]
    
    

    准备就绪

    如果你使用的是 Windows 机器,请输入:

    C:\[path-to-your-sdk-install]\android create avd –n [your avd name] –t [system image target]
    
    
  2. 如果一切顺利,你应该刚刚创建了一个 AVD。现在可以执行以下命令来启动它:

    [path-to-your-sdk-install]/emulator –avd [your avd name] –no-boot-anim
    
    

    准备就绪

  3. 你应该会立即看到一个模拟器弹出。给它一点时间启动。一旦完全启动,你可以看到锁屏,这意味着你可以启动 ADB,为我们提取一些 APK 文件。你可以通过输入以下命令来提取 APK 文件:

    adb pull /system/app/Contacts.apk
    
    

    以下截图为实际操作示例:

    准备就绪

    如果你需要其他示例,可以查看system/app/目录的内容,找到联系人应用或其他应用,如下面的截图所示:

    准备就绪

    你应该刚刚将联系人应用复制到你的本地设备上。如果这部分内容让你感到困惑,请参考上一章;它涵盖了如何创建模拟器以及如何从模拟器复制设备。

如何操作…

你应该在硬盘上有一个你想要检查的 APK 文件的本地副本。我们现在可以开始检查应用程序的证书。要查看应用程序的公钥证书和签名,首先需要解压 APK 文件。如果你知道如何解压归档文件,这非常简单,因为 APK 文件实际上是被重命名的 ZIP 归档文件。你可以通过执行以下步骤来解压归档文件:

  1. 如果你使用的是 Windows 机器,你可能需要确保已安装 WinZip。你需要做的就是使用 WinZip 打开 APK 文件,它应该像其他任何 ZIP 存档一样打开。在 Linux Debian 机器上,你需要将此文件复制到一个带有 ZIP 扩展名的文件中,以便 WinZip 可以为我们愉快地解压:

    cp Contacts.apk Contacts.zip
    
    
  2. 将存档解压到容易记住的地方;你可以通过执行以下命令来完成:

    unzip Contacts.zip
    
    

    如何操作…

    解压存档后,你的目录应该如下截图所示:

    如何操作…

  3. 定位名为META-INF的文件夹。这个文件夹包含签名文件和实际的CERT.RSA文件,这是自签名的公钥证书;你可以使用随 Java JDK 捆绑的 keytool 来查看它,你应该在尝试此配方之前安装 Java JDK。使用以下命令打印证书:

    keytool –printcert –file META-INF/CERT.RSA
    
    

    如何操作…

    现在你面前的是声明公钥持有者的证书。

  4. 要查看与应用程序内容相关的实际签名,请定位到META-INF文件夹下的名为CERT.SF的文件。你可以在 Windows 上通过记事本或其他可用的文本编辑器打开它来查看,或者在 Unix/Linux 机器上执行以下命令:

    cat [path-to-unzipped-apk]/META-INF/CERT.SF
    
    

    现在你面前应该有签名文件。它包含了应用程序中包含的资源文件的加密哈希;以下截图为例:

    如何操作…

    jarsigner工具尝试验证应用程序内容时,会使用此文件;它会计算CERT.SF文件中列出的资源的加密哈希,并将其与每个资源的摘要进行比较。在之前的截图中,哈希—SHA-1 Digests—已经进行了 base64 编码。

工作原理…

META-INF文件夹是一个非常重要的资源,因为它有助于建立应用程序的完整性。由于该文件夹中的内容在应用程序内容的加密安全中扮演着重要角色,因此有必要讨论该文件夹的结构以及其中应该包含什么内容以及原因。

META-INF文件夹内,你应该至少能找到以下内容:

  • MANIFEST.MF:此文件声明资源,与CERT.SF文件非常相似。

  • CERT.RSA:这是前面讨论过的公钥证书。

  • CERT.SF:此文件包含了应用程序中所有在应用程序签名中已计算的资源。它被添加以适应 JAR 特定的加密签名。

  • CERT.RSA:这是一个 X.509 v3 证书;其中的信息由 keytool 以下列方式结构化:

    • 所有者:此字段用于声明公钥持有者,其中包含有关与此个人相关的国家和组织的一些基本信息。

    • 颁发者:此字段用于声明将公钥与声明持有者关联的 X.509 证书的颁发者。这里提到的人或组织是有效为密钥持有者担保的人。他们是确立证书中列出的公钥真实性的人。

    • 序列号:这是用于标识已颁发证书的识别码。

    • 有效期限:此字段指定了证书及其相关属性可以被颁发者验证的期限。

    • 证书指纹:此字段保存了证书的摘要和。它用于验证证书是否被篡改。

数字签名是通过使用受信任第三方的私钥加密证书来计算的。在大多数 Android 应用程序中,“受信任的第三方”是开发者。这意味着此签名是通过使用他/她自己的私钥(通常与公钥关联的私钥)加密证书生成的。这种数字签名用法在功能上可能是正确的——它利用了数字签名机制的功能——但它不如依赖像证书颁发机构(CA)这样的受信任第三方那样健壮。毕竟,任何人都可以通过用自己的密钥签署来说他们开发了 Twitter 应用,但没有人可以说他们拥有 VeriSign 或赛门铁克的私钥!

如果证书是自签名的,开发者在填写与证书相关的信息时可以发挥其创造性。Android 包管理器并不努力验证证书的颁发者、所有者或任何其他证书详细信息的有效性或是否是实际存在的实体。例如,“所有者”无需明确提及关于开发者的任何有效个人信息,或者“颁发者”可以是完全虚构的组织或个人。尽管这样做是可能的,但强烈建议不要这样做,因为它使得应用程序非常难以被信任;毕竟,移动应用程序经常被存储和使用在非常私人的设备上,而那些得知公钥证书虚构细节的人可能不再信任这样的应用程序。

生成可信任应用程序证书的最佳方式是通过合格证书颁发机构(CA),你可以生成自己的公钥和私钥对之后,请求签署公钥证书,或者请求 CA 生成带有公钥证书的公私钥对,因为他们通常会验证证书中发布的信息。像赛门铁克等 CA 和安全供应商经常提供一系列服务以方便生成可信任的公钥证书,其中一些服务专门用于支持 Android 应用程序开发。

本教程的下一部分包含了一些关于公钥证书的有用链接,供您查看。

还有更多内容...

你也可以在 Linux 的命令行工具中使用 OpenSSL 库执行以下步骤来查看完整的公钥证书:

  1. 确保你已经安装了 OpenSSL;如果没有,你可以使用以下命令安装 OpenSSL:

    apt-get install openssl
    
    
  2. 安装完成后,如果你处于解压后的APK目录的根目录下,可以使用以下命令查看证书:

    openssl pcks7 –inform DER –in META-INF/CERT.RSA –noout –print_certs –text
    
    

    你应该在终端屏幕上看到类似以下截图的内容:

    还有更多...

    上一个截图的后半部分如下:

    还有更多...

    前一个截图的证书最后一部分是颁发证书的 CA 的实际数字签名。

另请参阅

签名 Android 应用

所有 Android 应用在安装到 Android 设备之前都必须进行签名。Eclipse 和其他 IDE 几乎可以为你处理应用签名的所有工作;但为了让你真正理解应用签名是如何工作的,你应该尝试使用 Java JDK 和 Android SDK 中的工具亲自签一个应用。

首先,关于应用签名的一些背景知识。Android 应用签名只是 JAR 签名的重新利用。多年来,它一直被用来验证 Java 类文件存档的真实性。Android 的 APK 文件与 JAR 文件并不完全相同,并且比 JAR 文件包含更多的元数据和资源;因此,Android 团队需要对 JAR 签名进行调整以适应 APK 文件的结构。他们通过确保 Android 应用中包含的额外内容成为签名和验证过程的一部分来实现这一点。

所以,在不过多透露关于应用签名的情况下,让我们获取一个 APK 文件并进行签名。在后续的演练中,我们将尝试在我们签署后的应用安装在 Android 设备上,作为一个简单的方式来验证我们确实正确地签了名。

准备工作

在开始之前,你需要安装以下内容:

  • Java JDK:这包含了所有必要的签名和验证工具

  • APK 文件:这是一个待签名的 APK 示例

  • WinZip:这是 Windows 机器上必需的操作

  • 解压:这是 Ubuntu 机器上必需的操作

考虑到你可能使用的是一个已经签名的 APK 文件,你首先需要从 APK 文件中删除证书和签名文件。为此,你需要执行以下步骤:

  1. 解压 APK 文件。重新解包 APK 文件是浪费时间的;所以,如果你在这一步需要帮助,请参考检查应用程序证书和签名的菜谱。

  2. 解压 APK 文件后,你需要删除META-INF文件夹。Windows 用户可以直接打开解压后的APK文件夹,使用图形用户界面删除META-INF文件夹。在 Unix/Linux 系统上,可以通过执行以下命令从命令行界面完成:

    rm –r [path-to-unzipped-apk]/META-INF
    
    

    现在你应该准备好签署应用程序了。

如何操作...

通过执行以下步骤可以为你的 Android 应用程序签名:

  1. 你首先需要为自己设置一个密钥库,因为它将保存你用来签署应用程序的私钥。如果你已经有了一个密钥库,可以跳过这一步。在 Windows 和 Unix/Linux 发行版上生成全新的密钥库,你需要执行以下命令:

    keytool –genkey –v -keystore [nameofkeystore] –alias [your_keyalias] –keyalg RSA –keysize 2048 –validity [numberofdays]
    
    
  2. 输入这个命令后,keytool 将帮助你为你的密钥库设置密码;你应该确保输入一些你真正能记住的东西!另外,如果你打算将这个密钥库用于实际用途,请确保将其保存在一个非常安全的地方!

  3. 在为你的密钥库设置好密码后,keytool 将开始提示你输入用于构建证书的信息;请密切关注所请求的信息,并尽可能诚实回答——尽管以下截图并未展示这一点:如何操作...

    现在你应该已经设置了一个全新的密钥库,其中安全地保存了你的新私钥、公钥和自签名证书,并为你提供加密保护。

  4. 你现在可以使用这个全新的密钥库来签署应用程序,通过执行以下命令即可完成:

    jarsigner –verbose –sigalg MD5withRSA –digestalg SHA1 –keystore [name of your keystore] [your .apk file] [your key alias]
    
    
  5. 系统将提示你输入密钥库的密码。一旦你正确输入,jarsigner将开始原地签署应用程序。这意味着它将修改你提供的 APK 文件,通过添加带有所有证书和签名相关细节的META-INF文件夹。如何操作...

    就是这样。签署应用程序就是如此简单。我也无意中展示了如何重新签署应用程序,即替换原本与应用程序一起分发的签名。

工作原理...

首先,让我们看看提供给 keytool 的选项:

  • -genkey:这个选项告诉 keytool 你想要生成一些密钥。

  • -v:这个选项启用详细输出;然而,这个命令是可选的。

  • -keystore:这个选项用于定位你希望用来存储生成的密钥的密钥库。

  • -alias:这个选项是为你生成的密钥对的别名。

  • -keyalg:这个选项说明了用于生成密钥的加密算法;你可以选择使用 RSA 或 DSA。

  • -keysize:此选项指定你将要生成的密钥的实际位数。

  • -validity:此选项提到生成的密钥将有效的天数;Android 官方推荐使用超过 10,000 天的值。

Keytool 实际上对公钥和私钥的操作是将公钥包装在一个 X.509 v3 证书内进行存储。此证书用于声明公钥持有者的身份,并且可以用来确认提到的公钥属于声明的持有者。这需要像 CA 这样的可信第三方的参与,但 Android 并不要求以这种方式确认公钥。关于这些证书如何被使用和构建的更多信息,请参考 检查应用程序证书和签名 的食谱。

jarsigner 的选项将在以下命令后详细描述:

jarsigner –verbose –sigalg MD5withRSA –digestalg SHA1 –keystore [nameof your keystore] [your .apk file] [your key alias]

以下部分解释了前述命令的属性:

  • -verbose:用于启用详细输出。

  • -sigalg:用于提供签名过程中将要使用的算法。

  • -digestalg:用于提供将计算 .apk 文件中每个资源的签名的算法。

  • -keystore:用于指定你想要使用的密钥库。

  • [你的 .apk 文件]:这是你打算签名的 .apk 文件。

  • [你的密钥别名]:这是你与密钥/证书对关联的别名。

参见

验证应用程序签名

在前面的食谱中,我们讲解了应用程序是如何签名的以及如何生成密钥以安全地签名它们。这个食谱将详细说明如何验证应用程序签名。能够手动完成这一操作非常重要,因为它不仅让你了解验证实际是如何工作的,而且也是深入了解加密应用程序安全性的门户。

准备开始

要执行此食谱,你需要以下内容:

  • JDK

  • 一个待验证的已签名应用程序样本

这就是你需要的一切。让我们开始吧!

如何操作...

要验证应用程序签名,你需要执行以下步骤:

  1. Java JDK 有一个名为 jarsigner 的工具,它能够处理所有繁重的工作;你需要做的就是执行以下命令:

    jarsigner –verify –verbose [path-to-your-apk]
    
    
  2. 现在你需要做的是在屏幕上寻找jar 已验证的字样;这表示应用程序签名已经经过验证。

检查 AndroidManifest.xml 文件

应用程序清单可能是对 Android 应用程序安全专家来说最重要的信息来源。它包含了有关应用程序权限以及构成应用程序的各个组件的所有信息,并且向我们详细介绍了这些组件将如何被允许与平台上的其他应用程序交互。我将使用这个食谱作为讨论应用程序清单、其结构以及示例清单中每个组件含义的好借口。

准备工作

在开始之前,你需要安装以下软件:

  • Windows 上的 WinZip

  • Java JDK

  • 一个便捷的文本编辑器;通常 Vi/Vim 可以胜任,但 Emacs、Notepad++和 Notepad 也都很好用;在这里我们不需要花哨的东西。

  • Android SDK(这里并不意外!)

你可能还需要获取一个名为apktool的工具;它使得解码AndroidManifest.xml文件变得非常简单。实际上,它所做的只是重新格式化另一个 Android SDK 工具的输出。设置它非常简单,你只需要执行以下步骤:

  1. 下载工具;你可以在android-apktool.googlecode.com/files/apktool1.5.2.tar.bz2找到它。

    如果你已经安装了 Android SDK,你可以将刚刚下载的 apktool 解压到 SDK 文件夹中的platforms-tools目录下,具体来说:

    C:\\[path to your sdk]\sdk\platform-tools\
    
    

    或者对于 Linux 机器:

    /[path to your sdk]/sdk/platform-tools/
    
    

    请确保你获取了apktool.jar文件和 apktool 脚本,与其他文件放在同一目录下;不要将其放在自己的子文件夹中!

  2. 如果你不想下载 Android SDK,还有一些依赖项需要你下载。它们可以在code.google.com/p/android-apktool/downloads/list下载。

    特别是,如果你使用的是 Windows 电脑,你应该在android-apktool.googlecode.com/files/apktool-install-windows-r05-ibot.tar.bz2获取 apktool。

    如果你在使用 Linux Debian 系统,你应该在这个地址获取相应的工具android-apktool.googlecode.com/files/apktool-install-linux-r05-ibot.tar.bz2

    你还需要确保所有下载的文件都在同一个目录下。

  3. 你应该能够启动它,并且可以通过以下方式测试它:

    在 Windows 上:

    C:\[path-to-apktool]\apktool -help
    
    

    在 Debian Linux 上:

    /[path-to-apk-too]/apktool -help
    
    

    如果你完成了所有这些步骤,你就可以进入下一步,也就是实际剖析一个AndroidManifest.xml文件。

如何操作...

要获取给定应用程序包的AndroidManifest.xml文件副本,你需要执行以下步骤:

  1. 你需要做的就是将 apktool 指向你的 APK 文件。我们将使用之前食谱中从模拟器中提取的Contacts.apk应用程序。在命令提示符中输入以下内容,并确保你的工作目录——你当前所在的终端/命令提示符目录——是提取 apktool 的目录。

    在 Debian Linux 上:

    /[path-to-apktool]/apktool d -f –s [apk file] decoded-data/
    
    

    在 Windows 上:

    C:\[path-to-apktool]/apktool d –f –s [apk file] decoded-data/
    
    

    例如,如果你使用的是Contacts.apk应用程序,并且你想将所有解码的文件保存到一个名为decoded的文件夹中,你可以在 Linux 机器上输入以下命令:

    ~/adt-bundle-linux-x86_64/sdk/platform-tools/apktool d –f –s Contacts.apk decoded
    
    

    如何操作...

  2. 现在你可以查看应用程序清单了。它应该在你上一步选择提取的文件夹中,在一个名为AndroidManifest.xml的文件里。要查看它,只需拿出你最喜欢的文本编辑器——Linux 用户,你的操作系统几乎捆绑了一百万个文本编辑工具——并将其指向AndroidManifest.xml文件。

    在 Linux 上:

    vi [path-to-your-decoded-data]/AndroidManifest.xml
    
    

    或者,你也可以通过执行以下命令在终端屏幕上显示清单:

    cat [path-to-your-decoded-data]/AndroidManifest.xml
    
    

    在 Windows 上:

    C:\Windows\System32\notepad.exe [path-to-decoded-data]\AndroidManifest.xml
    
    
  3. 你可以在终端屏幕上看到清单——如果你使用的是 Linux 机器——或者记事本会自动弹出并打开清单。你们中的一些人可能不理解屏幕上的这些乱码是什么,或者这些信息有多宝贵,因此下一个食谱将包含对应用程序清单结构所有重要部分的解释:如何操作...

    所以,你可能正盯着AndroidManifest.xml文件中的乱码信息。它的意义以及为什么这一切都很重要,在接下来的演练食谱中会有说明。它提供了关于某些元素及其属性如何工作的良好背景。我只涵盖了与安全和应用程序安全评估最相关的元素背景。

工作原理...

为了帮助你理解应用程序清单,我将在这里向你展示清单的结构,并解释最重要部分的意义。如果你想了解更多关于 Android 清单语言的信息,你应该查看本食谱的另请参阅部分。

清单的结构如下:

<?xml version="1.0" encoding="utf-8"?>

<manifest>

  <uses-permission /> <permission /> <permission-tree /> <permission-group /> <instrumentation /> <uses-sdk /> <uses-configuration /> <uses-feature /> <supports-screens /> <compatible-screens /> <supports-gl-texture />
  <application>
    <activity>
      <intent-filter>
        <action />
        <category />
        <data />
      </intent-filter>
      <meta-data />
    </activity>
    <activity-alias>
      <intent-filter> . . . </intent-filter>
      <meta-data />
    </activity-alias>
    <service>
      <intent-filter> . . . </intent-filter>
      <meta-data/>
    </service>
    <receiver>
      <intent-filter> . . . </intent-filter>
      <meta-data />
    </receiver>

    <provider>
      <grant-uri-permission />
      <meta-data />
      <path-permission />
    </provider>
    <uses-library />
  </application>
</manifest>

提示

下载示例代码

你可以从你在www.packtpub.com的账户下载你购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给你。

这究竟意味着什么呢?首先,第一行更多是关于文件类型和 Android 清单,几乎与它的作用和目的无关。从.xml扩展名你可能已经看出,它是一个可扩展标记语言XML)文件。这意味着 Android 清单是用 XML 语言编写的。XML 基本上是用于创建任何你希望的语言的格式;一些资料坦率地将其描述为定义标记语言的的语言。XML 旨在成为一组描述几乎所有事情的规则!

所以,当你看到以下代码时,你就知道紧随其后的行是一个使用 UTF-8 编码的 XML 版本 1 的 XML 文件:

<?xml version="1.0" encoding="utf-8"?>

接下来谈谈 Android 特有的部分:

<manifest>

此元素是应用程序清单中条目的开始标签;它标志着开始,被称为 XML 文档的根元素。下一个标签声明应用程序需要给定权限:

<uses-permission android:name="string"/>

这通常是在你安装应用程序时根据权限类型显示的字符串。android:name属性指定了权限的名称;例如,如果你的应用程序需要使用设备的摄像头服务,它的清单中应该有以下代码:

<uses-permission android:name="android.permission.CAMERA">

下一个元素类型如下:

<permission android:description="string resource" android:icon="drawable resource" android:label="string resource" android:name="string" android:permissionGroup="string" android:protectionLevel=["normal" | "dangerous" | "signature" | "signatureOrSystem"] />

此元素用于定义权限;例如,当开发者认为其他应用程序要与特定应用程序组件交互时,需要特殊的唯一权限。这个元素相当有趣;让我们看看它的属性:

  • android:description:此属性用于定义当用户被提示授予权限时作为权限描述显示的字符串。

  • android:icon:此属性用于定义当用户被提示授予权限时显示的描述性图标。

  • android:label:此属性用作当用户被提示授予权限时权限的名称,例如,网络访问和读取短信。

  • android:name:此属性是权限的实际名称。这是在应用程序的清单中查找的字符串,以确定它是否具有此权限,例如android.permission.Camera

  • android:protectionLevel:此属性用于指示与此权限相关的风险级别。这些级别被分类如下:

    • "dangerous":此级别通常分配给任何允许应用访问敏感用户数据或操作系统配置数据的权限。这用于保护任何可能被用于危害用户的函数或数据的访问。

    • "normal":此级别用于指示任何授予访问不带来固有风险的数据或服务的权限。

    • "signature":当权限需要自主授予与定义该权限的应用具有相同证书的任何应用时,会设置此级别,即AndroidManifest.xml中带有相关<permission>标签的应用。

    • "signatureOrSystem":当权限需要自主授予与定义该权限的应用具有相同证书的任何应用时,会设置此级别。

你应该密切关注protectionLevel属性中使用的值,尤其是那些需要专业进行应用评估的人。尝试思考开发者决定的保护级别是否恰当。你需要能够确保与此权限相关的风险能够明确地指示给用户。

protectionLevel的另一个关键属性在于它决定了在应用安装前向用户显示哪些权限。用户总是会被提示授予危险保护级别的权限,而普通权限只有在用户明确请求时才会显示。另一方面,signaturesignatureOrSystem权限在应用安装前不会显示给用户。这意味着如果应用在signaturesignatureOrSystem保护级别下被授予了风险权限,用户将对此一无所知。在检查应用清单时请考虑这一点,因为它将帮助确定应用如何向用户传达风险。接下来是下一个元素类型!

<application>

此元素用于定义一个应用的开始。关于安全,此元素的重要之处在于其属性以及它们如何影响在此元素内定义的组件。为了简洁起见,这里省略了属性定义;你需要参考在developer.android.com/guide/topics/manifest/application-element.html可获取的官方文档以获取更多详细信息。

此元素的一个重要属性是,某些属性仅为元素内部定义的组件的相应属性设置默认值;这意味着其组件将能够覆盖它们。这些可覆盖属性中的一个显著元素是称为permission的属性,它声明了其他应用与之交互所需的权限。这意味着如果一个应用设置了一个给定的权限,而它的一个组件设置了不同的权限作为其属性,那么组件的权限将优先考虑。如果组件用普通权限覆盖了危险权限,这可能会带来相当大的风险。

其他属性不能被它们的组件覆盖。这取决于属性中设定的值,并应用于每一个单独的组件。组件包括以下属性:

  • debuggable:此属性指定给定的组件或组件组是否可调试。

  • enabled:此属性指定安卓应用框架是否能够启动或运行此元素定义的组件;默认为true。只有当设置为false时,它才会覆盖所有组件的值。

  • description:这个属性仅是一个用于描述应用的字符串。

  • allowClearUserData:此属性是一个标志,用于确定用户是否可以清除与 app 关联的数据;默认情况下,它被设置为true,在一些平台上非系统应用不能将其设置为其他值。

以下元素是应用程序组件的定义,允许开发者为它们决定某些属性:

<activity
  android:exported=["true" | "false"]
  android:name="string"
  android:permission="string"
  android:enabled=["true" | "false"]
  android:permission="string"
...other attributes have been omitted
>
`android:exported`: This attribute is used to decide whether the components of other applications will be able to interact with this element. All application components—services, broadcast receivers, and content providers—have this attribute in common.

What's interesting here is the default behavior of this attribute, if it is not explicitly set for this element. Whether or not it will be "`exported`" partly depends on whether intent filters are defined for the activity or not. If intent filters are defined and the value is not set, the Android system assumes that the component intends to respond to interaction from external application components and will allow them to interact with it, given that the initiator of the interaction has the necessary permissions to do so. If no intent filters are defined and the attribute value is not set, the Android application framework will only allow explicit intents to be resolved against the component.

There is another caveat. Because of the way in which older Android API levels work, there are attributes that can override the default value; for applications that set either `android:minSdkVersion` or `android:targetSdkVersion` to 16 or lower, the default value is `true`. For applications that set `android:minSdkVersion` or `android:targetSdkVersion` as equal to or higher than 17, the default value is `false`.

This is very valuable information because it will help us determine an application's attack surface—it determines how potentially malicious applications will interact with its components—and quite literally determine the difference between a good security assessment and an ineffective one.

`android:name`: This attribute specifies the class file that contains the Java code for the component; I've added it here because you will need to know this value should you want to launch explicit intents aimed at a given component. All component types have this attribute in common.`android:permission`: This attribute is used to specify the permission required to interact with the component.`android:enabled`: This attribute is used to indicate whether the system is allowed to start/instantiate the component:

<service android:enabled=["true" | "false"] android:exported=["true" | "false"] android:icon="drawable resource" android:isolatedProcess=["true" | "false"] android:label="string resource" android:name="string" android:permission="string">


It is used to define the attributes of a service; some XML attributes are unique to services, namely:

*   `android:isolatedProcess`: This attribute indicates if the service will run in an isolated process with no permissions.

    ```

        <receiver android:enabled=["true" | "false"] android:exported=["true" | "false"] android:icon="drawable resource" android:label="string resource" android:name="string" android:permission="string" android:process="string" ></receiver>

    ```kt

    This element declares the broadcast receiver component:

    ```

        <provider android:authorities="list" android:enabled=["true" | "false"] android:exported=["true" | "false"] android:grantUriPermissions=["true" | "false"] android:icon="drawable resource" android:initOrder="integer" android:label="string resource" android:multiprocess=["true" | "false"] android:name="string" android:permission="string" android:process="string" android:readPermission="string" android:syncable=["true" | "false"] android:writePermission="string" ></provider>

    ```kt

    It defines the components of the content provider type. Seeing that the content providers are basically database-like components, they would need to be able to define the controls for accessing their data structures and content. The following attributes help them to do just that:

*   `android:writePermission`: This attribute specifies the name of the permission components from other applications that this content provider is in charge of. It is a must-have in order to change or augment data structures.
*   `android:readPermission`: This attribute specifies the name of the permission components from other applications that this content provider is in charge of. It is a must-have in order to read from or query the data structures.
*   `android:authorities`: This attribute specifies a list of names identifying the URI authorities. Usually, these are the Java classes that implement the provider: 

    ```

        <intent-filter android:icon="drawable resource" android:label="string resource" android:priority="integer" ></intent-filter>

    ```kt

另请参阅

通过 ADB 与活动管理器交互

对于任何初露头角的安卓安全专家来说,了解安卓调试桥ADB)是非常重要的。ADB 允许你直接与本地服务和资源进行交互,例如包管理器、活动管理器以及其他各种对安卓系统操作至关重要的守护进程,这些都被安卓系统所使用。本指南将提供如何通过演示一些你可以执行的命令与活动管理器交互的详细信息。

准备就绪

在开始之前,你需要准备以下物品:

  • 安卓 SDK 工具

  • 可以是虚拟设备,查看检查 AndroidManifest.xml 文件的菜谱以了解如何创建和启动一个,或者是物理的安卓设备

如何操作…

要使用应用程序管理器启动活动,你需要执行以下步骤:

  1. 使用以下命令在你的安卓设备上打开一个 shell:

    adb shell
    
    
  2. 找到一个要启动的活动;你可以通过使用包管理器搜索设备上已安装的活动列表来实现这一点。

    pm list packages
    
    

    大量的包列表应该开始从你的屏幕上倾泻而下;其中任何一个都可以作为一个很好的示例:

    如何操作…

  3. 选择你想要启动的活动后,执行以下命令:

    am start [package name]
    
    

还有更多…

除了启动活动之外,你还可以通过使用 start 命令接受的意图参数,指定要发送到活动的意图,如下所示:

am start <INTENT> < --user UID | current >

<INTENT> 参数可以由允许你详细描述一个意图的几个参数组成。

  • -a [动作]:此参数指定要指定的动作的字符串标签。它有助于详细说明被发送意图的预期目的或“动作”。

  • -d [数据 URI]:此参数指定要附加到意图的数据 URI。它指向处理意图的应用程序要使用的数据。

  • -t [MIME 类型]:此参数指定意图中包含的数据的 MIME 类型。

  • -c [类别]:此参数指定意图的类别。

  • -n [组件]:此参数指定了意图指定的包的目标组件。它用于精确调整意图的目标。

  • -f [标志]:此参数指定意图标志。它用于描述应如何尊重意图,并允许你控制尊重意图的应用程序的一定数量的行为。

  • -e [额外键] [字符串值]:此参数添加与给定键关联的字符串值。某些意图定义允许你向应用程序传递字符串值的字典。当意图被尊重时,将访问这些字符串值。

  • -e [额外键] [字符串值]:此参数的功能与 -e 相同。

  • -ez [额外键] [布尔值]:此参数将布尔值与名称关联。

  • -ei [额外键] [整数值]:此参数将整数值与名称关联。

  • -el [额外键] [长整数值]:此参数将长数字值与名称关联。

  • -ef [额外键] [浮点数值]:此参数将浮点数值与名称关联。

  • -eu [额外键] [URI 值]:此参数将 URI 与名称关联。

  • -ecn [额外键] [组件名称]:此参数将组件名称(将被转换为 ComponentName 对象)与名称关联。

  • -eia [额外键] [整数值, 整数值,...]:此参数允许你将整数数组与名称关联。

  • -efa [额外键] [浮点值, 浮点值,...]:此参数与-eia相同,不同之处在于在这种情况下,您可以将一系列浮点数值与一个名称关联。

并非所有意图参数都是必须的。要让此命令逻辑上合理,只需要意图目标组件或动作值;这些规则适用于所有针对应用程序的意图。

可选的--user参数允许您指定应用程序应以哪个用户身份运行。如果没有为活动提供此参数,它将以 ADB 用户的身份运行。

您还可以将标志与意图关联。要获取完整选项列表,请参考意图规范 - Android 开发者部分中的标记链接。

使用此命令的工作方式类似于以下命令:

am start –n com.android.MyPackage/com.android.MyPackageLaunchMeActivity –e MyInput HelloWorld –a android.intent.MyPackageIntentAction –c android.intent.category.MyPackageIntentCategory

您也可以使用活动管理器启动服务;您可以通过使用startservice命令来完成此操作:

am startservice <package name>/<component name> <INTENT>

使用此命令的工作方式如下:

am startservice com.android.app/com.android.app.service.ServiceComponent
you can also specify

活动管理器支持的另一个功能是停止服务和进程。当应用程序占用所有系统资源并减慢系统速度时,这非常有用。以下是使用活动管理器杀死进程的方法:

kill < --user UID | current > <package>

与之前的命令一样,UID参数是可选的。在这里,此参数允许您将kill命令限制为以给定用户身份运行的包。如果未指定,ADB 将尝试杀死与给定包关联的所有用户的运行进程。

有关 Android 活动管理器支持的其他命令,请参见Android 调试桥 - Android 开发者部分中的标记链接。

另请参阅

通过 ADB 提取应用程序资源

以下指南将展示如何对您的 Android 应用程序进行一些侦查。具体来说,找出它们用来存储重要信息的数据结构类型以及它们存储的信息类型,例如,高分、密码、联系人和电子邮件。除了允许您将高分设置为负数,这还是一种有效的方法,可以允许您从后端影响应用程序行为。它还让您了解应用程序如何保护用户数据,例如,数据是否加密?如何加密?应用程序是否保护用户数据的完整性?在进行逆向工程和评估应用程序安全性时,这也是一项非常有用的技能。

准备工作

不幸的是,对于这一点,您需要一个“已 root”的手机或模拟器,因为您已经拥有模拟设备上的 root 权限。

如果你想要访问其他应用程序的资源,你需要获得 root 权限。如果你想研究市场上应用程序的行为,使用 ADB 将它们从你的设备上拉下来并在虚拟设备上安装没有任何阻碍。

你还需要安装 Android SDK。

如何操作…

在 Android 设备上列出文件可以通过以下方式完成:

  1. 使用以下命令在你的 Android 设备上启动 shell:

    adb shell [options]
    
    
  2. 导航到/data/data/目录:

    cd /data/data/
    
    

    该目录应类似于以下截图:

    如何操作…

    如果你列出文件权限、创建、修改和其他元数据,它应该看起来像以下截图:

    如何操作…

    注意数据目录的所有者和组,这是列表从左数第一和第二个列。这里所有者是实际的应用程序。默认情况下,Linux 将每个应用程序作为其自己的 Linux 用户运行,这本质上是应用程序沙箱操作的方式。当一个应用程序被赋予对它本质上无法访问的资源的权限时,Linux 会将它放入相关的用户组。

  3. 如果你希望一次性查看所有应用程序资源和元数据,请执行以下命令:

    ls –alR */
    
    

    如何操作…

    但是,通常情况下,除非你将输出重定向到文件,否则你不会希望屏幕被大量的目录列表淹没。你可能只想显示数据库:

    ls –alR */databases/
    
    

    如何操作…

    或者,也许只显示每个应用程序在/files/目录中保存的文件或其他内容:

    ls –alR */files/
    
    

    如何操作…

    或者,你可以通过指定扩展名来搜索特定类型的文件,以下是一些例子:

    ls –al */*/*.xml
    ls –al */*/*.png
    ls –al */*/*.mp3
    
    
  4. 找到你要找的文件后,你需要使用古老的adb pull命令将它们复制到你的机器上:

    adb pull /data/data/[package-name]/[filepath] 
    
    

还有更多…

我们在这里真正做的只是列出不同的文件类型。其中一种是 sqlite3 数据库,你可能在一些目录中看到的 DB 文件。我敢肯定你很想知道如何打开它们并看看里面有什么。以下是操作方法。

在开始之前,你需要确保已安装 sqlite3,它随 Android SDK 一起提供。

  1. 使用以下命令将 DB 文件提取到你的机器上的位置:

    adb pull /data/data/[package-name]/databases/[database-filename] 
    
    
  2. 使用 sqlite3 加载.db文件:

    sqlite3 [database-filename]
    
    

    如果你需要一个示例,请查看以下截图:

    还有更多…

在本章中,我们介绍了一些保护应用程序的机制,一些涉及应用程序间通信、应用程序权限以及加密签名和文件系统相关保护的基本保护措施。

你应该从这儿带走的是执行安全机制所需的一些技巧和窍门。这使你能够独立于执行这些机制的 Android 设备来评估这些机制的有效性,并且还能让你直接与它们交互,希望这能帮助你更好地理解它们。

第三章:Android 安全评估工具

在本章中,我们将介绍以下内容:

  • 安装和设置 Santoku

  • 设置 drozer

  • 运行 drozer 会话

  • 枚举已安装的软件包

  • 枚举活动

  • 枚举内容提供者

  • 枚举服务

  • 枚举广播接收器

  • 确定应用程序攻击面

  • 启动活动

  • 编写一个 drozer 模块——一个设备枚举模块

  • 编写一个应用程序证书枚举器

引言

我们已经介绍了所有 Android 开发基础,并介绍了所有 Android 开发工具。现在是我们开始深入了解 Android 黑客和安全评估工具的时候了。

本章介绍了一个名为drozer的利用和 Android 安全评估框架——正式名称为Mercury——由 MWR Labs 的一些人开发。同时本章还介绍了一个基于 Debian 的 Linux 发行版,名为Santoku,它基本上就像是移动安全评估的 BackTrack 或 Kali Linux。这里我们将介绍如何设置它并使其运行。

在我们开始设置 drozer 并编写一些示例脚本之前,对于你来说非常重要的一件事是要了解一些关于 drozer 的运作方式以及它是如何在 Android 安全评估游戏中解决一些问题的。

drozer 分为两部分:一部分是在你的本地机器上运行的“控制台”,另一部分是安装在目标 Android 设备上的“服务器”。当使用控制台与 Android 设备交互时,你实际上是在将 Java 代码注入到 drozer 代理中,该代理在实际设备上执行。

为什么要这样设计呢?在 drozer 出现之前,编写以应用程序漏洞为目标的利用意味着为了利用特定的漏洞,你需要编译一个 Android 应用,将其部署到目标手机上,并检查是否有效。如果无效,你需要重新进行整个流程!这种做法非常繁琐,会使得 Android 安全评估变得像一件苦差事。drozer 通过代理 drozer 代理传递命令到设备,使得部署和测试利用变得简单,这意味着你永远不需要接触 Android 开发环境或多次重新编译利用应用。

drozer 被称为框架,因为它允许你通过编写自己的模块或插件来扩展其功能,并适应你的需求。它本质上是最接近移动安全评估的 Metasploit。

标准 drozer 框架的另一个效果是,它本质上是一个 Android 应用程序——其中的一部分——没有权限,这意味着你在 Android 设备上成功执行的任何利用将自动变得非常可移植,并且成功所需的权限级别非常低。目的是展示一个“无权限”应用程序在利用 Android 设备及其上托管的应用程序时可以多么有效。

关于 drozer 的基本背景知识就介绍到这里。至于本章的其余部分,你可能需要一些 Python 编程语言的基础知识,因为 drozer 的模块是用 Python 开发的。如果你了解 Java 反射,或者知道如何开发一些 Android 应用,也会有所帮助。如果你从未认真开发过东西,或者通常不使用 Python 编程,不要担心——我会确保详细解释所有的 Python 代码。

那么不再犹豫,让我们开始吧!

安装和设置 Santoku

viaForensics 的开发者们开发了一个基于 Ubuntu 的非常酷的发行版,其中包含移动安全评估工具,名为 Santoku。以下指南将展示如何设置你自己的安装。我首先做这个是因为你可能想要在你的 Santoku 操作系统安装中安装并运行 drozer。

准备就绪

首先,我们将进行一些下载。从santoku-linux.com/download获取最新 Santoku 映像的副本。

如何操作...

下载最新版本的 Santoku 后,你可以按照以下步骤开始设置:

  1. 首先,你可以使用 Ubuntu 启动盘创建器或 Windows 的通用 USB 安装程序将 Santoku 映像写入 USB 存储棒,可从www.pendrivelinux.com/downloads/Universal-USB-Installer/Universal-USB-Installer-1.9.4.7.exe获取。

  2. 将你下载的 Santoku 映像写入 USB 磁盘。

  3. 使用通用 USB 安装程序,执行以下步骤:

    1. 启动通用 USB 安装程序,在步骤 1中选择尝试未列出的 Linux ISO如何操作...

    2. 点击浏览,选择你 Santoku ISO 的路径,如前一个截图所示。

    3. 步骤 3中,选择你希望写入映像的 USB 闪存盘。

    4. 点击创建,然后放松休息,同时你的安装磁盘映像正在准备中。

  4. 在插入 USB 设备的情况下重启你的主机;打开启动菜单并选择从 USB 磁盘启动。

  5. 一旦从 USB 启动盘启动,你应该会看到以下屏幕:如何操作...

  6. 在启动屏幕上,选择install – 直接启动安装程序

  7. 安装应该从以下截图所示的屏幕开始:如何操作...

  8. 按照安装向导的提示操作,直到开始安装。这个过程非常容易理解,对于之前安装过 Ubuntu 的人来说应该很熟悉。

    安装完成后,你应该会看到一个全新的 Santoku 桌面,如下截图所示:

    如何操作...

还有更多...

如果你打算在虚拟机上安装,你需要获取 VirtualBox 的副本。对于 Windows 和 Unix/Linux 用户,可以在www.virtualbox.org/wiki/Downloads找到。

下载并安装 VirtualBox 后,你需要通过以下步骤创建一个新的虚拟机:

  1. 点击 VirtualBox 窗口左上角的新建按钮。

  2. 创建虚拟机对话框应该会弹出。在名称字段中输入Santuko,或者你也可以根据喜好为新的虚拟机命名。

  3. 类型下拉菜单中选择Linux

  4. 版本下拉菜单中选择Ubuntu并点击下一步还有更多...

  5. 此时会出现内存大小对话框;默认设置为512。这个设置是足够的;但是,如果你的宿主机有足够的内存,你可以更慷慨一点。确定内存大小后,点击下一步

  6. 硬盘设置对话框会出现;选择现在创建虚拟硬盘选项并点击下一步

  7. 你将看到硬盘文件类型对话框;选择VDI(VirtualBox 磁盘映像)选项并点击下一步

  8. 物理硬盘上的存储对话框应该会出现;选择动态分配选项;因为你会很可能在这个虚拟机的硬盘上安装和下载许多应用程序和工具。点击下一步

  9. 文件位置和大小对话框应该会出现。你可以接受这里的默认设置;8 吉字节足以存储所有初始操作系统数据及工具。如果你需要更多空间,可以配置虚拟机占用更多的存储空间;一切由你决定。选择合适的大小后,点击下一步

  10. 你的虚拟机现在应该设置好了;你需要为其配置一个可启动的实时 CD。为此,点击设置

  11. 设置对话框出现后,点击设置对话框左侧面板中的存储

  12. 控制器: IDE部分下,点击控制器: IDE部分标签旁边的第一个按钮添加 CD/DVD 设备还有更多...

  13. 会弹出一个VirtualBox 问题对话框;在这个对话框上,点击选择磁盘。你应该会看到一个文件对话框。

  14. 导航到并选择你已下载的 Santoku 映像。

  15. 你现在可以启动新的 Santoku 虚拟机并开始安装。还有更多...

设置 drozer

安装并设置 drozer 是相当简单的;设置过程适用于 Windows 7 和 Unix/Linux 类型的系统。

如何操作...

在开始编写一些 drozer 脚本并了解利用和枚举模块之前,你需要获取适合你系统的 drozer 安装程序。以下是操作方法:

  1. 访问 www.mwrinfosecurity.com/products/drozer/community-edition/ 获取 drozer 框架的副本;当然,这里我将讨论社区版。如果你愿意花钱购买非免费版本,请访问 products.mwrinfosecurity.com/drozer/buy如何操作...

    Windows 用户应点击 drozer (Windows 安装程序) 选项;它应该会立即开始下载 drozer-installer-[version].zip 文件。

    如何操作...

    Unix/Linux 用户根据你的发行版或操作系统类型,选择 drozer (Debian/Ubuntu Archive) 文件或 drozer (RPM) 包 文件。

  2. 下载与你的系统兼容的 drozer 版本后,根据你的系统,你需要执行以下操作:

    对于 Windows 用户:

    1. 你需要将 drozer-installer-[version].zip 文件解压到你能轻松记住的位置/路径中。![如何操作... 1. 解压后,运行 ZIP 归档中包含的名为 setup.exe 的文件。应该会如以下截图所示启动安装向导:如何操作...

    2. 安装向导设置好后,你需要做的就是按照提示操作,注意配置对话框,并确保记下 drozer 将安装在系统上的位置;你需要经常访问此路径以使用 drozer。安装开始后,你应该会看到以下对话框出现:如何操作...

    3. 安装完成后,你应该已经将 drozer 安装到你指定的路径中。默认情况下,这被配置在 C 驱动的根目录下,如下截图所示:如何操作...

      对于 Unix/Linux 用户:

      drozer 框架以与你的系统相关的包文件格式提供,因此对于 Debian 用户是 DEB 文件,对于 Red Hat 用户是 RPM 文件。你只需使用包管理器打开此文件,其余的它会处理。Debian 用户可以使用以下命令来安装 drozer:

      dpkg –I drozer-[version].deb
      
      
  3. 安装完 drozer 后,你应该尝试运行它。如何运行部分取决于你的操作系统。

    对于 Windows 用户:

    1. 打开命令提示符并转到你安装 drozer 的路径。如前所述,默认情况下这是 C:\drozer 路径。

    2. 通过执行以下命令调用 drozer:

       C:\drozer\drozer
      
      

      你应该看到类似于以下截图的输出:

      如何操作...

    3. 作为诊断测试,尝试调用 drozer 控制台。如果出现问题,它会在告诉你设备(这里未连接)不可用或拒绝连接之前,通知你错误。执行以下命令:

       C:\drozer\drozer console
      
      

      除非你已经足够聪明地解决了错误,否则你应该看到与以下屏幕截图相似的输出:

      如何操作...

      这个错误意味着 drozer 无法找到你的 Java 安装。

  4. 假设你已经安装了 Java,你可以将 drozer 添加到你的系统 PATH 变量中。

    在 Windows 上增强你的 PATH 变量非常直接;你可以通过执行以下步骤开始:

    1. 打开 我的电脑

    2. 点击 系统属性

    3. 在屏幕的 控制面板 部分下,点击 高级系统设置如何操作...

    4. 应该会弹出 用户访问控制 提示。如果你有管理员权限,只需点击 确定 或输入管理员密码。

    5. 系统属性 对话框中,点击标记为 环境变量... 的按钮。如何操作...

    6. 一旦弹出 环境变量 对话框,在标记为 系统变量 的部分,向下滚动到名为 Path 的变量并点击 编辑…如何操作...

    7. 应该会弹出一个对话框,允许你编辑 PATH 变量。在变量值的末尾添加以下字符串:

             ;C:\Program Files\Java\jre7
      

对于 Unix/Linux 用户:

  1. 你可以通过从你的终端窗口执行以下命令来调用 drozer:

    drozer
    
    
  2. 如果一切正常,你应该看到与 Windows 版 drozer 完全相同的输出。

  3. 如果 Java 尚未添加到你的 PATH 变量中,执行以下命令以添加它:

    PATH=$PATH:`which java
    
    

    为了使这个持久化,将之前的命令行添加到你的 /home/[user]/.bashrc 文件的末尾。

还有更多…

在 drozer 能够启动并运行之前,你需要在 Android 设备上安装 drozer 代理。这很简单;以下是操作方法:

  1. 假设设备通过 USB 连接到你的主机,你可以按照以下方式安装 drozer.apk 文件:

    adb install drozer.apk
    
    
  2. 为了使这个工作,你需要确保目标 Android 设备已启用 未知来源USB 调试

    启动 drozer 后,你应该看到以下内容:

    还有更多…

  3. 为了使从命令行界面使用 drozer 控制台更加方便,你也可以将 drozer 本身添加到你的系统 PATH 变量中。

    对于 Windows 用户:

    1. 按照前一个菜谱描述的方式访问 环境变量 对话框。

    2. 将以下字符串添加到你的 PATH 变量中:

             ;C:\drozer\drozer
      
      

    如果你是一个 Unix/Linux 用户,从你的终端执行以下命令:

    PATH=$PATH:`which drozer`
    
    

    为了使这个持久化,将之前的命令行添加到你的 /home/[user]/.bashrc 文件的末尾。

如果 DEB 文件安装失败,还有另一种相对无痛的方法来安装 drozer。要在不使用 DEB 包的情况下安装 drozer,请执行以下步骤:

  1. 首先,通过执行以下命令获取 Python 开发头文件和软件包的副本:

    apt-get install python-dev
    
    
  2. 通过执行以下命令获取 Python 安装工具的副本:

    apt-get install python-setuptools
    
    
  3. 为你的 Debian 系统安装 32 位支持库:

    apt-get install ia32-libs-i386
    
    
  4. 安装 Python 依赖项;第一个是 protobuf,你可以通过执行以下命令来安装:

    easy_install –allow-hosts pypi.python.org protobuf==2.4.1
    
    
  5. 安装 protobuf 之后,你需要为 Python 安装 twisted,可以通过执行以下命令来完成:

    easy_install twisted==10.2.0
    
    
  6. 接下来你需要做的是获取一份可在 www.mwrinfosecurity.com/system/assets/571/original/drozer-2.3.2.tar.gz 下载的 drozer 独立架构包。

  7. 下载后,将其解压到你选择的某个目录中。解压后,它应该包含一个名为 drozer-[version]-py2.7.egg 的文件。然后你可以通过执行以下命令来安装这个 EGG:

    easy_install drozer-[version]-py2.7.egg
    
    

    就这样——drozer 应该已经准备好大显身手了!

运行 drozer 会话

既然已经设置好 drozer 并准备就绪,你就可以在示例 Android 设备上运行一些 drozer 会话了——最好是已经安装了 drozer 代理的设备。

以下教程将带你了解设置 drozer 会话的基础知识以及如何通过 drozer 控制台快速运行一些简单的模块。

如何操作...

在继续本教程之前,你需要在你的机器上安装 drozer 控制台,在目标设备上安装 drozer 代理。如果这些都准备好了,你可以通过执行以下步骤来开始你的 drozer 控制台会话:

  1. 使用 ADB,设置一些端口转发,前提是你连接了某种设备:

    adb forward tcp:31415 tcp:31415
    
    
  2. 您需要确保已启动 drozer 嵌入式服务器。你需要通过设备上的应用程序界面来启动它。只需在设备上找到 drozer 代理;它应该会出现在其他应用程序中,但由于你可能刚刚安装了它,你可能会看到一条关于它的通知,并且可以从通知菜单中启动它。

  3. 点击通过 drozer 代理用户界面标记为 嵌入式服务器 的按钮。你应该会看到如下截图所示的屏幕:如何操作...

  4. 将标记为 禁用 的按钮向右拖动。现在它应该显示 启用,并且用户界面中 服务器详情 部分下的 启用 标签应该已激活,如下截图所示:如何操作...

  5. 然后,你可以通过执行以下命令来连接 drozer 控制台:

    drozer console connect
    
    

    然后 drozer 应该进入控制台模式,允许你开始执行命令和模块。

    如何操作...

枚举已安装的包

drozer 代理已经设置好了,你也成功启动了 drozer 控制台;你可以开始运行一些 drozer 模块,真正与设备的安全性进行交互。

以下教程详细介绍了如何使用 drozer 框架执行诸如枚举已安装的包并根据包名进行过滤等新任务。

如何操作...

当你搭建并运行了你的 drozer 框架后,你可能想要开始在 Android 设备上摸索和尝试。你可能想要做的一件有用的事情是列出设备上安装的所有包。你可以通过从你的 drozer 控制台触发以下命令来实现这一点:

dz> run app.package.list

你应该会在屏幕上看到类似以下内容开始出现:

如何操作...

它是如何工作的...

让我们来看看 drozer 的源代码,确切地了解它是如何与包管理器 API 接口来获取所有这些有用信息的。我将解释大部分模块背后的代码,以便你了解 drozer 是如何工作的,并在本章的后面指导你自己编写一个 drozer 模块!毕竟,框架的意义就在于——构建你自己的模块和插件。

非 Python 用户/开发者请注意!你可能需要一些 Python 背景知识才能阅读这段源代码;尽管如此,考虑到 Python 的语义性,即使你从未编写过 Python 代码,你也应该能很容易地跟上。drozer 设计的一个额外好处是,它们基本上镜像了 Android Java API,使得模块开发对 Android 开发者来说很容易上手。所以,总之,你暂时还不需要去拿本 Python 的书来读。如果你之前写过 Android 应用,这将非常容易理解。无论如何,废话少说——让我们来看一些代码!

注意

以下代码可以在github.com/mwrlabs/drozer/blob/master/src/drozer/modules/app/package.py(第 99-121 行)找到。

def add_arguments(self, parser):
  parser.add_argument("-a", "--package", default=None, help="the identifier of the package to inspect")
  parser.add_argument("-d", "--defines-permission", default=None, help="filter by the permissions a package defines")
  parser.add_argument("-f", "--filter", default=None, help="keyword filter conditions")
  parser.add_argument("-g", "--gid", default=None, help="filter packages by GID")
  parser.add_argument("-p", "--permission", default=None, help="permission filter conditions")
  parser.add_argument("-u", "--uid", default=None, help="filter packages by UID")

def execute(self, arguments):
  if arguments.package == None:
    for package in self.packageManager().getPackages(common.PackageManager.GET_PERMISSIONS | common.PackageManager.GET_CONFIGURATIONS | common.PackageManager.GET_GIDS | common.PackageManager.GET_SHARED_LIBRARY_FILES):
      self.__get_package(arguments, package)
  else:
    package = self.packageManager().getPackageInfo(arguments.package, common.PackageManager.GET_PERMISSIONS | common.PackageManager.GET_CONFIGURATIONS | common.PackageManager.GET_GIDS | 
            common.PackageManager.GET_SHARED_LIBRARY_FILES)

      self.__get_package(arguments, package)

def get_completion_suggestions(self, action, text, **kwargs):
  if action.dest == "permission":
    return android.permissions

def __get_package(self, arguments, package):
  application = package.applicationInfo

execute()方法在你从控制台触发app.activity.info模块时会被调用。它实际上是模块执行实际工作的入口点。

我们看到对包管理器的调用,self.packageManager().getPackages(…);这返回一个包含每个包对象及其权限、配置、GID 和共享库的列表。脚本在每个包对象上调用self.__get_package()来将其输出到 drozer 控制台。当通过命令行参数提供特定包时,也会执行相同的操作。

如果你想要获取这段代码的副本,可以从 drozer 的官方 GitHub 仓库获取,如果你足够用心去 Google,找到它是非常容易的。为了让大家更方便,我在本食谱的另请参阅部分提供了一个代码仓库的 URL。

还有更多...

dz> run app.package.list命令是对 Android 包管理器的一个封装;因此,你能做的很酷的一件事就是基于应用名称进行筛选,如下所示:

dz> run app.package.list –f [application name]

在这里,[application name]是你想要检查的应用程序或包的名称。以下是一个例子:

dz> run app.package.list –f facebook

drozer 中的另一个用于提取信息的枚举类型模块是app.package.info,它会获取关于一个包的以下信息:

  • 权限

  • 配置

  • 群组 ID

  • 共享库

你可以通过从你的 drozer 控制台发出以下命令来使用这个模块:

dz> run app.package.info --help

以这种方式使用时,它将提取关于你 Android 设备上所有软件包的相关信息。

自然地,你可能希望将此信息缩小到特定的软件包:

dz> run app.package.info –-package [package name]

你也可以使用以下开关的简写方式:

dz> run app.package.info –a [package name]

这是一个示例:

dz> run app.package.info –a com.android.browser

更多内容...

之前截图中显示输出的快速解释如下:

  • 应用程序标签:应用程序显示的名称

  • 进程名称:此应用程序运行的进程名称

  • 版本:安装的应用程序版本

  • 数据目录:将用于存储与该应用程序特别关联的用户数据和应用程序的目录的完整路径

  • APK 路径:设备上实际 Android 应用程序包文件的路径

  • UID:与应用程序关联的用户 ID;它在 Android 系统上执行的所有操作都将使用与此用户 ID 关联的访问权限,除非它让其他应用程序和进程代表它执行操作

  • GID:与该应用程序用户 ID 关联的系统群组 ID;通常,这些 ID 基于授予应用程序的特殊权限数量与之关联

  • 共享库:此应用程序使用的共享库的完整路径

  • 共享用户 ID:此应用程序允许使用的共享用户 ID

  • 使用权限:授予此应用程序的权限列表

另一个例子,如果你有一个 Nexus 设备,可以按照以下方式针对 Google 服务框架运行:

dz> run app.package.info –a com.google.android.gsf

之前的命令应该会产生如下截图所示的输出:

更多内容...

你还可以使用app.package.info模块做另一件很酷的事情,那就是基于权限查找软件包。你可以通过执行以下命令来完成:

dz> run app.package.info –p [permission label]

一个例子如下:

dz> run app.package.info –p android.permission.INTERNET

这为什么这么酷?好吧,你可能想知道所有具有一组危险权限的应用程序。我的意思是,你知道你的应用程序中有多少个具有INTERNET权限或其他危险权限的吗?不知道吧?正是如此!

另请参阅

枚举活动

drozer 还提供了一个有用的模块,用于枚举目标 Android 设备上可用的活动组件信息。以下食谱演示了此模块的使用。

如何操作...

你可能有时候想要找出设备上安装并导出的活动。drozer 框架使这变得相当简单,以下是操作方法:

从你的 drozer 控制台执行以下命令:

dz> run app.activity.info

此命令将列出设备上导出的所有活动。

还有更多...

你可能想要获取有关设备上活动的更多信息;例如,列出所有具有特定名称或名称中包含特定字符串的应用程序,如"browser"或"facebook",这些应用程序具有什么权限,甚至搜索未导出的活动。以下是操作方法:

通过执行以下命令,根据名称搜索活动:

dz> run app.activity.info –-filter [activity name]

这将列出名称中带有[activity name]的所有活动。例如:

dz> run app.activity.info –-filter facebook

与所有 Unix 风格或 Linux 风格的命令一样,这个命令也有一个快捷方式:

dz> run app.activity.info –f facebook

上一个命令应该会产生如下截图所示的输出:

还有更多...

你还可以指定要检查活动的包。

按照以下方式在给定包中搜索活动:

dz> run app.activity.info –-package [package name]

你也可以使用此命令的快捷方式:

dz> run app.activity.info –a [package name]

这是一个例子:

dz> run app.activity.info –a com.android.phone

上一个命令应该会产生如下截图所示的输出:

还有更多...

另请参阅

枚举内容提供者

与枚举活动和包类似,drozer 也提供了一些模块来列出所有内容提供者及其一些信息。以下食谱将介绍如何使用app.provider.info模块进行操作。

如何操作...

让我们开始枚举内容提供者。

  1. 从你的 drozer 终端执行以下命令:

    dz> run app.provider.info
    
    
  2. 这将返回有关内容提供者的以下信息:

    • 权限 - 实现其 SQLite 前端的类名称

    • 读取权限

    • 写入权限

    • 授予 URI 权限

    • 路径

它是如何工作的...

让我们看看app.provider.info模块的代码。

注意

以下代码在github.com/mwrlabs/drozer/blob/766329cacde6dbf1ba05ca5dee36b882041f1b01/src/drozer/modules/app/provider.py

def execute(self, arguments):
  if arguments.package == None:
    for package in self.packageManager().getPackages      (common.PackageManager.GET_PROVIDERS |         common.PackageManager.GET_URI_PERMISSION_PATTERNS):
      self.__get_providers(arguments, package)
  else:
    package = self.packageManager().getPackageInfo(arguments.package, common.PackageManager.GET_PROVIDERS | common.PackageManager.GET_URI_PERMISSION_PATTERNS)

    self.__get_providers(arguments, package)

def get_completion_suggestions(self, action, text, **kwargs):
  if action.dest == "permission":
    return ["null"] + android.permissions

def __get_providers(self, arguments, package):
  providers = self.match_filter(package.providers, 'authority', arguments.filter)

  if arguments.permission != None:
    r_providers = self.match_filter(providers, 'readPermission',       arguments.permission)
    w_providers = self.match_filter(providers, 'writePermission',       arguments.permission)

代码中第一个值得注意的部分是脚本调用包管理器的地方。它看起来像这样:

self.packageManager().getPackages(common.PackageManager.GET_PROVIDERS | common.PackageManager.GET_URI_PERMISSION_PATTERNS)

脚本通过调用 Android 包管理器获取一个包列表,并抛出一些标志以确保获取到带有授权 URI 权限模式的提供者。接下来我们看到,一旦包管理器收集了关于内容提供者的详细信息,脚本就会调用一个名为 __get_provider() 的函数,该函数提取有关提供者读取和写入权限的信息(如果有的话)。通过 match_filters() 调用进行一些简单的字符串匹配,__get_provider() 函数基本上在定义内容提供者权限的部分查找某个字符串值。这个字符串值由 readPermission 标记,表示从内容提供者读取所需的权限,或者由 writePermission 标记,出人意料的是,它表示写入内容提供者所需的权限。在此之后,它会在将提供者对象打印到控制台之前重置该对象。

还有更多...

与 drozer 中的其他 .info 模块类似,你可以通过以下方式添加过滤器信息:

  • 基于包名称搜索:

    dz> run app.provider.info –a [package name]
    
    

    或者:

    dz> run app.provider.info –-package [package name]
    
    
  • 基于权限搜索:

    dz> run app.provider.info –p [Permission label]
    
    

    或者:

    dz> run app.provider.info –-permission [permission label]
    
    

另请参阅

枚举服务

你可能还想了解设备上安装了哪些服务。drozer 有一个名为 app.service.info 的模块,可以提取有关服务的有用信息。

如何操作...

从你的 drozer 控制台执行以下命令:

dz> run app.service.info –-package [package name]

不带参数运行此命令将列出目标设备上安装的所有服务。运行时它看起来会像以下截图:

如何操作...

你还可以使用以下过滤器来缩小搜索范围:

  • 基于权限搜索:

    dz> run app.service.info –p [permission label]
    dz> run app.service.info –-permission [permission label]
    
    
  • 基于服务名称搜索:

    dz> run app.service.info –f [Filter string]
    dz> run app.service.info. –filter [filter string]
    
    
  • 你还可以选择列出未导出的服务,例如以下:

    dz> run app.service.info –u
    dz> run app.service.info –-unexported
    
    
  • 最后,如果你想了解其他开关和选项的信息,可以随时运行 –help 选项,如下所示:

    dz> run app.service.info –-help
    
    

    之前的命令应该会产生如下截图所示的输出:

    如何操作...

它是如何工作的…

app.service.info 模块通过 API 调用包管理器,其工作方式与其他 .info.list 类型的 drozer 模块类似。以下是来自 drozer/master/src/drozer/modules/service.py 的包管理器调用:

def execute(self,arguments):
  if arguments.package == None:
    for package in self.packageManager().getPackageInfo      (common.PackageManager.GET_SERVICES |         common.PackageManager.GET_PERMISSIONS):
      self.__get_servcies(arguments, package)
  else:
    package = self.packageManager().getPackageInfo(arguments.package, common.PackageManager.GET_SERVICES | common.PackageManager.GET_PERMISSIONS)
    self.__get_services(arguments,package)

脚本会检查是否将特定包作为参数传递,这是 execute 方法中的第一段代码:

if arguments.package == None:

如果没有定义参数或包名,脚本会获取一个包列表,并通过调用self.__get_services()方法来遍历它们,这个方法通过字符串匹配从self.packageManager().getPackageInfo(arguments.package,common.PackageManager.GET_SERVICES | common.PackageManager.GET_PERMISSIONS)调用返回的数据来确定一些包属性;例如,在寻找具有特定权限的服务时,它会执行以下操作:

services = self.match_filter(services, "permission", arguments.permission)

这是为了提取具有所需权限的服务列表。

参见

枚举广播接收器

广播接收器通常包含有关应用程序攻击面的有用信息,并可能为攻击者提供执行任意代码到传播信息等多种机会;因此,在针对应用程序的安全评估中不能忽略它们。drozer 的开发者深知这一点,并提供了模块来帮助获取有关广播接收器的信息。

下面的指南详细介绍了app.broadcast.info模块的不同调用选项,以此演示其功能。

如何操作...

广播接收器的枚举是通过以下命令执行的:

dz> run app.broadcast.info

前一个命令的输出应类似于以下截图:

如何操作...

这个app.broadcast.info模块具有其他.info模块所有的酷功能,以及一些特定的广播接收器选项。

你可以指定一个特定的包,从中提取接收器的信息;以下命令是一个示例:

dz> run app.broadcast.info –a [package]

下面的命令是另一个示例:

dz> run app.broadcast.info –-package [package]

你也可以基于它们的名称搜索和列出广播接收器;例如:

dz> run app.broadcast.info –f [filter]

或者使用更长的形式:

dz> run app.broadcast.info –-filter [filter]

另一个选项是选择包含未导出的接收器:

dz> run app.broadcast.info –u
dz> run app.broadcast.info –-unexported

最后,你可以选择是否在请求的信息中包含意图过滤器;例如:

dz> run app.broadcast.info –i

或者:

dz> run app.broadcast.info –-show-intent-filters

参见

确定应用程序的攻击面

在您的应用程序安全评估过程中,您可能想要了解给定应用程序的攻击面是什么。drozer 有一个非常整洁的模块可以帮助您确定这一点。就这个模块而言,应用程序的攻击面仅是指导出组件的数量。

如何操作...

从您的 drozer 控制台执行以下命令:

dz> app.package.attacksurface [package name]

此命令将列出由包管理器 API 确定的给定包的所有导出活动。

例如,您可以尝试针对一个示例包按以下方式运行它:

如何操作...

它的工作原理...

让我们看看 app.package.attacksurface 模块的代码。我认为这可能是最有趣的模块之一,通过阅读其代码应该能激发您如何以应用程序的形式编写自动化测试工具的想法。当您想要进行大规模的自动化应用程序扫描时,它绝对会派上用场!

来自 drozer-master/src/mrw/droidhg/modules/package.py 的代码如下:

from drozer import android
from drozer.modules import common, Module
class AttackSurface(Module,common.Filters, common.PackageManager):

def execute(self,arguments):
  If arguments.package != None:
    Package = self.packageManger().getPackageInfo(arguments.package, common.PackageManager.GET_ACTIVITIES | common.PackageManager.GET_RECEIVERS | common.PackageManager.GET_PROVIDERS | common.PackageManager.GET_SERVICES)
    application = package.applicationInfo
    activities = self.match_filter(package.activities, 'exported',True)
    receivers = self.match_filter(package.receivers, 'exported', True)
    providers = self.match_filter(package.proviers, 'exported', True)
    services = self.match_filter(package.services, 'exported', True)
    self.stdout.write("Attack Surface:\n")
    self.stdout.write(" %d activities exported\n" % len(activities))
    self.stdout.write(" %d broadcast receivers exported\n" % len(receivers))
    self.stdout.write(" %d content providers exported\n" % len(providers))
    self.stdout.write(" %d services exported\n" % len(services))
    if (application.flags & application.FLAG_DEBUGGABLE) != 0:
      self.stdout.write("is debuggable\n")
    if package.sharedUserId != None:
      self.stdout.write("Shared UID (%s)\n" % package.sharedUserId)
  else:
  self.stdout.write("Package Not Found\n")

这里的代码很多,但这个模块的好处在于它遵循与包管理器接口的相同风格。该模块从包管理器获取关于服务、活动、广播接收器和内容提供者的信息,并尝试确定它们是否根据包管理器导出。确定了哪些组件是导出的,它只是枚举它们,并在屏幕上打印导出组件的数量。该模块所做的就是尝试确定应用程序是否可调试以及是否使用共享用户 ID,这对于攻击面来说是非常有价值的信息。我将在下一章解释为什么。

另请参阅

启动活动

活动是促进用户交互的应用程序组件。在应用程序安全评估期间,了解哪些应用程序可以在没有权限的情况下启动可能很有用,以防它们中的任何一个提供了对敏感数据的访问或如果在不正确的上下文中启动导致应用程序崩溃。除了通过 drozer 控制台与活动互动的明显好处之外,它还是一个很好的响应式介绍,可以与应用程序组件互动,因为您实际上可以从终端看到您的 Android 设备对您的命令做出响应。所以,不再赘述,让我们开始一些活动吧!

如何操作...

你需要选择一个要启动的活动,但由于你无法本质上知道可启动活动在哪里或者它们叫什么,我认为我应该在教程中包含查找可启动活动的过程。

  1. 使用 app.activity.info 模块查找一些活动:

    dz> run app.activity.info –-package [package name]
    
    

    在下一步中,你需要选择一个包和一个活动。习惯于多次运行这个命令;如果你打算进行 Android 渗透测试,你会经常使用它。

  2. 找到你要找的活动后,你可以向它发送一些启动意图,并在你的 Android 设备屏幕上看到它弹出。以下是操作方法:

    dz> run app.activity.start –-action [intent action] –-category [intent category] –-component [package name] [component name]
    
    

    在这里,[intent action] 是目标活动设置的意图过滤器的动作属性,[intent category] 是目标活动设置的意图过滤器的类别属性,你可以从第一步的命令中获得。

这是一个你可以尝试的例子:

dz> run app.activity.start –-action android.intent.action.MAIN –-category android.intent.category.LAUNCHER –-component com.android.browser com.android.browser.BrowserActivity

它的工作原理...

让我们查看 drozer 的源代码,确切地了解它是如何启动某些活动的。

注意

以下代码可以在github.com/mwrlabs/drozer/blob/master/src/drozer/modules/app/activity.py(第 166-174 行)找到。

.... #some code has been omitted for brevity
def execute(self,arguments)
  intent = android.Intent.fromParser(arguments)

  if len(intent.flags) == 0:
    intent.flags.append('ACTIVITY_NEW_TASK')

  if intent.isValid():
    self.getContext().startActivity(intent.buildIn(self))
  else:
    self.stderr.write('invlaid intent: one of action or component must be set')
...#some code has been omitted for brevity

我们可以看到,drozer 通过参数解析器获取用户提供的参数后,将这些参数打包成一个意图;在检查意图是否有效后,它发送这个意图。这与来自 Android 应用的意图工作方式相同。

还有更多...

你可以使用 app.activity.forintent 模块查找要启动的活动。

这个便捷的模块允许你根据给定的意图动作和类别搜索活动;以下是操作方法:

dz> run app.activity.forintent –-action [intent action] –category [intent category]

这是一个例子:

dz> run app.activity.forintent –-action android.intent.action.VIEW –-category android.intent.category.DEFAULT

另请参阅

编写一个 drozer 模块——设备枚举模块

本教程通过演示构成 drozer 模块开发的实际步骤,解释了如何实际开发 drozer 模块。以下设备信息枚举器获取有关部分硬件和操作系统版本的信息。

如何操作...

让我们开始编写一个 drozer 设备枚举模块:

  1. 打开文本编辑器并输入以下代码:

    from drozer.modules import Module
    class Info(Module):
      name = "Get Device info"
      description = "A module that returns information about the device and hardware features"
      examples = "run ex.device.info"
      date = "10-11-13"
      author = "Keith Makan"
      license = "GNU GPL"
      path = ["ex","device"]
      def execute(self,arguments):
        build = self.new("android.os.Build")
        self.stdout.write("Getting device info...\n")
        self.stdout.write("[*] BOARD : %s\n" % (build.BOARD))
        self.stdout.write("[*] BOOTLOADER : %s\n" % (build.BOOTLOADER))
        self.stdout.write("[*] BRAND : %s\n" % (build.BRAND))
        self.stdout.write("[*] CPU_ABI : %s\n" % (build.CPU_ABI))
        self.stdout.write("[*] CPU_ABI2 : %s\n" % (build.CPU_ABI2))
        self.stdout.write("[*] DEVICE : %s\n" % (build.DEVICE))
        self.stdout.write("[*] DISPLAY : %s\n" % (build.DISPLAY))
        self.stdout.write("[*] FINGERPRINT : %s\n" % (build.FINGERPRINT))
        self.stdout.write("[*] HARDWARE : %s\n" % (build.HARDWARE))
        self.stdout.write("[*] MANUFACTURER : %s\n" % (build.MANUFACTURER))
        self.stdout.write("[*] MODEL : %s\n" % (build.MODEL))
        self.stdout.write("[*] TAGS : %s\n" % (build.TAGS))
    
  2. 将该文件保存为 ex.device.info

  3. 创建一个目录来保存你未来所有的 drozer 模块,并将 ex.device.info 文件保存在其中。

  4. 启动 drozer 控制台并执行以下命令:

    dz> module repository create [path-to-your-module-dir]/repo
    
    
  5. 然后执行以下命令:

    dz> module install [path-to-your-module-dir]/ex.device.info
    
    
  6. 如果没有语法错误或故障,drozer 应该已经安装了你的新模块。现在你可以使用以下命令执行它:

    dz> run ex.device.info
    
    

    之前命令的输出应该类似于以下截图中的输出:

    如何操作...

接下来的几个食谱都是关于编写一些有用的模块来扩展你的 drozer 框架;在每一个中,我将演示一些你会在本书后面发现有用的关键模块开发技能。

它是如何工作的...

为了开始这个解释,我想讨论一下你刚刚为你新的 drozer 模块编写的代码,以及它是如何设法提取关于你设备的信息的。

首先,我想谈谈 drozer 模块的结构。你编写的每个模块都将从以下这行开始:

import drozer.modules import Module
class Info(Module)

第一行本质上是包含了一些来自modules库的代码,并让 drozer 模块能够访问到它们操作所需的所有魔法方法和属性。第二行称为类声明的头部,并标记了 Python 中对象定义的开始。你可能注意到了头部中的(Module)部分;这就是Info类如何采用Module类的属性,从语义上讲,这类似于 Java 中的继承。

接下来的几行如下:

name = ""
description = ""

license = ""

这些只是 drozer 用来将一些元数据关联到模块,并使文档标准化、易于执行的一些变量——这里没有技术性的内容。继续:

def execute(self, arguments):

这段特定的代码称为函数头,并标记了 Python 函数定义的开始。这个函数的特殊之处在于,它是执行模块所有繁重工作的方法,类似于 Java 类中的Main方法。让我们谈谈execute方法期望传递的参数:

  • self:这是正在定义的类的实例。它被解析到类中的每个函数,以便它们可以访问类实例。

  • arguments:这是从控制台传给 drozer 模块的分析后的参数字典。

最后,我们有以下这段代码:

build = self.new("android.os.Build")

好吧,除了解引用self对象和使用一个名为new的魔法方法之外,我们看到一个字符串值android.os.Build作为参数传递。这个字符串是 Android Java API 中的一个 Java 类的名称,而new方法使用了一种称为 Java 反射的东西来实例化持有我们想要打印到屏幕上所有信息的Build类。

代码的其余部分看起来像下面这样:

self.stdout.write("[*] BOARD : %s\n" % (build.BOARD))

上面的代码只是简单地打印出设备信息。

另请参阅

编写一个应用程序证书枚举器

在这个教程中,我将向你展示如何编写一个证书枚举器,它所做的不过是提取应用程序证书作为十六进制摘要并在屏幕上输出。我之所以包含这部分内容,首先是因为它演示了如何与包管理器接口并获取本节其他模块无法获取的信息。其次,当你寻找使用相同公钥签名的所有应用程序时,它可能有助于你获取应用程序签名。这很有用,因为开发者和恶意软件作者通常会用同一把钥匙为他们的多数应用程序签名。它还将允许你识别可能共享资源并自主互相授权权限的应用程序;这一过程将在下一节详细讨论。

如何操作...

  1. 打开你喜欢的文本编辑器并输入以下代码:

    from drozer.modules import Module, common
    from drozer import android
    import M2Crypto
    import subprocess
    from OpenSSL import crypto
    class Info(Module,common.Filters,common.PackageManager):
      name = "Print the Signer certificate for an application"
      description = "this module allows you to print the signer x509 certificate for a given applicaiton"
      examples = "run ex.cert.info -p com.android.browser"
      author = "Keith Makan"
      date = "11-11-2013"
      license = "GNU GPL"
      path = ["ex","cert"]
      def add_arguments(self, parse):
        parse.add_argument("-p","--package",default=None,help="The Package Name")
      def execute(self,arguments):
        pm = self.packageManager()
        if arguments.package == None:
          for info in pm.getPackages(common.PackageManager.GET_SIGNATURES):
            self.stdout.write("[*] certificate info for {%s}\n" % (info.packageName))
            self.__print_certs(info)
        elif arguments.package != None:
          self.stdout.write("[*] certificate info for {%s}\n" % (arguments.package))
          info = pm.getPackageInfo(arguments.package,common.PackageManager.GET_SIGNATURES)
          self.__print_certs(info)
        else:
          self.stdout.write("[!] cannot process arguments : '%s'\n" % (repr(arguments)))
      def __print_certs(self,info):
        sigs = info.signatures[0].toCharsString()
        sigs = sigs + '\n'
        temp_cert = open("/tmp/cert.crt","w")
        end = 2
        #converting to DER file
        for start in range(0,len(sigs)-2,2):
          temp_cert.write(chr(int(sigs[start:end],16)))
          end +=2
        temp_cert.flush()
        temp_pem = open("/tmp/cert.pem","w")
        temp_pem.flush()
        temp_pem.close()
        certtext = subprocess.check_output(["openssl","x509","-inform","DER","-in","/tmp/cert.crt","-outform","PEM","-out","/tmp/cert.pem","-text"])
        temp_pem = open("/tmp/cert.pem","r")
        pem_cert_string = temp_pem.read()
        temp_pem.close()
        x509cert = crypto.load_certificate(crypto.FILETYPE_PEM,pem_cert_string)
        m2crypto_crt = M2Crypto.X509.load_cert_string(pem_cert_string,1)
        self.stdout.write("[*] Version : %s\n" % (x509cert.get_version()))
        self.stdout.write("[*] Issuer : %s\n" % (self._print_x509Name(x509cert.get_issuer())))
        self.stdout.write("[*] Subject : %s\n" % (self._print_x509Name(x509cert.get_subject())))
        self.stdout.write("[*] Algorithm : %s\n" % 
          (x509cert.get_signature_algorithm()))
        self.stdout.write("[*] NotBefore : %s\n" % (x509cert.get_notBefore()))
        self.stdout.write("[*] NotAfter : %s\n" % (x509cert.get_notAfter()))
        self.stdout.write("[*] Key Length : %s\n" % (x509cert.get_pubkey().bits()))
        self.stdout.write("[*] Public Key : \n%s\n" % (self._print_key(m2crypto_crt)))
        self.stdout.write("\n")
        #self.stdout.write("\n%s\n" % (certtext))
      def _print_x509Name(self,xname):
        return ''.join(["%s=%s " % (i[0],i[1]) for i in xname.get_components()])
      def _print_key(self,m2cert):
        return m2cert.get_pubkey().get_rsa().as_pem()
    
  2. 将其保存到你的模块仓库中;如果你还没有仓库,只需在你的机器上创建一个文件,用来保存你所有的模块。你可以通过从你的 drozer 控制台执行以下命令来安装模块:

    dz> module install [path to your module code]
    
    

    当这一切准备就绪,你可以使用以下命令运行该模块:

    run external.cert.info –p com.google.android.gsf
    
    

    你的屏幕上应该会显示类似以下截图的内容:

    如何操作...

第四章:利用应用程序

在本章中,我们将介绍以下内容:

  • 通过 logcat 泄露信息

  • 检查网络流量

  • 通过活动管理器进行被动意图嗅探

  • 攻击服务

  • 攻击广播接收器

  • 列举易受攻击的内容提供者

  • 从易受攻击的内容提供者中提取数据

  • 向内容提供者插入数据

  • 列举易受 SQL 注入攻击的内容提供者

  • 利用可调试的应用程序

  • 对应用程序进行中间人攻击

引言

到目前为止,我们已经介绍了一些基本开发和安全评估工具,甚至还涵盖了扩展和定制这些工具的一些示例。本章将重点介绍如何使用这些工具分析 Android 应用程序,以识别漏洞并为它们开发利用程序。尽管如此,鉴于应用程序功能的任意性以及 Android 应用程序开发人员几乎无限的创造力,不难看出评估 Android 应用程序的安全性必须被视为一门艺术。对于作为安全审计师、分析师、顾问或爱好者的你来说,这意味着可以肯定,永远不会有一个完全自动化的方法来分析 Android 应用程序的安全性。几乎总是,你需要依赖自己的创造力和分析能力,为 Android 应用程序的安全性提供一个具体的评估。

在我们开始深入探讨一些应用程序之前,重要的是要明确 Android 应用程序的安全问题,定义一些目标,并列举应用程序的攻击面。在接下来的几节中,我们将讨论应用程序安全的一些通用目标以及为实现这些目标应该实施的控件。之所以讨论应用程序安全目标非常重要,是因为这有助于确保在评估应用程序安全性时,你拥有正确的心态和原则。同时,它也使得审核应用程序安全变得简单,只需验证这些控件的存在,然后开发利用这些控件缺失或不足的方法。

那么应用程序安全的目的是什么?

保护用户数据

应用程序常常被托付与用户相关的非常敏感的数据,以下是一些例子:

  • 密码

  • 认证令牌

  • 联系人

  • 通信记录

  • IP 地址或域名到敏感服务

如果每个应用程序有此倾向,其数据将被缓存,并且通常会明确地将用户内容保存在数据库、XML 文件或其他任何磁盘存储格式中;它们有自由使用所需的任何文件格式或存储机制。评估这些数据存储的安全性同样重要,就像评估和审计在线或基于云的数据库和信息存储机制一样,特别是由于应用程序中存储的信息可能影响网站和其他云服务的安全性。例如,如果攻击者从应用程序传播到云服务的认证凭据,他/她立即可以访问实际的云服务。同样考虑在线银行应用,以及这些应用存储的双重认证令牌以及它们的存储方式——短信收件箱?真的吗!

应用程序需要实施许多在线数据库所使用的控制措施,这些措施独立于 Android 操作系统所提供的;即确保以下属性的控制措施:

  • 机密性

  • 完整性

  • 可用性

  • 不可抵赖性

  • 认证

我们将在后面的章节中讨论如何确保这些控制措施。现在,你需要集中精力理解的是,当这些控制措施不被执行时,用户所承担的风险。

保护应用彼此之间(隔离与权限分离)

应用程序通过 Android 沙盒受到保护,这意味着每个应用程序都被分配了一个用户 ID,本质上只能访问自己的资源。这是就 Android 中 Linux 部分而言的应用程序隔离的故事。Android 引入了一些自己的保护机制,以防止应用程序相互滥用组件和数据;最值得注意的是 Android 权限框架,它在应用程序级别上操作,并由应用程序中间件执行。它的存在是为了将 Linux 访问控制机制转换为应用程序级别,反之亦然。从更实际的角度来说,这意味着每次应用程序被授予一个权限,可能意味着相关的 UID 被分配了一个相应的 GID。例如,android.permission.INTERNET 权限被映射到 inet 组。任何被授予此权限的应用程序将被放置在 inet 组中。

应用通常由许多经典应用组件的实例组成,如服务、内容提供者、活动和广播接收器。为了保护这些组件免受恶意或任何非意图的损害,应用开发者必须与用户沟通并减轻他们的应用在服务和数据访问方面给用户带来的风险。应用开发者还应尊重这些资源的完整性。通过确保他们只请求必要的权限并且不过分要求被授予的权限,安全开发这两大原则可以通过权限框架来实施。关键是确保开发者遵循最小权限原则。通过确保访问给定应用的组件和数据需要正确的权限,并且只向整个系统提供必要的服务和组件,可以在一定程度上防止恶意应用,即在不必要时不要导出组件。

在分析应用对其数据和组件实施的隔离措施时,考虑访问它们所需的权限是非常重要的。获取这些权限是否容易?访问给定组件所需的权限是否被赋予了正确的保护级别?一个不好的例子是,仅使用android.permission.SEARCH权限就能轻松搜索和获取用户银行对账单的应用。

保护敏感信息的通信

应用开发者保护应用存储的数据是不够的,他们还需要注意这些信息的通信方式。例如,考虑一个应用虽然安全地存储了用户数据,但却允许将其传达给未经授权的第三方。如果通信不安全,那么世界上所有的数据存储安全都毫无意义!

通信可以通过以下方式完成:

  • 组件间通信:应用通常需要在其各自组件之间发送信息,例如,在广播接收器和活动之间。考虑到这种通信可能通过意图和意图过滤器来促进,并且意图过滤器的非排他性,未授权的应用可能以各种方式截取此通信。

  • 应用间通信:应用之间的数据通信应以防止未授权应用篡改、截取或获取数据的方式进行。

  • 跨设备通信:应用可能会利用 NFC、蓝牙、GMS 或 Wi-Fi 等通信媒介传输敏感数据。应用开发者必须采取适当的预防措施,确保以这种方式通信的数据的保密性、完整性和不可抵赖性。

因此,在审核应用程序的通信故障时,查看提供以下内容的控制措施非常重要:

  • 接收和发起应用程序之间的身份验证

  • 访问控制,防止未经授权的第三方/应用程序获取通信数据或控制通信流程

所以,希望你已经阅读了介绍,并对安全应用程序所期望的控制措施有了很好的了解;因为接下来,我将介绍如何验证这些控制措施是否已经实施,以及如何利用这些控制措施的缺失。

通过 logcat 信息泄露

Android 应用程序可能会固有地或由于有害影响而泄露敏感信息。当这种情况发生时,它被称为信息泄露漏洞。这个环节讲述了如何通过检查应用程序开发人员用作调试工具的 Android logcat 来检查应用程序潜在的敏感信息泄露。我们还将讨论如何利用 Android 内置的一个基准测试工具,使 logcat 检查更有成效。

准备工作

在我们开始之前,你需要以下内容:

  • 一个通过 ADB 连接到你的机器的模拟器或 Android 设备,这将需要在你 Android 设备上启用 USB 调试

  • Android 调试桥ADB

在开始这个环节之前,你应该已经下载并更新了你的 Android SDK。你应该已经适当地设置了你的PATH变量,或者你应该处于包含适当工具/二进制文件的工作目录中。

如何操作...

首先,让我们通过 ADB 启用调试。在 Windows 或 Linux 上执行以下命令:

adb logcat

这只有在您处于正确的的工作目录下才会起作用,对于 Linux 用户来说是 [path-to-sdk]/sdk/platform-tools/,对于 Windows 用户来说是 [path-to-sdk]\sdk\platformtools\

这将输出一些软件和硬件级别事件的日志信息。自然地,我们希望将关注点放在我们正在检查安全漏洞的事件和应用程序上。幸运的是,logcat 能够筛选日志信息。以下是所有选项的详细说明:

adb logcat [options] [filter]

其中[options]可以是以下任何一个——为了简洁和切中要点,我省略了一些:

  • -v <format>:这个选项设置输出的格式;可能是briefprocesstagthreadrawtimethreadtimelong

  • -d:这个选项会转存日志文件并退出

[filter]是一个tag:priority命令列表,下面将进行讨论:

  • tag:它是标识日志组件的字符串。日志组件是输出日志的字符串。例如,如果日志输出如下所示:

    E/ClockAlarmWidget( 6590): [AlarmWidgetIdManager] getListItem()
    

    ClockAlarmWidget,在之前代码中高亮的部分将是日志组件标签。在/之前的部分称为优先级。这里,优先级是Error,由E表示。

  • priority:可以是以下任何一个:

    • V, verbose:它启用了详细日志记录

    • D, debug:它启用了调试日志记录

    • I, Info:它启用了信息目的的日志记录

    • W, Warn:它启用了所有警告信息的日志记录

    • E, Error:它启用了错误日志记录

例如,如果你想监控Error级别优先级日志组件及更高级别的日志,你会使用以下命令:

adb logcat *:E

*表示我们希望所有日志组件标签的Error级别优先级。

另一个有效筛选日志的方法是将 logcat 输出转储到一个文本文件中,并使用grep(大多数 Linux/Unix 发行版随附)或 Windows 用户的文本编辑器如 Notepad++进行搜索。Notepad++和grep的下载页面链接在本食谱的另请参阅部分提供。对于 Windows 用户,如果你真的想进行一些基于强大正则表达式的匹配,有一个名为 WinGrep 的 Microsoft 版本的grep。WinGrep 下载页面的链接也已在食谱的另请参阅部分提供。

一旦决定了如何搜索文本,只要你知道如何在日志中找到你要找的内容,实际上怎么做并不重要。你可以通过执行以下命令转储日志文件的输出:

adb logcat > output.txt

这同样适用于 Linux 终端或 Windows 命令提示符。你也可以将输出“管道化”——这意味着将一个程序的输出传递给另一个程序的输入——直接进入另一个程序,如下所示。这在 Windows 命令提示符或 Linux 终端中都可以工作。

adb logcat | [other program]

如果你使用grep,你可以通过执行以下命令来完成:

adb logcat | grep [pattern]

其中[pattern]是你搜索的文本模式,例如:

adb logcat | grep ApplicationManager

我真的不想在这里写一个关于如何使用grep的完整教程。如果你想利用grep或 WinGrep 更强大的功能,请查看本食谱的另请参阅部分。

这里有一些你可能觉得有用的例子;监控与网页相关的信息的日志文件:

adb logcat | grep [Cc]ookie
adb logcat | grep "http[s]*"
adb logcat | grep "ftp[s]*"

我知道这些例子不是非常严格,但它们足以匹配网页地址。

如何操作...

之前的日志是由三星 Galaxy S3 手机上的 Google Play 商店应用生成的。

你还可以尝试捕获通过日志文件泄露的一些登录或认证类型的令牌字符串:

adb logcat | grep –i "[\w\s_-]*token[\w\s_-]*"

在日志文件中寻找有价值的信息时,通常一个好主意是寻找那些否则需要权限才能获取的信息,或者直接导致你获得其他应用程序保护的信息的知识。例如,如果应用程序记录了用户登录他/她的 LinkedIn 个人资料后返回的 cookie 值,这会危险吗?

是的!实际上你刚刚绕过了需要知道他/她的 LinkedIn 密码的需求,或者你的应用程序需要被授予 LinkedIn 应用程序中某些身份验证功能的权限。在你阅读日志文件的时间里,你应该尽量关注查找这类信息。

如何操作...

案例证明!这里记录的 cookie 是由 Android LinkedIn 应用程序在 Galaxy S3 手机上有害地泄露的。在发现 Facebook Android SDK 中的主要安全漏洞中可以找到这种漏洞的另一个真实世界示例。相关链接在参见以下内容部分提供。

还有更多...

当然,应用程序通常是为了响应硬件或软件事件而开发的,可以通过广播接收器或其他应用程序或系统服务的意图来实现。自然而然地,你会想知道应用程序是如何响应这些事件的,或者它们在响应这类事件时的行为是否可能有害。那么问题来了,在不按下音量键、锁定和解锁屏幕、自己按键的情况下,你如何创建/发送这些事件给正在测试的应用程序呢?答案是 Android Monkey 测试框架。它旨在向应用程序发送系统和硬件级别的事件,以便开发者可以衡量应用程序处理这些事件的效率。它在某种程度上可以作为应用程序的设备事件“模糊测试”框架。

在解释如何使用它之前,重要的是要提到,对安装在个人 Android 设备上的应用程序运行 Monkey 测试可能不是一个好主意,无论是你的还是别人的。这是因为应用程序对 Monkey 测试的响应可能会导致被“猴子”的应用程序受损,导致应用程序数据丢失,甚至使手机崩溃。除非你有适当的权限或接受可能会丢失或损坏你正在测试的应用程序存储的数据,否则你只能在模拟器或专门用于安全测试的设备上进行此操作。

使用这个框架的一种方式是通过 ADB 连接一个设备,并通过命令提示符或终端执行以下命令:

adb shell monkey –p [package] –v [event count]

其中[package]是你想要发送这些事件的包/应用程序的名称,[event count]是你想要发送的随机事件的数量。以下是针对 Flipboard 应用程序使用它的一个例子:

adb shell monkey –p Flipboard.app –v 10

这将向 Flipboard 应用程序发送 10 个随机选择的事件,并报告应用程序的行为。

参见以下内容

检查网络流量

众所周知,应用程序可以利用安卓设备上可用的网络服务,许多应用程序被开发为基于云服务的界面。这意味着理解它如何与互联网服务通信是安全风险概况中非常重要的一部分——应用程序向其用户和设备暴露的风险集合。

在这个指南中,我将向你展示一些新颖的方法,你可以使用它们直接从安卓设备监控网络流量,使用的是一直很受欢迎的Wireshark

准备工作

在我们开始之前,你需要在本地机器和安卓设备上安装一些工具。以下是你会需要获取的工具:

如何操作…

一旦你设置好所有工具并准备就绪,你可以通过执行以下步骤来监控你的 Android 设备的流量:

  1. 假设你的 Android 设备已经获得 root 权限,你应该创建一个目录来存放你的 TCPdump 二进制文件,如下所示:

    在 Android 设备上,通过 ADB 按照出现的顺序执行以下命令:

    su
    mkdir /data/tcpdump/
    chmod 755 /data/tcpdump/
    
    

    然后在本地机器上,在你下载了适用于 Android 的 TCPdump 版本的文件夹中,执行以下命令:

    adb push tcpdump /data/tcpdump/.
    adb shell chmod 755 /data/tcpdump/tcpdump
    
    
  2. 一旦将 TCPdump 的 Android 版本上传到设备并标记为可执行。你应该确保 Android 设备上可用 Netcat,尝试运行以下命令:

    nc
    
    

    如何操作…

    这只是一个健全性检查,大多数 Android 版本默认都会安装 Netcat。如果没有,可以在 Google Source Android GitHub 仓库中找到带有 NDK Makefile 的适用于 Android 的版本,地址是android.googlesource.com/platform/external/netcat/+/master。要了解如何使用此 Makefile,请参考第八章中的跨编译本地可执行文件菜谱,本地利用与分析

  3. 为了确保一切正常工作,在确认你的 Android 设备上已经安装了 TCPdump 和 Netcat 之后,你可以实际捕获一些网络流量并尝试执行以下命令:

    ./data/tcpdump/tcpdump –w - | nc –l –p 31337
    
    

    如果一切正常,你应该会在屏幕上看到以下内容:

    如何操作…

    要查看一些实际输出,你可以尝试打开一个会向 Web 发送请求的应用程序或使用一些网络 API。

  4. 如果一切顺利,你应该能够将 TCPdump 的输出传递给本地设备上安装的 Wireshark。为此,你首先需要通过 ADB 设置一些端口转发,这是通过执行以下命令完成的:

    adb forward tcp:12345 tcp:31337
    
    
  5. 一旦设置好端口转发,你就可以在本地机器上通过执行以下命令来使用 Netcat:

    netcat 127.0.0.1 12345
    
    
  6. 这意味着所有流量都被正确转发。你应该能够将输出传递给 Wireshark,Wireshark 将对其进行解释,并便于进行深度数据包检查和其他有用的事情。要在本地机器上执行输出传递给 Wireshark 的命令,请执行以下操作:

    adb forward tcp:12345 tcp:31337 && netcat 127.0.0.1 12345 | wireshark –k –S –i – 
    
    

    几秒钟后,如果一切正常,你应该会看到 Wireshark 启动。以下内容将显示在你的屏幕上:

    如何操作…

工作原理...

在这个教程中,我们使用了 Netcat、Wireshark 和 TCPdump 直接从 Android 设备提取网络流量进行分析和深度包检查。鉴于在演练中对命令行参数和工具组合的解释非常少,这个教程详细说明了每个操作是如何执行以及为什么这样执行。

在第一步中,执行了以下命令以在 Android 设备上创建一个目录,用于承载 TCPdump 的安装:

su; mkdir /data/tcpdump/; chmod 755 /data/tcpdump/

su命令代表Substitute UserSU),它允许我们获取 root 权限——这是在没有提供参数时su的行为。我们使用su假设的 root 权限包括能够修改和查看 Android 文件系统上的任何目录或文件。这是必需的,因为我们是在/data/文件夹内创建tcpdump目录。

执行su之后,我们执行了带有参数/data/tcpdump/mkdir命令,在/data/文件夹下创建了tcpdump/目录。

紧接着是chmod命令——它是改变模式的缩写,参数为755。它修改了/data/tcpdump文件夹的访问模式,允许低权限用户访问tcpdump路径。这是必需的,因为我们将会使用adb push命令将tcpdump二进制文件存储在此路径下。

创建了tcpdump文件夹后,我们执行了以下命令:

adb push tcpdump /data/tcpdump/.
adb shell chmod 755 /data/tcpdump/tcpdump

这些命令确保了tcpdump二进制文件存储在tcpdump路径下。第一个命令将push命令传递给adb,参数为tcpdump,这是适用于 Android 的 TCPdump 版本。您会注意到,在/data/tcpdump文件夹下为tcpdump二进制文件提供了一个点作为名称;这是确保被复制的文件在复制后保持其文件名的简写方式。这是显而易见的,因为我们从本地机器复制了一个名为tcpdump的文件,在 Android 设备上也同样被命名为tcpdump

push命令之后是带有参数chmod 755 /data/tcpdump/tcpdumpadb shell命令,它改变了tcpdump二进制文件的访问模式,允许低权限用户执行它。

在第二步中,我们使用了nc命令——这是 Netcat 的缩写。这个工具作为与网络服务交互的多功能工具。在这个教程中,我们将使用它来读写网络连接中的数据。不带任何参数运行nc会打印使用说明。这让我们可以确认nc是否正常运行,并确认它实际上已经安装在 Android 设备上。

在第 3 步中,我们使用了带有–w参数的tcpdump,这允许我们指定一个文件进行写入,第二个参数确保输出会同时写入到终端屏幕。作为我们执行的命令的一部分,我们还指定了如下内容:| nc –l –p 31337。在操作系统术语中,|字符被称为管道,它将前一个程序的输出作为输入传递给管道后面的程序。Netcat 使用–l参数被调用,这导致 Netcat 监听作为–p命令行开关参数提供的端口上的连接。在这个上下文中,这意味着tcpdump的原始二进制网络流量作为输入传递给 Netcat;这意味着它将从端口号码31337输出这些原始流量。

在第 4 步中,我们使用了 ADB 的端口转发功能。它允许我们将 Android 设备上的一个端口(作为第二个参数tcp:12345)与本地机器上的一个端口(作为第一个参数tcp:31337)连接起来。你会注意到我们将端口12345与端口31337耦合,并告诉前一步的 Netcat 在端口31337上监听连接。这样我们就可以通过本地机器上的端口31337与 Netcat 实例进行交互。简单来说,Android 设备上的端口31337变成了我们本地机器上的端口12345

在第 5 步中,我们使用参数127.0.0.1启动了 Netcat,这是我们的本地机器的地址(称为环回地址),以及12345,这是我们上一步转发的端口。这告诉 Netcat 连接到本地机器的端口12345;由于端口12345与 Android 设备上的端口31337耦合,这意味着我们通过本地端口12345的代理与端口31337进行交互。这样做的结果是,我们可以从本地机器捕获传递到 Android 设备上 Netcat 的网络流量。

在第 6 步中,我们将所有与本地机器相关的命令组合在一起,以确保 Wireshark 获取原始二进制网络流量并为我们的解释。我们使用以下参数启动了 Wireshark:

  • –k:根据 Wireshark 手册,此参数执行以下操作:

    • 立即开始捕获会话。如果指定了-i标志,捕获将使用指定的接口。

    • 否则,Wireshark 会搜索接口列表,如果有非环回接口,则选择第一个非环回接口;如果没有非环回接口,则选择第一个环回接口。

    • 如果没有接口,Wireshark 会报告错误并且不会开始捕获。

  • –S:此参数指定快照长度,即每个数据包捕获的字节数。如果没有给出长度参数,则捕获整个数据包。

  • –i:此参数指定了捕获数据包的输入来源。这里我们再次提供了 – 符号,告诉 Wireshark 从标准输入读取数据。这样做是因为 Wireshark 的输入是通过 Netcat 的管道传输给它的。

如果想要更有趣地使用这个想法,你可以尝试构建工具,通过在由 Android 设备产生的网络流量上运行入侵检测系统IDS)或其他以安全为重点的网络监控工具(如 Snort),来分析 Android 流量中的活跃威胁。这个想法将非常适合进行恶意软件和漏洞分析。

另请参阅

通过活动管理器进行被动意图嗅探

想要广泛传播关于应用程序及其组件的信息,一个有效的方法是窃听应用程序间的通信。你可以通过请求活动管理器中最近意图的信息来实现这一点。

这个方法非常直接,正如你所发现的,如果你愿意编写一些 Python 脚本,可以通过 drozer(在第三章,Android 安全评估工具中介绍)来完成。iSec Partners 的团队开发了一个能够执行此操作的应用程序,以下食谱中讨论的 drozer 模块的大部分灵感都来自他们的应用。想要了解如何获取这个应用,请查看本食谱的另请参阅部分。

准备工作

在我们实际编写这个模块之前,我们需要对 drozer 代理进行一些修改,使其具有从活动管理器请求意图信息的必要权限。最简单的方法是通过其 AndroidManifest.xml 文件增加 drozer 请求的权限。这里,我将向你展示如何使用 Eclipse 来完成这个操作。

  1. 首先,你需要从以下网站获取 drozer 代理及其依赖项的副本:

  2. 下载并保存到同一文件夹后,你可以打开 Eclipse 并将它们作为 Android 项目导入。对于每一个项目,在 Eclipse 打开后,导航到文件 | 导入。![准备就绪]

  3. 点击Android文件夹,然后选择Existing Android Code into Workspace,并点击Next。![准备就绪]

  4. 在这一点上,Eclipse 会要求你指定一个要导入的文件夹。你需要添加在第 1 步下载的文件夹之一。要选择文件夹,点击浏览...,文件选择对话框将会弹出。![准备就绪]

  5. 使用文件对话框,导航到下载 drozer 代理和依赖项的文件路径。你需要以这种方式添加它们。

    确保以这种方式导入每个文件夹。否则,Eclipse 将无法成功构建 drozer 代理。

  6. 导入所有项目后,你需要编辑 drozer 代理的AndroidManifest.xml。你通过在 Eclipse 中双击drozer-agent project文件夹中的AndroidManifest.xml文件来进行编辑(确保在编辑前选择AndroidManifest.xml标签,以便直接编辑 XML)。然后,输入以下行:

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

    如果步骤正确,AndroidManifest.xml文件应该看起来像以下截图:

    准备就绪

    就这样!你刚刚为 drozer 代理添加了一个额外的权限。现在你可以将 drozer 代理导出为 APK 文件,上传到你的设备上,开始工作了。

    请注意,在安装修改后的代理之前,你可能需要卸载设备上当前安装的 drozer 代理。

如何操作...

这样,drozer 代理的安装就完成了。现在我们可以开始开发意图嗅探模块了。

  1. 导航到你的 drozer 模块仓库;如果你还没有设置,请参考第三章中的编写一个 drozer 模块——一个设备枚举模块食谱,了解如何操作。进入模块仓库后,创建一个名为ex.sniffer.intents的文件,并输入以下内容(以下代码将包含在本书的代码仓库中):

    from drozer.modules import Module,common
    from drozer.modules import android
    class Intents(Module, common.PackageManager):
      name = "Dump recent intents to the console"
      description = "This module allows you to see the most recent intents that were sent, via the ActivityManager"
      examples = "run ex.sniffer.intents"
      author = "[your name]"
      date = "[the date]"
      license = "GNU GPL"
      path = ["ex","sniffer"]
      def execute(self,arguments):
        self.stdout.write("[*] initializing intent sniffer…\n")
        context = self.getContext()
        activityService = context.getSystemService("activity")
        self.stdout.write("[*] got system service ..\n")
        recentTasks = activityService.getRecentTasks(1000,1)
        self.stdout.write("[*] recentTasts Extracted..\n")
        list_length = recentTasks.size()
        self.stdout.write("[*] Extracted %s tasks ..\n" % (list_length))
        for task in range(list_length):
          cur_task = recentTasks.get(task)
          cur_taskBaseIntent = cur_task.baseIntent
          self.stdout.write("\t[%d] %s\n" % (task,cur_taskBaseIntent.toString()))
    
  2. 完成后,通过执行以下命令将模块安装到 drozer 中:

    dz> module install [path-to-module-repo]/ex.sniffer.intent
    
    
  3. 然后通过执行以下命令运行它:

    dz> run ex.sniffer.intents
    
    

    你应该会看到类似于以下截图的内容:

    如何操作...

工作原理...

意图嗅探脚本实际上非常简单。下面我会分解它的操作原理以及它是如何实际嗅探到意图的。

意图嗅探器调用了Context.getSystemService()方法,并传递了ACTIVITY_SERVICE标志的标识符,它只是一个值为"activity"的字符串。这返回了ActivityManager类的一个实例,使脚本能够与活动管理器交互,并调用如ActivityManager.getRecentTasks()等方法。这个方法接收两个参数,第一个是一个整数,表示脚本希望从活动管理器接收的RecentTaskInfo对象的最大数量;第二个是一个指定最近活动类型的标志。在这个例子中,脚本被编写为请求完整列表,不省略任何任务。我之所以这样编写脚本,是因为发送到启动每个最近任务的意图与RecentTaskInfo对象捆绑在一起,作为一个名为RecentTaskInfo.baseIntent的字段。然后脚本可以使用它来提取有关意图的一些有用信息,例如组件名称、标志、动作和类别。为了快速简便,脚本随后记录了对Intent.toString()方法的调用,该方法仅将意图信息格式化为字符串并返回。

当然,你可以对意图信息进行更智能的解析。你甚至可以尝试找出是哪个包发出了原始调用。尽管这非常困难,但如果能完成,这将是一个非常有价值的 drozer 模块。

另请参阅

攻击服务

服务可能看起来并不危险,并且它们坚持在后台工作。但它们是为了支持其他应用程序组件而开发的,并且可能执行非常敏感的操作,如登录在线个人资料、重置密码,甚至通过作为主机系统服务的代理来促进一些潜在危险的过程。无论如何,在应用程序评估期间,它们绝不能被忽视。

什么时候服务是脆弱的?当一个服务能被用来对用户进行滥用、提升另一个应用程序/用户的权限,或者用来提取敏感信息时,这个服务就是可被利用的。这意味着你需要能够与服务进行交互,这意味着它必须是可导出的,或者能从意图、文件或网络堆栈等消息格式中响应/接受输入。另一个需要考虑的是与服务交互需要什么类型的权限——它是否是一个潜在危险的服务,执行非常敏感的操作,或者可能被滥用导致应用程序甚至设备出现拒绝服务DoS)状况(即攻击者通过强制服务停止工作或拒绝提供服务来阻止访问服务)!更不用说如果这个潜在危险的服务根本不需要任何权限,应用程序及其用户将会陷入多么糟糕的境地!

OWASP GoatDroid project. Try reading through this, and think about the possible dangers and risks for this setup:
<service android:name=".services.LocationService" >
  <intent-filter>
    <action android:name="org.owasp.goatdroid.fourgoats.      services.LocationService" />
  </intent-filter>
</service>
</application>
  <uses-permission android:name="android.permission.SEND_SMS" />
  <uses-permission android:name="android.permission.CALL_PHONE" />
  <uses-permission android:name="android.permission.    ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.    ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.INTERNET" />
</manifest>

我在这里突出了一些重要的区域。你应该注意到,名为.services.LocationService的服务可能通过 GPS 服务或地理定位 API 确定用户的位置,而且启动时不需要任何权限!考虑到应用程序本身会被授予android.permission.ACCESS_COARSE_LOCATIONandroid.permission.ACCESS_FINE_LOCATION这两个权限,这意味着攻击者如果足够接近这个服务(可能是物理访问设备,或者在用户设备上安装了恶意应用程序),很有可能会未经授权使用这项服务。

上一个示例来自OWASPS GoatDroid项目,有关 GitHub 仓库的链接请参见另请参阅部分。

所以这就是从代码源头上看这个漏洞的样子,或者说从开发者/逆向工程师的角度来看。现在让我们实际使用 drozer 来攻击一些易受攻击的服务,并给你这个漏洞攻击者的视角。

如何操作...

下面是如何寻找一些易受攻击的服务的方法:

  1. 给定一个示例应用程序,找出哪些服务是可导出的。你可以通过 drozer 执行以下命令来实现:

     dz> run app.service.info –-permission null
    
    

    正如在上一章中我解释的,这个命令可以找到那些不需要任何权限的服务。

  2. 找到一批服务后,你可以使用以下命令来启动它们:

    dz> run app.service.start –-action [ACTION] –-category [CATEGORY] –-data-uri [DATA-URI] –-component [package name] [component name] –-extra [TYPE KEY VALUE] –-mimetype [MIMETYPE]
    
    

    作为一个简单的例子,以下是启动com.linkedin.android应用程序中一个服务的方法:

    dz> run app.service.start –-component com.linkedin.android com.linkedin.android.authenticator.AuthenticationService
    
    

    在启动和停止这些服务时运行 logcat 总是一个好主意,以防它们可能会泄露一些关于它们操作方式的敏感信息,以及泄露一些认证凭据或其他有用的数据。

    当然,如果你想通过意图向服务发送一些数据,你需要知道你针对的服务意图过滤器看起来是什么样子。而且,如果你还没有猜到,了解这些的最简单方式就是检查应用程序清单。如果你需要回顾如何做到这一点,请参考第二章《参与应用程序安全》中的检查 AndroidManifest.xml 文件部分。

  3. 你要寻找的 XML 代码大致如下所示:

    <service android:name=".authenticator.AuthenticationService" android:exported="true">
        <intent-filter>
          <action android:name="android.accounts.AccountAuthenitcator" />
        </intent-filter>
      <meta-data android:name="android.accounts.AccountAuthenticator"
      android:resource="@xml/authenticator" />
    </service>
    
    AndroidManifest.xml file of the Android LinkedIn application.
    
  4. 要向此服务发送意图,你可以在 drozer 控制台执行以下命令:

    dz> run app.service.start –-component com.linkedin.android com.linkedin.android.authenticator.AuthenitactionService –-action anroid.accounts.AccountAuthenitcator
    
    

顺便一提,一些服务可能会与本地库交互,并将意图接收的数据传递给 C/C++数据结构,如栈或基于堆的变量。在审计需要通过意图传递数据的服务安全性时,你应该始终尝试识别意图数据可能导致的任何潜在内存破坏漏洞。在检查其他应用程序组件类型是否存在漏洞时,请记住这一点,因为任何应用程序组件都可能引发这类漏洞。

当发送手工制作的意图时,一些默认的系统服务的行为会相当奇怪。考虑以下发送到com.android.systemui的意图示例:

dz> run app.service.start –-component com.android.systemui com.android.systemui.PhoneSettingService

在三星 Galaxy S3 上的结果如下:

如何操作...

这是一个典型的拒绝服务(DoS)漏洞示例。系统 UI 服务没有预想到包含空元数据或额外数据字段的意图。因此,当发送不带额外数据的意图时,会导致空指针异常,整个服务随之崩溃。这个漏洞看起来可能不算严重,因为它只是一个 UI 服务。但如果关键的安全机制或与安全相关的服务的 UI 组件依赖于系统 UI 服务运行才能操作(例如,可能是锁屏或设置应用程序),这个简单的不带数据意图可能会引发非常复杂、风险相当高的漏洞。

为了帮助你了解这里的危险性,想象一下你的手机上安装了一个恶意应用程序,它不断地向你的系统 UI 服务发送有害意图。这导致它一次又一次地崩溃,屏幕上充满了弹窗和警告,有效地阻止了你对手机用户界面的交互。这将是一个相当讨厌的 bug,而且安装时不需要任何权限!

另请参阅

攻击广播接收器

广播接收器响应硬件和软件级别的事件;它们通过意图获取这些事件的提醒。通常,广播接收器可能会使用通过意图发送的信息来执行敏感操作,而且这种方式可能会受到恶意广播或接收的数据的影响。

在利用广播接收器时,挑战在于确定输入是否可信以及程度如何。为此,你可能需要对目标应用程序中的广播接收器的意图过滤器定义进行有效的模糊测试,或者如果你能弄到源代码,阅读实际代码,以找出接收器操作哪种类型的数据以及如何操作。

与之前的食谱一样,这里我们将看到一个经典易受攻击的广播接收器的示例。以下示例同样来自 OWASP GoatDroid 项目:

 <receiver
    android:name=".broadcastreceivers.SendSMSNowReceiver"
    android:label="Send SMS" >
    <intent-filter>
        <action android:name="org.owasp.goatdroid.fourgoats.SOCIAL_SMS" />
    </intent-filter>
</receiver>
</application>

    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />

</manifest>

代码中的关键问题是,这个应用程序将被授予android.permission.SEND_SMS权限,同时其.SendSMSNowReceiver易受攻击的接收器没有得到适当的权限保护,从而暴露给其他应用程序。

这并不是这类漏洞的全部;还有另一部分。仅仅因为接收器允许其他应用程序与其交互,并不意味着它一定可以被利用;要验证它是否可被利用,你实际上可以尝试执行本食谱后面讨论的一些命令,如果可能的话,阅读接收器的部分源代码。

以下是确定接收器如何处理org.owasp.goatdroid.fourgoats.SOCIAL_SMS动作的代码:

public void onReceive(Context arg0, Intent arg1) {
  context = arg0;
  SmsManager sms = SmsManager.getDefault();

  Bundle bundle = arg1.getExtras();
  sms.sendTextMessage(bundle.getString("phoneNumber"), null,
    bundle.getString("message"), null, null);
    Utils.makeToast(context, Constants.TEXT_MESSAGE_SENT, Toast.LENGTH_LONG);
}

代码中的关键问题是接收器直接从bundle对象中获取值,而没有首先检查调用应用程序或提供的值,并将其插入到sendTextMessage调用中。这意味着任何应用程序都将能够发送任意的、不受控制的短信。

好的,那么这就是一个经典广播接收器漏洞的样子;让我们看看如何使用 drozer 实际利用这些漏洞。

如何做到这一点...

要向广播接收器发送意图,你可以执行以下命令:

dz> run app.broadcast.send –-action [ACTION] –-category [CATEGORY] –-component [PACKAGE COMPONENT] –data-uri [DATA_URI] –extra [TYPE KEY VALUE] –flags [FLAGS*] –mimetype [MIMETYPE]

例如,在本食谱的介绍部分,我们看到了一个可以接受电话号码和短信的接收器。要攻击该接收器,你会执行以下命令:

dz> run app.broadcast.send –-action org.owasp.goatdroid.fourgoats.SOCIAL_SMS –-component org.owasp.goatdroid.fourgoats org.owasp.goatdroid.fourgoats.broadcastreceivers.SendSMSNowReceiver –-extra string phoneNumber 1234567890 –-extra string message PWNED

执行之前的命令将向电话号码1234567890发送包含消息PWNED的文本信息。

它是如何工作的…

在这个食谱中,我们滥用了保护org.owasp.goatdroid.fourgoats.broadcastreceivers.SendSMSNowReceive广播接收器的不足权限。这个组件缺乏权限保护,允许没有SEND_SMS权限的攻击者实际发送短信。这种危险在于,恶意攻击者可以开发针对这个接收器的应用程序,向高级服务发送短信或从设备泄露信息。

实际上,许多安卓木马和基于安卓的恶意软件都利用这种模式从受害者那里窃取钱财;有数百个实际例子。关于其中一些的好资源,请参阅另请参阅部分。希望这能让你意识到,对于像这样的广播接收器来说,权限不足是多么危险。

另请参阅

枚举容易受到攻击的内容提供者

内容提供者通常包含大量有价值的信息,比如用户的电话号码或 Twitter 密码,你可能想要找出恶意攻击者是否有可能获取这些信息。确定内容提供者是否容易受到攻击的最佳方式,就是尝试自己对其进行攻击。

要对你能够攻击的内容提供者进行攻击,与许多应用程序级别的攻击一样,通常归结于向应用程序发送一个恶意意图。对于内容提供者来说,你的意图将通过它包含的 URI 字符串来精确指向目标,因为此 URI 标识了哪个内容提供者应该处理该意图。

那么问题来了——我们如何找出哪些 URI 可以使用?一个简单的解决方案就是猜测它们,但这可能需要很长时间!drozer 有一个名为app.provider.info的模块,可以为你解决这个问题。

本文档详细介绍了几个你可以用来查找可能容易受到攻击的内容提供者的 drozer 模块。

如何操作...

要找到一些很可能容易受到攻击的内容提供者,你需要执行以下操作:

  1. 使用 drozer 查找不需要权限的内容提供者非常容易;你只需要在 drozer 控制台中执行以下命令:

    dz> run app.provider.info –-permission null
    
    

    前面的命令列出了所有不需要任何读写权限的内容提供者。

  2. 一旦找到合适的内容提供者,你可能想要枚举它具有权限的 URI;你可以使用以下命令来完成这个操作:

    dz> run app.provider.finduri [package]
    
    

    在前面的命令中,[package]是你想要提取信息包的完整名称。

  3. 下面的命令是一个你可以尝试的示例:

    dz> run app.provider.finduri com.android.providers.downloads
    
    

所以你刚才所做的就是找到了一个可能的入口点,该入口点可以访问给定包在内容提供者中保存的数据。下一个食谱将讨论如何提取这些数据。

它是如何工作的...

.finduri 模块非常直接;它实际上使用了一种非常“狡猾”的方法来枚举可能的内容 URI。它基本上是打开应用程序的 DEX 文件,并扫描未解析的文件,寻找类似于有效内容 URI 格式的字符串字面量。之所以这样做非常有效,是因为应用程序开发人员通常将这些作为静态字符串保存在应用程序的源代码中。以下是 Python 脚本的实际源代码。它来自 github.com/mwrlabs/drozer/blob/master/src/drozer/modules/common/provider.py

 def findContentUris(self, package):

    self.deleteFile("/".join([self.cacheDir(), "classes.dex"]))

    content_uris = []
    for path in self.packageManager().getSourcePaths(package):
// This is where the script requests the application path from the 
// package manager, which will determine where the actual .apk file
// is stored.
        strings = []

        if ".apk" in path:
            dex_file = self.extractFromZip("classes.dex", path,self.cacheDir())
// In this line you can see the script extract the "classes.dex"
// file from the .apk file

            if dex_file != None:
                strings = self.getStrings(dex_file.getAbsolutePath())

                dex_file.delete()

                # look for an odex file too, because some system packages do not
                # list these in sourceDir
            strings += self.getStrings(path.replace(".apk",".odex")) 
        elif (".odex" in path):
            strings = self.getStrings(path)

        content_uris.append((path, filter(lambda s: ("CONTENT://"in s.upper()) and ("CONTENT://" != s.upper()), strings)))
// In this you can see the script actually search for the literal //"CONTENT://" or "content://" in the extracted .dex file.

return content_uris

另请参阅

从易受攻击的内容提供者中提取数据

如果某些内容提供者的 URI 不需要读取权限,或者将GrantURI设置为true,你可能可以使用一些 drozer 工具从中提取数据。在某些情况下,读取/写入权限的发放和执行方式也会将内容提供者的数据暴露给攻击。

本指南将介绍一些简单技巧,你可以用它们来了解提供者中存储的信息类型。本指南接着上一条,并假定你已经枚举了一些内容 URI,并确定与之交互和查询相关 URI 时不需要任何或权限不足。

如何操作...

找到一个 URI 后,你可以使用前一个指南中详细描述的命令进行查询,具体为:

run app.provider.info –-permission null
run app.provider.finduri [package]

前面的命令会为你提供一些相当有用的目标 URI;然后你可以执行以下命令来提取一些数据:

dz> run app.provider.query [URI]

以下是一个简单示例;关于许多与内容提供者相关的脚本,drozer 帮助文档都使用这个例子:

dz> run app.provider.query content://settings/secure

下面是一个易受攻击的内容提供者的示例。在这个例子中,攻击者使用了 drozer 提取的关于用户银行交易的信息;请参阅以下屏幕截图以查看查询命令的输出:

如何操作...

某些内容提供者支持查询文件,尤其是那些文件管理器类型的程序。如果内容提供者没有限制应用程序允许读取的文件类型和路径,这意味着攻击者可能能够执行路径遍历,访问内容提供者实际意图提供的文件之外的目录,或者在许多情况下,允许攻击者从受害者设备上的敏感目录中提取文件。要提取文件,你可以使用以下命令:

dz> run app.provider.download [URI]

在前面的命令中,URI 是你希望从内容提供者那里提取的文件的 URI。如果在处理这类查询的内容提供者实际实现部分没有进行输入保护或过滤,你可以注入文件路径并利用这种缺乏保护来枚举设备文件系统中其他区域的文件及其内容;你可以通过尝试不同的文件路径来实现,如下所示:

dz> run app.provider.download content://[valid-URI]/../../[other file path]    [local-path]

在前面的命令中,[valid-URI] 是脆弱的内容提供者有权处理或已注册处理的 URI,[other file path] 是你希望提取的文件的路径,[local-path] 是你希望这个文件被 "下载" 的文件路径。以下是一个示例:

dz> run app.provider.download content://vulnerabledatabase/../../../system/etc/hosts /tmp/hostsFileExtracted.txt

对于那些有黑客攻击/审计 Web 应用程序经验的人来说,这类似于 Web 应用程序中的路径遍历和本地文件包含漏洞。它也使 Android 应用面临许多相同的风险。这种漏洞的一些实际示例已经针对一些非常流行的应用程序报告;有关示例,请参阅食谱的 另请参阅... 部分。

如果你的内容提供者使用 PATTERN_LITERAL 匹配类型设置路径级别的权限,那么 Android 权限框架只有在请求的路径与你的路径完全匹配时才会执行检查以保护你的内容提供者!以下是一个示例的屏幕截图:

如何操作...

当前示例来自 MWR 实验室的 Sieve Android 应用,该应用内置了一些漏洞;有关下载页面的链接,请参阅 另请参阅 部分。

在前面的屏幕截图中,我们可以看到这个应用使用 PATTERN_LITERAL 类型的匹配来保护 Keys 路径,这意味着如果我们尝试使用 drozer 进行查询,结果将如下所示:

run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Keys

下面的屏幕截图显示了之前命令的输出:

如何操作...

前面的屏幕截图显示了由于 drozer 没有与提供者交互所需的权限而导致的权限拒绝。但是,如果我们简单地在路径后加上/,它仍然有效,结果如下:

run app.provider.query content://com.mwr.example.siever.DBContentProvider/Keys/

下面的屏幕截图显示了前面命令的输出:

如何操作...

路径中添加了一个正斜杠,因此PATTERN_LITERAL检查未能找到content://com.mwr.example.sieve.DBConentProvider/Keys路径,而是找到了content://com.mwr.example.sieve.DBConentProvider/Keys/路径。这意味着查询内容提供者的应用程序将需要/Keys/路径的权限,该路径未定义,因此不需要权限,这使得查询能够顺利解决。在之前的屏幕截图中,我们可以看到在这种情况下,恶意应用程序能够提取用户针对 Sieve 密码管理应用程序的登录 PIN 码详情。

另请参阅

向内容提供者中插入数据

与任何以数据库为中心的应用程序一样,内容提供者也可能具备向其 SQLite 数据库或文件存储中插入数据的能力;如果任何内容提供者没有使用适当的写入权限来限制此功能,攻击者可能会恶意地向 SQLite 数据库中插入数据。本教程将讨论如何执行此类攻击;在下一章中,我们将查看导致这些漏洞的实际代码,并讨论一些补救措施。

如何操作...

在我们向内容提供者插入数据之前,我们需要了解数据库的架构或列设置是什么样的;您可以使用以下命令从您的 drozer 控制台枚举此信息:

dz> run app.provider.columns [URI]

在前面的命令中[URI]是你希望了解的 URI。例如,如果你想针对 Sieve 运行它,你会执行以下命令:

dz> run app.provider.columns content://com.mwr.example.seive.DBContentProvider/Passwords

前面的命令将产生如下截图所示的输出:

如何操作...

枚举数据库列之所以有用,是因为它可能有助于你针对内容提供者构建未来的攻击;你可能需要了解一些关于模式的信息,以便知道你可能感兴趣从中提取和插入的列和行。

当你了解了数据库的结构以及可能需要哪些列名以便正确构建查询时,你可以使用以下命令将数据插入内容提供者:

dz> run app.provider.insert [URI] [--boolean [name] [value]] [--integer [name] [value]] [--string [name] [value]]...

在前面的命令中,[URI] 是指向相关数据库的 URI,而 --boolean--integer--string 是你应该提供的标志,以将给定的数据片段标记为给定的数据类型。此模块支持以下数据类型:

--boolean –-double –-float –-integer –-long –-string –short

每个都需要[name]值,这表示列名,以及[value],表示你希望插入的实际值。

下面的代码是一个示例:

dz> run app.provider.insert –-int _id 12 –-int from_account 31337 –-int to_account –-int amount 31337    content://com.example.vulnerabledatabase.contentprovider/statements 

下面的例子是虚构的。content://com.example.vulnerabledatabase.contentprovider/statement URI 在你的设备上可能不存在,除非你已经明确开发了一些处理它的应用。

下面是针对 Sieve 的工作示例:

dz>  run app.provider.insert content://com.mwr.example.sieve.DBContentProvider/Passwords –-int _id 3 –-string username injected –-string service injected –-string password woopwoop –-string email myspam@gmail.com 

当你查询 Sieve 的密码 URI 并执行之前的命令后,将返回以下数据:

如何操作...

我们可以清楚地看到,对于 _id 3 的数据,我们刚刚注入的数据实际上出现在数据库中。这意味着我们刚刚成功用一些伪造数据破坏了 Passwords 数据库中的数据。在实际情况下,这可能允许攻击者更改用户的密码或删除它们,从而拒绝用户访问相关账户;更具体地说,在像 Sieve 这样的密码管理应用(这里仅作为示例)中,攻击者能够阻止用户访问他们存储的密码,甚至可能是他们的 Gmail、Twitter 或 LinkedIn 账户。

关于示例的一个小注:我们注入密码字符串 woopwoop 仅作为标记,以确保我们可以注入密码数据——它只是一个很容易识别的字符串;如果你要测试这个密码,它可能不会起作用。实际上,要注入一个有效的密码,你需要注入密码的 base64 编码值。

枚举 SQL 注入漏洞的内容提供者

与网络应用一样,Android 应用可能会使用不可信的输入来构建 SQL 查询,并以可被利用的方式进行。最常见的情况是应用没有对任何 SQL 输入进行清理,也没有限制对内容提供者的访问。

你为什么要阻止 SQL 注入攻击?嗯,假设你处于一种经典情况,通过查询数据库来为用户提供授权。代码可能类似于以下这样:

public boolean isValidUser(){ 
u_username = EditText( some user value );
u_password = EditText( some user value );
//some un-important code here...
String query = "select * from users_table where username = '" +  u_username + "' and password = '" + u_password +"'";
SQLiteDatabase db
//some un-important code here...
Cursor c = db.rawQuery( p_query, null );
return c.getCount() != 0;
}
occurs more often in real-world applications. So when auditing Android code for injection vulnerabilities, a good idea would be to look for something that resembles the following:
public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {
   SQLiteDBHelper sdbh = new StatementDBHelper(this.getContext());
   Cursor cursor;
   try {
//some code has been omitted  
   cursor = sdbh.query(projection,selection,selectionArgs,sortOrder);
   } finally {
      sdbh.close();
   }
   return cursor;
}

在前面的代码中,projectionselectionselectionArgssortOrder 变量都没有直接来自外部应用程序。如果内容提供者被导出并授予 URI 权限,或者如我们之前所见,不需要任何权限,这意味着攻击者将能够注入任意的 SQL 来增强恶意查询的评估方式。

让我们看看实际上是如何使用 drozer 攻击易受 SQL 注入攻击的内容提供者的。

如何操作...

在这个教程中,我将讨论两种 SQL 注入漏洞:一种是 SQL 语句的选择子句可注入,另一种是投影可注入。使用 drozer,查找可注入选择子句的内容提供者非常容易:

dz> run app.provider.query [URI] –-selection "1=1" 

前文提到的方法将尝试向由内容提供者解析的 SQL 语句中注入所谓的逻辑恒真式,最终由数据库查询解析器处理。由于这里使用的模块的性质,你可以判断它是否真的起作用了,因为它应该返回数据库中的所有数据;也就是说,选择子句的条件被应用于每一行,并且因为它总是返回真,所以每一行都会被返回!

你还可以尝试任何总是为真的值:

dz> run app.provider.query [URI] –-selection "1-1=0"
dz> run app.provider.query [URI] –-selection "0=0"

dz> run app.provider.query [URI] –-selection "(1+random())*10 > 1" 

下面是一个故意使用易受攻击的内容提供者的例子:

dz> run app.provider.query content://com.example.vulnerabledatabase.contentprovider/statements –-selection "1=1"

它返回了被查询的整个表,如下面的截图所示:

如何操作...

当然,你也可以在 SELECT 语句的投影部分注入,即语句中 FROM 之前的部分,即 SELECT [投影] FROM [表] WHERE [选择子句]

另请参阅

利用可调试的应用程序

应用程序可以被标记为可调试,以便在进行功能测试和错误跟踪时,允许你在应用程序执行过程中设置断点,从而大大简化这些操作。为此,可以在应用程序在设备上运行时查看虚拟机堆栈并挂起或恢复线程。

不幸的是,谷歌应用商店上的一些应用程序仍然被标记为可调试。这不一定总是世界末日,但如果应用程序希望保护任何认证数据、密码地址或存储在应用程序内存中的任何值,被标记为可调试意味着攻击者可以非常容易地获取这些数据。

本文档讨论了如何从一个可调试的应用程序中泄露变量值。攻击者也可能能够通过应用程序触发远程代码执行,并在应用程序的上下文中运行一些代码。

这里使用的例子是安卓版的华尔街日报应用,在撰写本文时,它是谷歌 Play 商店中作为可调试应用程序发布的应用之一。

如何操作...

你需要做的第一件事是确定应用程序是否可调试。这相当简单,因为一个应用程序是否可调试直接取决于其应用程序清单。在 Android 应用程序清单的应用元素中的debuggable字段。要枚举和利用可调试的应用程序,你需要执行以下步骤:

  1. 要检查一个应用程序是否可调试,你可以提取清单文件,或者从你的 drozer 控制台执行以下命令:

    dz> run app.package.debuggable 
    
    

    这将列出所有设置为可调试的包,并显示它们被授予的权限。以下截图显示了一个包列表:

    如何操作...

    你可能会问自己,像这样的简单漏洞在现实世界中真的存在吗?嗯,是的,实际上仍然存在!以下截图显示了一个相对知名的应用程序,它被发布到谷歌 Play 市场时是可调试的:

    如何操作...

    本例显示.debuggable模块的输出,表明华尔街日报阅读器应用是可调试的。

  2. 一旦你确定了一个好的目标,你应该使用如下命令来启动它:

    dz> run app.activity.start –-component com.example.readmycontacts com.example.readmycontacts.MainActivity 
    
    
  3. 一旦运行起来,你可以使用 ADB 获取为该虚拟机实例打开的 Java 调试协议端口;以下是操作方法:

    adb jdwp
    
    

    你应该会看到如下内容:

    如何操作...

  4. ADB 返回的数字是你可以用来连接到虚拟机的端口,但在你的机器上这样做之前,你需要通过adb转发这个端口;以下是操作方法:

    adb forward tcp:[localport] jdwp:[jdwp port on device]
    
    

    对于截图中给出的例子,你需要执行以下命令来转发端口:

    如何操作...

  5. 现在你可以从你的机器上访问运行这个应用的虚拟机。从这一点开始,你可以依赖 Java 调试器连接到虚拟机;你可以通过运行以下命令来实现:

    jdb –attach localhost:[PORT]
    
    

    你将使用的[PORT]端口是上一步转发的端口;在这个例子中,那将是31337。通过jdb连接的操作如下:

    jdb –attach localhost:31337
    
    

    下面的截图显示了前面命令的输出:

    如何操作...

  6. 然后,你将连接到在 Android 设备上运行这个应用虚拟机;然后你可以执行诸如提取与应用程序编译的类信息等操作;这是通过在jdb会话中执行以下命令完成的:

    classes
    
    

    这将产生类似于以下的输出:

    如何操作...

  7. 你还可以通过执行以下命令枚举每个类的方法:

    > methods [class-path]
    
    

    在前面的命令中,[class-path]是你想要了解的类的完整类路径。

  8. 以下截图演示了针对名为com.example.readmycontacts的应用程序包执行前一个命令的情况。这里我们正在提取有关.MainActivity类的信息,这是调用启动活动的类。如何操作...

  9. 你甚至可以进一步深入,列出给定类的“字段”或类属性名称和值;这是通过在 JDB 内部执行以下命令来完成的:

    > fields [class name ]
    
    

    例如:

    > fields com.example.readmycontacts.MainActivity
    
    

作为一名 Android 应用程序黑客,你为什么会对从类文件中的字段读取值感兴趣?因为开发人员可能会经常将敏感信息显式地存储在类文件中,而不是从云端获取;因此你可以期待在类的字段中保存诸如密码、API 令牌、单点登录令牌、默认用户名以及通常用于身份验证或其他敏感操作的数据。

对于某些 Android 操作系统,特别是任何未打补丁的 Gingerbread 设备及更低版本。这个漏洞可能意味着恶意应用程序能够在另一个应用程序的上下文中执行任意命令。为什么只有 Gingerbread 及更低版本?因为在 Dalvik 虚拟机更新到 Gingerbread 之前,Dalvik 导致可调试的应用程序即使在没有运行 ADB 的情况下也会尝试连接到 Java 调试线协议端口;这意味着能够在目标设备上打开网络套接字的恶意应用程序能够接受来自可调试应用程序的连接,并且因为 Java 调试的工作方式,能够执行任意代码。有关此行为的更多详细信息,请访问Android 市场中的可调试应用程序文章中的链接,以及不同版本的 Dalvik 虚拟机代码的链接。

你还可以使用 Java 调试器做更多的事情;对于那些想了解更多关于它的信息的读者,我在另请参阅部分包含了一些有用的链接。

另请参阅

应用程序中的中间人攻击

手机用户经常在咖啡店、图书馆以及任何可用之处通过公共 Wi-Fi 网络访问互联网。不幸的是,由于某些应用程序的开发方式,它们仍然可能成为中间人MITM)攻击的受害者。对于那些不了解 MITM 攻击的人来说,它们本质上是一种允许攻击者截取你与网络设备通信的攻击;如果你想在非移动环境中了解这些攻击的危险性和技术细节,请查看另请参阅部分的一些链接。

为什么我们应该关注手机上的中间人攻击(MITM)呢?因为,如果不受信任的渠道到网络资源的信任度很高,攻击者可能会做任何事情,从对你的设备上运行的应用程序进行指纹识别,到详细记录你曾经到过的每一个地方,你大概的居住和工作地点,甚至可能控制你手机上的某些应用程序,如果手机安全性不高或可以被 root,甚至可能控制整部手机。一些非常流行的应用程序中存在实际的安全漏洞,这些漏洞可能被中间人攻击所利用;查看另请参阅部分中的链接了解其中一些。

本指南展示了如何在 Android 手机上执行 MITM 攻击,以及一种在 MITM 攻击期间可能使用的简单漏洞利用,即 DNS 投毒。

这里有一个小警告,即执行 MITM 攻击所使用的 Ettercap 工具并没有官方提供任何 Windows 支持版本。不过,如果你没有 Ubuntu 或 Debian Linux 机器,你可以设置一个,只需下载 Ubuntu 的 CD/DVD 镜像并在 Oracle 的 Virtualbox 或 VMware 中使用虚拟机运行。要了解如何安装虚拟机,请查看 第三章 Android Security Assessment Tools安装和设置 Santuko 食谱的 还有更多... 部分。如果你真的想在 Windows 机器上使用 Ettercap,你可以查看 另请参阅 部分中非官方 Windows 二进制文件的下载链接。

准备就绪

为了让整个过程变得更简单,我将向大家展示如何下载一个让 MITM 攻击变得非常简单的工具。你可以使用以下命令下载 Ettercap:

sudo aptitude install ettercap-graphical

下面的截图显示了前面命令的输出:

Getting ready

下载并设置后,你就可以开始 MITM 攻击了。

如何操作...

让我们按照以下步骤开始:

  1. 在我们开始设置 MITM 攻击之前,你需要为 Ettercap 设置 DNS 欺骗插件;你需要做的唯一一件事就是为保存在 Linux 机器上的 /usr/share/ettercap/etter.dns 的 Ettercap DNS 配置脚本添加一些有用的地址。How to do it...

    etter.dns 文件应该看起来像下面这样:

    How to do it...

    编辑这个文件后,它应该看起来像下面这样:

    How to do it...

    地址 192.168.10.102 应该替换成你的机器的互联网地址,因为你想使用你的机器来欺骗 DNS 服务器,这意味着你的机器将充当 DNS 服务器。

  2. 一旦 DNS 插件设置正确,你可以通过从终端或命令提示符执行以下命令来启动 MITM 攻击:

    ettercap –T –I [interface] –M ARP:remote –P dns_spoof /[address of target] /[address of gateway]/
    
    

    在前面的命令中,[interface] 是你用来连接网络的网络接口,可能是以太网或无线接口。[address of target] 是你的安卓设备的互联网地址;你可以在安卓手机的 设置 | Wi-Fi | [网络名称] | IP 地址 下找到这个地址。[address of gateway] 是这个网络的默认网关的互联网地址。这种攻击利用 地址解析协议 (ARP) 缺乏认证的弱点,让你的手机误认为你攻击的机器就是实际的网关。

  3. 例如,如果你的网关 IP 地址是 192.168.10.1 而你的安卓设备 IP 是 192.168.10.106,以下是设置 MITM 攻击的方法:

    sudo ettercap –T –i wlan0 –M ARP:remote –P dns_spoof /192.168.10.1/ /192.168.10.106/
    
    

    你可以互换最后两个地址;只要它们都在,顺序无关紧要。执行这个命令后,你应该能在终端上看到以下内容出现:

    如何操作...

  4. 一段时间后,你应该能看到类似以下截图的内容,这是由 Ettercap 记录的流量:如何操作...

  5. 一旦你开始使用这个“中毒”的网络启动一些应用程序,你就能在攻击者机器上看到一些奇怪的事情发生;例如,你将能够看到你的安卓应用程序发送的 DNS 请求;以下截图显示了 Flipboard 应用程序发送的 DNS 请求:如何操作...

    这个输出是由 Wireshark 生成的。

  6. 如果你的机器上配置了一个网页服务器,你可以通过伪装成 LinkedIn 和 Google 等网站,向你的安卓手机提供一些内容;以下是演示这一点的截图:如何操作...

    这里还有一个例子;以下截图显示了被拦截的www.google.com的请求:

    如何操作...

显然,这些不是 LinkedIn 和 Google 的网页;实际上,这里返回的页面是从本地机器到网络的。这可能是一个非常平凡的演示,但它涵盖了攻击的难点部分,即建立 MITM(中间人攻击)上下文,攻击者能够控制应用程序向互联网发出的响应。

在建立了 MITM 上下文之后,你可以利用移动浏览器,使用诸如 Metasploit 及其browser_autopwn模块之类的东西,或者使用一些社交工程工具来镜像这些网站——社会工程工具包在这方面非常出色。有关这些优秀工具的信息链接,请参阅另请参阅部分。

除了普通的 MITM 攻击之外,还有一些特定于 Android 的 MITM 攻击类别,即针对那些使用未加密的addJavaScriptInterface WebKit 和相关 API 调用的应用程序。关于这个漏洞的更多信息,请参阅Android WebView 的冒险文章和Android 系统中的 WebView 攻击的链接,在另请参阅部分。

另请参阅

第五章:保护应用

在本章中,我们将介绍以下技巧:

  • 保护应用组件

  • 使用自定义权限保护组件

  • 保护内容提供者路径

  • 防范 SQL 注入攻击

  • 应用签名验证(防篡改)

  • 通过检测安装程序、模拟器和调试标志来实现篡改保护

  • 使用 ProGuard 移除所有日志消息

  • 使用 DexGuard 进行高级代码混淆

引言

到目前为止,我们已经了解了如何建立和自定义一个环境,以便发现并利用 Android 应用中的漏洞。在本章中,我们将讨论几种保护技术,使逆向工程师和攻击者更难以进行操作。

开发应用时常见的错误之一是不小心暴露了应用组件。我们将重点关注如何防止组件被暴露并可供其他应用访问。如果需要共享数据,我们还将了解如何通过自定义权限限制访问。

入侵或篡改检测是所有优秀防御系统的基石,为此,我们将尝试检测是否正在发生攻击,以及我们的应用是否在受威胁的状态下运行。

在本章的最后,我们将介绍两个让逆向工程师工作更加困难的技巧。我们将了解如何使用代码混淆以及自定义 ProGuard 配置,从应用中移除所有日志消息并隐藏敏感的 API 调用。

网络传输过程中数据保护的主题在第七章,安全网络中进行讨论,而如何通过加密保护静止数据的安全将在第九章,加密与开发设备管理策略中进行介绍。

保护应用组件

应用组件可以通过正确使用AndroidManifest.xml文件以及在代码层面强制进行权限检查来得到保护。这两个应用安全因素使得权限框架相当灵活,并允许你以非常细致的方式限制访问你组件的应用数量。

你可以采取许多措施来锁定对组件的访问,但在做任何事情之前,你应该确保你了解组件的目的,为什么需要保护它,以及如果恶意应用开始向你的应用发送意图并访问其数据,用户将面临哪些风险。这被称为基于风险的安全方法,建议你在配置AndroidManifest.xml文件并为你的应用添加权限检查之前,首先诚实回答这些问题。

在这个技巧中,我详细列出了一些你可以采取的措施来保护通用组件,无论它们是活动、广播接收器、内容提供者还是服务。

如何操作...

首先,我们需要审查你的 Android 应用程序AndroidManifest.xml文件。android:exported属性定义了一个组件是否可以被其他应用程序调用。如果你的应用程序组件不需要被其他应用程序调用,或者需要显式地防止与 Android 系统其他部分的组件交互—除了应用程序内部的组件—你应在应用组件的 XML 元素中添加以下属性:

<[component name] android:exported="false">
</[component name]>

在这里,[组件名称]可以是活动、提供者、服务或接收器。

它的工作原理…

通过AndroidManifest.xml文件执行权限意味着对不同应用组件类型有不同的含义。这是因为可以用来与它们交互的不同的进程间通信IPC)机制。对于每个应用组件,android:permission属性执行以下操作:

  • 活动: 限制可以成功调用 startActivitystartActivityForResult 的外部应用组件为具有所需权限的组件

  • 服务: 限制可以绑定(通过调用bindService())或启动(通过调用startService())服务的的外部应用组件为具有指定权限的组件

  • 接收器: 限制具有指定权限的外部应用组件向接收器发送广播意图的数量

  • 提供者: 限制对通过内容提供者可访问的数据的访问

每个组件 XML 元素的android:permission属性会覆盖<application>元素的android:permission属性。这意味着,如果你没有为你的组件指定任何所需的权限,并且在<application>元素中指定了一个,它将适用于其中包含的所有组件。尽管通过<application>元素指定权限并不是开发者经常做的事情,因为这样做会影响组件对 Android 系统本身的友好性(也就是说,如果你使用<application>元素覆盖了一个活动的所需权限),主屏幕启动器将无法启动你的活动。话虽如此,如果你足够谨慎,不需要任何未经授权的交互发生在你的应用程序或其组件上,你应该使用<application>标签的android:permission属性。

提示

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

另请参阅

使用自定义权限保护组件

Android 平台定义了一套默认权限,用于保护系统服务和应用程序组件。在很大程度上,这些权限在大多数通用情况下是有效的,但是当在应用程序之间共享定制功能或组件时,通常需要更具体地使用权限框架。这可以通过定义自定义权限来实现。

本教程展示了如何定义你自己的自定义权限。

如何操作…

让我们开始吧!

  1. 在添加任何自定义权限之前,你需要为权限标签声明字符串资源。你可以通过编辑你的应用程序项目文件夹下的 res/values/strings.xml 文件来实现这一点:

    <string name="custom_permission_label">Custom Permission</string>.
    
  2. 通过向你的 AndroidManifest.xml 文件添加以下几行,可以为你的应用程序添加正常保护级别的自定义权限:

    <permission   android:name="android.permission.CUSTOM_PERMISSION"
        android:protectionLevel="normal"
        android:description="My custom permission"
        android:label="@string/custom_permission_label">
    

    我们将在它是如何工作的...部分解释android:protectionLevel属性的含义。

  3. 使用此权限与使用任何其他权限一样;你需要将其添加到应用程序组件的android:permission属性中。对于活动:

    <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. 为了使用控制所有与你的权限相关的路径的读取和写入权限的权限来保护你的内容提供者,你可以在你的 Android 清单中添加以下 provider 元素:

    <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. 自然地,内容提供者会有几个他们希望从中提供内容的内容路径。你可以按照以下方式为它们添加读取和写入权限:

    <provider  
      android:writePermission="[write permission name]"
      android:readPermission="[read permission name]">
    </provider>
    

    android:writePermissionandroid:readPermission 标签用于声明,每当外部应用想要执行任何读取相关(query)或写入相关(updateinsert)的操作时,它们必须拥有指定的权限才能这么做。

    提示

    一个常见的错误是认为授予写入权限会隐式地授予读取权限,但实际上,这不应是默认行为。Android 很好地遵循最佳实践,要求分别声明读取和写入权限。

    下面是来自 Android Google Chrome 应用的实际示例:

    <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. 另一件值得一提的事情是 授权 URI 机制。你可以在提供者级别配置它以应用于所有路径,或者在路径级别配置,这只会影响相关路径。然而,如果在路径级别指定权限而在提供者级别授权 URI,这有点奇怪,因为实际上这意味着没有设置任何权限!完全建议开发人员不要在提供者级别使用授权 URI 权限,而应该按路径使用。所以,只有当你需要确保任何应用在仍具有保护其他路径权限的情况下,能够查询、插入或更新某个特定路径时,才应按以下方式操作:

    <provider ...>
    <grant-uri-permission android:path="[path name]" />
    </provider>
    

    你还可以使用 pathPrefixpathPattern 属性指定一个路径范围,以授权 URI 权限。pathPrefix 将确保授权 URI 机制适用于所有以给定前缀开头的路径。pathPattern 将确保授权 URI 机制适用于所有与给定模式匹配的路径。例如:

    <grant-uri-permission android:path="[path name]" 
                     android:pathPrefix="unsecured"/>
    

    这将应用授权 URI 权限到所有以 "unsecured" 字符串开头的路径,例如:

    • 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. 当我们实例化 RssItemDAO 时,我们使用带有参数化 SQL insert 语句字符串的 insertStatement 对象进行编译。这只需要做一次,并且可以多次重用进行多次插入:

    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 映射到索引 1title 映射到索引 2

  2. 现在,当我们向数据库中插入一个新的 RssItem 对象时,我们按语句中出现的顺序绑定每个属性:

    public long save(RssItem item) {
      insertStatement.bindString(1, item.getContent());
      insertStatement.bindString(2, item.getLink());
      insertStatement.bindString(3, item.getTitle());
      return insertStatement.executeInsert();
    }
    

    请注意,我们调用了executeInsert这个辅助方法,它返回新创建行的 ID。使用SQLiteStatement语句就这么简单。

  3. 下面的代码展示了如何使用SQLiteDatabase.query来获取与给定搜索词匹配的RssItems

    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 的通配符语法来匹配标题列中任何部分的文本。

另请参阅

应用签名验证(防篡改)

安卓安全的核心基石之一是所有应用都必须进行数字签名。应用开发者使用私钥证书的形式对应用进行签名。无需使用证书授权机构,实际上,更常见的是使用自签名证书。

证书通常都有定义的过期日期,而谷歌应用商店要求证书的有效期截止日期在 2033 年 10 月 22 日之后。这突显了我们的应用签名密钥在整个应用生命周期中保持一致的重要性。其中一个主要原因是保护并防止应用升级,除非旧版本和升级后的.apk文件的签名完全相同。

那么,既然已经有了这种验证,为什么还要增加一个签名一致性的检查呢?

攻击者修改你的应用程序的.apk文件的过程会破坏数字签名。这意味着,如果他们想在安卓设备上安装这个.apk文件,就需要使用不同的签名密钥重新签名。这样做可能有各种动机,从软件盗版到恶意软件都有可能。一旦攻击者修改了你的应用,他们可能会通过各种替代应用商店或更直接的途径,如电子邮件、网站或论坛进行分发。因此,本指南的目的是保护我们的应用、品牌和用户免受这种潜在风险。幸运的是,在运行时,安卓应用可以查询PackageManager以获取应用签名。本指南展示了如何将当前应用签名与你所知的应该一致的签名进行比较。

准备就绪

本食谱使用 Keytool 命令行程序,并假定您已经创建了一个包含私钥的 .keystore 文件。如果没有,您可以使用 Eclipse 中的 Android 工具导出向导创建应用签名密钥,或者通过在终端窗口中使用以下命令的 Keytool 程序:

keytool -genkey -v -keystore your_app.keystore
-alias alias_name -keyalg RSA -keysize 2048 -validity 10000

如何操作...

首先,您需要找到证书的 SHA1 签名/指纹。我们将把这个硬编码到应用中,并在运行时与之比较。

  1. 从终端窗口使用 Keytool,您可以输入以下内容:

    keytool -list -v -keystore your_app.keystore
    
    

    系统会提示您输入 keystore 密码。

    Keytool 现在将打印出 keystore 中包含的所有密钥的详细信息。找到您的应用密钥,在证书指纹标题下,您应该看到一个十六进制格式的 SHA1。下面是一个使用示例 keystore 的证书的 SHA1 值样本 71:92:0A:C9:48:6E:08:7D:CB:CF:5C:7F:6F:EC:95:21:35:85:BC:C5

    如何操作...

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

  3. 移除冒号后,您应该得到类似这样的结果:

    private static String CERTIFICATE_SHA1 = "71920AC9486E087DCBCF5C7F6FEC95213585BCC5";
    

    移除冒号的一个快速简便的方法是将哈希复制粘贴到以下网站,并按下验证按钮:

    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. 我们现在比较我们签名的证书的哈希、应用中硬编码的哈希以及当前签名证书的哈希。如果它们相等,我们可以确信应用没有被重新签名:

    CERTIFICATE_SHA1.equalsIgnoreCase(currentSignature);
    

如果一切正常,并且正在运行的是我们签名的 .apk 版本,validateAppSignature() 方法将返回 true。然而,如果有人编辑了 .apk 文件并重新签名,currentSignature 将不会与 CERTIFICATE_SHA1 匹配。所以,validateAppSignature() 将返回 false。

提示

记得确保哈希以大写形式存储,或者使用 String.equalsIgnoreCase() 方法进行比较。

还有更多...

这种技术应被视为足以阻止当前的自动化应用重新打包。然而,了解其局限性是值得的。由于签名证书的哈希值被硬编码在 .apk 文件中,熟练的逆向工程师有可能剖析 .apk 文件并用新证书的哈希替换 SHA1。这使得 verifyAppSignature 调用可以正常通过。此外,verifyAppSignature 的方法调用也可能被完全移除。这两种选项都需要时间和逆向工程技能。

在讨论签名时,我们不能不提到 bug 8219321,它是由 Bluebox 安全在 2013 年 Blackhat USA 上公布的,也被称为 Master Key 漏洞利用。此漏洞此后已被谷歌和 OEM 厂商修复。关于这一漏洞的完整分解和分析可以在www.saurik.com/id/17找到。

对篡改检测的响应

当然,这完全取决于你的应用程序。最明显和简单的解决方案是在启动时检查篡改,如果检测到,可以选择退出应用程序并给用户一条解释原因的消息。此外,你可能还希望了解有关妥协的情况。因此,向你的服务器发送通知是适当的。另外,如果你没有服务器并正在使用像 Google Analytics 这样的分析工具,你可以创建一个自定义的“篡改”事件并报告它。

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

另请参阅

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

在这个食谱中,我们将查看三个额外的检查,这些检查可能表明一个被篡改、被破坏或敌对的环境。这些检查设计在你准备发布时激活。

如何操作...

这些篡改检查可以位于应用程序中的任何位置,但最合理的是让它们从单独的类或父类中的多个位置被调用。

  1. 检测 Google Play 商店是否是安装程序:

      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;
      }
    

它是如何工作的...

检测安装程序是否为 Google Play 商店是一个简单的检查,即安装程序应用的包名与 Google Play 商店的包名是否匹配。具体来说,它会检查安装器的包名是否以com.google.android开头。如果你只通过 Google 商店分发,这是一个有用的检查。

Java 反射 API 使得在运行时检查类、方法和字段成为可能;在这种情况下,它允许我们覆盖那些会阻止普通代码编译的访问修饰符。模拟器检查使用反射来访问隐藏的系统类android.os.SystemProperties。警告一下:使用隐藏 API 可能会有风险,因为它们可能会在 Android 版本之间发生变化。

debuggable被启用时,可以通过 Android 调试桥连接并进行详细的动态分析。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 配置以确保它们能正确运行。

准备就绪

首先,我们将在 Android 应用程序上启用 ProGuard:

  1. 如果你使用带有 Android ADT 插件的 Eclipse 开发应用程序,你需要找到你的工作区,并导航到包含你的应用程序代码的文件夹。找到之后,你应该会看到一个名为project.properties的文本文件:准备就绪

    要启用 ProGuard,你需要确保以下行被取消注释:

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

    这假设你有 Android SDK 的默认文件夹结构,因为之前的配置包括一个静态路径,即/tools/proguard/proguard-android.txt。如果你没有正确的文件夹结构或你没有使用 Eclipse 的 Android Developer's Toolkit 插件,你可以获取proguard-android.txt文件并将其放在应用程序工作文件夹的上一级文件夹中。在这种情况下,你可以按以下方式配置这个目录:

    proguard.config=proguard-android.txt:proguard-project.txt
    
  2. Android Studio 配置需要以下行在你的buildType发布到 Gradle 构建文件中:

    android {
    ...
        buildTypes {
            release {
                runProguard true
                proguardFile file('../proguard-project.txt)
                proguardFile getDefaultProguardFile('proguard-android.txt')
            }
        }
    }
    
  3. 保留对proGuard-android.txt文件的引用很重要,因为它包含了 Android 特定的排除项,如果没有它们,应用程序很可能会无法运行。以下是proguard-android.txt文件的一个摘录,指导 ProGuard 保留在活动中可能被 XML 属性onClick使用的方法:

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

如何操作...

一旦为你的项目启用了 ProGuard,有两个简单的步骤可以确保移除所有的日志消息。

  1. 为了让 ProGuard 成功找到所有的日志语句,我们必须使用一个包装类来包装 Android 日志:

    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-project.txt文件中插入以下自定义 ProGuard 配置:

    -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 中的 Android Tools 导出向导

    • 在项目根目录下的终端窗口中,输入以下命令:

      对于 Antant release

      对于 Gradlegradle assembleRelease

它的工作原理...

当你以发布模式构建应用程序时,构建系统会在取消注释proguard.config属性时检查它,并在打包应用程序(.apk)之前使用 ProGuard 处理应用程序的字节码。

当 ProGuard 处理字节码时,assumeNoeffects属性允许它完全删除这些代码行——在这种情况下,所有相关的android.util.Log方法。使用优化配置和日志包装器,我们让 ProGuard 安全地识别所有对各种android.util.Log方法的调用。启用优化的一个额外好处是,优化代码可以提高混淆因子,使其更难以阅读。

还有更多...

让我们更仔细地看看 ProGuard 的一些输出和限制。

ProGuard 输出

这些是应用 ProGuard 到 Android .apk后的输出文件:

  • mapping.txt:顾名思义,这包含了混淆后的类、字段名和原始名称之间的映射关系,这对于使用伴随工具ReTrace去混淆由混淆应用程序产生的堆栈跟踪/错误报告至关重要

  • Seeds.txt:这列出了没有被混淆的类和成员

  • Usage.txt:这列出了从.apk文件中删除的代码

  • Dump.txt:这描述了.apk文件中所有类文件的内部结构

提示

还值得注意的是,每次构建的输出文件都会被 ProGuard 覆盖。保存每个应用程序版本的mappings.txt文件的副本至关重要;否则,将无法转换堆栈跟踪。

限制

使用 ProGuard 混淆应用程序增加了逆向工程、理解和利用应用程序所需的时间和技能水平。然而,逆向仍然是可能的;因此,这绝不应该成为保护应用程序的唯一手段,而应该是整体安全策略的一部分。

另请参阅

  • 使用 DexGuard 进行高级代码混淆的技巧,其中讨论了 ProGuard 的姊妹产品 DexGuard,用于更深入的 Android 特定混淆

  • Android Developers 网站上的ProGuard工具网页

  • ProGuard 的官方网站在这里

  • 这里的 ProGuard 示例配置

使用 DexGuard 进行高级代码混淆

DexGuard 是一个商业优化和混淆工具,由Eric Lafortune(ProGuard 的开发者)编写。它用于替代 ProGuard。与针对 Java 的 ProGuard 不同,DexGuard 专门针对 Android 资源和 Dalvik 字节码。对于开发者来说,一个关键优势是源代码保持可维护和可测试,而编译后的输出既优化又加固。

通常来说,使用 DexGuard 更为安全,因为它针对 Android 进行了优化,并提供额外的安全功能。在本教程中,我们将基于上一个教程的签名验证检查,实现其中的两个功能:API 隐藏和字符串加密。

  • API 隐藏:这使用反射来伪装对敏感 API 和代码的调用。它非常适合隐藏攻击者想要攻击的关键区域。例如,许可证检查检测将是软件盗版者的目标,因此这是一个需要加强防护的重点区域。当被反编译时,基于反射的调用要难以解读得多。

  • 字符串加密:这会对源代码中的字符串进行加密,防止被逆向工程师查看。这对于隐藏 API 密钥和其他在代码中定义的常量特别有用。

我们使用 API 隐藏将特定的方法调用转换为基于反射的调用。这对于我们想要从攻击者那里隐藏的敏感方法特别有用,在本例中,就是验证签名方法。反射调用由作为字符串存储的类和方法签名组成。我们可以通过使用补充的字符串加密功能来加密这些反射字符串,进一步强化它。这为保护应用程序的敏感区域提供了一种强大的方法,例如篡改检测、许可证检查以及加密/解密。

注意

DexGuard 需要开发者许可证,可在www.saikoa.com/dexguard获取。

准备工作

假设 Android SDK Tools(版本 22 或更高)和 DexGuard 已被下载并解压到可访问的目录。示例将使用/Users/user1/dev/lib/DexGuard/目录,基于 DexGuard 版本 5.3。这里,我们将介绍如何在 Eclipse 中安装 DexGuard,并将其集成到 Ant 和 Gradle 构建系统中。安装后,应用程序将比 ProGuard 具有更高的安全级别。但是,我们将启用一些自定义配置来保护应用程序的敏感区域:

安装 DexGuard Eclipse 插件

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

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

  3. 如果一切顺利,当你在 Android 项目上右键点击时,在 Android 工具菜单中应该会注意到一个新的选项:

    导出优化和混淆应用程序包 (DexGuard)

  4. 你的项目现在将像往常一样编译并构建成一个 .apk 文件;然而,在幕后,DexGuard 将被用来优化和混淆应用程序。

为 Ant 构建系统启用 DexGuard

启用 Ant 很简单。在你的 Android 项目的 local.properties 配置文件中指定 DexGuard 目录。

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

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

为 Gradle 构建系统启用 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. 在你的 Android 项目的根目录中,创建一个名为 dexguard-project.txt 的新文件。

  2. 配置 DexGuard 加密敏感字符串。在这个例子中,我们使用了一个常见的模式来在接口中包含不可变常量,并使用上一食谱中使用的证书哈希,因为即使使用 ProGuard 混淆,这些常量在反编译后也很容易被读取。

  3. Constants 接口中加密特定字符串:

    -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. 最后一步是以发布模式构建/导出,以确保将 DexGuard 保护应用于生成的 .apk 文件:

    • Eclipse: 在项目上右键点击,然后选择 Android Tools | 导出优化和混淆的应用程序包… (DexGuard)

    • Ant: 在项目根目录的终端窗口中运行 ant release 命令

    • Gradle: 在项目根目录的终端窗口中运行 gradle releaseCompile 命令

还有更多...

这是与 ProGuard 的正面比较:

ProGuard DexGuard
缩减 X X
优化 X X
名称混淆 X X
字符串加密 X
类加密 X
反射 X
资产加密 X
资源 XML 混淆 X
转换为 Dalvik X
打包 X
签名 X
篡改检测 X

篡改检测是一个长期以来受到喜爱的方法,它使用一个实用程序库,并基于本章中其他食谱的相同原则工作。它之所以受到青睐,是因为它非常容易实现,因为它只是一行代码。

从 ProGuard 升级到 DexGuard 是无缝的,因为为 ProGuard 定义的任何自定义配置都是完全兼容的。这种兼容性的另一个好处是现有的 ProGuard 支持和专业知识社区。

另请参阅

第六章:逆向工程应用程序

在本章中,我们将涵盖以下内容:

  • 从 Java 编译到 DEX

  • 反编译 DEX 文件

  • 解释 Dalvik 字节码

  • 从 DEX 反编译到 Java

  • 反编译应用程序的本地库

  • 使用 GDB 服务器调试 Android 进程

引言

前一章讨论了应用程序中的缺陷;它们可以在不需要确切了解其开发方式的情况下被利用和发现。尽管有一些关于导致这一具体问题的常见源代码的详细解释,但我们不需要阅读源代码就知道 SQL 注入是可能的。在很大程度上,我们向成功利用迈出的第一步是从一个对应用程序行为实际细节一无所知的背景下来分析应用程序的行为。本章讨论的逆向工程旨在揭示应用程序内部工作的每一个细节,以便利用它。

当逆向工程应用于计算机软件时,它是了解某物如何工作并开发利用或滥用这些信息的方法的过程。例如,阅读内核驱动程序的源代码可能会导致发现潜在的记忆体破坏缺陷,如缓冲区的边界检查不正确。了解这一点可能允许你开发一个利用,鉴于这种漏洞存在的背景。逆向工程是任何安全专家最基本的技术,也是所有真正开发利用的核心。当利用和漏洞在导致成功利用的事件链中的某个地方被开发时,逆向工程就已经发生了。

安卓应用程序与其他计算机软件类型并无不同,因此它们同样可以被逆向工程。为了逆向一个应用程序,需要了解其构建方式、各个部分的作用及其原因。缺乏这些信息会导致无尽的、不眠之夜的模糊测试和暴力破解,而在大多数情况下,最终会以挫败告终。本章讨论了一些你可以用于提取应用程序内部工作信息的技巧,并讨论了一些恶意软件开发者和安全审计师用于滥用和逆向应用程序的新奇技巧。

在我们开始使用这些技巧之前,有一个问题需要被提出:为什么你要逆向工程一个安卓应用程序?

下面是几个回答这个问题的方法:

  • 阅读源代码:通常,许多漏洞对攻击者保持隐藏,仅仅因为它们在应用程序的“黑盒”评估期间没有表现出来。这并不意味着它们不容易受到攻击;引用一句话,“没有证据并不意味着证据不存在!”阅读应用程序的源代码是了解其弱点的最有效方式,而且往往比纯黑盒分析能发现更多的漏洞。阅读源代码仍然是理解应用程序的唯一具体方式;除了源代码,你不能信任任何东西;换句话说,文档在源代码证明之前都是谎言!

  • 泄露信息:应用程序中的一些漏洞并不直接来自代码的行为,而是来自应用程序存储的信息类型,例如静态私钥和密码、电子邮件地址、登录令牌、URI 以及其他敏感内容。破解应用程序可以让你访问其所有秘密。

  • 分析防御机制:通常,应用程序中常见的漏洞以最荒谬的方式进行保护。尽管缓解了常见的攻击路径,但应用程序是否能够抵御某些攻击完全取决于其源代码和配置。通常,如果没有源代码和内部配置,可能极其困难,有时甚至不可能,揭示它是如何自我保护的。阅读同一类别的大量应用程序的源代码,可以让你深入了解保护应用程序的最佳和最差方法,例如登录应用。阅读这些应用的许多源代码可以教你开发者如何创建针对认证暴力攻击、凭证嗅探攻击以及其他登录应用特定防御的防御措施。

  • 分析攻击技术:你可能对找出最新的 Android 恶意软件使用了哪些应用程序和系统级别的漏洞感兴趣。真正了解这一点,并将自己置于 Android 安全研究前沿的唯一方式,就是逆向工程 Android 应用程序。

带着这些目标,让我们开始着手食谱。

从 Java 编译到 DEX

下一份食谱将分解 DEX 文件格式;但在深入了解 DEX 文件之前,先了解将 Java 程序解释/编译为 DEX 程序的过程会很有用。演示从 Java 到 DEX 的编译的关键原因之一是因为本例中使用的文件将在下一份食谱中用来解释 DEX 文件格式。

准备工作

在开始之前,有几样东西你需要准备:

  • Java 开发工具包:我们需要它来编译 Java 代码到 class 文件

  • Android SDK:我们需要这个软件包中的某些工具,以便能够将 Java class 文件转换为 DEX 文件

  • 文本编辑器:我们需要一个文本编辑器,以便我们可以编写一个示例 Java 程序以转换成 DEX 程序

准备好所有这些后,我们可以开始准备一个 DEX 文件的样本。

如何操作...

要将 Java 程序编译成 DEX 程序,你需要执行以下操作:

  1. 打开你的文本编辑器,使用以下代码创建一个文件:

    public class Example{
      public static void main(String []args){
        System.out.printf("Hello World!\n");
      }
    }
    
  2. 将前一个文件保存为Example.java,然后通过在终端或命令提示符中输入以下内容来编译代码:

    javac –source 1.6 –target 1.6 Example.java
    
    
  3. 如果你已经准备好了CLASS文件,现在可以拿出一个名为dx的工具,位于以下路径:

    [SDK path]/sdk/platform-tools/dx
    
    

    如果你拥有 SDK 的 4.4 版本,可以在以下路径找到它:

    /sdk/built-tools/android-[version]/dx
    
    

    如何操作...

  4. 要准备 DEX 文件,你需要执行以下命令:

    [SDK path]/sdk/platform-tools/dx –-dex –-output=Example.dex Example.class
    
    

    如何操作...

    完成这些后,你应在当前目录中拥有一个名为Example.dex的文件;这是Example.class的 DEX 版本。

工作原理...

在第一步中,我们做了 Java 开发人员每天都会做的事情,描述了 Java 对象;我们的对象名为Example

在第二步中,我们将Example.java编译成了类文件。这里发生的情况是,Java 编译器获取我们编写的漂亮的语义代码并将其解析成一系列基于栈的 Java 虚拟机指令。

在第三步中,我们取CLASS文件,以及它的 Java 元数据和基于栈的指令,准备了一组资源、数据结构和 Dalvik 虚拟机能理解的基于寄存器的指令集合。以下是使用的dx命令的分解说明:

  • -dex:这个命令告诉dx你想创建一个 DEX 文件

  • -output=Example.dex:这个指令让dx知道我们希望将输出结果放入名为Example.dex的文件中。

  • Example.class:这是输入文件,即我们在第二步中编译的class文件

反编译 DEX 文件

DEX 文件,或称 Dalvik 可执行文件,是 Java 中CLASS文件在 Android 平台上的等价物。它们包含了定义 Android 应用程序行为的 Java 代码的编译格式。作为即将成为的 Android 安全专家,你自然会感兴趣了解这些文件是如何工作的以及它们究竟有什么用途。对 DEX 文件进行反编译是许多应用程序进行安全评估的一个重要部分;它们提供了关于 Android 应用程序行为的大量信息,并且常常能揭示从源代码视角无法获得的关于应用程序开发的信息。对 DEX 文件格式有一个良好的理解,并知道如何解释它,可能会导致发现新的漏洞,或者针对 Android 平台和 Dalvik 虚拟机的攻击利用的开发与改进。恶意软件可能很快就会开始利用 DEX 文件解释的方式来隐藏与其行为相关的细节。而只有那些真正了解 DEX 文件如何工作的少数有识之士,才能掌握新的 Android 恶意软件混淆技术,并拥有挫败它们所需的技能。本指南包括对 DEX 文件格式的详细分解,描述了 DEX 文件中每个字段的使用和解释方法。然后,它继续讨论如何将 DEX 文件反编译回易于阅读和逆向工程的 Java 源代码。

理解 DEX 文件格式

本指南专门用于分解和描述 DEX 文件每个重要部分。它逐个字段进行讲解,并直接基于用于解释 DEX 文件的 Dalvik 源代码进行分析。

接下来的几段提供了关于 DEX 文件不同部分出现位置的信息,例如在哪里可以找到对可打印字符串的引用,以及每个编译类的实际 DEX 代码的位置。DEX 文件有一个相当简单且易于理解的格式。DEX 文件的结构如下:

struct DexFile {
/* directly-mapped "opt" header */
  const DexOptHeader* pOptHeader;

/* pointers to directly-mapped structs and arrays in base DEX */
  const DexHeader*    pHeader;
  const DexStringId*  pStringIds;
  const DexTypeId*    pTypeIds;
  const DexFieldId*   pFieldIds;
  const DexMethodId*  pMethodIds;
  const DexProtoId*   pProtoIds;
  const DexClassDef*  pClassDefs;
  const DexLink*      pLinkData;
/*
    * These are mapped out of the "auxiliary" section, and may not be
    * included in the file.
*/
  const DexClassLookup* pClassLookup;
  const void*         pRegisterMapPool;       // RegisterMapClassPool

/* points to start of DEX file data */
  const u1*           baseAddr;

/* track memory overhead for auxiliary structures */
  int                 overhead;

/* additional app-specific data structures associated with the DEX */
  //void*               auxData;
};

注意

上述代码可以在github.com/android/platform_dalvik/blob/master/libdex/DexFile.h找到。

DEX 文件头

DEX 文件的第一部分称为 DEX 文件头。以下是根据 Dalvik 虚拟机中的libdex对 DEX 文件头的定义:

struct DexHeader {
  u1  magic[8];           /* includes version number */
  u4  checksum;           /* adler32 checksum */
  u1  signature[kSHA1DigestLen]; /* SHA-1 hash */
  u4  fileSize;           /* length of entire file */
  u4  headerSize;         /* offset to start of next section */
  u4  endianTag;
  u4  linkSize;
  u4  linkOff;
  u4  mapOff;
  u4  stringIdsSize;
  u4  stringIdsOff;
  u4  typeIdsSize;
  u4  typeIdsOff;
  u4  protoIdsSize;
  u4  protoIdsOff;
  u4  fieldIdsSize;
  u4  fieldIdsOff;
  u4  methodIdsSize;
  u4  methodIdsOff;
  u4  classDefsSize;
  u4  classDefsOff;
  u4  dataSize;
  u4  dataOff;
};

数据类型u1u4只是无符号整数类型的别名。以下是 Dalvik 虚拟机本身的Common.h头文件中的类型定义:

  typedef uint8_t             u1; /*8 byte unsigned integer*/
  typedef uint16_t            u2; /*16 byte unsigned integer*/
  typedef uint32_t            u4; /*32 byte unsigned integer*/
  typedef uint64_t            u8; /*64 byte unsigned integer*/
  typedef int8_t              s1; /*8 byte signed integer*/
  typedef int16_t             s2; /*16 byte signed integer*/
  typedef int32_t             s4; /*32 byte signed integer*/
  typedef int64_t             s8; /*64 byte signed integer*/

注意

上述代码可以在github.com/android/platform_dalvik/blob/master/vm/Common.h找到。

这样,我们就可以把预备工作做好。你现在对 DEX 文件的外观有了基本的了解,也对每个部分的位置有了一个基本的掌握。接下来的几段将详细解释每个部分的作用以及 Dalvik 虚拟机是如何使用它们的。

首先,DEX 文件中的第一个字段定义如下:

  u1  magic[8];           /* includes version number */

magic[8]保存一个“标记”,通常称为魔数,它保存了一组 DEX 文件独有的字符。DEX 文件的魔数是dex\n035,或者用十六进制表示,64 65 78 0a 30 33 35 00

下面是classes.dex的截图,显示了魔数以十六进制表示:

DEX 文件头

下一个字段定义如下:

  u4  checksum;           /* adler32 checksum */

下面的截图显示了 DEX 文件中 Adler32 校验和的样子:

DEX 文件头

这个 4 字节字段是整个头的校验和。校验和是对组成头的位执行一系列异或(XORs**)和加法操作的结果。它被检查以确保DexHeader文件内容没有损坏或错误更改。确保这个头没有被破坏非常重要,因为它决定了如何解释 DEX 文件的其他部分,并作为其余解释的路线图。因此,Dalvik 使用DexHeader文件来定位 DEX 文件的其他组件。

下一个字段是一个 21 字节的安全散列算法(SHA)签名,定义如下:

  u1  signature[kSHA1DigestLen]; /* SHA-1 hash length = 20*/

下面的截图显示了 SHA 摘要如何在 DEX 文件中出现:

DEX 文件头

kSHA1DigestLen定义为20,如果你还没猜到的话。这是因为 SHA1 的块长度被标准化为20。根据 Dalvik 代码中的一小段注释,这个摘要用于唯一标识 DEX 文件,并在签名后的 DEX 文件部分计算。计算 SHA 摘要的 DEX 文件部分是指定所有地址偏移和其他大小参数的地方,以及它们所引用的内容。

SHA 摘要字段之后是fileSize字段,定义如下:

  u4  fileSize;/* length of entire file */

下面的截图显示了fileSize字段在 DEX 文件中的样子:

DEX 文件头

fileSize字段是一个 4 字节字段,保存整个 DEX 文件的长度。这个字段用于帮助计算偏移量,轻松定位某些部分。它还帮助唯一标识 DEX 文件,因为它构成了 DEX 文件的一部分,这部分被送入安全散列操作:

  u4  headerSize;/* offset to start of next section */

下面的截图显示了headerSize字段在 DEX 文件中的样子:

DEX 文件头

headerSize保存整个DexHeader结构的大小(以字节为单位),正如注释所提示,它用于帮助计算文件中下一个部分开始的位置。

DEX 文件中的下一个字段是字节序标签,定义如下:

  u4  endianTag;

下面的截图显示了示例classes.dex文件的endianTag字段:

DEX 文件头

endianTag字段保存一个在所有 DEX 文件中相同的静态值。此字段的值12345678用于确保文件以正确的“字节序”或位顺序进行解释。一些架构更喜欢将最有效位向左,而另一些则喜欢向右;这称为架构的字节序。此字段通过允许 Dalvik VM 读取值并检查字段中数字出现的顺序,帮助确定架构使用的是哪一个。

接下来的linkSizelinkOff字段;当多个类文件编译成一个 DEX 文件时使用它们:

  u4  linkSize;
  u4  linkOff;

下一部分是映射部分偏移量,定义如下:

  u4  mapOff;

下一个字段stringIdsSize定义如下:

  u4  stringIdsSize;

DEX 文件头

stringIdsSize字段保存了StringIds部分的大小,其使用方式与其他大小字段相同,有助于计算StringIds部分相对于 DEX 文件开头的起始位置。

下一个字段stringIdsOff定义如下:

  u4  stringIdsOff;

此字段保存到实际stringIds部分的字节偏移量。它帮助 Dalvik 编译器和虚拟机跳转至此部分,而无需进行任何严格计算或反复读取文件以找到stringIds部分。在StringIdsOff字段之后是类型、原型、方法、类和数据 ID 部分的相同偏移量和大小字段——这些属性具有与stringIdsstringIdsOff字段完全相同的大小和偏移字段。这些字段的目的与stringIdsOffstringIdsSize字段相同,除了它们旨在为访问相关部分提供高效和简单的机制。如前所述,这意味着它将归结为要么多次重新读取文件,要么对相对起始地址进行一些简单的加法和减法。以下是大小和偏移字段定义:

  u4  typeIdsSize;
  u4  typeIdsOff;
  u4  protoIdsSize;
  u4  protoIdsOff;
  u4  fieldIdsSize;
  u4  fieldIdsOff;
  u4  methodIdsSize;
  u4  methodIdsOff;
  u4  classDefsSize;
  u4  classDefsOff;
  u4  dataSize;
  u4  dataOff;

所有这些大小和偏移字段保存的值都是要解释为,或包含需要形成 DEX 文件内部位置计算的一部分的值。这是它们都具有相同类型定义的主要原因,即无符号 4 字节整数字段。

StringIds 部分

StringIds部分完全由一系列地址组成——相对于 Dalvik 命名法的识别号——相对于 DEX 文件开头的位置,用于查找在Data部分定义的实际静态字符串的起始位置。根据 Dalvik VM 中的libdexStringIds部分中的字段定义如下:

struct DexStringId {
  u4 stringDataOff;      /* file offset to string_data_item */
};

所有这些定义都表明,每个字符串 ID 只是一个无符号的 4 字节字段,这并不奇怪,因为它们都是像DexHeader部分中找到的偏移值。以下是从一个示例classes.dex文件中的StringIds部分的截图:

StringIds 段

在前面的截图中,突出显示的值是之前提到过的地址,或来自StringIDs段的值。如果你取其中一个值,用正确的字节序读取它们,并跳到 DEX 文件中这个值的偏移所在的段,你最终会进入一个看起来像下面截图的段:

StringIds 段

如你所见,读取00 00 01 8a的样本值,由于文件格式的字节序,实际上指向了 DEX 文件中的字符串。以下截图显示了 DEX 文件中偏移0x018a位置的内容:

StringIds 段

如你所见,位置0x018a包含的值3c 69 6e 69 74 3e 00实际上是<init>的十六进制等价。

这基本上是编译器、反编译器和 Dalvik VM 在查找字符串值时所经历的过程。以下是来自libdex的代码提取,它正是这样做的:

DEX_INLINE const char* dexGetStringData(const DexFile* pDexFile,
  const DexStringId* pStringId) {
    const u1* ptr = pDexFile->baseAddr + pStringId->stringDataOff;

    // Skip the uleb128 length.
    while (*(ptr++) > 0x7f) /* empty */ ;

    return (const char*) ptr;
}

注意

 shows how the arguments are parsed and how the file data is used. It is as follows:
void dexFileSetupBasicPointers(DexFile* pDexFile, const u1* data){
    DexHeader *pHeader = (DexHeader*) data;

    pDexFile->baseAddr = data;
    pDexFile->pHeader = pHeader;
    pDexFile->pStringIds = (const DexStringId*) (data + pHeader-      >stringIdsOff);
...some code has been omitted for brevity
}

注意

前面的代码可以在github.com/android/platform_dalvik/blob/master/libdex/DexFile.cpp(第 269-274 行)找到。

通过名为data的指针解引用的字符数组是 DEX 文件的实际内容。前面的代码片段应该有效地展示了如何使用DexHeader字段找到 DEX 文件中的不同位置;代码中某些部分被突出显示以展示这一点。

TypeIds

接下来是TypeIds段。这个段包含了关于如何找到每种类型的字符串标签的信息。在我们了解这是如何工作的之前,让我们先看看TypeIds是如何定义的:

struct DexTypeId {
  u4  descriptorIdx;      /* index into stringIds list for type descriptor */
};

注意

前面的代码可以在github.com/android/platform_dalvik/blob/master/libdex/DexFile.h(第 270-272 行)找到。

如注释所述,这个值持有一个 ID,或者更确切地说,是StringIds段中某物的索引,这是被描述类型的字符串标签。以下是一个从TypeIds段中定义的第一个值——示例值的例子:

TypeIds 段

像之前一样,这个值被读取为03。像之前一样,我们需要考虑文件的字节序,这是StringIds段中一个值的索引,具体来说,是StringIds段中第四个定义的字符串 ID。如下所示:

TypeIds 段

第四个定义的值是0x01af,它进而对该值在数据段中的偏移进行解引用:

TypeIds 段

在上一个截图中,我们可以看到值LExample,这可能会让人有点困惑,因为我们明确将我们的类定义为ExampleL是什么意思?这个字符串实际上是按照 Dalvik 类型描述语言对类型的描述,这与 Java 的方法、类型和类签名非常相似。实际上,它的工作方式完全一样。关于 Dalvik 的类型、方法和其他描述或签名的完整分解可以在source.android.com/devices/tech/dalvik/dex-format.html找到。在我们的例子中,类名前的L值表示Example是一个类或对象的描述名称。当 Dalvik 编译器和虚拟机查找和构建类型时,它们遵循相同的基本过程。现在我们理解了这一部分是如何工作的,我们可以继续下一部分,即ProtoIds部分。

ProtoIds 部分

ProtoIds部分保存了一组原型 ID,用于描述方法;它们包含有关每个方法的返回类型和参数的信息。以下是你在libdex文件中看到的命令:

struct DexProtoId {
  u4  shortyIdx;          /* index into stringIds for shorty descriptor */
  u4  returnTypeIdx;      /* index into typeIds list for return type */
  u4  parametersOff;      /* file offset to type_list for parameter types */
};

结构非常容易理解。名为shortyIdx的无符号 4 字节字段保存了一个字符串 ID 的索引,该字符串 ID 在StringIds部分中定义,用于给出原型的简短描述;这个描述几乎与 Dalvik 中的类型描述一样工作。如果你还没猜到,returnTypeIdx保存了一个索引,该索引解引用了TypeIds部分中的一个值。这是返回类型的描述。最后,parametersOff保存了方法参数列表的地址偏移量。以下是从Example.dex中的ProtoIds部分的示例。这是我们示例 DEX 文件中ProtoIds部分的样子:

ProtoIds 部分

FieldIds 部分

FieldIds部分与其他部分类似,由一组引用StringIdsTypeIds的字段组成,但专门用于描述类中的字段。以下是来自libdex的 DEX 文件FieldIds的官方定义:

struct DexFieldId {
  u2  classIdx;           /* index into typeIds list for defining class */
  u2  typeIdx;            /* index into typeIds for field type */
  u4  nameIdx;            /* index into stringIds for field name */
};

注意

上述代码可以在github.com/android/platform_dalvik/blob/master/libdex/DexFile.h#L277找到。

我们在这里可以看到三个字段构成了类型的描述,分别是它所属的类(由classIdx字段中的类 ID 标识)、字段的类型(如stringintbool等,详细在TypeId中,并通过typeIdx变量中保存的值进行解引用),以及类型的名称,即我们之前讨论过的规范中的定义。这个值,与所有字符串值一样,存储在数据部分,并通过StringIds部分中存储在nameIdx中的值进行解引用。以下是我们FieldIds部分的截图:

字段 ID 部分

让我们继续下一部分,即MethodIds部分。

方法 ID 部分

每个方法 ID 的字段定义如下:

struct DexMethodId {
  u2  classIdx;           /* index into typeIds list for defining class */
  u2  protoIdx;           /* index into protoIds for method prototype */
  u4  nameIdx;            /* index into stringIds for method name */
};

注意

上述代码可以在github.com/android/platform_dalvik/blob/master/libdex/DexFile.h#L286找到。

方法所属的类通过classIdx字段中存储的值来解除引用。这完全与TypeIds部分的方式相同。此外,每个方法都有一个原型引用与之关联。这保存在protoIdx变量中。最后,nameIdx变量存储了对构成方法定义的字符的引用。以下是我们Example.dex文件中方法定义的一个示例:

(Ljava/lang/String;)V

理解上述定义的最佳方式是从右向左阅读。分解这个定义,它读作如下:

  • V: 这表示 void 类型,即方法的返回类型。

  • (): 这表示接下来将指定方法参数的类型规范。

  • java/lang/String;: 这是String类的标识符。这里,第一个也是唯一的参数是一个字符串。

  • L: 这表明紧跟此字符的类型是一个类。

  • [: 这表明紧跟此字符的类型是指定类型的数组。

因此,将这些信息综合起来,该方法返回 void,并接受来自String类的对象数组。

这是我们的示例中MethodIds部分的屏幕截图:

![方法 ID 部分

类定义部分

ClassDefs部分定义如下:

struct DexClassDef {
  u4  classIdx;           /* index into typeIds for this class */
  u4  accessFlags;
  u4  superclassIdx;      /* index into typeIds for superclass */
  u4  interfacesOff;      /* file offset to DexTypeList */
  u4  sourceFileIdx;      /* index into stringIds for source file name */
  u4  annotationsOff;     /* file offset to annotations_directory_item */
  u4  classDataOff;       /* file offset to class_data_item */
  u4  staticValuesOff;    /* file offset to DexEncodedArray */
};

这些字段相当容易理解,从classIdx字段开始,正如注释所暗示的,它在TypeIds部分中保存一个索引,表示文件类型。AccessFlags字段保存一个数字,表示其他对象如何访问此类,并描述了其某些用途。以下是标志定义的方式:

enum {
  ACC_PUBLIC       = 0x00000001,       // class, field, method, ic
  ACC_PRIVATE      = 0x00000002,       // field, method, ic
  ACC_PROTECTED    = 0x00000004,       // field, method, ic
  ACC_STATIC       = 0x00000008,       // field, method, ic
  ACC_FINAL        = 0x00000010,       // class, field, method, ic
  ACC_SYNCHRONIZED = 0x00000020,       // method (only allowed on natives)
  ACC_SUPER        = 0x00000020,       // class (not used in Dalvik)
  ACC_VOLATILE     = 0x00000040,       // field
  ACC_BRIDGE       = 0x00000040,       // method (1.5)
  ACC_TRANSIENT    = 0x00000080,       // field
  ACC_VARARGS      = 0x00000080,       // method (1.5)
  ACC_NATIVE       = 0x00000100,       // method
  ACC_INTERFACE    = 0x00000200,       // class, ic
  ACC_ABSTRACT     = 0x00000400,       // class, method, ic
  ACC_STRICT       = 0x00000800,       // method
  ACC_SYNTHETIC    = 0x00001000,       // field, method, ic
  ACC_ANNOTATION   = 0x00002000,       // class, ic (1.5)
  ACC_ENUM         = 0x00004000,       // class, field, ic (1.5)
  ACC_CONSTRUCTOR  = 0x00010000,       // method (Dalvik only)
  ACC_DECLARED_SYNCHRONIZED =
  0x00020000,       // method (Dalvik only)
  ACC_CLASS_MASK =
  (ACC_PUBLIC | ACC_FINAL | ACC_INTERFACE | ACC_ABSTRACT
  | ACC_SYNTHETIC | ACC_ANNOTATION | ACC_ENUM),
  ACC_INNER_CLASS_MASK =
  (ACC_CLASS_MASK | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC),
  ACC_FIELD_MASK =
  (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL
  | ACC_VOLATILE | ACC_TRANSIENT | ACC_SYNTHETIC | ACC_ENUM),
  ACC_METHOD_MASK =
  (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL
  | ACC_SYNCHRONIZED | ACC_BRIDGE | ACC_VARARGS | ACC_NATIVE
  | ACC_ABSTRACT | ACC_STRICT | ACC_SYNTHETIC | ACC_CONSTRUCTOR
  | ACC_DECLARED_SYNCHRONIZED),
};

superClassIDx字段还包含一个指向TypeIds部分中类型的索引,用于描述超类的类型。SourceFileIDx字段指向StringIds部分,并允许 Dalvik 查找此类实际源代码的位置。对于classDef结构来说,另一个重要的字段是classdataOff,它指向 Dalvik 文件内部的一个偏移量,描述了类的更多重要属性,即代码的位置以及代码量。classDataOff字段指向包含以下结构的偏移量:

/* expanded form of class_data_item. Note: If a particular item is
 * absent (e.g., no static fields), then the corresponding pointer
 * is set to NULL. */
struct DexClassData {
  DexClassDataHeader header;
  DexField*          staticFields;
  DexField*          instanceFields;
  DexMethod*         directMethods;
  DexMethod*         virtualMethods;
};

DexClassDataHeader文件包含有关类的元数据,即静态字段、实例字段、直接方法和虚拟方法的大小。Dalvik 使用这些信息来计算每个方法可以访问的内存大小的重要参数,并且这也是检查字节码所需信息的一部分。这里一个有趣的字段组是DexMethod,定义如下:

struct DexMethod {
  u4 methodIdx;    /* index to a method_id_item */
  u4 accessFlags;
  u4 codeOff;      /* file offset to a code_item */
};

这个组包含了指向组成类的实际代码的引用。代码偏移量保存在codeOff字段中;methodIdaccessFlags字段也是结构的一部分。

既然我们已经讨论了在普通 DEX 文件中大多数事物是如何结合在一起的,我们可以继续使用一些自动化工具来进行反编译。

准备工作

在开始反编译之前,你需要确保已经安装了几种工具,即安卓 SDK。

如何操作…

现在你已经理解了 DEX 文件的格式和结构,你可以按照以下步骤使用dexdump工具进行反编译。

安卓 SDK 包含一个名为dexdump的工具,它位于 SDK 的sdk/build-tools/android-[version]/dexdump文件夹下。要反编译 DEX 文件,只需将其作为参数传递给dexdump。以下是操作方法:

[SDK-path]/build-tools/android-[version]/dexdump classes.dex

在这里,[SDK-path]指的是你的 SDK 路径,而classes.dex是你想要解析的 DEX 文件。对于我们之前的示例,你可以执行以下命令来编译我们在之前章节中的 Java 代码文件:

[SDK-path]/build-tools/android-[version]/dexdump Example.dex

我们示例的输出如下所示:

如何操作…

还有更多内容...

安卓 SDK 还有一个名为dx的工具,能够以更接近 DEX 文件格式的方式分解 DEX 文件。你很快就会明白为什么:

还有更多...

不幸的是,dx只针对CLASS文件进行操作,通过将它们编译成 DEX 文件然后执行指定操作。因此,如果你有一个想要操作的CLASS文件,你可以执行以下命令来查看相应 DEX 文件的内容和语义结构:

dx –dex –verbose-dump –dump-to=[output-file].txt [input-file].class

dx可以在 Android SDK 包的sdk/build-tools/android-[version]/路径下找到:

还有更多...

对于我们的示例,即Example.class,输出将如下所示:

000000: 6465 780a 3033|magic: "dex\n035\0"
000006: 3500          |
000008: 3567 e33f     |checksum
00000c: b7ed dd99 5d35|signature
000012: 754f 9c54 0302|
000018: 62ea 0045 3d3d|
00001e: 4e48          |
000020: 1003 0000     |file_size:       00000310
000024: 7000 0000     |header_size:     00000070
000028: 7856 3412     |endian_tag:      12345678
00002c: 0000 0000     |link_size:       0
000030: 0000 0000     |link_off:        0
000034: 7002 0000     |map_off:         00000270
000038: 1000 0000     |string_ids_size: 00000010
00003c: 7000 0000     |string_ids_off:  00000070
000040: 0800 0000     |type_ids_size:   00000008
000044: b000 0000     |type_ids_off:    000000b0
000048: 0300 0000     |proto_ids_size:  00000003
00004c: d000 0000     |proto_ids_off:   000000d0
000050: 0100 0000     |field_ids_size:  00000001
000054: f400 0000     |field_ids_off:   000000f4
000058: 0400 0000     |method_ids_size: 00000004
00005c: fc00 0000     |method_ids_off:  000000fc
000060: 0100 0000     |class_defs_size: 00000001
000064: 1c01 0000     |class_defs_off:  0000011c
000068: d401 0000     |data_size:       000001d4
00006c: 3c01 0000     |data_off:        0000013c
                      |
                      |

输出左侧的列以十六进制详细列出了文件偏移量及其内容。右侧的列则包含了语义值以及每个偏移量和值的解释。

请注意,为了简洁起见,部分输出已被省略;只包含了包含DexHeader文件在内的部分。

另请参阅

解释 Dalvik 字节码

你可能已经了解到,Dalvik VM 在结构和操作上与 Java VM 略有不同;其文件和指令格式也有所区别。Java VM 是基于栈的,这意味着字节码(之所以这样命名,是因为每条指令都是一个字节长)通过在栈上推入和弹出指令来工作。Dalvik 字节码被设计成类似于 x86 指令集;它还使用了一种类似 C 语言风格的调用约定。你很快就会看到每种调用方法是如何在调用另一个方法之前负责设置参数的。有关 Dalvik 代码格式的设计和一般注意事项的更多详细信息,请参阅另请参阅部分中名为 General Design—Bytecode for the Dalvik VM, Android Open Source project 的条目。

解释字节码意味着实际上能够理解指令格式是如何工作的。这一节旨在为你提供理解 Dalvik 字节码所需的参考和工具。让我们深入研究字节码格式,了解其工作原理以及所有这些都意味着什么。

理解 Dalvik 字节码

在深入字节码的具体内容之前,了解一些背景知识是很重要的。我们需要了解字节码是如何执行的。这将帮助你理解 Dalvik 字节码的属性,并确定在给定执行上下文中,了解一个字节码是什么与它意味着什么之间的区别,这是一项非常有价值的技能。

Dalvik 虚拟机逐个执行方法,必要时在方法间进行分支跳转,例如当一个方法调用另一个方法时。每个方法可以被视为 Dalvik VM 执行的独立实例。每个方法都有一个私有的内存空间,称为栈帧,它包含足够的空间以容纳执行该方法所需的数据。每个栈帧还包含对 DEX 文件的引用;自然地,方法需要这个引用以便引用 TypeIds 和对象定义。它还持有一个程序计数器实例的引用,这是一个控制执行流程的寄存器,可用于跳转到其他执行流程。例如,在执行 "if" 语句时,根据比较结果,方法可能需要在不同的代码部分之间跳转。栈帧还包含称为寄存器的区域,这些寄存器用于执行诸如加、乘、移动值等操作,有时这也意味着将参数传递给其他方法,如对象构造函数。

字节码由一系列操作符和操作数组成,每个操作符对其提供的操作数执行特定操作。一些操作符还概括了复杂的操作,如调用方法。这些操作符的简单和原子性是它们如此健壮、易于阅读和理解,并支持像 Java 这样复杂的高级语言的原因。

关于 Dalvik 需要注意的一个重要事项,与所有中间代码表示一样,是 Dalvik 字节码的操作数的顺序。对于相关操作,操作的目标总是出现在源操作数之前。例如,以下操作的顺序:

move vA,vB

这意味着寄存器 B 的内容将被放置在寄存器 A 中。这种顺序的流行术语是"目标-然后-源";这意味着操作结果的 目标首先出现,然后是指定源的 操作数。

操作数可以是寄存器,每个方法(独立执行的实例)都有一组寄存器。操作数还可以是字面值(指定大小的有符号/无符号整数)或给定类型的实例。对于如字符串这样的非原始类型,字节码会引用在 TypeIds 部分定义的类型。

有多种指令格式决定了给定操作码可以使用多少寄存器和类型实例作为参数。你可以在source.android.com/devices/tech/dalvik/instruction-formats.html找到这些详细信息。阅读这些定义是非常值得的,因为 Dalvik 指令集中的每个操作码及其详细信息仅是操作码格式的一种实现。尝试理解格式 ID,因为它们在阅读指令格式时非常有用。

在了解了基础知识之后,相信你已经至少浏览了操作码和操作码格式,我们可以继续以使字节码具有语义性的方式来转储它。

准备工作

在开始之前,你需要一个名为 baksmali 的 Smali 反编译器。为了方便起见,我们将介绍如何设置你的路径变量,以便你可以从计算机的任何位置使用 baksmali 脚本和 JAR 文件,而无需每次都明确引用它。以下是设置方法:

  1. code.google.com/p/smali/downloads/list,或新仓库bitbucket.org/JesusFreke/smali/download获取 baksmali JAR 文件的副本。特别寻找baksmali[version].jar文件——其中[version]是最新可用的版本。

  2. 将其保存在一个方便命名的目录中,因为需要下载的两个文件在同一个目录中会让事情简单得多。

  3. 下载 baksmali 包装脚本;它允许你避免每次需要运行 baksmali JAR 时都显式调用java –jar命令。你可以在code.google.com/p/smali/downloads/list或新仓库bitbucket.org/JesusFreke/smali/downloads获取该脚本的副本。将其保存在与 baksmali JAR 文件相同的目录下。此步骤不适用于 Windows 用户,因为这是一个 bash 脚本文件。

  4. 将 baksmali jar 文件的名称更改为baksmali.jar,省略版本号,以便你在步骤 2 中下载的包装脚本能够找到它。你可以在 Linux 或 Unix 机器上使用以下命令来更改名称:

    mv baksmali-[version-number].jar baksmali.jar
    
    

    你也可以使用你的操作系统使用的任何窗口管理器;只要将名称更改为baksmali.jar,你就是正确操作的!

  5. 然后,你需要确保 baksmali 脚本可执行。如果你使用的是 Unix 或 Linux 操作系统,可以通过以下命令来设置:

    chmod +x 700 baksmali
    
    
  6. 将当前文件夹添加到你的默认PATH变量中。

    完成这些后,你可以反编译 DEX 文件了!查看下一节了解如何操作。

如何操作...

现在,你已经下载并设置好了 baksmali,想要将一些 DEX 文件反编译成语义丰富的 smali 语法;以下是操作方法。

从你的终端或命令提示符执行以下命令:

baksmali [Dex filename].dex

如何操作...

这个命令将输出 DEX 文件的内容,就像是一个被解压的 JAR 文件,但所有的源文件都是.smali文件,包含了一种名为 smali 的语义 Dalvik 字节码的轻微翻译或方言:

如何操作...

让我们看一下由 baksmali 生成的 smali 文件,并了解每条字节码指令的含义。代码如下:

.class public LExample;
.super Ljava/lang/Object;
.source "Example.java"

# direct methods
.method public constructor <init>()V
    .registers 1

    .prologue
    .line 1
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

.method public static main([Ljava/lang/String;)V
    .registers 4

    .prologue
    .line 3
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

    const-string v1, "Hello World!\n"

    const/4 v2, 0x0

    new-array v2, v2, [Ljava/lang/Object;

    invoke-virtual {v0, v1, v2}, Ljava/io/PrintStream;->printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;

    .line 4
    return-void
.end method

请注意,由于 baksmali、Android 的 Dalvik 虚拟机以及 Java 语言在持续改进,你可能会看到与之前代码示例略有不同的结果。如果你遇到了这种情况,不要慌张;之前的示例代码仅用于供你学习参考。你仍然可以将本章的信息应用到 baksmali 生成的代码中,其前几行如下所示:

.class public LExample;
.super Ljava/lang/Object;
.source "Example.java"

这些只是关于实际被反编译的类的元数据;它们提到了类名、源文件和超类(这个方法继承的类)。你可能从Example.java的代码中注意到,我们从未显式地从另一个类继承,尽管在反编译时,Example.java似乎有一个父类:这是如何可能的?因为所有 Java 类都隐式地从java.lang.Object继承。

接下来,下一组行更有趣。它们是Example.java构造函数的 smali 代码:

# direct methods
.method public constructor <init>()V
    .registers 1

    .prologue
    .line 1
    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    return-void
.end method

第一行,.method public constructor <init>()V,是接下来要声明的方法。它表示名为init的方法返回 void 类型,并且具有 public 访问标志。

接下来包含代码的那一行,即:

.registers 1

这表示该方法只使用了一个寄存器。方法在运行前会知道需要多少个寄存器。我稍后会提到它需要的那个寄存器。接下来是一行看起来像以下代码的行:

.prologue

这声明了接下来的方法是prologue,这是每个 Java 方法都有的。它确保调用继承形式的方法(如果有)。这解释了为什么下一行,包含以下代码,似乎调用了另一个名为init的方法:

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

但这次它从java.lang.Object类中取消引用。这里的invoke-direct方法接受两个参数:p0寄存器和需要调用的方法的引用。这由Ljava/lang/Object;-><init>()V标签指示。invoke-direct操作码的描述如下:

"invoke-direct用于调用一个非静态的直接方法(一个本质上不可覆盖的实例方法,要么是一个private实例方法,要么是一个构造函数)。"

注意

有关摘录可以在 source.android.com/devices/tech/dalvik/dalvik-bytecode.html 找到。

因此总结一下,它所做的就是调用 java.lang.Object 类的构造函数,这是一个非静态直接方法。

让我们继续看 smali 代码的下一行:

return-void

它所做的正如它看起来那样,即返回一个 void 类型并退出当前方法,将执行流程返回到调用它的方法。

根据官方网站的定义,这个操作码是“从一个 void 方法返回”。

这并没有什么复杂的。接下来以句点(".")开头的行,像其他行一样,是一段元数据,或者是由 smali 反编译器添加的脚注,以帮助添加关于代码的一些语义信息。.end 方法行标记了此方法的结束。

主方法的代码如下。在这里,你会看到一些将反复出现的代码形式,即当参数传递给方法时以及调用它们时生成的代码。由于 Java 是面向对象的,当你调用另一个对象的方法时,你做的很多事情就是传递参数和从一种对象类型转换为另一种。因此,一个好主意是学习如何通过将执行这些操作的 Java 代码反编译为 smali 代码来识别这种情况。主方法的代码如下:

.method public static main([Ljava/lang/String;)V
    .registers 4

    .prologue
    .line 3
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

    const-string v1, "Hello World!\n"

    const/4 v2, 0x0

    new-array v2, v2, [Ljava/lang/Object;

    invoke-virtual {v0, v1, v2}, Ljava/io/PrintStream;->printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;

    .line 4
    return-void
.end method

根据第一行 .method public static main([Ljava/lang/String;)V,该方法接受 java.lang.String 类型的数组并返回 void,如下所示:

([Ljava/lang/String;)V

接下来看方法名称,它还指出主方法是静态的,并且具有 public 访问标志。

在方法头之后,我们看到以下代码片段,它表明正在形成 sget-object 操作:

sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;

根据官方网站的描述,这个操作码是“使用指定的静态字段执行标识的对象静态字段操作,在值寄存器中进行加载或存储”。

根据官方文档,sget-object 操作接受两个参数:

  • Dalvik 将使用一个寄存器来存储操作结果

  • 用于存储在所述寄存器中的对象引用

这实际上是在获取一个对象实例并将其存储在寄存器中。在这里,这个寄存器是第一个名为 v0 的寄存器。下一行如下所示:

const-string v1, "Hello World!\n"

之前的代码展示了 const-string 指令的作用。它的作用是获取一个字符串并将其保存在由第一个参数指示的寄存器中。这个寄存器是主方法框架中的第二个寄存器,名为 v1。根据官方网站,const-string 操作码的定义是“将指定索引的字符串引用移动到指定寄存器中”。

如果这里不明显,那么正在获取的字符串是 "Hello World\n"。

接下来,下一行也是const操作码家族的一部分,在这里被用来将0值移入名为v2的第三个寄存器:

const/4 v2, 0x0

这看起来可能有些随意,但下一行你会明白为什么它需要在v2寄存器中有一个0值。下一行的代码如下:

new-array v2, v2, [Ljava/lang/Object;

新数组的操作是构建一个给定类型和大小的数组,并将其保存在最左边的第一个寄存器中。在这里这个寄存器是v2,所以执行完这个操作码后,v2将保存一个类型为java.lang.Object且大小为0的数组;这是操作码第二个参数中v2寄存器的值。这也使得在执行此操作码之前将0值移入v2的操作变得清晰。根据官方网站的定义,这个操作码是“构建一个指定类型和大小的新的数组。类型必须是数组类型。”

下一行包含一个非常常见的操作码;确保你了解这个操作码家族的工作原理,因为你将会经常看到它。继续,下一行如下:

invoke-virtual {v0, v1, v2}, Ljava/io/PrintStream;->printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;

根据官方网站的定义,invoke-virtual操作码是“用于调用一个普通的虚拟方法(一个不是privatestaticfinal的方法,也不是构造函数)。”

invoke-virtual方法的参数如下工作:

invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB

其中vCvDvEvFvG是用于传递参数给被调用方法的参数寄存器,由最后一个参数meth@BBBB进行解引用。这意味着它接受一个 16 位的 方法引用,因为每个B字段表示一个 4 位的字段。总之,这个操作码在我们的Example.smali代码中所做的是调用一个名为java.io.PrintStream.printf的方法,该方法接受一个类型为java.lang.Object的数组和一个java.lang.String对象,并返回一个类型为java.io.PrintStream的对象。

就这样!你刚刚解读了一些 smali 代码。要习惯阅读 smali 代码需要一些练习。如果你想了解更多,请查看另请参阅部分中的参考资料。

另请参阅

将 DEX 反编译为 Java

我们知道,DEX 代码是从 Java 编译而来的,Java 是一种相当语义化、易于阅读的语言,现在肯定有人想知道是否有可能将 DEX 代码反编译回 Java?好消息是,这是可能的,当然,这取决于你使用的反编译器的质量和 DEX 代码的复杂性。这是因为除非你真正理解 DEX 代码是如何工作的,否则你将始终受制于你的 DEX 反编译器。有很多方法可以干扰流行的反编译器,比如反射和非标准的 DEX 操作码变体,所以如果你希望这个方法意味着即使你不能阅读 DEX 代码,你也可以称自己为 Android 逆向工程师,那么你就错了!

话说回来,大多数 Android 应用中的 DEX 代码都是相当标准的,我们即将使用的反编译器可以处理一般的 DEX 文件。

准备工作

在开始之前,你需要从互联网上获取一些工具。

  • Dex2Jar:这是一个从 APK 文件中提取 DEX 文件并输出包含相应类文件的 JAR 的工具;你可以在code.google.com/p/dex2jar/获取它。访问这个 URL 并下载适合你操作系统的版本。

  • JD-GUI:这是一个 Java 类文件反编译器,你可以在jd.benow.ca/获取它。它支持 Linux、Mac 和 Windows 系统。

如何操作...

要将一个 DEX 文件样例反编译成 Java 代码,你需要执行以下步骤:

  1. 假设我们从 APK 或 DEX 文件开始。在这种情况下,你需要先将 DEX 文件转换为 Java 的CLASS文件。以下是使用Dex2jar进行转换的方法:

    dex2jar [Dex file].dex
    
    

    对于我们的示例,你会执行以下语句:

    dex2jar Example.dex
    
    

    输出应该看起来像以下截图:

    如何操作...

    如果你正确执行了这些步骤,你应在工作目录或当前目录中拥有一个名为Example_dex2jar.jar的文件:

    如何操作...

  2. 所以现在我们已经有了我们的类文件,我们需要将它们转换回 Java 代码。JD-GUI是我们将用来解决问题的工具。要启动JD-GUI,你只需要执行JD-GUI工具附带的JD-GUI可执行文件。以下是 Linux 上的操作方法;从你的终端执行以下命令:

    jd-gui
    
    

    它应该会生成一个看起来像以下截图的窗口:

    如何操作...

  3. 当这个窗口出现时,你可以通过点击文件夹图标来打开一个类文件;接下来应该会出现以下文件选择对话框:如何操作...

    一旦这个对话框打开,你应该导航到包含我们从Example.dex文件解析出的Example.class文件的路径。如果你能找到它,JD-GUI将如下显示代码:

    如何操作...

  4. 你可以使用JD-GUI保存源文件;你需要做的就是在工具栏上点击文件菜单,选择保存所有源文件,然后提供一个目录来保存它:如何操作...

反编译应用程序的原生库

反编译 Android 原生库相当简单;毕竟,它们只是从 ARM 平台编译的 C/C++目标文件和二进制文件。因此,反编译它们只需找到一个像 Linux 中“非常流行”的objdump这样的反编译器,它可以处理 ARM 二进制文件,而正如我们所发现的,Android NDK 已经为我们解决了这个问题。

在深入了解这个过程之前,你需要确保你有正确的工具。

准备工作

为这个食谱做准备只需确保你有 Android NDK 包的最新副本;你可以在这里获取一份副本。

如何操作...

反编译原生库只需调用 Android NDK 工具链提供的工具之一,即objdump;它已经预编译,包含了允许objdump解释特定于 ARM 二进制文件的字节序和代码结构的所有插件。

要反编译一个 Android 原生库,你需要从终端或命令提示符执行以下命令:

arm-linux-androideabi-objdump –D [native library].so

这是一个示例:

如何操作...

其中arm-linux-androideabi-objdump位于 Android NDK 的toolchains/arm-linux-androideabi-[version]/prebuilt/[arch]/bin/文件夹下,其中[arch]是与你的机器相关的架构或构建版本。在这个例子中,我使用的是 Linux x86_64 机器。

要利用objdump输出的信息,你需要了解 ARM 平台的操作码格式和指令,以及一些关于 ELF 格式的内容。我在另请参阅部分提供了一些好的参考资料,包括一个名为 Sieve 的 Android 应用程序的链接,该程序用于演示本食谱中使用的某些命令。

另请参阅

使用 GDB 服务器调试 Android 进程

大多数内存破坏、缓冲区溢出和恶意软件分析的专家每天都会使用类似 GDB 的工具进行调试。无论你关注哪个平台,检查内存和执行应用程序进程的动态分析都是任何逆向工程师的基本工作;这当然也包括 Android。以下方法将向你展示如何使用 GDB 调试在 Android 设备上运行的过程。

准备就绪

为了完成这个方法,你需要准备以下内容:

如何操作...

要使用gdbserver调试实时 Android 进程,你需要执行以下步骤:

  1. 第一步是确保你有一个已经 root 的 Android 设备或者一个正在运行的模拟器。这里我不打算详细说明设置模拟器的过程,但如果你对让一个模拟的 Android 设备运行起来的细节不清晰,请参考第二章中的检查应用程序证书和签名的方法,参与应用程序安全。如果你已经知道如何创建一个模拟的 Android 设备,你可以使用以下命令启动它:

    [SDK-path]/sdk/tools/emulator –no-boot-anim –memory 128 –partition-size 512
    
    

    如何操作...

  2. 一旦模拟器或目标设备启动并运行,你应该使用 ADB shell 访问设备。你可以通过执行以下命令来实现:

    abd shell
    
    

    你还需要确保你有 root 权限。模拟器默认授予 root 权限,不过,如果你在实机上这样做,可能需要首先执行su替代用户命令。

  3. 然后,你需要将系统目录挂载为可读写,这样我们就可以将gdbserver的副本放入其中。以下是在 adb shell 中重新挂载目录的方法,执行以下命令:

    mount
    
    

    如何操作...

    这应该会输出一些关于每个块设备挂载位置的信息;我们关心的是/system目录。记下提及/system的行中打印的/dev/路径。在之前的示例中,名为/dev/block/mtdblock0的设备被挂载在/system上。

  4. 使用以下命令重新挂载目录:

    mount –o rw,remount [device] /system
    
    

    如何操作...

  5. 现在你已经准备好将gdbserver的副本放入设备中。以下是在非 Android 机器上执行此操作的方法:

    adb push [NDK-path]/prebuilt/android-arm/gdbserver/gdbserver /system/bin
    
    

    如何操作...

  6. 一旦gdbserver在目标设备上,你可以通过将其附加到一个运行中的进程来启动它;但在你这样做之前,你需要获取一个示例进程 IDPID)。你可以通过在目标设备上以下列方式启动ps命令来做到这一点:

    ps
    
    

    ps命令将列出当前运行进程的信息摘要;我们对其中一个当前运行进程的 PID 感兴趣。以下是我们正在运行的模拟器中ps命令输出的一个例子:

    如何操作...

    在前面的截图中,你可以看到第二列标题为PID;这是你要查找的信息。这里用作例子的日历,其 PID 为766

    如何操作...

  7. 拿到一个有效的 PID 后,你可以通过执行以下命令使用gdbserver连接到它:

    gdbserver :[tcp-port number] –-attach [PID]
    
    

    其中[tcp 端口号码]是你希望允许连接的 TCP 端口号,PID 当然是你在上一步获取的 PID 号码。如果操作正确,gdbserver应该会产生以下输出:

    如何操作...

  8. 一旦gdbserver启动并运行,你需要确保你将从目标 Android 设备转发 TCP 端口号,这样你就可以从你的机器连接到它。你可以通过执行以下命令来完成这个操作:

    adb forward tcp:[device port-number] tcp:[local port-number]
    
    

    这是adb端口转发的例子:

    如何操作...

  9. 然后,你应该在 Linux 机器上启动预构建的gdb,它位于路径android-ndk-r8e/toolchains/arm-linux-androideabi-[version]/prebuilt/linux-x86_64/bin/下。你可以在上述 NDK 路径内运行以下命令来启动它:

    arm-linux-androideabi-gdb
    
    

    这是它启动方式的截图:

    如何操作...

  10. 一旦gdb启动并运行,你应该尝试通过在gdb命令提示符下发出以下命令,将其连接到运行目标设备的gdb实例:

    target remote :[PID]
    
    

    其中[PID]是你在第 8 步使用adb转发的本地 TCP 端口号。以下是这个操作的截图:

    如何操作...

    就这样!你已经可以与运行在 Android 设备上的进程的内存段和寄存器进行交互了!

第七章:安全网络

在本章中,我们将涵盖以下内容:

  • 验证自签名 SSL 证书

  • 使用 OnionKit 库中的 StrongTrustManager

  • SSL 固定证书验证

引言

安全套接层SSL)是客户端和服务器之间加密通信的核心部分之一。其主要部署是用在网页浏览器上,以加密消息并确保与第三方服务在进行在线交易(如购买 DVD 或网上银行)时的信任级别。与网页浏览器不同,安卓应用左上角没有挂锁图标,无法提供视觉提示表明连接是安全的。不幸的是,已经有应用开发者跳过了这一验证的情况。这一点在论文《为什么 Eve 和 Mallory 喜欢安卓:安卓 SSL(不)安全性分析》(www2.dcsec.uni-hannover.de/files/android/p50-fahl.pdf)中被强调。

在本章中,我们将探讨在安卓上使用 SSL 的一些常见陷阱,特别是与自签名证书相关的问题。主要焦点是如何使 SSL 更强大,以帮助防御前一章提到的某些漏洞。毕竟,安卓应用实际上是胖客户端。因此,为什么不利用与网页浏览器相比的额外能力,执行额外的验证,并对我们信任的证书和证书根施加限制。

尽管这超出了本书的范围,但网络服务器的配置对有效的网络安全是一个重要因素。应用程序能做得很少的常见攻击方式包括 SSL 剥离、会话劫持和跨站请求伪造。然而,这些风险可以通过健壮的服务器配置来缓解。为了帮助这一点,SSL 实验室最近发布了一份最佳实践文档,可在www.ssllabs.com/downloads/SSL_TLS_Deployment_Best_Practices_1.3.pdf获取。

验证自签名 SSL 证书

安卓支持使用 SSL 与标准的安卓 API 组件,如HTTPClientURLConnection。但是,如果你尝试连接到一个安全的 HTTPS 服务器 URL,可能会遇到SSLHandshakeException。常见的问题包括:

  • 颁发服务器 SSL 证书的证书机构(CA)没有包括在安卓系统中包含的约 130 个 CA 中,因此被视为未知

  • 服务器 SSL 证书是自签名的

  • 服务器没有配置中间 SSL 证书

如果服务器没有配置中间证书,只需安装它们,允许连接代码验证信任的根。然而,如果服务器使用自签名证书或由 CA 颁发的证书,但该 CA 不被安卓信任,我们需要自定义 SSL 验证。

通常的做法是与拥有自签名 SSL 证书的服务器进行开发和测试,只在生产环境中使用付费的 CA 签名证书。因此,本指南特别关注于健壮地验证自签名 SSL 证书。

准备工作

对于本指南,我们将导入自签名的 SSL 证书到应用中,为此,我们将运行一些终端命令。这一部分将介绍在你的机器上下载 SSL 证书文件的工具和命令。

在本指南后面的部分,我们需要用到最新版本的 Bouncy Castle 库来创建和导入证书到信任库中。我们选择 Bouncy Castle,因为这是一个健壮的开源密码学库,Android 有内置支持。你可以在www.bouncycastle.org/latest_releases.html找到bcprov.jar文件。下载并保存到当前工作目录。对于这个指南,我们将其保存到了一个名为libs的本地目录,所以引用.jar文件的路径是/libs/bcprov-jdk15on-149.jar(这是本书编写时的最新版本)。

我们需要从服务器获取一个自签名的 SSL 证书文件;如果你是手动创建的或已经有了,可以跳过这一部分,继续指南的后续内容。

要创建或下载 SSL 证书,我们需要利用一个名为OpenSSL的开源 SSL 工具包:

  • Mac – 幸运的是,从 Mac OS X 10.2 版本开始,OpenSSL 就已经包含在内。

  • Linux – 许多 Linux 发行版预装了编译好的 OpenSSL 软件包。如果没有,可以从www.openssl.org/source/下载并构建源代码,或者在 Ubuntu 上,应该执行apt-get install openssl

  • Windows – 从源代码构建或使用 Shining Light Productions 提供的第三方 Win32 安装程序(slproweb.com/products/Win32OpenSSL.html)。

在终端窗口中,输入以下命令从服务器获取证书,其中server.domain可以是 IP 地址或服务器名称:

Openssl s_client -showcerts -connect server.domain:443 </dev/null.

证书详情将在控制台输出中显示。复制并粘贴以-----BEGIN CERTIFICATE-----开始,以-----END CERTIFICATE-----结束的证书定义,到一个新文件中,并将其保存为mycert.crt。重要的是不要包含任何额外的空白或尾随空格。

下面的屏幕截图展示了android.comOpenssl –showcerts命令示例:

准备工作

如果你还没有服务器,并且想要创建一个新的自签名证书来使用,我们首先需要使用 OpenSSL 工具包生成一个私有的 RSA 密钥。在终端窗口中输入以下内容:

openssl genrsa –out my_private_key.pem 2048

这将创建私钥文件my_private_key.pem。下一步是使用上一步生成的私钥生成证书文件。在终端中,输入:

openssl req -new -x509 -key my_private_key.pem -out mycert.crt -days 365

按照屏幕上的提示填写证书详情。请注意,通用名称通常是您的服务器 IP 地址或域名。

准备工作就这些!接下来的一节我们应该手头有一个证书文件。

如何操作...

让我们开始吧!

  1. 您应该有一个 SSL 证书,格式为 CRT/PEM 编码,在文本编辑器中打开时,看起来像这样:

    -----BEGIN CERTIFICATE-----
    WgAwIBAgIDA1MHMA0GCSqGSIb3DQEBBQUAMDwxCzAJBgNVBAYTAlVTMRcwFQYDVQQK
    …
    -----END CERTIFICATE-----
    

    对于这个示例,我们将使用名为mycert.crt的证书。

  2. 为了将证书打包到应用中,我们创建并导入证书到一个.keystore文件,我们将这个文件称为我们应用程序的信任存储。

  3. 在终端窗口中,设置CLASSPATH变量,以便以下命令可以访问bcprov.jar文件:

    $export CLASSPATH=libs/bcprov-jdk15on-149.jar
    
    

    bcprov-jdk15on-149.jar文件的先前命令路径应与-providerpath参数相匹配。

  4. 现在,使用以下keytool命令创建并导入证书:

    $ keytool -import -v -trustcacerts -alias 0 /
    -file <(openssl x509 -in mycert.crt) /
    -keystore customtruststore.bks /
    -storetype BKS /
    -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider /
    -providerpath libs/bcprov-jdk15on-149.jar 
    -storepass androidcookbook 
    
    
  5. 系统会提示您信任该证书,输入yes

    Trust this certificate? [no]: yes
    
    

    输出文件为customtruststore.bks,其中添加了公共证书。信任存储受密码保护,密码为androidcookbook,在应用程序中加载信任存储时,我们将在代码中引用此密码。我们将–storetype参数设置为BKS,表示 Bouncy Castle Keystore 类型,这也解释了.bks扩展名。可以将多个证书导入到信任存储中;例如,开发和测试服务器。

    提示

    密钥库与信任存储之间的区别

    尽管它们是同一类型的文件(.keystore),实际上也可以是同一个文件,但我们倾向于使用不同的文件。我们使用术语信任存储来定义一组您预期与之通信的第三方公共证书。而密钥库用于私钥,并且应该存储在受保护的位置(即不在应用程序中)。

  6. 将信任存储文件复制到 Android 应用程序的raw文件夹中;如果该文件夹不存在,请创建它:

    /res/raw/customtruststore.bks

  7. raw目录加载本地信任存储到KeyStore对象:

    private static final String STORE_PASSWORD = "androidcookbook";
    
    private KeyStore loadKeyStore() throws Exception {
        final KeyStore keyStore = KeyStore.getInstance("BKS");
        final InputStream inputStream = context.getResources().openRawResource(
            R.raw.customtruststore);
        try {
          keyStore.load(inputStream, STORE_PASSWORD.toCharArray());
          return keyStore;
        } finally {
          inputStream.close();
        }
      }
    

    在这里,我们创建了一个类型为 BKS(Bouncy Castle Keystore)的 KeyStore 类实例,这与我们创建的类型相匹配。方便的是,有一个 .load() 方法,它接收已加载的 .bks 文件的输入流(InputStream)。你会注意到,我们使用的是创建信任存储时使用的同一个密码,用于打开、验证和读取内容。密码的主要用途是验证信任存储的完整性,而不是强制实施安全措施。特别是由于信任存储包含服务器的公钥证书,将其硬编码并不是安全问题,因为证书很容易从 URL 访问到。然而,为了使攻击者更难以攻击,这可以作为 DexGuard 字符串加密的一个好选择,如第五章《保护应用程序》中所述。

  8. 扩展 DefaultHttpClient 以使用本地信任存储:

    public class LocalTrustStoreMyHttpClient extends DefaultHttpClient {
    
        @Override
        protected ClientConnectionManager createClientConnectionManager() {
          SchemeRegistry registry = new SchemeRegistry();
          registry.register(new Scheme("http", PlainSocketFactory
              .getSocketFactory(), 80));
          try {
            registry.register(new Scheme("https", new SSLSocketFactory(
                loadKeyStore()), 443));
          } catch (Exception e) {
            e.printStackTrace();
          }
          return new SingleClientConnManager(getParams(), registry);
        }
      }
    

    我们重写了 createClientConnectionManager 方法,以便我们可以注册一个新的 SSLSocketFactory 接口以及我们的本地信任存储。为了代码示例的简洁,这里我们捕获了异常并将错误打印到系统日志;然而,建议在使用实时代码时实现适当的错误处理并减少日志记录的信息量。

  9. 使用 HttpClient 编写一个示例 HTTP GET 请求:

      public HttpResponse httpClientRequestUsingLocalKeystore(Stringurl)
          throws ClientProtocolException, IOException {
        HttpClient httpClient = new MyHttpClient();
        HttpGet httpGet = new HttpGet(url);
        HttpResponse response = httpClient.execute(httpGet);
        return response;
      }
    

    这展示了如何构建一个简单的 HTTP GET 请求,并使用 LocalTrustStoreMyHttpClient 类,该类不会抛出 SSLHandshakeException,因为来自服务器的自签名证书可以成功验证。

提示

注意

我们为所有 HTTPS 请求定义了一个明确的信任存储。请记住,如果后端服务器证书发生更改,应用程序将停止信任连接并抛出 SecurityException

这就完成了这个方法;我们可以与受 SSL 保护并由我们的自签名 SSL 证书签名的互联网资源进行通信。

还有更多内容...

通常,在处理 SSL 时,一个常见的错误是捕获并隐藏证书和安全异常。这正是攻击者依赖的做法,以欺骗一个不知情的应用程序用户。关于 SSL 错误,你选择如何处理是主观的,取决于应用程序。然而,阻止网络通信通常是确保数据不会通过可能受损的通道传输的一个好步骤。

在生产环境中使用自签名 SSL 证书

安卓应用程序开发人员通常在编译/构建时就知道他们正在与之通信的服务器。他们甚至可能控制这些服务器。如果你遵循这里提到的验证步骤,那么在生产环境中使用自签名证书是没有安全问题的。优点是,你可以使自己免受证书颁发机构妥协的影响,并节省 SSL 证书续费的费用。

HttpsUrlConnection

使用HttpsURLConnection API 没有额外的安全好处,但你可能更喜欢它。为此,我们采用稍微不同的方法,创建一个自定义的TrustManager类,它验证我们的本地信任库文件:

  1. 创建一个自定义的TrustManager类:

    public class LocalTrustStoreTrustManager implements X509TrustManager {
    
      private X509TrustManager mTrustManager;
    
      public LocalTrustStoreTrustManager(KeyStore localTrustStore) {
        try {
          TrustManagerFactory factory = TrustManagerFactory
              .getInstance(TrustManagerFactory.getDefaultAlgorithm());
          factory.init(localTrustStore);
    
          mTrustManager = findX509TrustManager(factory);
          if (mTrustManager == null) {
            throw new IllegalStateException(
                "Couldn't find X509TrustManager");
          }
        } catch (GeneralSecurityException e) {
          throw new RuntimeException(e);
        }
      }
    
      @Override
      public void checkClientTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        mTrustManager.checkClientTrusted(chain, authType);
      }
    
      @Override
      public void checkServerTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        mTrustManager.checkServerTrusted(chain, authType);
      }
    
      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return mTrustManager.getAcceptedIssuers();
      }
    
      private X509TrustManager findX509TrustManager(TrustManagerFactory tmf) {
        TrustManager trustManagers[] = tmf.getTrustManagers();
        for (int i = 0; i < trustManagers.length; i++) {
          if (trustManagers[i] instanceof X509TrustManager) {
            return (X509TrustManager) trustManagers[i];
          }
        }
        return null;
      }
    
    }
    

    我们实现了X509TrustManager接口,我们LocalTrustStoreTrustManager类的构造函数接受一个KeyStore对象,这是我们在之前的步骤中加载的。如前所述,这个KeyStore对象被称为信任库,因为它包含我们信任的证书。我们使用信任库初始化TrustManagerFactory类,然后使用findX509TrustManager()方法获取X509TrustManager接口的系统特定实现。然后我们保留对这个TrustManager的引用,它使用我们的信任库来验证连接中的证书是否可信,而不是使用系统信任库。

  2. 这是一个使用HttpsURLConnection和上一步创建的自定义TrustManager类进行 HTTP GET请求的例子:

      public InputStream uRLConnectionRequestLocalTruststore(String targetUrl)
          throws Exception {
        URL url = new URL(targetUrl);
    
        SSLContext sc = SSLContext.getInstance("TLS");
        sc.init(null, new TrustManager[] { new LocalTrustStoreTrustManager(
            loadKeyStore()) }, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    
        HttpsURLConnection urlHttpsConnection = (HttpsURLConnection) url.openConnection();
        urlHttpsConnection.setRequestMethod("GET");
        urlHttpsConnection.connect();
        return urlHttpsConnection.getInputStream();
      }
    

    我们使用LocalTrustStoreTrustManager类初始化SSLContext,这样当我们调用sc.getSocketFactory()时,它将使用我们的TrustManager实现。通过使用setDefaultSSLSocketFactory()覆盖默认设置,将其设置在HttpsURLConnection上。这就是你需要成功连接到使用URLConnection的自签名 SSL 资源的全部操作。

反模式——不应该做的事情!

这是一个反模式,不幸的是,当开发人员尝试使用自签名证书或由不受信任的证书颁发机构签名的 SSL 证书时,它被发布在各种论坛和留言板上。

在这里,我们看到X509TrustManager接口的不安全实现:

public class TrustAllX509TrustManager implements X509TrustManager {

  @Override
  public void checkClientTrusted(X509Certificate[] chain, String authType)
      throws CertificateException {
    // do nothing, trust all :(
  }

  @Override
  public void checkServerTrusted(X509Certificate[] chain, String authType)
      throws CertificateException {
    // do nothing, trust all :( 
  }

  @Override
  public X509Certificate[] getAcceptedIssuers() {
    return null;
  }
}

从代码中可以看出,checkServerTrusted方法没有实现任何验证,因此所有服务器都被信任。这使得 HTTPS 通信容易受到中间人(MITM)攻击,这完全失去了使用证书的意义。

另请参阅

使用 OnionKit 库中的 StrongTrustManager

在这个教程中,我们将利用 Guardian Project 的工作成果,增强我们应用程序对 SSL 连接的验证。具体来说,我们将使用StrongTrustManager

准备工作

OnionKit 作为一个 Android 库项目进行分发。在我们开始这个教程之前,从 GitHub 页面下载 OnionKit 库(github.com/guardianproject/OnionKit)。

然后,像添加其他任何 Android 库项目一样,提取并添加到你的项目中。

如何操作...

让我们开始吧!

  1. 集成StrongTustManager类再简单不过了。只需替换你的HttpClient实现即可。因此,更改以下代码:

    public HttpResponse sampleRequest() throws Exception {
        HttpClient httpclient = new DefaultHttpClient();
        HttpGet httpget = new HttpGet("https://server.com/path?apikey=123");
        HttpResponse response = httpclient.execute(httpget);
        return response;
    }
    

    修改为以下内容:

    public HttpResponse strongSampleRequest() throws Exception {
        StrongHttpsClient httpclient = new StrongHttpsClient(context);
        ch.boye.httpclientandroidlib.client.methods.HttpGet httpget = new HttpGet(
            "https://server.com/path?apikey=123");
        HttpResponse response = httpclient.execute();
        return response;
      }
    

    在你的代码中,将org.apache.http.*的导入改为ch.boye.httpclientandroidlib.*。OnionKit 使用的HttpGetHttpResponse对象来自另一个名为httpclientandroidlib的库(也包含在 OnionKit 中)。httpclientandroidlib是针对 Android 重新打包的HttpClient 4.2.3 版本,它包含了相较于 Android SDK 中标准HttpClient库的更新和错误修复。

  2. 启用通知功能:

    httpclient.getStrongTrustManager().setNotifyVerificationFail(true)
    

    这是一个有用的功能,它通知用户在验证过程中出现了问题,同时他们当前连接的互联网资源是不安全的。

  3. 启用证书链的完全验证:

    httpclient.getStrongTrustManager().setVerifyChain(true);
    

    启用verifyChain可以确保在建立 HTTPS 连接时调用TrustManager.checkServerTrusted server(…)方法时,验证整个证书链。此设置默认启用。

  4. 启用对弱加密算法的检查:

    httpclient.getStrongTrustManager().setCheckChainCrypto(true);
    

    这会检查证书链中是否存在颁发者使用了 MD5 算法的情况,这种算法被认为是弱算法,应当避免使用。此设置默认启用。

还有更多...

在本章中,我们使用了HttpClient API;你可能会想知道为什么,因为HttpClient API 在 Android 中已被弃用。为了澄清,谷歌弃用了包含在 Android SDK 中的HttpClient版本,因为存在多个现有错误。谷歌目前建议使用URLConnection。但是,如前所述,OnionKit 使用一个单独的、更新的、修复过的HttpClient API 库,因此不应视为已弃用。

Orbot 和 Tor 网络

Tor 项目是一个免费的 Onion 路由实现,它提供了互联网匿名和抵抗流量监控的功能。Orbot 是一个免费的 Android 应用程序,它提供了一个专门供其他 Android 应用使用的代理。

OnionKit 的另一个关键特性是允许你的应用通过 Orbot 代理连接到互联网,从而使其互联网流量匿名化。

OrbotHelper 类有助于确定是否安装并运行了 Orbot 应用,并提供方便的方法来启动和使用它。

锁定和 CACert

StrongTrustManager 类确实提供了一些有限的证书锁定功能,当与 Guardian Projects 的另一个库 CACert 结合使用时,通过限制信任的根证书颁发机构。

我们将在下一章中详细讨论 SSL 锁定,并创建我们自己的 TrustManager 类,专门锁定适合 CA 和自签名证书的 SSL 证书链。

另请参阅

SSL 锁定

需要一个证书颁发机构(CA)来解决常规网络客户端中的密钥分发问题,例如网页浏览器、即时通讯和电子邮件客户端。它们需要与许多服务器通信,应用程序开发人员对这些服务器事先并不了解。正如我们在之前的食谱中所讨论的,通常我们知道应用与之通信的后端服务器或服务,因此建议限制其他 CA 根证书。

Android 目前信任大约 130 个 CA,不同制造商和版本之间略有差异。它还限制其他 CA 根证书,增强连接的安全性。如果这些 CA 中的一个被攻破,攻击者可以使用被攻破 CA 的根证书为我们的服务器域名签署和颁发新证书。在这种情况下,攻击者可以针对我们的应用完成 MITM 攻击。这是因为标准的 HTTPS 客户端验证会将这些新证书视为可信。

SSL 锁定是限制信任对象的一种方式,通常采用以下两种方法之一:

  • 证书锁定

  • 公钥锁定

就像本章中验证自签名 SSL 证书的食谱一样,证书固定将信任的证书数量限制为本地信任库中的证书。当使用 CA 时,你会在本地信任库中包含你的服务器 SSL 证书以及证书的根签名和任何中间证书。这允许对整个证书链进行完全验证;因此,当被破坏的 CA 签署新证书时,这些证书将无法通过本地信任库的验证。

公钥固定遵循同样的理念,但实现起来稍微复杂一些。除了在应用中捆绑证书外,还需要从 SSL 证书中提取公钥的额外步骤。然而,额外的努力是值得的,因为公钥在证书续期之间保持一致。这意味着当 SSL 证书续期后,无需强制用户升级应用。

在这个食谱中,我们将使用Android.com作为示例,针对几个证书公钥进行固定。该食谱由两个不同的部分组成;第一部分是一个独立的 Java 工具,用于处理并获取链中所有 SSL 证书的公钥,并将它们转换为 SHA1 哈希以嵌入/固定在你的应用中。我们嵌入公钥的 SHA1 哈希,因为这更安全。

第二部分涉及应用代码以及如何在运行时验证固定公钥,并决定是否信任特定的 SSL 连接。

如何操作...

让我们开始吧!

  1. 我们将创建一个名为CalcPins.java的独立 Java 文件,在命令行上运行它以连接并打印证书公钥的 SHA1 哈希。由于我们处理的是由 CA 签名的证书,链中将有二个或更多证书。这个第一步主要是初始化和获取传递给fetchAndPrintPinHashs方法的参数:

    public class CalcPins {
    
      private MessageDigest digest;
    
      public CalcPins() throws Exception {
        digest = MessageDigest.getInstance("SHA1");
      }
    
      public static void main(String[] args) {
        if ((args.length == 1) || (args.length == 2)) {
          String[] hostAndPort = args[0].split(":");
          String host = hostAndPort[0];
          // if port blank assume 443
          int port = (hostAndPort.length == 1) ? 443 : Integer
              .parseInt(hostAndPort[1]);
    
          try {
            CalcPins calc = new CalcPins();
            calc.fetchAndPrintPinHashs(host, port);
          } catch (Exception e) {
            e.printStackTrace();
          }
        } else {
          System.out.println("Usage: java CalcPins <host>[:port]");
          return;
        }
      }
    
  2. 接下来,我们定义PublicKeyExtractingTrustManager类,它实际上负责提取公钥。当套接字连接时,将调用checkServerTrusted方法,并带上完整的X509Certificates链,这在后面的步骤中会展示。我们取得链(X509Certificate[]数组),并调用cert.getPublicKey().getEncoded();来获取每个公钥的字节数组。然后我们使用MessageDigest类来计算密钥的 SHA1 哈希。由于这是一个简单的控制台应用,我们将 SHA1 哈希输出到System.out

    public class PublicKeyExtractingTrustManager implements X509TrustManager {
    
        public X509Certificate[] getAcceptedIssuers() {
          throw new UnsupportedOperationException();
        }
    
        public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
          throw new UnsupportedOperationException();
        }
    
        public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
          for (X509Certificate cert : chain) {
            byte[] pubKey = cert.getPublicKey().getEncoded();
            final byte[] hash = digest.digest(pubKey);
            System.out.println(bytesToHex(hash));
          }
        }
      }
    
  3. 然后,我们按照以下方式编写bytesToHex()工具方法:

    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);
      }
    

    我们使用一个工具方法将字节数组转换成大写十六进制字符串,在输出到System.out之前,这样它们就可以嵌入到我们的 Android 应用中。

  4. 最后,我们使用从main方法传递过来的主机和端口来打开到主机的SSLSocket连接:

    private void fetchAndPrintPinHashs(String host, int port) throws Exception {
        SSLContext context = SSLContext.getInstance("TLS");
        PublicKeyExtractingTrustManager tm = new PublicKeyExtractingTrustManager();
        context.init(null, new TrustManager[] { tm }, null);
        SSLSocketFactory factory = context.getSocketFactory();
        SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
        socket.setSoTimeout(10000);
        socket.startHandshake();
        socket.close();
      }
    

    我们使用自定义的 PublicKeyExtractingTrustManager 类初始化 SSLContext 对象,该类依次将每个证书的公钥哈希打印到控制台,以便在 Android 应用中嵌入。

  5. 从终端窗口,使用 javac 编译 CalcPins.java 并使用 java 带有 hostname:port 作为命令行参数运行命令。示例使用 Android.com 作为示例主机:

    $ javac CalcPins.java 
    $ java -cp . CalcPins Android.com:443
    
    

    然而,你可能发现直接在 IDE 中创建 CalcPins.java 作为简单的 Java 项目,然后将其导出为可运行的 .jar 文件会更容易。

    可运行的 .jar 文件的示例终端命令如下:

    $ java -jar calcpins.jar android.com:443
    
    

    如果公钥提取成功,你将看到哈希的输出。这个示例输出展示了 Android.com 主机的三个 SSL 证书公钥的 pins:

    B3A3B5195E7C0D39B8FA68D41A64780F79FD4EE9
    43DAD630EE53F8A980CA6EFD85F46AA37990E0EA
    C07A98688D89FBAB05640C117DAA7D65B8CACC4E
    
    

    现在,我们继续这个方法的第二部分,在 Android 应用项目中验证 SSL 连接。

  6. 现在我们有了 pins,我们从终端复制它们到一个 String 数组中:

    private static String[] pins = new String[] {
          "B3A3B5195E7C0D39B8FA68D41A64780F79FD4EE9",
          "43DAD630EE53F8A980CA6EFD85F46AA37990E0EA",
          "C07A98688D89FBAB05640C117DAA7D65B8CACC4E" };
    
  7. 实现一个自定义的 TrustManager 类来验证 pins:

    public class PubKeyPinningTrustManager implements X509TrustManager {
    
      private final String[] mPins;
      private final MessageDigest mDigest;
    
      public PubKeyPinningTrustManager(String[] pins)
          throws GeneralSecurityException {
        this.mPins = pins;
        mDigest = MessageDigest.getInstance("SHA1");
      }
    
      @Override
      public void checkServerTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        // validate all the pins
        for (X509Certificate cert : chain) {
          final boolean expected = validateCertificatePin(cert);
          if (!expected) {
            throw new CertificateException("could not find a validpin");
          }
        }
      }
    
      @Override
      public void checkClientTrusted(X509Certificate[] chain, String authType)
          throws CertificateException {
        // we are validated the server and so this is not implemented.
        throw new CertificateException("Cilent valdation not implemented");
      }
    
      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return null;
      }
    

    PubKeyPinningTrustManager 构造函数内部使用 pins 数组进行验证。同时创建一个 MessageDigest 实例来生成传入 SSL 证书公钥的 SHA1 哈希。注意,对于这个示例,我们没有实现 checkClientTrusted()getAcceptedIssuers() 方法;请参阅 增强功能 部分。

  8. 验证证书:

    private boolean validateCertificatePin(X509Certificate certificate)
          throws CertificateException {
        final byte[] pubKeyInfo = certificate.getPublicKey().getEncoded();
        final byte[] pin = mDigest.digest(pubKeyInfo);
        final String pinAsHex = bytesToHex(pin);
        for (String validPin : mPins) {
          if (validPin.equalsIgnoreCase(pinAsHex)) {
            return true;
          }
        }
        return false;
      }
    

    我们提取公钥并计算 SHA1 哈希,然后使用前面提到的 bytesToHex() 方法将其转换为十六进制字符串。验证过程简化为一个简单的 String.isEquals 操作(实际上,我们使用 equalsIgnoreCase 以防大小写不匹配)。如果证书中的 pin 与嵌入的 pin 不匹配,将抛出 CertificateException 并且不会建立连接。

  9. 我们可以像本章前面讨论的 LocalTrustStoreTrustManager 类一样集成 PubKeyPinningTrustManager。以下是它与 HttpsURLConnection 一起使用的示例:

    TrustManager[] trustManagers = new TrustManager[] { new PubKeyPinningTrustManager(pins) };
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, null);
        HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
        urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
        urlConnection.connect();
    

总之,我们提取了证书公钥并生成了 SHA1 哈希值,以便嵌入到我们的应用程序中。在运行时使用这些值来验证 SSL 连接的 SSL 证书的公钥。这不仅保护了其他 CA 被破坏的风险,同时也使 MITM 攻击者更难以行动。好处在于我们采用的是严格的行业标准 SSL 基础设施。

还有更多...

了解这个方法的改进之处和限制是非常重要的。

增强功能

为了最大程度的安全,每次您建立服务器连接时,都应该验证 SSL 固定。然而,这会与每个连接的性能产生权衡;因此,您可以将之前的代码调整为每个会话检查最初几次连接。尽管这显然会降低安全性。同时,包括 Android 的默认信任管理器验证将进一步增加安全性。

validateCertificatePin方法非常适合 DexGuard 的 API 隐藏,如第五章 保护应用程序中所述。

限制

尽管 SSL 固定使得 MITM 攻击者更加难以攻击,但这并不是一个 100%的解决方案(没有哪种安全解决方案是 100%的)。iSECPartners 提供了一个有趣的库,旨在绕过固定技术(iSECPartners/android-ssl-bypass)。

然而,如第五章所述的防篡改方法可以用来减轻.apk修改和运行在模拟器上的能力。

另请参阅

第八章:本地利用与分析

在本章中,我们将涵盖以下内容:

  • 检查文件权限

  • 跨编译本地可执行文件

  • 竞争条件漏洞的利用

  • 栈内存破坏利用

  • 自动化本地 Android 模糊测试

引言

到目前为止,我们已经涵盖了 Android 平台上应用程序的大多数高级方面;本章关注一些本地方面——支持应用层组件的所有内容。本地方面包括系统守护进程、为系统架构编译的二进制可执行文件,以及文件系统和设备级配置的组件。Android 系统的这些方面任何一方面可能导致安全漏洞并使 Android 设备——尤其是智能手机——上的权限提升,因此在 Android 系统的完整安全审查中不能被忽视。

本章节还涵盖了如何捡起一些基本的内存破坏利用缺陷。然而,请注意,本章并不包括所有已知的内存利用风格和技术。但所涵盖的内容足以使你能够自学大部分其他技术。对于想要深入兔子洞的人来说,这一章还包括了关于其他技术的好文章和信息来源。

为什么要研究本地利用技术?嗯,你还有什么其他方法可以获取手机的根权限呢?根利用通常是通过滥用 Android 设备中的本地漏洞来工作的,这些漏洞允许权限提升到足以允许对 Android 设备上的根(或超级用户)账户持久访问。自然地,这些漏洞可能表现为对 Android 设备无拘束定制的门户,但它们也为恶意软件和远程攻击者打开了大门;不难看出,允许某人获取你手机上超级用户权限的漏洞是个坏主意!因此,任何称职的移动安全审计师都应该能够识别可能导致此类利用的任何潜在漏洞。

检查文件权限

在本地环境中提升权限的最常见利用方式之一是滥用操作系统中文件系统权限设置的方式——或者说访问权限——的不一致和不完善。有无数的漏洞和权限提升攻击方法滥用文件权限的实例,无论是全局可执行易受攻击的二进制文件上的setuid标志,如susymlink,还是对由超级用户拥有的应用程序可全局读取和写入的文件的竞争条件攻击;例如,pulse audio CVE-2009-1894。

能够清楚地识别文件系统呈现的任何潜在入口点是定义 Android 原生攻击面的良好起点。本节中的演练详细介绍了几种方法,你可以使用这些方法通过 ADB shell 与设备交互时找到可能启用利用的危险或潜在文件。

鉴于以下教程主要详细介绍了寻找权限不足或权限不一致的文件的方法,为了理解为什么执行某些命令,你需要掌握的一个基本技能是了解基于 Linux 或 Unix 的操作系统如何定义文件权限。顺便一提:在某些 Linux 圈子中,将文件和目录权限称为访问权限是很常见的;在这里,这些术语将互换使用。

基于 Linux 或 Unix 的操作系统定义文件权限时涉及以下内容:

  • 文件的潜在用户(简称o),这些用户不属于其他用户类别。

  • 文件的所有者(简称u

  • 对文件所有者所属用户组的访问控制(简称g

以这种方式对用户进行分类允许互斥性,使用户能够精细调整谁可以访问文件。这意味着可以根据文件和每个可能用户来指定访问权限。

对于每组用户(组、其他用户和所有者),定义了五个访问控制属性,分别为:

  • 文件的读取能力(r);决定哪些用户可以实际读取文件内容。

  • 文件的写入能力(w);控制谁被允许增加或修改文件内容。

  • 文件的执行能力(x);决定给定用户组是否被允许执行文件的指令。

  • 设置组 ID 的能力(s);如果文件可执行,这定义了根据其组权限如何增加用户的权限。此权限可能允许低权限用户提升其权限以执行某些任务;例如,替换一个将任何用户的权限提升到 root 或它所希望的用户权限的用户——当然是在认证成功的情况下!

  • 设置用户 ID 的能力(s);这决定了文件所有者的用户 ID 以及随之而来的所有访问权限是否可以传递给执行进程。

每个这些属性都可以用助记符(使用缩写)或以八进制格式编码的逐位的字面值来定义。对于初学者来说,这可能是一个令人困惑的描述,这就是为什么本节包含了一个小表格,定义了二进制和八进制(基数为 8 的数字)的值。

为什么是基数为 8?因为二进制中的基数为 8 允许三个位的空间,每个位描述每个属性的布尔值;1表示开启(或真)和0表示关闭(或假):

描述 二进制值 十进制值
读取 100 4
写入 010 2
执行 001 1

这些是通过添加二进制值来组合的。下面是一个描述该组合的表格:

描述 读取 写入 执行
--- --- --- ---
读取 100 4 110
写入 010 2
执行 001 1

这些权限是为每一组用户明确指定的;这意味着每个用户都有一个权限位,由于有三个用户组,分别是文件所有者、组和其他用户——通常被称为“世界”。权限位还包括一个额外的位来定义setuidsetguid以及粘性位。

粘性位是一种访问权限,它允许只有文件或目录的所有者才能删除或重命名文件或目录。当指定时,它会在ls命令显示的访问权限位中作为一个T符号出现。

结构如下所示:

所有者 其他
r w x

关于文件访问权限的基础知识就这么多;如果你仔细阅读了前面的段落,你应该有足够的知识来发现 Android 本地访问权限的最基本缺陷。

为了正确理解供应商在设备构建中添加的差异,你需要对“默认”或标准的 Android 文件系统的结构和访问权限设置有所了解。

检查文件权限

以下是默认或标准文件系统文件夹及其目的的概要,根据 Linux 文件系统层次结构标准和 Jelly Bean 上的init.rc脚本。下一教程“检查系统配置”中的另请参阅部分提供了其他平台的init.rc脚本的参考资料。

文件夹 目的
/acct cgroup的挂载点——CPU 资源的会计和监控
/cache 临时存储正在进行的下载,也用于非重要数据
/data 包含应用和其他特定于应用程序存储的目录
/dev 设备节点,如同经典的 Linux 系统,尽管不广泛用于设备和硬件驱动访问
/etc /system/etc/的符号链接,包含配置脚本,其中一些在启动引导过程中启动
/mnt 临时挂载点,类似于许多其他传统的 Linux 系统
/proc 包含关于进程的数据结构和信息,如同传统的基于 Linux 或 Unix 的系统
/root 通常是一个空目录,但类似于许多 Linux/Unix 系统上的 root 用户的主目录
/sbin 包含用于系统管理任务的重要实用程序的文件夹
/sdcard 外部 SD 卡的挂载点
/sys sysfs的挂载点,包含导出的内核数据结构
/system 在系统构建期间生成的不可变(只读)二进制文件和脚本;在许多 Android 系统中,这也包含系统拥有的应用程序
/vendor 为设备特定的增强保留的目录,包括二进制文件、应用程序和配置脚本
/init 在引导过程中,内核加载后执行的init二进制文件
/init.rc init二进制文件的配置脚本
/init[device_name].rc 设备特定的配置脚本
/ueventd.rc uevent守护进程的配置脚本
/uevent[device_name].rc uevent守护进程的设备特定配置脚本
/default.prop 包含系统全局属性的配置文件,包括设备名称
/config configfs的挂载点
/storage 从 4.1 设备开始的添加目录;用作外部存储的挂载点
/charger 一个本地独立应用程序,显示电池充电进度

请记住,设备制造商的版本可能会有所不同;将这些视为最基本的、未修改的文件系统布局和目的。通常,制造商在使用其中一些文件路径时也会犯错误,违背了它们的预期用途,因此要关注这些文件夹的目的和默认访问权限。

本节不会详细介绍文件系统布局;然而,在另请参阅部分有一些关于 Android 和 Linux 文件系统的语义、布局和约定的好资源。

让我们看看如何在 Android 系统上寻找有趣的基于文件或目录的目标。以下演练假设你在被评估的设备上拥有 ADB shell 权限。

准备就绪

为了使用以下示例中提到的命令,你需要能够安装find二进制文件或 Android 的 Busybox;安装说明可以在www.busybox.net/以及本章末尾的设置 Busybox部分找到,该部分位于自动化原生 Android 模糊测试菜谱中。

如何操作...

若要根据文件的访问权限搜索文件,你可以在 ADB shell 中执行以下命令来查找可读文件;首先,对于全世界可读的文件,这个命令可以解决问题:

find [path-to-search] –perm  0444 –exec ls –al {} \;

请查看以下截图以获取示例输出:

如何操作...

上述截图——以及本节后续的截图——来自一个已获得 root 权限的三星 Galaxy S3。这里,命令行指令包含了一个重定向到/dev/null的操作,以省略因权限拒绝引起的错误输出。

提示

对于非 Linux/Unix 用户的一个小警告

/dev/null 对于输出来说就像一个“黑洞”,允许 Linux/Unix 用户将其作为一个放置不希望看到的输出的地方。作为一个额外的好处,它还会返回一个值,让您知道写入操作是否成功。

接下来,如果您在寻找全局可写文件,可以使用以下参数找到它们:

find [path-to-search] –perm  0222 –exec ls –al {} \;

查看以下截图以获取示例输出:

如何操作...

对于对所有用户设置了可执行权限的文件:

find [path-to-search] –perm  0111 –exec ls –al {} \;

您并非必须使用八进制格式;find 命令也理解用户集合和权限的常用简写。

例如,要查找除了所有者组之外所有人可读的文件,您可以这样指定权限:

find [path-to-search] –perm  a=r –exec ls –al {} \;

查看以下截图以获取示例输出:

如何操作...

之前的规格将确保只有完全匹配的文件;这意味着返回的文件必须只具有指定的位。如果您寻找至少设置了指定位以及任何其他位的文件——您可能大多数时间都会这样做——您可以通过在前面示例中包含 - 符号作为前缀来指定权限。对于八进制模式,这将按以下方式工作:

find [path-to-search] –perm  -444 –exec ls –al {} \;

查看以下截图以获取示例输出:

如何操作...

这至少会匹配所有用户集合设置了读位的文件,这意味着将匹配 445、566、777 等权限位。而 344、424、222 等则不会匹配。

您可能感兴趣的几个非常实用的访问权限模式包括查找具有 setuid 的可执行文件:

find [path-to-search] –perm  -4111 –exec ls –al {} \;

查看以下截图以获取示例输出:

如何操作...

在前面的截图中,我们看到使用前面的命令找到了 su 二进制文件。如果您在 Android 设备上找到这个二进制文件,这总是表明设备已经被 root。

您还可以查找对所有用户具有 setguid 和执行权限的文件:

find [path-to-search] –perm  -2111 –exec ls –al {} \;

查看以下截图以获取示例输出:

如何操作...

find 命令还允许您将用户作为搜索条件的一部分;例如:

  • 您可以如下列出属于 root 用户的所有文件:

    find [path-to-search] –user 0 –exec ls –al {} \;
    
    
  • 您可以如下列出所有系统用户的文件:

    find [path-to-search] –user 1000 –exec ls –al {} \;
    
    
  • 您也可以根据组 ID 设置来列出文件,如下所示:

    find [path-to-search] –group 0 –exec ls –al {} \;
    
    

你可能想要了解你的 Android 系统上的每个用户——或者更确切地说,是每个应用——可以访问多少内容,为此你可能想要构建一个用户 ID 的列表——或者更重要的是,应用的 UID。最简单的方法是转储 /data/data 目录中文件的访问权限,因为它包含了大多数安装在 Android 设备上的应用的数据。然而,要从 ADB shell 访问这个列表,你需要有 root 或系统账户的访问权限,或者任何具有等效权限的账户;这在模拟器上很容易获得——它会自动授权。另外,如果你选择这样做,你可以向 XDA 开发者网站发起几个搜索,寻找 root 手机的方法。XDA 开发者网站可以在 www.xda-developers.com/ 找到。

对手机进行 root 操作有好有坏;在这种情况下,它允许你更详细地检查文件系统和访问权限。然而,另一方面,如果 root 权限的访问没有得到妥善管理,它可能会让你的手机面临许多非常严重的攻击!因此要吝啬你的 root 权限,并且只在需要时临时 root 手机。

接下来,如果你列出 /data/data 目录中的所有文件,你应该会看到以下内容;这是从三星 Galaxy S3 中获取的:

如何操作...

你可能注意到了每个应用的命名约定很奇怪,即 u[number]_a[number],这表示的是应用安装的用户配置文件的 u[配置文件编号] ——因为某些 Android 版本支持多个用户配置文件,从 Jelly Bean 及其之后的版本开始——以及 a[number],它是应用程序 ID。

你可以使用应用程序 ID 通过加上这个数字到 10000 来构建应用的实际系统用户 ID(UID);例如,对于用户名为 u0_a170 的 Mozilla 安装,相应的 UID 将是 10170。要找到所有拥有这个 UID 作为所有者的文件,你接下来会执行这个命令:

find /data/data/ -user 10170 –exec ls –al {} \;  2> /dev/null

以下是样本输出的截图:

如何操作...

你可以通过查看本食谱 另请参阅 部分提到的 Android_filesystem_config.h 文件来找到其他用户名。

还有很多...

可以使 find 命令输出更有用的一个命令是 stat。这个命令显示文件属性,并允许你指定这些详情的显示格式。stat 命令具有众多功能,使得查找权限设置错误的文件比仅仅通过 find –exec 命令调用 ls –al 要更加具有信息量。

你可以将 statfind 一起使用如下:

find . –perm [permission mode] –exec stat –c "[format]" {} \;

例如,如果你想显示以下内容:

  • %A:以人类可读格式显示的访问权限

  • %u:文件所有者的用户 ID

  • %g:文件所有者的组 ID

  • %f:文件的原始十六进制模式

  • %N:带有引用的文件名,如果是符号链接则解引用

你可以通过执行以下命令来完成此操作:

find . –perm [permission] –exec stat –c "%A %u %g  %f  %N" {} \;

此命令生成的输出如下——这里示例使用 -0666 作为示例权限模式:

还有更多...

另请参阅

跨编译本地可执行文件

在我们能够在 Android 设备上开始破坏堆栈和劫持指令指针之前,我们需要一种方法来准备一些易受攻击的示例应用程序。为此,我们需要能够编译本地可执行文件,而要做到这一点,我们需要使用 Android 本地开发工具包中的一些优秀应用程序。

如何操作...

要跨编译你自己的本地 Android 组件,你需要执行以下操作:

  1. 准备一个目录来开发你的代码。你需要做的就是创建一个你想命名为“模块”名称的目录,例如,你可以像我在这里的示例中一样,将目录命名为 buffer-overflow。创建该目录后,你还需要创建一个名为 jni/ 的子目录。你必须这样命名它,因为 NDK 中的编译脚本会特别寻找这个目录。

  2. 一旦你有了这些目录,你就可以创建一个 Android.mk 文件。在你的 jni 目录中创建这个文件。Android.mk 文件基本上是一个 Make 文件,它准备了一些你编译的属性;以下是它应该包含的内容:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    # give module name
    LOCAL_MODULE    := buffer-overflow  #name of folder
    # list your C files to compile
    LOCAL_SRC_FILES :=  buffer-overflow.c #name of source to compile
    # this option will build executables instead of building library for Android application.
    include $(BUILD_EXECUTABLE)
    
    
  3. 一旦你正确设置了所需的 jni 目录结构和 Android.mk,你就可以开始编写一些 C 代码了;以下是一个你可以使用的示例:

    #include <stdio.h>
    #include <string.h>
    void vulnerable(char *src){
      char dest[10]; //declare a stack based buffer
      strcpy(dest,src);
      printf("[%s]\n",dest); //print the result
      return;  }
    
    void call_me_maybe(){
      printf("so much win!!\n");
      return;  }
    
    int main(int argc, char **argv){
      vulnerable(argv[1]); //call vulnerable function
      return (0);  }
    

    请确保此文件与 jni 目录中的 Android.mk 文件一起出现。

  4. 现在是乐趣的一部分;你现在可以编译你的代码了。你可以通过调用 NDK 构建脚本来完成这个操作,令人惊讶的是,这是通过执行以下命令完成的:

    [path-to-ndk]/ndk-build
    
    

    在这里,[path-to-ndk] 是你的 Android NDK 的路径。

如果一切顺利,你应该看到类似以下的输出:

如何操作...

还有更多...

只编译是不够的;我们需要能够修改正常可执行文件的编译方式,这样我们才能利用并研究某些漏洞。我们将在这里移除的保护措施是一种保护函数栈不被以允许被利用的方式破坏的保护——大多数利用。在移除这保护之前,详细说明这种保护是如何实际工作的,并展示移除保护后的差异将是有用的。做好心理准备——ARMv7 汇编代码即将到来!

我们可以使用随 NDK 捆绑的objdump工具来转储这个可执行文件的反汇编代码;自然你会期望任何普通的 Linux 或 Unix 发行版中捆绑的标准objdump工具都能正常工作,但这些可执行文件是专门为嵌入式 ARM 设备交叉编译的。这意味着字节序可能不同;可执行文件的结构也可能是普通objdump无法理解的。

为了确保我们可以使用正确的objdump工具,Android 团队确保了与 ARM 可执行文件兼容的版本随 NDK 一起打包。你应在 NDK 的/toolchains/arm-linux-androideabi-[version]/prebuilt/linux-x86-64/bin/路径下找到它;尽管你可以使用任何arm-linux-androideabi版本,但坚持使用最新版本总是更简单。

在前述文件夹中的objdump二进制文件将被命名为类似arm-linux-androideabi-objdump的名字。

要使用它,你需要做的就是指向/buffer-overflow/obj/local/armeabi/目录根部的二进制文件,这个文件应该出现在你的jni目录中,并执行以下命令:

[path-to-ndk]/toolchains/arm-linux-Androideabi-[version]/prebuilt/linux-x86_64/bin/arm-linux-Androideabi-objdump –D /[module name]/obj/local/armeabi/[module name] | less

对于我们的示例,命令看起来会像这样:

[path-to-ndk]/toolchains/arm-linux-Androideabi-4.8/prebuilt/linux-x86_64/bin/arm-linux-Androideabi-objdump –D /buffer-overflow/obj/local/armeabi/buffer-overflow | less

这将产生相当多的输出;我们感兴趣的是围绕"脆弱"函数编译的函数。我将输出重定向到less,这样我们就可以滚动和搜索文本;接下来你应该在less打开objdump输出时按下/字符,并输入<vulnerable>,然后按回车

如果你正确完成了这些步骤,你的屏幕应该会显示以下输出:

00008524 <vulnerable>:
 8524:  b51f        push  {r0, r1, r2, r3, r4, lr}
 8526:  4c0a        ldr  r4, [pc, #40]  ; (8550 <vulnerable+0x2c>)
 8528:  1c01        adds  r1, r0, #0
 852a:  4668        mov  r0, sp
 852c:  447c        add  r4, pc
 852e:  6824        ldr  r4, [r4, #0]
 8530:  6823        ldr  r3, [r4, #0]
 8532:  9303        str  r3, [sp, #12]
 8534:  f7ff ef7e   blx  8434 <strcpy@plt>
 8538:  4806        ldr  r0, [pc, #24]  ; (8554 <vulnerable+0x30>)
 853a:  4669        mov  r1, sp
 853c:  4478        add  r0, pc
 853e:  f7ff ef80   blx  8440 <printf@plt>
 8542:  9a03        ldr  r2, [sp, #12]
 8544:  6823        ldr  r3, [r4, #0]
 8546:  429a        cmp  r2, r3
 8548:  d001        beq.n  854e <vulnerable+0x2a>
 854a:  f7ff ef80   blx  844c <__stack_chk_fail@plt>
 854e:  bd1f        pop  {r0, r1, r2, r3, r4, pc}
 8550:  00002a7c   andeq  r2, r0, ip, ror sl
 8554:  00001558   andeq  r1, r0, r8, asr r5

00008558 <main>:
 8558:  b508        push  {r3, lr}
 855a:  6848        ldr  r0, [r1, #4]
 855c:  f7ff ffe2   bl  8524 <vulnerable>
 8560:  2000        movs  r0, #0
 8562:  bd08        pop  {r3, pc}

提示

只是一个小提示

在上述objdump输出中,最左边的列显示了指令的偏移量;紧随其后的由:字符分隔的列,保存了代码的实际十六进制表示;再往后的列显示了相关汇编指令的人类可读助记符。

注意之前objdump输出中加粗的代码。位于8526偏移的指令加载了从程序计数器(pc)寄存器当前值起0x40地址偏移处内存中的内容;这个地址保存了一个特殊的值,称为栈金丝雀

提示

这通常被称为金丝雀,因为实际的金丝雀曾被矿工用来确保矿井通道是安全的可供探索。

这个值被放置在堆栈上,介于局部变量和已保存的指令及基指针之间;这样做是为了如果攻击者或错误指令足以破坏堆栈,影响到那里保存的值,那么它也将需要破坏或更改堆栈守护者,这意味着程序能够检查这个值是否发生了变化。这个值来自一个加密安全(据称是)的伪随机数生成器,并在程序运行时存储在内存中,以避免可靠地预测这个值。

接下来,我们看到位于偏移量852c-8530的指令将堆栈守护者放入r3r4寄存器中。偏移量8532的后续指令确保在危险的strcpy调用(位于偏移量8534)之前将堆栈守护者放置在堆栈上。到目前为止,所有代码完成的工作只是在strcpy调用之后将值放在堆栈上——实际上是靠近printf函数。从偏移量85428544,从寄存器r4和堆栈上放置的位置获取堆栈守护者的值,加载到r2r3寄存器中,然后在偏移量8546进行比较。如果它们不匹配,我们看到位于854a的指令将被执行,这将基本上导致程序中断,而不是正常退出。所以,总结一下,它从文件中的某个偏移量获取堆栈守护者,将其放入寄存器和堆栈上的另一个副本,并在退出前检查是否有任何变化。

你可能会注意到,尽管这防止了已保存的指令指针被破坏,但它根本没有保护局部变量!根据它们在内存中的布局——它们与守护者和堆栈上的其他缓冲区的关系位置——仍然有可能恶意地破坏堆栈上的其他变量。在某些特殊情况下,这仍然可能被滥用,恶意地影响进程的行为。

那么现在我们如何移除这个烦人的保护措施,以便我们可以正确地破坏堆栈并获得控制指令指针的能力呢?由于堆栈守护者是编译器级别的保护措施——这意味着它是可执行编译器强制实施的——我们应该能够修改 NDK 可执行文件的编译方式,使得堆栈保护不被强制执行。

尽管这在 Android 系统上的二进制文件中可能很少是实际情况,但这仍然是非常可能发生的事情。我们移除这个保护是为了模拟基于堆栈的溢出漏洞。

要移除保护,你需要更改 NDK 使用的一些 GCC 编译器扩展。为此,你需要:

  1. 导航到/toolchains/arm-linux-Androideabi-4.9/目录,找到一个名为setup.mk的文件。请注意,你的 NDK 可能使用不同版本的arm-linux-androideabi。如果以下步骤不起作用或没有达到预期效果,你应该尝试移除栈保护:还有更多...

  2. 接下来你可能想要备份setup.mk文件。我们即将更改 NDK 的默认编译配置,因此备份总是好的。你可以通过将脚本复制到另一个名称略有不同的文件来创建一个临时的备份。例如,你可以通过执行以下命令备份setup.mk文件:

    cp setup.mk setup.mk.bk
    
    

    还有更多...

  3. 备份之后,你应该在你喜欢的文本编辑器中打开setup.mk文件,并移除标志,特别是包含-fstack-protector切换的那一个;查看以下截图以获得更清晰的信息:还有更多...

    移除指定标志后,你的setup.mk文件应该看起来像这样:

    还有更多...

  4. 完成上述操作后,你可以使用ndk-build脚本编译你的可执行文件的全新副本,然后将其传递给androideabi-objdump。在没有栈保护的情况下,你的代码应该看起来像这样:

    000084bc <vulnerable>:
     84bc:  b51f        push  {r0, r1, r2, r3, r4, lr}
     84be:  1c01        adds  r1, r0, #0
     84c0:  a801        add  r0, sp, #4
     84c2:  f7ff ef8a   blx  83d8 <strcpy@plt>
     84c6:  4803        ldr  r0, [pc, #12]  ; (84d4 <vulnerable+0x18>)
     84c8:  a901        add  r1, sp, #4
     84ca:  4478        add  r0, pc
     84cc:  f7ff ef8a   blx  83e4 <printf@plt>
     84d0:  b005        add  sp, #20
     84d2:  bd00        pop  {pc}
     84d4:  0000154a   andeq  r1, r0, sl, asr #10
    
    000084d8 <main>:
     84d8:  b508        push  {r3, lr}
     84da:  6848        ldr  r0, [r1, #4]
     84dc:  f7ff ffee   bl  84bc <vulnerable>
     84e0:  2000        movs  r0, #0
     84e2:  bd08        pop  {r3, pc}
    
    

注意到与前一个可执行文件版本中的指令没有任何关联。这是因为我们移除的-fstack-protector编译器标志告诉 GCC 自主寻找可能潜在破坏函数栈的任何函数实例。

另请参阅

利用竞态条件漏洞的攻击行为。

竞态条件在 Android 平台上引起了很多问题和权限提升攻击;其中许多允许恶意攻击者获得 root 权限。

基本上,竞态条件是由多线程(允许多个进程同时运行的平台)系统在采用抢占式进程调度时缺乏强制互斥所引起的。抢占式调度允许任务调度器预先中断线程或正在运行的进程,这意味着不需要首先等待任务准备好被中断。这使得竞态条件成为可能,因为通常开发者没有使应用程序以能够适应来自进程调度器的任意和不可预测的中断的方式运行;结果是,依赖访问可能共享的资源(如文件、环境变量或共享内存中的数据结构)的进程总是在“竞速”,以获取这些资源的首次和独占访问权。攻击者通过首先获取这些资源并加以篡改,以这种方式滥用这种情况,从而可能导致进程操作受损或允许他们恶意影响进程的行为。一个简单的例子是,一个程序检查正在验证身份的用户是否在给定文件中的有效用户名列表中;如果此进程不能适应抢占式调度器,它可能只能在恶意用户通过将自己的用户名添加到列表中篡改文件之后访问该文件,从而允许他们被验证。

在本演练中,我将详细说明一些基本的竞态条件漏洞,并讨论其他潜在原因;我还将详细说明一些最基本的竞态条件漏洞的利用方法。演练最后会提供有关过去基于 Android 的竞态条件漏洞的参考资料和有用信息来源;其中大部分是在撰写本文那年报告的。

竞态条件漏洞的利用取决于几个因素,攻击者至少必须能够做到以下几点:

  • 获取易受攻击进程正在争夺访问的资源: 如果一个进程没有对其外部资源实施互斥访问,但攻击者又无法访问这些相同资源,那么这种情况下的利用潜力并不大。如果不是这样,那么每个进程进行的每一次非互斥访问都将是可以被利用的。这包括每次进程在未经信号量或自旋锁检查的情况下取消对内存中指针的引用,这种情况可能发生数十亿次!

  • 恶意影响这些资源: 如果进程在攻击无法增加或恶意修改资源的环境中不独占地访问其资源,那么这样做不会有太大帮助。例如,如果一个进程访问攻击者只能读取的共享内存或文件,除非这会导致易受攻击的进程崩溃,考虑到进程的语义优先级;例如,防病毒程序、入侵检测系统或防火墙。

  • 使用时/检查时窗口大小 (TOU/TOC): 这本质上是应用程序检查资源访问权限和实际访问资源之间的时间差,或者更确切地说,是调度器中断的可能性。竞态条件的可利用性很大程度上取决于这个时间差,因为利用行为本质上是在这个时间框架内争夺访问权限,以恶意影响资源。

考虑到这些条件,让我们看看一些构建的竞态条件漏洞示例以及如何在 Android 上利用它们。

准备工作

在我们开始利用竞态条件之前,我们需要准备一个示例。以下是操作方法:

  1. 我们将准备一个嵌入式 ARM Android 平台——在此示例中为 Jelly Bean 模拟器——这可能会导致竞态条件漏洞。以下代码详细描述了一个易受攻击进程的行为:

    #include <stdio.h>
    #include <unistd.h>
    #include <errno.h>
    #define MAX_COMMANDSIZE 100
    int main(int argc,char *argv[],char **envp){
      char opt_buf[MAX_COMMANDSIZE];
      char *args[2];
      args[0] = opt_buf;
      args[1] = NULL;
      int opt_int;
      const char *command_filename = "/data/race-condition/commands.txt";
      FILE *command_file;
      printf("option: ");
      opt_int = atoi(gets(opt_buf));
      printf("[*] option %d selected...\n",opt_int);
      if (access(command_filename,R_OK|F_OK) == 0){
        printf("[*] access okay...\n");
        command_file = fopen(command_filename,"r");
        for (;opt_int>0;opt_int--){
          fscanf(command_file,"%s",opt_buf);
        }
        printf("[*] executing [%s]...\n",opt_buf);
        fclose(command_file);
      }
      else{
        printf("[x] access not granted...\n");
      }
      int ret = execve(args[0],&args,(char **)NULL);
      if (ret != NULL){
        perror("[x] execve");
      }
      return 0;
    }
    

    按照在交叉编译本地可执行文件一节中详细描述的相同过程编译此文件,并将其部署到您的安卓设备上。尝试将其部署到作为可执行文件和任何安卓系统用户可读的分区或文件夹中(如何操作请参考第一章,安卓开发工具中的复制文件到/从 AVD 中一节)。在本节中,我们使用作为/system的已挂载分区,该分区在其他菜谱中以读写权限重新挂载。请注意,这可能会导致 NDK 发出一些警告,但只要一切编译成可执行文件,就可以继续操作!

  2. 您还需要将commands.txt文件放在代码中提到的目录中,即/data/race-condition/command.txt。这需要在/data路径中创建一个竞争条件文件夹。关于如何做到这一点的良好示例可以在第四章,利用应用程序中的检查网络流量一节中找到,因为我们需要为TCPdump创建类似的设置。

  3. 您需要在安卓设备上为这个可执行文件设置setuid权限;您可以在将其部署到设备后执行以下命令来完成此操作:

    chmod 4711 /system/bin/race-condition
    
    

    这个命令还确保系统上的任何用户都有执行权限。请注意,您需要 root 权限才能执行此命令。我们正在模拟setuid二进制文件的效果以及它可能导致的任意代码执行。

我们已经为利用做好了一切设置;现在可以详细说明这种利用方法了。

如何操作...

要利用这个有漏洞的二进制文件,您需要执行以下操作:

  1. 运行 ADB shell 进入安卓设备;如果您使用的是模拟器或已获得 root 权限的设备,您应该可以使用su来获取另一个应用程序的访问权限。

    尝试访问一些对您的用户没有设置执行、读取或写入权限的 root 拥有的文件夹和文件。这里我选择用户10170作为示例,当您尝试访问/cache/目录时,您应该会看到抛出的Permission denied消息:

    如何操作...

  2. 让我们利用race-condition二进制文件。我们通过在commands.txt文件中添加另一个命令来实现,即/system/bin/sh,这将为我们打开一个 shell。您可以通过执行以下命令来完成此操作:

    echo "/system/bin/sh" >> /data/race-condition/commands.txt
    
    

    /system/bin/sh命令现在应该是commands.txt文件中的最后一个条目,这意味着,如果我们希望通过菜单选择它,需要选择选项 5。

    如何操作...

  3. 在安卓设备上执行race-condition,并输入5作为选项。有漏洞的二进制文件将执行sh命令,并赋予您 root 权限。

  4. 通过尝试将目录更改为/cache来测试你的 root 访问权限。如果你运行的是 Jelly Bean 或更高版本的 Android,你不应该看到任何Permission denial消息,这意味着你刚刚将自己的权限提升到了 root!!如何操作...

前面的例子旨在详细说明竞态条件的基本概念,即当一个应用程序访问任何其他进程都可以修改的文件,并将其用于以 root 用户身份执行操作时。还有更复杂和微妙的情形会导致竞态条件,一个常见被利用的情况涉及到符号链接。这些漏洞源于应用程序无法区分文件和符号链接,这使得攻击者可以通过精心构造的符号链接来修改文件,或者当一个文件读取符号或硬链接但不能确定链接目标的真实性时,这意味着链接可以被恶意重定向。要了解关于竞态条件漏洞的更现代的例子,请查看另请参阅部分中的链接。

另请参阅

栈内存损坏利用

栈内存利用可能不是 Android 错误和安全漏洞的最常见来源,尽管这类内存损坏错误仍然有可能影响到即使拥有 ASLR、StackGuard 和 SE Linux 等保护措施的原生 Android 可执行文件。此外,大部分 Android 市场份额由那些对栈和其他基于内存的利用没有强有力保护的设备组成,尤其是 2.3.3 版本的 Gingerbread 设备。除了与安全研究的直接相关性之外,包括基于栈的利用讨论和演练的另一个重要原因是它为更高级的利用技术提供了很好的入口。

在本节中,我们将详细说明如何利用常见的基于栈的内存损坏漏洞来控制执行流程。

准备工作

在开始之前,你需要准备一个易受攻击的可执行文件;以下是操作方法:

  1. 创建一个包含通常的jni文件夹和与之前菜谱相同命名约定的目录。如果你需要回顾,请查看本章中的跨编译本地可执行文件的菜谱。

  2. jni文件夹中写这段代码到一个.c文件中:

    #include <stdio.h>
    #include <string.h>
    void
    vulnerable(char *src){
      char dest[10]; //declare a stack based buffer
      strcpy(dest,src); //always good not to do bounds checking
      printf("[%s]\n",dest); //print the result
      return;  }
    
    int
    main(int argc, char **argv){
      vulnerable(argv[1]); //call vulnerable function
      printf("you lose...\n");
      return (0);  }
    

    这个代码与之前的例子惊人地相似。实际上,你可以编辑之前的示例代码,因为它只在几行代码上有所不同。

  3. 使用之前的ndk-build脚本编译代码。

  4. 将代码部署到 Android 设备或模拟器上;在以下示例中,我使用了模拟的 Android 4.2.2 设备。

当你设置好代码后,可以继续将二进制文件推送到你的模拟器或设备上——如果你愿意接受挑战的话。

如何操作...

要利用基于栈的缓冲区溢出,你可以执行以下操作:

  1. 在你的模拟器上多次启动应用程序,每次都提供更大的输入,直到它无法正常退出执行,你的 Android 系统报告段错误。

    尝试记住你给应用程序输入了多少个字符,因为你需要使用gdbserver给出相同的数量来触发崩溃。以下是可执行文件正常运行的截图:

    如何操作...

    你应该看到 GDB 输出exited normally,这表明进程的返回码相同,没有中断或强制它停止。

    当输入过多时,应用程序会以段错误退出,这在 GDB 中看起来像这样:

    如何操作...

  2. gdbserver中启动应用程序,提供一个“不安全”的输入量,即会导致崩溃的输入量。对于我们的代码,这应该是超过 14 到 16 个字符的任何输入。在这个例子中,我输入了大约 16 个字符,以确保我覆盖了正确的内存部分。

  3. 运行androideabi-gdb并连接到远程进程。如果你需要回顾如何进行这一步,请查看第六章中使用 GDB 服务器调试 Android 进程的菜谱,逆向工程应用

  4. 使用 GDB 设置几个断点。在blxstrcpy之前设置一个断点,再在之后设置一个,如下截图所示:如何操作...

    提示

    你可以使用break命令或简写为b来设置断点,并给出代码行的偏移量或指向持有指令的地址的指针;因此,在内存值前有*字符。

  5. 当你设置好断点后,通过gdbsever重新运行应用程序,并使用 Android GDB 重新连接。按照后面解释的内容,逐步执行每个断点。你需要在 GDB 提示符中输入continue,或者简写为c。GDB 将继续执行程序,直到达到断点。

    你应该首先到达的是strcpy调用之前的断点;我们在这里设置一个断点,以便你可以看到strcpy调用前后堆栈的变化。理解这一点至关重要,这样你才能在开始覆盖返回地址之前计算出要给应用程序多少数据。以下屏幕截图显示了这一点:

    如何操作...

    这是vulnerable函数在调用strcpy之前堆栈的快照;除了为局部变量准备了一些空间外,还没有发生太多事情。一旦到达第一个断点,你应该通过打印一些内存内容来检查堆栈。

    在以下示例中,通过在 GDB 中执行这个x命令来展示:

    x/32xw $sp
    
    

    这个命令告诉 GDB 打印出sp(堆栈指针)寄存器中包含的内存地址的 32 个十六进制字;以下是您应该看到的内容:

    如何操作...

    你会注意到有几个值被突出显示;这些值是由函数序言中的指令传递到堆栈的,该指令如下:

    push  {r0, r1, r2, r3, r4, lr}
    
    

    提示

    之前命令中使用的push指令确保了调用函数的寄存器值被保留。这条指令有助于确保当执行的函数将控制权返回给调用它的函数时,堆栈能恢复到原始状态。

    push指令中使用的值之一是lr或链接寄存器。链接寄存器通常保存当前函数的返回地址。在这里,lr寄存器保存的值是0x000084f5。我们稍后会尝试用我们自己的值覆盖它;几分钟内,你应该能看到我们的输入是如何改变这个值的,所以暂时请记住它。

    你想要这么做是因为在vulnerable函数中更下面的指令,具体如下:

    pop   {pc}
    
    

    这条指令将保存的lr值直接移动到程序计数器寄存器中;这导致执行在保存在lr寄存器中的地址继续。如果我们能覆盖保存的lr值,我们实际上可以在vulnerable函数末尾控制执行分支的位置。下一步将介绍如何精确计算以及输入程序中的内容,以确保你如前所述控制执行。

  6. 继续到下一个断点。一旦 GDB 达到这个断点,strcpy应该已经将你的输入写入堆栈。此时检查堆栈应该得到以下输出:如何操作...

    你应该注意到 0x000084f5 的值变为了 0x00008400;它们非常相似,因为当 strcpy 将我们的输入写入缓冲区时,它部分地用跟随我们字符串的 NULL 字节覆盖了保存的 lr 值;这就是为什么 0xf5 被替换为 0x00。现在我们知道,我们的 16 个字符的输入覆盖了保存的返回地址的一个字节。这意味着要完全覆盖 2 字节的返回地址,我们需要添加 2 字节的输入——容纳 NULL 字节——最后 4 个字节是新的返回地址。以下是它的工作原理:

    strcpy 调用之前,栈有以下结构:

    无关紧要的栈内容 输入缓冲区字段 保存的 lr 值
    0xbee6fc75 0xbee6fb44 0xbee6fb50

    在使用 16 字节输入的 strcpy 调用之后,栈有以下结构:

    无关紧要的栈内容 输入缓冲区字段 保存的 lr 值
    …0xbee6fc75 16 个字符 0x00000

    加粗的 0x00 值是我们输入的 NULL 字节;基于此,我们需要输入 16 个字符加上 2 个字符作为新的返回地址,如下所示:

    无关紧要的栈内容 输入缓冲区字段 保存的 lr 值
    …0xbee6fc75 [16 个字符] 0x00000

    在这里,0x?? 字符表示我们给 strcpy 调用提供的额外输入字符,以覆盖返回地址;同样,我们在额外输入字符后看到了 0x00 字符。

  7. 使用给定的输入重新启动 GDB 服务器;尝试跳过 printf "you lose" 调用并检查它是否被执行——这是一种检查你是否成功重定向执行流程的简单方法。以下是你可以获取一个重定向执行流程的示例地址的方法。通过在 GDB shell 中执行以下命令来反汇编主部分:

    disass main
    
    

    这将产生以下输出:

    0x000084ec <+0>:  push {r3,lr}
    0x000084ee <+2>:  ldr r0,[r1, #4]
    0x000084f0 <+4>:  bl 0x84d0 <vulnerable>
    0x000084f4 <+8>:  ldr r0, [pc, #8]
    0x000084f6 <+10>:  add r0,pc
    0x000084f8 <+12>:  blx 0x83f8
    0x000084fc <+16>:  movs r0,#0
    0x000084f3 <+18>:  pop {r3,pc}
    0x00008500 <+20>:  andeq r1,r0,r2,asr,r5
    
    

    0x000084f8blx 指令显然是调用 printf 的,如果我们想跳过它,我们需要获取紧随其后的指令的地址,即 0x000084fc。更具体地说,我们将以下内容作为输入提供给我们的程序:

    [16 个填充字符] \xfc\x84

    由于架构的字节序,指定返回地址的字节是反序给出的。

  8. 使用 GDB 服务器重新启动应用程序,这次给它以下输入:

    echo –e "1234567890123456\xfc\x84"`
    
    

    如果一切顺利,你不应该看到应用程序打印 "you lose" 消息,而是直接退出。

你不仅仅可以跳过简单的print指令;在某些情况下,你甚至可以完全控制运行具有此类漏洞的程序的过程。有关如何执行此操作的信息,请参阅另请参阅部分中标题为《无返回的返回导向编程》的链接。关于一般内存破坏攻击的好资源,请参阅另请参阅部分中的《内存破坏攻击,(几乎)完整历史》以及《为了乐趣和利润而破坏堆栈》链接。

另请参阅

自动化原生 Android 模糊测试

模糊测试是发现可利用漏洞或系统实用程序中错误的好方法。它允许审计员针对畸形和可能的恶意输入衡量文件处理程序和其他应用程序的有效性,并帮助确定系统上是否存在任何容易利用的入口点。它还是自动化安全测试的绝佳方式。

Android 与任何其他系统并无不同,它也有无数的有趣模糊测试目标。Android 设备的攻击面并不仅限于 Java 应用层;实际上,有时基于原生可执行文件或系统实用程序的不当输入处理或对某些情况的安全响应,才会出现 root 漏洞。模糊测试是发现这些情况和 Android 设备上可能的 root 漏洞的好方法。

我将在这里介绍如何将一个名为Radamsa的模糊测试生成器移植到 Android 平台,并安装一些将帮助你编写使用 Radamsa 的健壮模糊测试脚本的实用程序。

准备工作

在开始移植之前,你需要获取 Radamsa 模糊器的副本;以下是操作方法:

  1. 确保你的 Linux 机器上安装了CURLWget。Wget 可以正常工作,但按照 Radamsa 网站的建议,你可以通过执行以下命令来安装依赖项(仅限 Ubuntu 机器):

    sudo apt-get install gcc curl
    
    

    运行这个命令应该会产生类似于以下截图的输出:

    准备就绪

  2. 下载完成后,你可以按照以下方式获取 Radamsa 源代码副本:

    curl http://ouspg.googlecode.com/files/radamsa-0.3.tar.gz > radamsa-0.3.tar.gz
    
    

    运行这个命令应该会产生类似于以下截图的输出:

    准备就绪

  3. 然后,你应该通过执行以下命令来提取 Radamsa 源代码:

    tar –zxvf radamsa-0.3.tar.gz
    
    

    如果你正确执行了这个命令,你的输出应该类似于以下截图:

    准备就绪

    完成后,你的目录应该看起来像下面这样:

    准备就绪

    现在一切准备就绪;我们可以开始设置jni目录结构并编译 Radamsa 以用于 Android。

如何操作...

要为 Android 跨编译 Radamsa,你应该做以下操作:

  1. 在这个目录中解压 Radamsa 源代码后,你应该有一个名为radamsa-0.3的目录;你应该创建一个名为jni的目录,就像我们在跨编译本地可执行文件的菜谱中所做的那样。

  2. 制作一份用于缓冲区溢出食谱的Android.mk文件副本,并将其放入jni目录中;你的目录应该类似于以下截图:如何操作...

  3. 将包含 Radamsa 源的radamsa.c文件复制到jni目录中,如下截图所示:如何操作...

  4. 获取一份Android.mk文件并将其放入jni文件夹中。

    复制你的Android.mk文件应该与以下截图中的演示类似:

    如何操作...

  5. 编辑上一步复制的Android.mk文件,使其看起来像下面这样:如何操作...

  6. 设置好Android.mk文件后,你可以执行ndk-build命令;你应该得到以下输出:如何操作...

    这意味着构建失败了。GCC 还向你展示了哪些代码行导致了错误。实际上,这是一个通过其余代码级联的问题,即typedef,它将一个无符号长整型别名为in_addr_t;在下一步中,我们将修复此问题以成功编译 Radamsa。

  7. 在你喜欢的代码编辑器中打开radamsa.c文件——最好是可以显示行号的。滚动到第3222行;如果你使用的是 vim 文本编辑器,你应该会看到以下代码:如何操作...

  8. radamsa.c代码的3222行,将in_addr_t类型名称替换为无符号长整型。当你正确更改后,代码应该看起来像这样:如何操作...

  9. 你还应该删除2686行的typedef命令;编辑行之前,它应该看起来像这样:如何操作...

    注释掉之后,它应该看起来像以下这样:

    如何操作...

  10. 修改radamsa.c源代码以使 NDK GCC 编译器满意后,你可以运行ndk-build脚本。如果你一切都做对了,你的输出应该看起来像这样:如何操作...

  11. 成功构建可执行文件后,你可以将其推送到 Android 模拟器,如下所示——假设你已经设置好了,并且你已经将系统分区重新挂载为可写:如何操作...

  12. 推送 Radamsa 可执行文件后,你可以通过在 Android 模拟器上执行以下命令来测试它:

    radamsa –-help
    
    

    这应该生成以下输出:

    如何操作...

  13. 你可以在一些测试输入上运行 Radamsa,以确保一切正常工作。例如,看看以下命令是如何运行 Radamsa 的,以确保一切正常并处于工作状态:

    echo "99 bottles of beer on the wall" | radamsa
    
    

    运行此命令应该会产生类似于以下截图的输出:

    如何操作...

就这样!Radamsa 在 Android 上运行起来了。下一部分将讨论设置一个简单的模糊测试脚本并将其指向 dexdump,尝试生成一些崩溃,并希望找到一些可利用的漏洞。

如果你打算进行一些模糊测试,你最终需要进行一些 bash 脚本编写,以精确地定位 Radamsa 的目标,并自动报告引起有趣行为的输入数据。不幸的是,Android 平台并没有包含使 bash 脚本编写强大的所有工具;它们甚至没有 bash shell 应用程序,主要是因为它不是必需的。

我们可以使用sh壳来进行脚本编写,但 bash 功能更强大且更健壮,而且大多数人更习惯于 bash 脚本编写。因此,本食谱的下一部分将解释如何在 Android 平台上运行 Busybox。

设置 Busybox

要在 Android 上获取 Busybox 实用程序(一系列有用的终端应用程序的软件包),你需要执行以下操作:

  1. benno.id.au/Android/busybox获取 Android 端口的副本;在示例中,我们使用wget来执行此操作:设置 Busybox

  2. 然后,你需要准备一个busybox目录在你的 Android 模拟器上——假设你已经设置好并准备好启动。

    对于这个示例,busybox目录是在/data/文件夹中创建的;由于它是可写和可执行的,任何挂载有写、读和执行权限的分区的文件夹都应该工作得很好。

    设置 Busybox

  3. 当你为 Busybox 创建了一个专用目录后,你可以使用以下命令将其推送到模拟器:

    adb push [path to busybox] /data/busybox/.
    
    

    你应该做类似于以下截图的操作:

    设置 Busybox

  4. 当你将busybox二进制文件的一个副本推送到你的模拟器后,你可以通过在模拟器上执行以下命令来安装这些二进制文件:

    /data/busybox –-install
    
    

    以下是一个来自三星 Galaxy S3 智能手机的例子:

    设置 Busybox

    执行此命令后,你的busybox文件夹应该看起来像下面这样:

    设置 Busybox

模糊测试 dexdump

现在你已经让测试用例生成器运行起来,并且安装了 Busybox 工具,你可以开始生成一些崩溃了!

在这个例子中,我们将看到如何设置一个简单的脚本来对 dexdump 进行一些“愚蠢”的模糊测试,dexdump 是一个剖析 Android DEX 文件并打印其内容的工具:

  1. 在开始之前,你需要一个 DEX 文件样本;你可以通过使用 Android SDK 编写一个示例“hello world”类型的程序来获得,或者直接获取前一章食谱中创建的Example.dex文件。如果你想生成这个文件,请参考第六章中的从 Java 编译到 DEX食谱,逆向工程应用

  2. 创建一个目录来存放你生成输入测试用例文件的基准目录。这是在 Android 模拟器上,你的脚本将生成文件的文件夹。测试它们,如果它们导致任何崩溃,复制那些有趣的文件;/data/目录再次成为进行这项工作的好地方,不过模拟一个 SD 卡并将数据保存在那里也是不错的选择。

  3. 在你进行模糊测试的目录中——即在上一步创建的目录——创建一个包含以下代码的 bash 脚本:

    #!/bin/bash
    ROOT=$1
    TARGET=dexdump
    ITER=$2
    for ((c=0;1;c++))
    do
     cat $ROOT | radamsa -m bf,br,sr -p bu > fuzz.dex
     $TARGET -d fuzz.dex 2>&1 > /dev/null
     RET_CODE=$?
     echo "[$c] {$RET_CODE} ($WINS)"
     test $RET_CODE -gt 127 && cp fuzz.dex win-dexdump_$ITER"_"$c.dex && WINS=`expr $WINS + 1`
    done
    
    
  4. 通过在模拟器上执行以下命令来在 bash 中运行脚本:

    /data/busybox/bash; /data/busybox/source [fuzz script name] [example.dex]
    
    

现在你可以开始进行模糊测试了!

工作原理...

在本食谱的如何操作...部分的第一部分,我们介绍了交叉编译一个名为 Radamsa 的流行的模糊测试生成器。我们所做的大部分工作在交叉编译本地可执行文件食谱中已有解释。当 NDK 构建脚本因为一个类型定义而无法编译 Radamsa 时,事情变得有趣;以下是它看起来像什么样子:

typedef unsigned long in_addr_t;

这导致构建脚本失败,因为 NDK 构建脚本使用的 GCC 编译器——特别是支持 ARM 应用程序二进制接口的编译器——未能识别类型定义的效果。

提示

当引用了由该语句定义的类型时,它会导致 GCC 停止并报告它基本上不知道in_addr_t是什么。这个问题通过替换in_addr_t别名提及的地方为完整的无符号长整型变量,并注释掉typedef语句,从而消除了对typedef的需求而得到解决。

一旦这个问题得到解决,Radamsa 就可以成功编译并被部署到 Android 设备上。

然后我们编写了一个临时的模糊测试脚本到目标 dexdump。为了确保你们在这个配方中确切了解自己在做什么,详细说明 bash 脚本的作用是很重要的。

前几条指令确保我们有一些有用的助记符来帮助我们引用传递给脚本的参数。这些指令——在#!/bin/bash指令之后出现——只是为一些变量名赋值。

赋值这些变量后,脚本进入一个for循环,有一个哨兵值——该值限制了for循环迭代的次数——除非被用户或操作系统明确停止,否则这将导致脚本无限迭代。

for循环内部,我们看到以下这行代码:

cat $ROOT | radamsa -m bf,br,sr -p bu > fuzz.dex

这条指令只是获取由ROOT变量指向的文件,并将其提供给 Radamsa。然后 Radamsa 对文件应用一些随机变换。

对 DEX 文件进行请求的随机变换后,Radamsa 将输出重定向到一个名为fuzz.dex的文件,这是样本 DEX 文件的“模糊”版本。

然后,用模糊后的 DEX 文件作为参数调用 dexdump;以下是它的样子:

$TARGET -d fuzz.dex 2>&1 > /dev/null

所有输出都被重定向到/dev/null,因为我们可能不会对它感兴趣。这行代码还将来自STDIN(标准输出文件)的所有输出重定向到STDERR文件(标准错误输出文件)。这允许将程序生成的所有输出——那些可能会使屏幕混乱的内容——重定向到/dev/null

下一条指令如下所示:

RET_CODE=$?

这记录了最后一条命令的退出码;在这个例子中,它是dexdump

脚本这样做是因为它将揭示关于dexdump如何退出的信息。如果dexdump正常退出执行,返回码将是0;如果由于输入损坏等原因导致dexdump异常退出或停止,退出码将非零。

更有趣的是,如果故障需要操作系统通过使用进程间信号来停止 dexdump,返回码将大于 127。这些返回码是我们感兴趣生成的,因为它们给出了由于给定的 dexdump 输入而暴露了相对严重缺陷的强烈指示。像段错误这样的错误,通常在使用内存的无效部分时以错误的方式发生,总是产生大于 127 的返回码。关于退出码或更准确地说退出状态如何工作的更多细节,请参见另请参阅部分中的使用 Shell - 理解退出码链接。

接下来,剩余的代码如下所示:

echo "[$c] {$RET_CODE} ($WINS)"
test $RET_CODE -gt 127 && cp fuzz.dex win-dexdump_$ITER"_"$c.dex && WINS=`expr $WINS + 1

这部分代码的第一条指令简单地帮助我们追踪脚本当前正在执行哪个迭代——通过打印 $c 值。它还会打印出前一次 dexdump 运行的返回码以及发生了多少次值得注意的停止。

在打印出提到的“状态指示器”之后,脚本将 RET_CODE 变量的值与 127 进行比较;如果这个值更大,它会复制导致此错误的样本输入,并将 WINS 变量增加 1 以反映生成了另一个值得注意的错误。

另请参阅

第九章:加密和开发设备管理策略

在本章中,我们将涵盖以下内容:

  • 使用加密库

  • 生成对称加密密钥

  • 保护 SharedPreferences 数据

  • 基于密码的加密

  • 使用 SQLCipher 加密数据库

  • 安卓 KeyStore 提供者

  • 设置设备管理策略

引言

本章节的主要焦点将是如何正确使用加密技术,以在设备上安全地存储数据。我们从创建一个一致的加密基础开始,包括我们自己的加密实现库,以在旧设备上支持更强的加密算法。

我们要解决的一个直接问题就是生成对称加密密钥;然而,默认设置并不总是更安全。我们将查看具体参数以确保最强加密,并回顾一个常见的反模式和一个限制生成密钥安全的操作系统漏洞。

然后,我们探讨了多种使用第三方库或称为Android KeyStore的系统服务来安全存储加密密钥的方法,该服务在安卓 4.3 中引入。更进一步,我们学习如何完全避免在设备上存储密钥,使用密钥派生函数从用户的密码或 PIN 码生成密钥。

我们将介绍如何有效地集成 SQLCipher,以确保你的应用程序的 SQLite 数据库得到加密,从而显著提高你的应用数据的安全性。

我们将以设备管理 API 结束本章,该 API 旨在让企业实施设备策略和保护措施,进一步保护设备。我们实施了两项虚构(但合理)的企业政策,以确保设备启用了加密存储并满足锁屏超时要求。

使用加密库

安卓使用 Java 作为核心编程语言的好处之一是它包含了Java 加密扩展JCE)。JCE 是一套成熟、经过测试的安全 API。安卓使用 Bouncy Castle 作为这些 API 的开源实现。然而,Bouncy Castle 的版本在安卓版本之间有所不同;只有较新的安卓版本才能获得最新的修复。为了减少 Bouncy Castle 的大小,安卓定制了 Bouncy Castle 库并移除了一些服务和 API。例如,如果你打算使用椭圆曲线密码学ECC),在低于 4.0 的安卓版本上运行时,你会看到提供者错误。另外,尽管 Bouncy Castle 支持 AES-GCM 方案(我们将在下一个食谱中介绍),但在安卓上使用它必须单独包含。

为了解决这个问题,我们可以包含特定于应用程序的加密库实现。本指南将展示如何包含 Spongy Castle 库,它相对于 Android 的 Bouncy Castle 实现来说更加更新,提供了更高层次的安全,并支持更多的加密选项。

你可能会想“为什么使用 Spongy Castle 而不是直接包含 Bouncy Castle 库”。原因是 Android 已经包含了一个较旧的 Bouncy Castle 库版本,因此我们需要重命名这个库的包以避免“类加载器”冲突。所以,Spongy Castle 实际上是 Bouncy Castle 的重新打包。实际上,只要包名与org.bouncycastle不同,它可以是任何你想要的名称。

如何操作...

让我们在 Android 应用程序中添加 Spongy Castle。

  1. github.com/rtyley/spongycastle/#downloads下载最新的 Spongy Castle 二进制文件。

    查阅 MIT X11 许可证(与 Bouncy Castle 相同),以确保这与你打算使用的方式兼容。

  2. 在你应用程序的/libs目录中提取并复制 Spongy Castle 的.jar文件:

    • sc-light-jdk15on:核心轻量级 API

    • scprov-jdk15on:JCE 提供者(需要sc-light-jdk15on

  3. 在你的 Android 应用程序对象中包含以下static代码块:

    static {
      Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
    }
    

工作原理...

我们使用静态代码块来调用Security.insertProviderAt()。它确保我们捆绑在应用程序/libs文件夹中的 Spongy Castle 提供者优先使用。通过设置为1的位置,我们确保它优先于现有的安全提供者。

使用 Spongy Castle 与 JCE 的妙处在于,无需修改现有的加密代码。在本章中,我们展示了可以与 Bouncy Castle 或 Spongy Castle 同样良好工作的加密代码示例。

还有更多...

如前所述,代码可以从 GitHub 下载;但是,你也可以构建自己的版本。Spongy Castle 仓库的所有者Roberto Tyley包含了一个become-spongy.sh bash 脚本,该脚本将com.bouncycastle重命名为com.spongycastle。因此,你可以将其用于自己刚刚下载并更新版本的 Bouncy Castle 库,并将其转换为org.spongycastle或其他同样可爱且吸引人的名称。

注意

become-spongy.sh bash 脚本可以在gist.github.com/scottyab/8003892找到

另请参阅

生成对称加密密钥

对称密钥是指用于加密和解密的同一个密钥。为了在一般情况下创建加密安全的加密密钥,我们使用安全生成的伪随机数。这个方法演示了如何正确初始化SecureRandom类,以及如何用它来初始化高级加密标准AES)的加密密钥。AES 是比 DES 更受欢迎的加密标准,通常与 128 位和 256 位的密钥大小一起使用。

注意事项

如前一个菜谱所述,无论您是使用 Bouncy Castle 还是 Spongy Castle,代码上没有差异。

如何操作...

让我们创建一个安全的加密密钥。

  1. 编写以下函数以生成对称 AES 加密密钥:

    public static SecretKey generateAESKey(int keysize)
          throws NoSuchAlgorithmException {
        final SecureRandom random = new SecureRandom();
    
        final KeyGenerator generator = KeyGenerator.getInstance("AES");
        generator.init(keysize, random);
        return generator.generateKey();
      }
    
  2. 创建一个匹配 256 位 AES 密钥大小的 32 字节随机初始化向量(IV):

    private static IvParameterSpec iv;
    
    public static IvParameterSpec getIV() {
        if (iv == null) {
          byte[] ivByteArray = new byte[32];
          // populate the array with random bytes
          new SecureRandom().nextBytes(ivByteArray);
          iv = new IvParameterSpec(ivByteArray);
        }
        return iv;
      }
    
  3. 编写以下函数以加密任意字符串:

    public static byte[] encrpyt(String plainText)
        throws GeneralSecurityException, IOException {
        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, getKey(), getIV());
        return cipher.doFinal(plainText.getBytes("UTF-8"));
      }
    
      public static SecretKey getKey() throws NoSuchAlgorithmException {
        if (key == null) {
          key = generateAESKey(256);
        }
        return key;
      }
    
  4. 为了完整性,前面的代码片段展示了如何解密。唯一的不同是,我们使用Cipher.DECRYPT_MODE常量调用Cipher.init()方法:

    public static String decrpyt(byte[] cipherText)
          throws GeneralSecurityException, IOException {
          final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
          cipher.init(Cipher.DECRYPT_MODE, getKey(),getIV());
          return cipher.doFinal(cipherText).toString();
        }
    

在此示例中,我们只是将密钥和 IV 作为静态变量存储;在实际使用中并不建议这样做。一个简单的方法是将密钥以SharedPerferences方式持久化,并使用Context.MODE_PRIVATE标志,以便在应用程序会话之间提供一致的密钥。下一个菜谱进一步开发这个想法,使用SharedPerferences的加密版本。

它是如何工作的…

创建SecureRandom对象只需实例化默认构造函数即可。还有其他构造函数可用;然而,默认构造函数使用的是可用的最强提供者。我们将SecureRandom的实例传递给KeyGenerator类,并带上keysize参数,KeyGenerator类负责创建对称加密密钥。256 位通常被认为是“军用级别”,对于大多数系统来说,它被认为是加密安全的。

在这里,我们引入一个初始化向量(IV),简而言之,它增加了加密的强度,并且在加密多个消息/项目时至关重要。这是因为使用相同密钥加密的消息可以一起分析,以帮助提取消息。弱 IV 是有线等效隐私WEP)被破解的部分原因。因此,建议为每条消息生成一个新的 IV,并将其与密文一起存储;例如,你可以在密文前预先追加或连接 IV。

在实际的加密过程中,我们使用Cipher对象的 AES 实例,以新生成的SecretKeyENCRYPT_MODE模式下初始化。然后我们调用cipher.doFinal方法,传入明文字节,以返回包含加密字节的字节数组。

当使用Cipher对象请求 AES 加密模式时,一个常见的疏忽(在安卓文档中也存在)是简单地使用AES。然而,这默认为最简单且安全性较低的 ECB 模式,具体为AES/ECB/PKCS7Padding。因此,我们应该明确请求更强大的 CBC 模式AES/CBC/PKCS5Padding,如示例代码所示。

还有更多...

在这里,我们探讨如何使用一种称为AES-GCM的强加密模式,以及一个常见的反模式,该反模式降低了生成的密钥的安全性。

使用 AES-GCM 进行强对称加密

我们注意到,简单地定义AES并不会默认为最强模式。如果我们包含 Spongy Castle 库,我们可以使用更强大的 AES-GCM,它包括验证,并且可以检测密文是否被篡改。要在定义算法/转换字符串时使用 AES-GCM,请使用如下代码所示的AES/GCM/NoPadding

  final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SC");

反模式——设置种子

自从安卓 4.2 版本以来,SecureRandom的默认伪随机数生成器PRNG)提供者被改为 OpenSSL。这使得 Bouncy Castle 提供者之前存在的手动设置SecureRandom对象种子的能力被禁用。这是一个受欢迎的变化,因为开发者设置种子的反模式已经出现。

byte[] myCustomSeed = new byte[] { (byte) 42 };
secureRandom.setSeed(myCustomSeed);
int notRandom = secureRandom.nextInt();

在这个代码示例中,我们可以看到种子被手动设置为42,结果是notRandom变量总是等于同一个数字。尽管这对于单元测试很有用,但这破坏了使用SecureRandom生成加密密钥的任何增强安全性。

安卓的伪随机数生成器(PRNG)漏洞

如前所述,自安卓 4.2 以来,伪随机数生成器(PRNG)的默认提供者是 OpenSSL。然而,在 2013 年 8 月,发现了一个生成随机数的严重错误。这通过几个安卓比特币钱包应用的妥协得到了突出。这个问题涉及到安全随机数生成器的种子设置;它没有使用复杂且独特的系统指纹,而是被初始化为 null。其结果与之前从可预测数字生成的安全密钥的反模式类似。受影响的安卓版本包括 Jelly Bean 4.1、4.2 和 4.3。

在《关于 SecureRandom 的一些思考》的安卓博客文章中记录了一个修复方法,并提交给了 Open Handset Alliance 公司。然而,建议您从应用程序的onCreate()方法中调用此修复,以防该修复尚未应用到运行您应用程序的设备上。

注意事项

为了方便起见,这里提供了一个来自 GitHub 的 gist,其中包含了谷歌的代码,可以在gist.github.com/scottyab/6498556找到。

另请参阅

保护 SharedPreferences 数据

Android 为应用开发者提供了一个简单的框架,用于持久化存储基本数据类型的键值对。这个菜谱展示了伪随机生成的密钥的实际用途,并演示了Secure-Preferences的使用。它是一个开源库,包装了默认的 Android SharedPreferences 以加密键值对,从而保护它们免受攻击者的侵害。Secure-Preferences 兼容 Android 2.1+,并使用 Apache 2.0 许可,因此适合商业开发。

我应该补充一下,我是 Secure-Preferences 库的共同创建者和维护者。Secure-Preferences 的一个很好的替代品是名为Cwac-prefs的库,它由 SQLCipher 支持(在后面的菜谱中介绍)。

准备就绪

让我们添加 Secure-Preferences 库。

  1. 从 GitHub 下载或克隆 Secure-Preferences,地址是github.com/scottyab/secure-preferences

    Secure-Preferences 仓库包含一个 Android 库项目和示例项目。

  2. 就像通常那样,将库链接到你的 Android 项目中。

如何操作...

让我们开始吧。

  1. 使用 Android context简单初始化SecurePreferences对象:

    SharedPreferences prefs = SecurePreferences(context);
    
    Editor edit = prefs.edit();
    edit.putString("pref_fav_book", "androidsecuritycookbook");
    edit.apply();
    
  2. 下面是一些你可以添加到你的应用程序中的辅助方法,以便在你的应用程序对象中获取(安全的)偏好设置实例:

    private SharedPreferences mPrefs;
    public final SharedPreferences getSharedPrefs() {
        if (null == mPrefs) {
          mPrefs = new SecurePreferences(YourApplication.this);
        }
        return mPrefs;
      }
    

    在这里,YourApplication.this是对你的应用程序对象的引用。

  3. 然后,理想情况下,在一个基础的应用程序组件中,如BaseActivityBaseFragmentBaseService,你可以包含以下内容以获取(安全的)偏好设置对象的实例:

    private SharedPreferences mPrefs;
    protected final SharedPreferences getSharedPrefs() {
        if (null == mPrefs) {
          mPrefs = YourApplication.getInstance().getSharedPrefs();
        }
        return mPrefs;
      }
    

它是如何工作的...

Secure-Preferences 库实现了SharedPreferences接口;因此,与默认的 SharedPreferences 相比,与它交互不需要进行代码更改。

标准的 SharedPreferences 键和值存储在一个简单的 XML 文件中,Secure-Preferences 使用相同的存储机制;不同之处在于,键和值会使用 AES 对称密钥进行透明加密。在写入文件之前,键和值的密文会使用 base64 编码。

如果你检查以下 SharedPreference XML 文件,你会看到使用和不使用 Secure-Preferences 库的区别。你会看到来自 Secure-Preferences 库的文件是一系列看似随机的条目,这些条目无法揭示其用途。

  • 一个标准的 SharedPreferences XML 文件:

    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
    <int name="timeout " value="500" />
    <boolean name="is_logged_in" value="true" />
    <string name="pref_fav_book">androidsecuritycookbook</string>
    </map>
    
  • 使用 Secure-Preferences 库的 SharedPreferences XML 文件:

    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
    <string name="MIIEpQIBAAKCAQEAyb6BkBms39I7imXMO0UW1EDJsbGNs">
    HhiXTk3JRgAMuK0wosHLLfaVvRUuT3ICK
    </string>
    <string name="TuwbBU0IrAyL9znGBJ87uEi7pW0FwYwX8SZiiKnD2VZ7"> va6l7hf5imdM+P3KA3Jk5OZwFj1/Ed2
    </string>
    <string name="8lqCQqn73Uo84Rj">k73tlfVNYsPshll19ztma7U">
    tEcsr41t5orGWT9/pqJrMC5x503cc=
    </string>
    </map>
    

第一次实例化SecurePreferences时,会生成一个 AES 加密密钥并存储。这个密钥用于加密/解密通过标准SharedPreferences接口保存的所有将来的键/值。

共享首选项文件是使用Context.MODE_PRIVATE创建的,这强制执行应用沙箱安全,确保只有你的应用可以访问。然而,在已 root 的设备上,不能依赖沙箱安全。更准确地说,Secure-Preferences 是在混淆首选项;因此,这不应当被视为坚不可摧的安全措施。相反,将其视为一种快速获胜的方法,逐步提高 Android 应用的安全性。例如,它将阻止已 root 设备上的用户轻松修改你应用的 SharedPreferences。

Secure-Preferences 可以进一步增强,通过使用一种称为基于密码的加密PBE)的技术,根据用户输入的密码生成密钥,这将在下一章中介绍。

另请参阅

基于密码的加密

加密面临的一个较大问题是密钥的管理和安全存储。到目前为止,在之前的食谱中,我们接受将密钥存储在 SharedPreferences 中,正如谷歌开发者的博客所推荐的;然而,这对于已获得 root 权限的设备来说并不理想。在已 root 的设备上,你不能依赖 Android 系统的安全沙箱,因为 root 用户可以访问所有区域。这意味着,与未 root 的设备不同,其他应用可以获得提升的 root 权限。

在不安全的 app 沙盒环境中,基于密码的加密(PBE)是一个理想的选择。它提供了在运行时使用由用户通常提供的密码/密码来创建(或更准确地说是派生)加密密钥的能力。

另一个密钥管理的解决方案是使用系统密钥链;Android 的版本称为 Android KeyStore,我们将在后面的食谱中进行审查。

准备就绪

PBE 是 Java 加密扩展的一部分,因此它已经包含在 Android SDK 中。

在这个食谱中,我们将使用初始化向量(IV)和盐值作为密钥派生的一部分。我们在上一个食谱中介绍了 IV,它有助于创建更多的随机性。因此,即使使用相同的密钥加密相同的消息,也会产生不同的密文。盐值与 IV 相似,它通常是一个随机数据,作为加密过程的一部分添加,以提高其加密强度。

如何操作...

让我们开始吧。

  1. 首先,我们定义一些辅助方法来获取或创建 IV 和盐值。我们将它们作为密钥派生和加密的一部分来使用:

      private static IvParameterSpec iv;
    
      public static IvParameterSpec getIV() {
        if (iv == null) {
          iv = new IvParameterSpec(generateRandomByteArray(32));
        }
        return iv;
      }
    
      private static byte[] salt;
    
      public static byte[] getSalt() {
        if (salt == null) {
          salt = generateRandomByteArray(32);
        }
        return salt;
      }
    
      public static byte[] generateRandomByteArray(int sizeInBytes) {
        byte[] randomNumberByteArray = new byte[sizeInBytes];
        // populate the array with random bytes using non seeded secure random
        new SecureRandom().nextBytes(randomNumberByteArray);
        return randomNumberByteArray;
      }
    
  2. 生成 PBE 密钥:

    public static SecretKey generatePBEKey(char[] password, byte[] salt)
          throws NoSuchAlgorithmException, InvalidKeySpecException {
    
        final int iterations = 10000;
        final int outputKeyLength = 256;
    
        SecretKeyFactory secretKeyFactory = SecretKeyFactory
            .getInstance("PBKDF2WithHmacSHA1");
        KeySpec keySpec = new PBEKeySpec(password, salt, iterations, outputKeyLength);
        SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
        return secretKey;
      }
    
  3. 编写一个示例方法,展示如何使用新派生的 PBE 密钥进行加密:

    public static byte[] encrpytWithPBE(String painText, String userPassword)
          throws GeneralSecurityException, IOException {
    
        SecretKey secretKey = generatePBEKey(userPassword.toCharArray(),getSalt());
    
        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, getIV());
        return cipher.doFinal(painText.getBytes("UTF-8"));
      }
    
  4. 编写一个示例方法,展示如何使用新派生的 PBE 密钥解密密文:

    public static String decrpytWithPBE(byte[] cipherText, String userPassword)
          throws GeneralSecurityException, IOException {
    
        SecretKey secretKey = generatePBEKey(userPassword.toCharArray(),getSalt());
    
        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, secretKey, getIV());
        return cipher.doFinal(cipherText).toString();
      }
    

它是如何工作的...

在步骤 1 中,我们定义了类似于以前食谱中使用的方法。再次强调,为了能够解密加密数据,盐值和 IV 必须保持一致。例如,你可以为每个 app 生成一个盐值并将其存储在SharedPreferences中。此外,盐值的大小通常与密钥大小相同,在这个例子中是 32 字节/256 位。通常,你会在解密时将 IV 和密文一起保存以便检索。

在步骤 2 中,我们使用用户的密码通过 PBE 派生一个 256 位的 AES SecretKeyPBKDF2是一种常用于从用户密码派生密钥的算法;Android 对该算法的实现被记为PBKDF2WithHmacSHA1

作为PBEKeySpec的一部分,我们定义了在SecretKeyFactory内部使用的迭代次数,以生成密钥。迭代次数越多,密钥派生所需的时间就越长。为了防御暴力破解攻击,建议派生密钥的时间应超过 100 毫秒;Android 使用 10,000 次迭代来生成加密备份的加密密钥。

步骤 3 和 4 演示了如何使用Cipher对象和密钥进行加密和解密;你会注意到,这些方法与之前食谱中记录的方法非常相似。但当然,对于解密,IV 和盐值不是随机生成的,而是从加密步骤中重新使用。

还有更多…

在 Android 4.4 中,处理PBKDF2WithHmacSHA1和 Unicode 密码短语时,对SecretKeyFactory类进行了细微的更改。以前,PBKDF2WithHmacSHA1只查看密码短语中 Java 字符的低位 8 位;对SecretKeyFactory类的更改允许使用 Unicode 字符的所有可用位。为了保持向后兼容性,你可以使用这个新的密钥生成算法PBKDF2WithHmacSHA1And8bit。如果你使用 ASCII,这个更改不会影响你。

下面是一个如何保持向后兼容的代码示例:

SecretKeyFactory secretKeyFactory;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1And8bit");
} else {
secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
}

另请参阅

使用 SQLCipher 加密数据库

SQLCipher 是 Android 应用中实现安全存储的最简单方法之一,它兼容运行 Android 2.1+的设备。SQLCipher 使用 256 位 AES 加密每个数据库页面,采用 CBC 模式;此外,每个页面都有自己的随机初始化向量,以进一步提高安全性。

SQLCipher 是 SQLite 数据库的一个独立实现,它没有实现自己的加密,而是使用了广泛使用和测试的 OpenSSL libcrypto库。虽然这确保了更高的安全性和更广泛的兼容性,但它确实伴随着大约 7MB 的相对较大的.apk文件体积。这额外的重量可能是使用 SQLCipher 的唯一缺点。

根据 SQLCipher 网站的说法,在读写性能方面,大约有 5%的性能损失是微不足道的,除非你的应用正在执行复杂的 SQL 连接(但值得注意的是,这些在 SQLite 中也不太好)。对于商业开发来说,好消息是 SQLCipher for Android 不仅是开源的,而且还是基于 BSD 风格的许可发布的。

准备工作

首先,我们将下载并设置你的 Android 项目以使用 SQLCipher。

  1. 通过 SQLCipher GitHub 页面上的链接下载最新的二进制包,或者直接使用这个链接s3.amazonaws.com/sqlcipher/SQLCipher+for+Android+v3.0.0.zip

  2. 解压 ZIP 文件。

  3. /assets 目录将icudt46l.zip文件复制到应用程序的/assets目录。

  4. /libs目录包含几个 JAR 文件和包含本地库的文件夹。

  5. *.jar文件复制到你的应用程序的/libs目录。你可能已经在使用 Commons-codec 和/或 guava;如果是这样,请检查版本是否与 SQLCipher 兼容。

  6. 本地代码的 ARM 和 x86 实现均已包含;然而,你可能只需要基于 ARM 的本地库。因此,请将armeabi文件夹复制到你的应用程序的/libs目录下。

如何操作...

让我们创建一个加密的 SQLite 数据库。

  1. 处理 SQLite 数据库有几种方式,可以直接与SQLiteDatabase对象合作,也可以使用SQLiteOpenHelper。但是,通常如果你在应用中已经使用了 SQLite 数据库,只需将import android.database.sqlite.*声明替换为import net.sqlcipher.database.*即可。

  2. 创建加密的 SQLCipher 数据库最简单的方式是使用密码调用openOrCreateDatabase(…)

    private static final int DB_VERSION = 1;
      private static final String DB_NAME = "my_encrypted_data.db";
    
      public void initDB(Context context, String password) {
             SQLiteDatabase.loadLibs(context);
           SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(DB_NAME, password, null);
              database.execSQL("create table MyTable(a, b)");
    
      }
    
  3. 如果你正在使用SQLiteOpenHelper对象,你可能已经对其进行了扩展。在这个例子中,我们将假设你的扩展名为SQLCipherHelper。当你调用getWritableDatabase时,你会注意到需要传递一个字符串参数(数据库密码短语)与 SQLCipher 版本的SQLiteOpenHelper

    import net.sqlcipher.database.SQLiteOpenHelper;
    
    public class SQLCipherHelper extends SQLiteOpenHelper {
    private static final int DB_VERSION = 1;
    
    private static final String DB_NAME = "my_encrypted_data.db";
    
    public SQLCipherHelper (Context context) {
        super(context, DB_NAME, null, DB_VERSION);
        SQLiteDatabase.loadLibs(context);
    
    }
    }
    

提示

在使用SQLiteDatabase.loadLibs(context)语句完成任何数据库操作之前,需要加载 SQLCipher 本地库。理想情况下,此调用应位于内容提供者或应用程序对象的onCreate生命周期方法中。

工作原理…

示例代码展示了与 SQLite 数据库合作的两种最常见方式:直接使用SQLiteDatabase对象或使用SQLiteOpenHelper

需要注意的主要区别在于使用net.sqlcipher.database API 与默认 SQLite API 之间的区别在于,在创建或检索 SQLCipher 数据库对象时使用密码短语。SQLCipher 使用PBKDF2派生加密密钥,如前一个配方所介绍。在撰写本书时,默认配置生成了一个使用 4,000 次迭代的 256 位 AES 密钥。开发者需要决定如何生成密码短语。你可以基于每个应用使用 PRNG 生成,或者为了更大的随机性和安全性由用户输入。SQLCipher 使用派生的密钥透明地加密和解密。它还使用消息验证码(MAC)来确保数据的完整性和真实性,确保数据没有被意外或恶意篡改。

还有更多内容...

值得注意的是,由于 SQLCipher 的大部分代码是用本地 C/C++编写的,因此它与其他平台(如 Linux、Windows、iOS 和 Mac OS)兼容。

IOCipher

将 IOCipher 视为来自 Guardian 项目的 SQLCipher 久未联系的表亲。它提供了挂载加密虚拟文件系统的能力,允许开发者在他们的应用目录中透明地加密所有文件。与 SQLCipher 一样,IOCipher 依赖开发者管理密码,支持 Android 2.1+。

IOCipher 的一个巨大优势是它是java.io API 的一个克隆。这意味着从集成的角度来看,对现有的文件管理代码进行修改很少。不同之处在于,首先需要使用密码挂载文件系统,并且不是使用java.io.File,而是使用info.guardianproject.iocipher.File

尽管 IOCipher 使用了 SQLCipher 的部分内容,但它还不够成熟,但如果你希望保护的是文件而不是 SQLite 数据库中的数据,那么它值得研究。

另请参阅

Android KeyStore 提供者

在 Android 4.3 中,新增了一个功能,允许应用将私有的加密密钥保存在系统密钥库中。这个被称为 Android KeyStore 的功能只允许创建它们的应用访问,并且使用设备 PIN 码进行保护。

特别地,Android KeyStore 是一个证书存储,因此只能存储公钥/私钥。目前,无法存储诸如 AES 密钥之类的任意对称密钥。在 Android 4.4 中,椭圆曲线数字签名算法ECDSA)支持被添加到 Android KeyStore 中。本食谱讨论如何生成新密钥,以及如何将其保存和从 Android KeyStore 中获取。

准备开始

由于这个特性是在 Android 4.3 中添加的,请确保在 Android 清单文件中将最低 SDK 版本设置为18

如何操作...

让我们开始吧。

  1. 创建一个指向应用 KeyStore 的句柄:

    public static final String ANDROID_KEYSTORE = "AndroidKeyStore";
    
      public void loadKeyStore() {
        try {
          keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
          keyStore.load(null);
        } catch (Exception e) {
          // TODO: Handle this appropriately in your app
          e.printStackTrace();
        }
      }
    
  2. 生成并保存应用的关键对:

      public void generateNewKeyPair(String alias, Context context)
          throws Exception {
    
        Calendar start = Calendar.getInstance();
        Calendar end = Calendar.getInstance();
        // expires 1 year from today
        end.add(1, Calendar.YEAR);
    
        KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
    .setAlias(alias)
    .setSubject(new X500Principal("CN=" + alias))
    .setSerialNumber(BigInteger.TEN)
    .setStartDate(start.getTime())
    .setEndDate(end.getTime())
    .build();
    
        // use the Android keystore
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", ANDROID_KEYSTORE);
        gen.initialize(spec);
    
        // generates the keypair
        gen.generateKeyPair();
      }
    
  3. 使用给定的别名检索密钥:

      public PrivateKey loadPrivteKey(String alias) throws Exception {
    
        if (keyStore.isKeyEntry(alias)) {
          Log.e(TAG, "Could not find key alias: " + alias);
          return null;
        }
    
        KeyStore.Entry entry = keyStore.getEntry(KEY_ALIAS, null);
    
        if (!(entry instanceof KeyStore.PrivateKeyEntry)) {
          Log.e(TAG, " alias: " + alias + " is not a PrivateKey");
          return null;
        }
    
        return ((KeyStore.PrivateKeyEntry) entry).getPrivateKey();
      }
    

它是如何工作的...

KeyStore类自 API 级别 1 以来就已经存在。要访问新的 Android KeyStore,你可以使用一个特殊的常量"AndroidKeystore"

根据谷歌的文档,KeyStore类有一个奇怪的问题,即使你不是从输入流加载KeyStore,也需要调用load(null)方法;否则,你可能会遇到崩溃的情况。

在生成密钥对时,我们使用所需的详细信息填充KeyPairGeneratorSpec.Builder对象,包括我们稍后用于检索它的别名。在这个例子中,我们从当前日期开始设置了一个任意的验证期限为1年,并将序列号默认为TEN

从别名加载密钥就像加载keyStore.getEntry("alias", null)一样简单;从这里,我们将其转换为PrivateKey接口,以便我们可以在加密/解密中使用它。

还有更多...

在 Android 4.3 中,KeyChain类的 API 也得到了更新,允许开发者确定设备是否支持硬件支持的证书存储。这基本上意味着设备支持证书存储的安全元素。这是一个令人兴奋的增强功能,因为它承诺即使在根设备上也能保持证书存储的安全。然而,并不是所有设备都支持这个硬件特性。流行的设备 LG Nexus 4 使用 ARM 的 TrustZone 进行硬件保护。

另请参阅

设置设备管理策略

Device Admin 策略最早在 Android 2.2 中引入,它赋予应用程序更大的设备控制能力。这些功能主要针对企业应用开发者,因为它们具有控制性、限制性,可能具有破坏性,并提供了一种替代第三方移动设备管理MDM)解决方案的方法。通常,这不是针对消费者应用,除非已经存在信任关系,例如银行和银行应用。

本食谱将定义两个旨在加强设备的设备策略,这可能是企业移动安全政策的一部分:

  • 强制执行设备加密(这也确保设置了设备 PIN 码/密码)

  • 强制执行最大屏幕锁定超时

尽管设备加密不能替代确保应用程序数据正确加密,但它确实增加了整体设备安全性。减少最大屏幕锁定超时有助于在设备无人看管时保护设备。

对执行设备策略的应用程序数量没有限制。如果策略上有冲突,系统默认采用最安全的策略。例如,如果密码强度要求策略上有冲突,将应用最严格的策略以满足所有策略。

准备就绪

设备管理员策略在 2.2 版本中添加,但是,此功能以及对设备加密的具体限制直到 Android 3.0 才添加。因此,对于此食谱,请确保你针对的是高于 API 11 的 SDK。

如何操作...

让我们开始吧。

  1. 通过在res/xml文件夹中创建名为admin_policy_encryption_and_lock_timeout.xml的新.xml文件来定义设备管理策略,内容如下:

    <device-admin  >
        <uses-policies>
            <force-lock />
            <encrypted-storage />
        </uses-policies>
    </device-admin>
    
  2. 创建一个扩展了DeviceAdminReceiver类的类。这是与设备管理相关的系统广播的应用程序入口点:

    public class AppPolicyReceiver extends DeviceAdminReceiver {
    
      // Called when the app is about to be deactivated as a device administrator.
      @Override
      public void onDisabled(Context context, Intent intent) {
        // depending on your requirements, you may want to disable the // app or wipe stored data e.g clear prefs
        context.getSharedPreferences(context.getPackageName(),
            Context.MODE_PRIVATE).edit().clear().apply();
        super.onDisabled(context, intent);
      }
    
      @Override
      public void onEnabled(Context context, Intent intent) {
        super.onEnabled(context, intent);
    
        // once enabled enforce
        AppPolicyController controller = new AppPolicyController();
        controller.enforceTimeToLock(context);
    
        controller.shouldPromptToEnableDeviceEncrpytion(context);
      }
    
      @Override
      public CharSequence onDisableRequested(Context context, Intent intent) {
        // issue warning to the user before disable e.g. app prefs // will be wiped
        return context.getText(R.string.device_admin_disable_policy);
      }
    }
    
  3. 在你的 Android 清单文件中添加接收者定义:

    <receiver
           android:name="YOUR_APP_PGK.AppPolicyReceiver"
           android:permission="android.permission.BIND_DEVICE_ADMIN" >
           <meta-data
             android:name="android.app.device_admin"
             android:resource="@xml/admin_policy_encryption_and_lock_timeout" />
    
           <intent-filter>
             <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
             <action android:name="android.app.action.DEVICE_ADMIN_DISABLED" />
             <action android:name="android.app.action.DEVICE_ADMIN_DISABLE_REQUESTED" />
           </intent-filter>
    </receiver>
    

    定义接收者使得AppPolicyReceiver能够接收系统广播意图,以禁用/请求禁用管理员设置。你应该注意到,这是我们通过文件名admin_policy_encryption_and_lock_timeout在元数据中引用策略 XML 文件的地方。

  4. 设备策略控制器处理与DevicePolicyManager的通信以及任何特定于应用程序的逻辑。我们定义的第一个方法是供其他应用程序组件(如活动)验证设备管理员状态,并获得特定于设备管理员的意图:

    public class AppPolicyController {
    
      public boolean isDeviceAdminActive(Context context) {
        DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context
            .getSystemService(Context.DEVICE_POLICY_SERVICE);
    
        ComponentName appPolicyReceiver = new ComponentName(context,
            AppPolicyReceiver.class);
    
        return devicePolicyManager.isAdminActive(appPolicyReceiver);
      }
      public Intent getEnableDeviceAdminIntent(Context context) {
    
        ComponentName appPolicyReceiver = new ComponentName(context,
            AppPolicyReceiver.class);
    
        Intent activateDeviceAdminIntent = new Intent(
            DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
    
        activateDeviceAdminIntent.putExtra(
            DevicePolicyManager.EXTRA_DEVICE_ADMIN, appPolicyReceiver);
    
        // include optional explanation message
        activateDeviceAdminIntent.putExtra(
            DevicePolicyManager.EXTRA_ADD_EXPLANATION,
            context.getString(R.string.device_admin_activation_
    message));
    
        return activateDeviceAdminIntent;
      }
    
    public Intent getEnableDeviceEncryptionIntent() {
        return new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION);
      }
    
  5. AppPolicyController中,我们现在定义了实际执行锁定屏幕超时的方法。我们随意选择了3分钟的最大锁定时间,但这应该与企业安全政策保持一致:

      private static final long MAX_TIME_TILL_LOCK = 3 * 60 * 1000;
    
      public void enforceTimeToLock(Context context) {
        DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context
            .getSystemService(Context.DEVICE_POLICY_SERVICE);
    
        ComponentName appPolicyReceiver = new ComponentName(context,
            AppPolicyReceiver.class);
    
        devicePolicyManager.setMaximumTimeToLock(appPolicyReceiver,
            MAX_TIME_TILL_LOCK);
      }
    
  6. 根据设备的硬件和外部存储大小,加密设备可能需要一些时间。作为执行设备加密政策的一部分,我们需要一种方法来检查设备是否已加密或加密是否正在进行中:

    public boolean shouldPromptToEnableDeviceEncryption(Context context) {
        DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context
            .getSystemService(Context.DEVICE_POLICY_SERVICE);
        int currentStatus = devicePolicyManager.getStorageEncryptionStatus();
        if (currentStatus == DevicePolicyManager.ENCRYPTION_STATUS_INACTIVE) {
          return true;
        }
        return false;
      }
    }
    
  7. 我们定义了一个示例活动,以展示如何集成 AppPolicyController 以帮助指导用户启用系统设置并处理响应:

    public class AppPolicyDemoActivity extends Activity {
    
      private static final int ENABLE_DEVICE_ADMIN_REQUEST_CODE = 11;
      private static final int ENABLE_DEVICE_ENCRYPT_REQUEST_CODE = 12;
      private AppPolicyController controller;
      private TextView mStatusTextView;
    
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_app_policy);
        mStatusTextView = (TextView) findViewById(R.id.deviceAdminStatus);
    
        controller = new AppPolicyController();
    
        if (!controller.isDeviceAdminActive(getApplicationContext())) {
          // Launch the activity to have the user enable our admin.
          startActivityForResult(
              controller
                  .getEnableDeviceAdminIntent(getApplicationContext()),
              ENABLE_DEVICE_ADMIN_REQUEST_CODE);
        } else {
          mStatusTextView.setText("Device admin enabled, yay!");
          // admin is already activated so ensure policies are set
          controller.enforceTimeToLock(getApplicationContext());
          if (controller.shouldPromptToEnableDeviceEncrpytion(this)) {
            startActivityForResult(
                controller.getEnableDeviceEncrpytionIntent(),
                ENABLE_DEVICE_ENCRYPT_REQUEST_CODE);
          }
        }
    
      }
    
  8. 在这里,我们实现了 onActivityResult(…) 活动生命周期方法,以处理在启用设备管理和加密时,来自系统活动的结果:

      @Override
      protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == ENABLE_DEVICE_ADMIN_REQUEST_CODE) {
          if (resultCode != RESULT_OK) {
            handleDevicePolicyNotActive();
          } else {
            mStatusTextView.setText("Device admin enabled");
            if (controller.shouldPromptToEnableDeviceEncrpytion(this)) {
              startActivityForResult(
                  controller.getEnableDeviceEncryptionIntent(),
                  ENABLE_DEVICE_ENCRYPT_REQUEST_CODE);
            }
          }
    
        } else if (requestCode == ENABLE_DEVICE_ENCRYPT_REQUEST_CODE
            && resultCode != RESULT_OK) {
          handleDevicePolicyNotActive();
        }
      }
    
  9. 最后,我们添加了一个方法来处理如果用户选择不将此应用作为设备管理员激活的情况。在这个示例中,我们只是简单地发布了一条消息;然而,你可能会阻止应用运行,因为设备不符合企业安全策略:

      private void handleDevicePolicyNotActive() {
        Toast.makeText(this, R.string.device_admin_policy_breach_message,
            Toast.LENGTH_SHORT).show();
      }
    }
    

它的工作原理...

AppPolicyDemoActivity 展示了一个处理用户交互和回调的例子,这些回调来自系统活动,用于启用设备管理和设备加密的 onActivityResult(…)

AppPolicyController 封装了与 DevicePolicyManager 的交互,并包含了应用策略的逻辑。你可以在你的活动或片段中找到这段代码,但更好的做法是将它独立出来。

定义策略就像在设备管理员文件中的 <uses-policies> 元素中定义它们一样简单。这是在 Android 清单文件中 AppPolicyReceiver XML 声明的元数据元素中引用的:

<meta-data  android:name="android.app.device_admin"                android:resource="@xml/admin_policy_encryption_and_lock_timeout" />

由于设备管理员具有提升的权限,出于安全考虑,应用在安装时不会作为设备管理员启用。这是通过使用内置系统活动实现的,该活动通过使用具有特殊动作 AppPolicyController.getEnableDeviceAdminIntent() 的意图请求,如所示。这个活动通过 startActivityForResult() 启动,它将回调返回到 onActivityResult(…),用户可以选择激活或取消。设备管理员的非激活可能被视为违反企业安全策略。因此,如果用户没有激活它,可能足以简单地阻止用户使用应用,直到它被激活。

我们使用 DevicePolicyManager.isActive(…) 方法来检查应用是否作为设备管理员激活。通常,这个检查应该在应用程序的入口点执行,比如第一个活动。

AppPolicyReceiver 的工作是监听设备管理系统的活动。为了接收这些事件,首先你必须扩展 DeviceAdminReceiver 并在 Android 清单文件中定义 Receiver。在 OnEnabled() 回调中,我们强制执行锁屏超时,因为它不需要额外的用户输入。启用设备加密需要用户确认;因此,我们从活动中启动这个过程。

如果用户将此应用程序作为设备管理员禁用,AppPolicyReceiver也将收到onDisabled事件。如前所述,当用户将应用作为设备管理员禁用时,不同应用的处理方式会有所不同,这取决于企业安全政策。还有一个onDisableRequested回调方法,允许我们向用户显示特定信息,详细说明禁用应用程序的后果。在这个例子中,我们会清除 SharedPreferences,以确保在设备不符合要求时数据不会处于风险之中。

还有更多...

除了此食谱中使用的策略外,设备管理员还可以强制执行以下操作:

  • 启用密码

  • 密码复杂性(从 3.0 版本开始增加了更多控制)

  • 自 3.0 版本以来的密码历史

  • 在恢复出厂设置之前允许的最大密码失败尝试次数

  • 擦除设备(恢复出厂设置)

  • 锁定设备

  • 禁用锁屏小部件(自 4.2 版本起)

  • 禁用摄像头(自 4.0 版本起)

用户无法卸载处于活动状态的设备管理员应用。要卸载,他们必须首先将应用作为设备管理员停用,然后再卸载。这允许你在DeviceAdminReceiver.onDisabled()中执行任何必要的功能,例如,向远程服务器报告事件。

Android 4.4 引入了一个可选的设备管理功能常量,可以在 app 的manifest.xml文件中的<uses-feature>标签中使用,这表明应用需要设备管理功能,并确保在 Google Play 商店正确筛选。

禁用设备摄像头

Android 4.0 增加的一个有趣功能是能够禁用摄像头使用。这对于希望限制数据泄露的组织可能很有用。以下代码段展示了启用应用禁用摄像头使用的策略:

<device-admin  >
    <uses-policies>
        <disable-camera />
    </uses-policies>
</device-admin>

另请参阅

posted @ 2024-05-23 11:07  绝不原创的飞龙  阅读(12)  评论(0编辑  收藏  举报