安卓应用安全指南(三)
九、发布和销售您的应用
你可能决定通过出售你花了无数时间开发的应用来赚钱。随着移动领域最近的发展,个人开发者现在比以往任何时候都更容易营销、销售其应用并从中赚取收入。苹果有 iTunes 应用商店,黑莓有 AppWorld,安卓有市场。销售应用的过程很简单:注册成为应用销售者,然后在在线商店上发布你的应用。一旦获得批准,您的应用将立即可供 Android 用户下载。在这一章中,我们将更详细地研究这个过程,我将讲述如何让你的应用在 Android Market 上上市的基础知识。在这个过程中,我会谈到从你决定你的应用运行良好开始,到你决定把它发布到网上为止,都涉及到哪些步骤。当谈到在线销售你的应用时,我还会谈到另一个要点:收入保护。如果你的应用在任何在线商店变得受欢迎,那么你很可能会吸引那些想要“破解”和盗版你的应用的人。除非你打算免费发布你的应用,否则这可能会影响你的收入。我将花一些时间在这个主题上,并探索如何编写好的许可证密钥和注册例程来阻止盗版。在这一节中,我还将阐明如果你的应用发现自己处于一个敌对的环境中,它可能不得不经历的一些事情。
开发者注册
你还记得我们之前写的 Proxim 应用吗?让我们在 Android 市场上免费发布。我将带您了解发布应用的基础知识。在这种情况下,我不会输入任何允许我收款的具体财务信息(例如,我的银行账号),因为我不打算出售该应用。此外,我不想花太多时间告诉你如何注册成为一名开发人员,因为谷歌已经有很多关于这方面的有用信息和一套关于如何开始的综合文章。
在发布你的应用之前,你需要做的第一件事就是注册成为一名开发者。您可以使用现有的 Gmail 帐户进行注册。导航到 market.android.com/publish 的 并登录(参见图 9-1 )。在本文发表时,注册开发者的费用是 25 美元。你通过 Google Checkout 支付这个金额,而且是一次性的注册费用(见图 9-2 )。费用的存在是为了确保你是一个认真的开发者。据谷歌称,这有助于减少可能进入市场的“垃圾产品”的数量。
图 9-1 注册发布您的应用
图 9-2 注册费的支付
您的应用——已公开
外面是一片丛林。谁知道你的应用最终会在哪里?嗯,这大概是夸张了;但是正如我在这一章开始时提到的,任何可以访问 Android Market 的人都可以下载你的应用。如果这些下载转化为收入,那就太好了;不幸的是,在某些情况下,盗版会让你损失收入。盗版不是什么新鲜事。自从桌面计算时代开始,它就存在了。盗版的正式术语是侵犯软件版权;它意味着在没有适当授权的情况下将一个软件从一个设备复制到另一个设备。在大多数情况下,这仅仅意味着复制你没有购买和支付的软件。如果一个朋友买了一些软件,并给了你一份你没有付钱的拷贝,那么你就拥有了你没有购买的软件。在成长的过程中,我记得我是如何迫不及待地走进我买第一台 8088 电脑的商店(一个巨大的笨重的、坚不可摧的金属怪兽),把我每周的零花钱花在最新的游戏上。当时,我从未想过我参与了协助盗版。据我所知,我付了现金,收到了一个游戏作为回报。我从来没有意识到我支付的是从原始开发者那里购买软件的十分之一。我也不知道我的钱从来没有到达最初的开发商;它留在了店里。
开发者仍然会因为软件盗版而损失收入。你的软件有多受欢迎,以及你如何分发它,将在你的软件被盗版的程度中起到关键作用。例如,如果你允许在七天内免费试用你的应用,但允许完全访问它的所有功能,那么很可能有人会试图绕过这七天的试用期。如果成功了,那么这个人就不需要付费下载你的应用的完整版本了。侵犯版权的另一种隐蔽形式是代码盗窃。当有人下载你的软件,对其进行逆向工程,并复制代码时,就会发生这种情况。然后,这个人将你的代码重新包装成新产品,并以较低的价格出售。证明这种侵犯版权的唯一方法是下载新的和类似的应用,对其进行逆向工程,并寻找与你自己相同的编码结构。但是,如果代码被修改,这将是一项艰巨的任务来证明,甚至更难在法庭上打,因为涉及的成本很高。作为一个个人开发者,你可能不会有太多的资源用于打击盗版。因此,最好决定你是否想要保护你的应用免受盗版 — 的侵害,如果是的话,如何保护。
在这一部分,我将讨论一些你在做决定时需要考虑的话题。然后,如果你确信你需要保护你的应用免受盗版,我会给你一些例子,告诉你如何使用 Android 的许可证验证库(LVL) 来阻止未来的盗版者非法复制和分发你的应用。让我们从你的应用放在 Android Market 上时会发生什么开始。
可供下载
当你的应用出现在 Android 市场时,最终用户可以下载它。如果你对应用收费,那么显然最终用户在下载之前必须先购买它。一旦应用安装到设备上,你就可以使用 Android 调试桥(adb)将其复制到电脑上。adb 允许您以不同的方式与您的 Android 设备进行交互。您可以安装软件,打开 Linux shell 以浏览设备文件系统,以及将文件复制到设备或从设备复制文件。我已经在清单 9-1 中给出了 adb 特性的完整列表。你可以在你的 Android SDK 的平台-工具目录下找到 adb。对我来说,这个位置是在/Users/sheran/Android-SDK-MAC _ x86/platform-tools。
清单 9-1 。 亚行命令及功能
Android Debug Bridge version 1.0.29
-d - directs command to the only connected USB device
returns an error if more than one USB device is
present.
-e - directs command to the only running emulator.
returns an error if more than one emulator is running.
-s < serial number> - directs command to the USB device or emulator with
the given serial number. Overrides ANDROID_SERIAL
environment variable.
-p < product name or path> - simple product name like 'sooner', or
a relative/absolute path to a product
out directory like 'out/target/product/sooner'.
If -p is not specified, the ANDROID_PRODUCT_OUT
environment variable is used, which must
be an absolute path.
devices - list all connected devices
connect < host > [:<port>] - connect to a device via TCP/IP
Port 5555 is used by default if no port number is
specified.
disconnect [<host > [:<port>]] - disconnect from a TCP/IP device.
Port 5555 is used by default if no port number is
specified.
Using this command with no additional arguments
will disconnect from all connected TCP/IP devices.
device commands:
adb push < local > <remote> - copy file/dir to device
adb pull < remote > [<local>] - copy file/dir from device
adb sync [ <directory> ] - copy host- > device only if changed
(−l means list but don't copy)
(see 'adb help all')
adb shell - run remote shell interactively
adb shell < command> - run remote shell command
adb emu < command> - run emulator console command
adb logcat [ <filter-spec> ] - View device log
adb forward < local > <remote > − forward socket connections
forward specs are one of:
tcp:<port>
localabstract:<unix domain socket name>
localreserved:<unix domain socket name>
localfilesystem:<unix domain socket name>
dev:<character device name>
jdwp:<process pid > (remote only)
adb jdwp - list PIDs of processes hosting a JDWP transport
adb install [−l] [−r] [−s] < file > − push this package file to the device and install
it
('-l' means forward-lock the app)
('-r' means reinstall the app, keeping its data)
('-s' means install on SD card instead of internal
storage)
adb uninstall [−k] < package > − remove this app package from the device
('-k' means keep the data and cache directories)
adb bugreport - return all information from the device
that should be included in a bug report.
adb backup [−f < file>] [−apk|-noapk] [−shared|-noshared] [−all] [−system|-nosystem]
[<packages...>]
- write an archive of the device's data to < file > .
If no -f option is supplied then the data is written
to "backup.ab" in the current directory.
(−apk|-noapk enable/disable backup of the .apks
themselves
in the archive; the default is noapk.)
(−shared|-noshared enable/disable backup of the
device's
shared storage / SD card contents; the default is
noshared.)
(−all means to back up all installed applications)
(−system|-nosystem toggles whether -all automatically
includes
system applications; the default is to include
system apps)
(<packages... > is the list of applications to be
backed up. If
the -all or -shared flags are passed, then the
package
list is optional. Applications explicitly given
on the
command line will be included even if –nosystem
would
ordinarily cause them to be omitted.)
adb restore < file> - restore device contents from the < file > backup archive
adb help - show this help message
adb version - show version num
scripting:
adb wait-for-device - block until device is online
adb start-server - ensure that there is a server running
adb kill-server - kill the server if it is running
adb get-state - prints: offline | bootloader | device
adb get-serialno - prints: <serial-number>
adb status-window - continuously print device status for a specified device
adb remount - remounts the /system partition on the device read-write
adb reboot [bootloader|recovery] - reboots the device, optionally into the
bootloader or recovery program
adb reboot-bootloader - reboots the device into the bootloader
adb root - restarts the adbd daemon with root permissions
adb usb - restarts the adbd daemon listening on USB
adb tcpip < port> - restarts the adbd daemon listening on TCP on the
specified port
networking:
adb ppp < tty > [parameters] - Run PPP over USB.
Note: you should not automatically start a PPP connection.
<tty > refers to the tty for PPP stream. Eg. dev:/dev/omap_csmi_tty1
[parameters] - Eg. defaultroute debug dump local notty usepeerdns
adb sync notes: adb sync [ <directory> ]
<localdir > can be interpreted in several ways:
- If < directory > is not specified, both /system and /data partitions will be updated.
- If it is "system" or "data", only the corresponding partition
is updated.
environmental variables:
ADB_TRACE - Print debug information. A comma separated list of
the following values
1 or all, adb, sockets, packets, rwx, usb, sync,
sysdeps, transport, jdwp
ANDROID_SERIAL - The serial number to connect to. -s takes priority
over this if given.
ANDROID_LOG_TAGS - When used with the logcat option, only these debug
tags are printed.
对于希望将文件从安卓设备复制到自己电脑的人来说,拉和推命令非常有用。一般第三方 app 都存储在设备的 /data/app 目录下。首先,让我们看看应用目录 中有什么:
- 通过键入 adb shell 打开设备的外壳。
- 通过做 cd /data/app 将目录更改为 /data/app 。
- 使用 ls 列出内容。
您将看到类似于以下内容的输出:
$ ./adb shell
# cd /data/app
# ls
net.zenconsult.android.chucknorris-1.apk
test_limits_host
ApiDemos.apk
test_list_host
test_set_host
CubeLiveWallpapers.apk
test_iostream_host
test_iomanip_host
SoftKeyboard.apk
test_iterator_host
test_vector_host
test_algorithm_host
test_uninitialized_host
GestureBuilder.apk
test_sstream_host
test_char_traits_host
test_memory_host
test_ios_base_host
test_type_traits_host
test_ios_pos_types_host
test_streambuf_host
test_functional_host
test_string_host
再来看 net . Zen consult . Android . chuck Norris-1 . apk 包。我们可以抄下来看看。
要从设备复制包,您可以使用命令 adb pull 。就这么办吧。键入 exit 并按回车键,退出当前的亚行 shell 会话。接下来,键入以下内容:
adb pull /data/app/ net.zenconsult.android.chucknorris-1.apk.
这将把包复制到您的当前目录。如果您想要将文件复制到计算机上的其他位置,请用您选择的目录替换句点。您现在拥有了包文件的副本,就像它离开开发人员的计算机一样。我们可以进一步研究这个文件。
逆向工程
奇怪的排序不会仅仅停留在从设备复制包文件。他们会想更仔细地看看应用和代码。这就是逆向工程发挥作用的地方。逆向工程是获取编译后的二进制程序并生成等效的汇编或源代码以提高可读性的过程。在大多数情况下,获得源代码是最理想的情况,因为阅读源代码要比阅读汇编代码容易得多。将程序逆向工程成汇编代码的过程称为反汇编,从程序生成源代码称为反编译。你要明白,每个 CPU 都会有自己的汇编器,自己的汇编语言。这就是英特尔 x86 CPU 上的汇编代码与基于 ARM–的 CPU 上的汇编代码不同的原因。不过,我们不必达到如此低的水平。通常,工作到 Dalvik VM (DVM)级别就足够了。
DVM 还包含一个汇编器。出于解释的目的,假设 DVM 是 CPU。因此,必须使用这个汇编器来构建 Java 代码,以便在 DVM 上工作。当您使用 Android SDK 构建应用时,就会发生这种情况。将在 DVM 上运行的结果可执行文件被称为 Dalvik 可执行文件 (DEX ) 文件。您用 Java 编写代码,并使用标准的 Java 编译器( javac )将其编译成 Java 类文件。然后,要将这个类文件转换成 DEX 格式,可以使用名为 dx 的命令。您也可以在您的平台工具目录中找到这个工具。一旦生成了 DEX 文件,它就被打包成一个 APK 文件。你可能已经知道,APK 文件只不过是一个压缩文件。如果我想检查我的 APK 文件中的文件,我将如下提取该文件:
$ unzip net.zenconsult.android.chucknorris-1.apk
Archive: net.zenconsult.android.chucknorris-1.apk
inflating: res/layout/main.xml
inflating: AndroidManifest.xml
extracting: resources.arsc
extracting: res/drawable-hdpi/ic_launcher.png
extracting: res/drawable-ldpi/ic_launcher.png
extracting: res/drawable-mdpi/ic_launcher.png
inflating: classes.dex
inflating: META-INF/MANIFEST.MF
inflating: META-INF/CERT.SF
inflating: META-INF/CERT.RSA
$
请注意 DEX 文件。
幸运的是,Eclipse 将处理整个构建过程,并确保在我们的项目中插入、对齐和打包所有相关文件。我已经在图 9-3 中展示了整个构建过程。
图 9-3 。Android 构建流程
现在,您已经对应用是如何构建的有了一个简单的概念,让我们看看如何将它们分开。正如我们在提取 APK 文件的内容时看到的,我们可以直接访问 classes.dex 文件。因为我们认为 DVM 是我们的 CPU,这是我们的二进制。就像 Win32 PE 文件或 Linux ELF 文件一样,这个 DEX 文件是我们的二进制文件,因为它运行在我们的 CPU (DVM)上。谷歌也为我们提供了名为 dexdump 的工具(也可以在你的平台工具目录中找到)。如果我在提取的 classes.dex 文件上运行 dexdump ,我将获得关于文件如何构建的大量信息,包括成员、调用等等。清单 9-2 显示了典型的 dexdump 反汇编的样子。
清单 9-2 。 输出
$ dexdump –d classes.dex
...
...
Virtual methods -
#0 : (in Lnet/zenconsult/android/chucknorris/e;)
name : 'a'
type : '()Ljava/lang/String;'
access : 0x0011 (PUBLIC FINAL)
code -
registers : 16
ins : 1
outs : 2
insns size : 180 16-bit code units
0009d4: |[0009d4] net.zenconsult.android
.chucknorris.e.a:()Ljava/lang/String;
0009e4: 1202 |0000: const/4 v2, #int 0 // #0
0009e6: 1a00 5100 |0001: const-string v0,
"[`www.chucknorrisfacts.com/`](http://www.chucknorrisfacts.com/)" // string@0051
0009ea: 7020 2900 0f00 |0003: invoke-direct {v15, v0},
Lnet/zenconsult/android/chucknorris/e;.a:(Ljava/lang/String;)Ljava/io/InputStream;
// method@0029
0009f0: 0c05 |0006: move-result-object v5
0009f2: 7100 1600 0000 |0007: invoke-static {},
Ljavax/xml/parsers/DocumentBuilderFactory;.newInstance:()Ljavax/xml/
parsers/DocumentBuilderFactory; // method@0016
0009f8: 0c00 |000a: move-result-object v0
0009fa: 1a01 0000 |000b: const-string v1, "" // string@0000
0009fe: 2206 1100 |000d: new-instance v6,
Ljava/util/Vector; // type@0011
000a02: 7010 1000 0600 |000f: invoke-direct {v6},
Ljava/util/Vector;. < init>:()V // method@0010
000a08: 6e10 1500 0000 |0012: invoke-virtual {v0},
Ljavax/xml/parsers/DocumentBuilderFactory;.newDocumentBuilder:()Ljavax/xml
/parsers/DocumentBuilder; // method@0015
000a0e: 0c00 |0015: move-result-object v0
000a10: 6e20 1400 5000 |0016: invoke-virtual {v0, v5},
...
...
我想你明白了。反汇编的 DEX 文件很难阅读,就像 Linux 或 Windows 上反汇编的代码一样。这不是不可能的;但对于门外汉来说,这似乎是压倒性的。
多亏了一些非常聪明的人,他们也认为反汇编的 DEX 文件很难阅读,我们现在有了反汇编器,可以生成更可读的输出。一个名叫 JesusFreke 的天才为 DEX 文件格式构建了一个全新的汇编器和反汇编器。他分别称这些斯马利和巴克斯马利;他在 code.google.com/p/smali/发布了… DEX 文件。你可能想知道 smali 和 baksmali 有什么特别之处,所以我给你看一下 baksmali 反汇编的同一个文件的一些输出:
$ java -jar ∼/Downloads/baksmali-1.2.8.jar classes.dex
$ cd out/net/zenconsult/android/chucknorris/
$ ls
ChuckNorrisFactsActivity.smali b.smali d.smali
a.smali c.smali e.smali
$
这将文件分解成单个的文件,并且更容易检查。我们来看文件 b.smali 。清单 9-3 显示了反汇编代码。
清单 9-3 。 代码被 baks Mali反汇编
.class public final Lnet/zenconsult/android/chucknorris/b;
.super Ljava/lang/Thread;
# instance fields
.field private a:Lnet/zenconsult/android/chucknorris/a;
# direct methods
.method public constructor < init > (Lnet/zenconsult/android/chucknorris/a;)V
.registers 2
invoke-direct {p0}, Ljava/lang/Thread;- > <init > ()V
iput-object p1, p0, Lnet/zenconsult/android/chucknorris/b;- > a:Lnet/zenconsult
/android/chucknorris/a;
return-void
.end method
# virtual methods
.method public final run()V
.registers 3
new-instance v0, Lnet/zenconsult/android/chucknorris/e;
invoke-direct {v0}, Lnet/zenconsult/android/chucknorris/e;- > <init > ()V
iget-object v1, p0, Lnet/zenconsult/android/chucknorris/b;- > a:Lnet/zenconsult
/android/chucknorris/a;
invoke-virtual {v0}, Lnet/zenconsult/android/chucknorris/e;- > a()Ljava/lang/String;
move-result-object v0
invoke-interface {v1, v0}, Lnet/zenconsult/android/chucknorris/a;- > a
(Ljava/lang/String;)V
return-void
.end method
这并没有好到哪里去,但它明显更容易理解和遵循。另一个可以让你反汇编 DEX 文件的工具叫做 dedexer,它是由 Gabor Paller 编写的。你可以在 dedexer.sourceforge.net/[找到。](dedexer.sourceforge.net/)
一个明显更容易使用的工具是 dex2jar,你可以在code.google.com/p/dex2jar/找到它。这个工具帮助你解构安卓。dex 文件直接转换成 Java JAR 文件。生成 JAR 文件后,可以使用任何标准的 Java 反编译器来检索 Java 源代码。我用的是 JD-,或者 Java 反编译器,你可以在java.decompiler.free.fr/找到。
要运行 dex2jar ,只需从给定的 URL 下载并解压存档文件,然后运行。蝙蝠或。sh 文件,如图 9-4 所示。这将生成一个。jar 文件,除了它以 _dex2jar.jar 结尾之外,它的名字听起来很相似。如果在 JD-GUI 中打开这个文件,就可以看到重构后的 Java 源代码。在大多数情况下,反编译的代码可以在您的开发环境中重新编译,比如 Eclipse。
图 9-4 。在 classes.dex 文件上运行 dex 2 jarT5
图 9-5 向你展示了 JD-GUI 中反编译后的源代码是什么样子。JD-GUI 有一个简单直观的界面来浏览 JAR 文件源代码,甚至可以将源代码导出到 Java 文件中。
图 9-5 使用 JD-GUI 反编译 JAR 文件
有了这样不断发展的工具,坚定的用户下载、修改和重新打包你的应用就容易多了。如果你计划编写自己的保护机制来防止盗版,那么你有了一个好的开始。但这是你应该考虑的事情吗?我将在下一节中简要介绍这一点。
你应该许可吗?
这个问题是我看到开发者问的常见问题。你真的想花和开发你的应用一样多的时间去写一个许可程序吗?答案很主观,真的要看你的 app 是做什么的。如果你的应用有独特的或者比其他应用高效几倍的功能;或者,如果它展示了一种独特的感觉,可以确保它卖得很好,那么它可能值得考虑开发一个许可 例程。然而,请注意,当我说许可时,这并不意味着收费。你仍然可以为你的应用向用户收费;只是,如果你的应用没有监控许可的方法,那么最终用户将可以自由地复制和分发应用。
你可能会考虑开发许可程序的另一个原因是,如果你计划在未来开发更多的应用,并且你也想许可它们。在这种情况下,您可以简单地使用您已经创建的一个许可库。然而,有一点需要注意的是,你需要稍微改变每个应用的算法或许可证检查程序。因此,如果你的一个应用是盗版的,那么同样的技术将不会在其他应用上工作。
安卓许可证验证库
谷歌已经提供了 Android LVL 来帮助开发者保护他们的应用不被任意分发。将 LVL 添加到应用构建路径中,并使用其中的 API 来检查和验证用户许可。LVL 与 Android Market 应用(见图 9-6 )连接,然后与 Google market 服务器核对。根据您收到的响应,您可以选择是允许还是拒绝进一步使用应用。了解 LVL 的最好方法是在一个示例应用中使用它,我们就这么做吧。但是,在您继续之前,您需要注册成为应用发布者。不过,你现在不需要这么做。让我们从编写一个非常基本的应用开始,用它来测试我们的许可程序。清单 9-4 到 9-7 展示了这个基本应用的代码。
图 9-6 LVL 库与市场应用接口,然后与市场服务器接口
这个应用本身非常简单。它涉及到 Chuck Norris(正如你从前面的提取和逆向工程部分已经猜到的。)我们都知道也害怕查克·诺里斯。他的回旋踢是传奇性的,人们报道说它们经常是许多自然灾害的原因。为了向这位伟人致敬,我将创建一个应用,从一个名为查克·诺里斯事实( 、www.chucknorrisfacts.com/)的热门网站获取最新的查克·诺里斯事实。该应用将从该网站获取所有报价,并在我们的应用屏幕的文本区域显示随机报价。只需点击按钮获取另一个事实。我依赖于该网站报价的随机性,以确保每次都有新的报价出现。像往常一样,这个应用仅仅是一个例子,说明了你需要添加 LVL 检查的方式和位置。几乎没有错误检查,应用的功能也很少。说到这里,我不知道为什么我需要为自己辩护;这是查克·诺里斯的应用。仅此一点就足够了。你可能会注意到应用中有几个你可以改进的地方。请随意这样做。
清单 9-4 。
package net.zenconsult.android.chucknorris;
**import** android.app.Activity;
**import** android.os.Bundle;
**import** android.view.View;
**import** android.widget.Button;
**import** android.widget.TextView;
**public class** ChuckNorrisFactsActivity **extends** Activity **implements** CommsEvent {
**private** Activity activity;
**private** TextView view;
**private** CommsEvent event;
/** Called when the activity is first created. */
@Override
**public void** onCreate(Bundle savedInstanceState) {
**super**.onCreate(savedInstanceState);
setContentView(R.layout. *main* );
activity = **this**;
event = **this**;
view = (TextView) findViewById(R.id.*editText1*);
// Click Button
**final** Button button = (Button) findViewById(R.id.*button1*);
button.setOnClickListener(**new** View.OnClickListener() {
**public void** onClick(View v) {
view.setText("Fetching fact...");
CommsNotifier c = **new** CommsNotifier(event);
c.start();
}
});
}
@Override
**public void** onTextReceived(**final** String text) {
runOnUiThread(**new** Runnable() {
**public void** run() {
view.setText(text);
}
});
}
}
清单 9-5。
**package** net.zenconsult.android.chucknorris;
**public interface** CommsEvent {
**public void** onTextReceived(String text);
}
清单 9-6。【CommsNotifier.java】
**package** net.zenconsult.android.chucknorris;
**public class** CommsNotifier **extends** Thread {
**private** CommsEvent event;
**public** CommsNotifier(CommsEvent evt) {
event = evt;
}
**public void** run() {
Comms c = **new** Comms();
event.onTextReceived(c.get());
}
}
清单 9-7 。**【Comms.java】文件
**package** net.zenconsult.android.chucknorris;
**import** java.io.IOException;
**import** java.io.InputStream;
**import** java.util.Random;
**import** java.util.Vector;
**import** javax.xml.parsers.DocumentBuilder;
**import** javax.xml.parsers.DocumentBuilderFactory;
**import** javax.xml.parsers.ParserConfigurationException;
**import** org.apache.http.HttpResponse;
**import** org.apache.http.client.ClientProtocolException;
**import** org.apache.http.client.methods.HttpGet;
**import** org.apache.http.impl.client.DefaultHttpClient;
**import** org.apache.http.util.EntityUtils;
**import** org.w3c.dom.Document;
**import** org.w3c.dom.NamedNodeMap;
**import** org.w3c.dom.Node;
**import** org.w3c.dom.NodeList;
**import** org.xml.sax.SAXException;
**import** android.app.Activity;
**import** android.content.Context;
**import** android.util.Log;
**import** android.widget.Toast;
**public class** Comms {
**private final** String url = "[`www.chucknorrisfacts.com/`](http://www.chucknorrisfacts.com/)";
**private** DefaultHttpClient client;
**public** Comms() {
client = **new** DefaultHttpClient();
}
**public** String get() {
InputStream pageStream = doGetAsInputStream(url);
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.*newInstance*();
DocumentBuilder db = **null**;
Document doc = **null;**
String pageText = "";
Vector < String > quotes = **new** Vector < String > ();
**try** {
db = dbFactory.newDocumentBuilder();
doc = db.parse(pageStream);
NodeList nl = doc.getElementsByTagName("div");
**for** (**int** x = 0; x < nl.getLength(); ++x) {
Node node = nl.item(x);
NamedNodeMap attributes = node.getAttributes();
**for** (**int** y = 0; y < attributes.getLength(); ++y) {
**if** (attributes.getNamedItem("class") ! = null) {
Node attribute =
attributes.getNamedItem("class");
**if** (attribute.getNodeValue()
.equals("views-
field-title")) {
NodeList children =
node.getChildNodes();
**for** (**int** z = 0; z <
children.getLength(); ++z) {
Node child =
children.item(z);
**if** (child.getNodeName()

.equalsIgnoreCase("span"))
quotes.add
(child.getTextContent());
}
}
}
}
}
Random r = **new** Random();
pageText = quotes.get(r.nextInt(quotes.size() - 1));
pageStream.close();
} **catch** (SAXException e) {
// **TODO** Auto-generated **catch** block
e.printStackTrace();
} **catch** (IOException e) {
// **TODO** Auto-generated catch block
e.printStackTrace();
} **catch** (ParserConfigurationException e) {
// **TODO** Auto-generated catch block
e.printStackTrace();
}
**return** pageText;
}
**public** String doGetAsString(String url) {
HttpGet request = **new** HttpGet(url);
String result = "";
**try** {
HttpResponse response = client.execute(request);
**int** code = response.getStatusLine().getStatusCode();
**if** (code == 200) {
result = EntityUtils.*toString*(response.getEntity());
} **else** {
Log.*e*("CN", "Non 200 Status Code " + code);
}
} **catch** (ClientProtocolException e) {
// **TODO** Auto-generated catch block
e.printStackTrace();
} **catch** (IOException e) {
// **TODO** Auto-generated catch block
e.printStackTrace();
}
**return** result;
}
**public** InputStream doGetAsInputStream(String url) {
HttpGet request = **new** HttpGet(url);
InputStream result = **null**;
**try** {
HttpResponse response = client.execute(request);
**int** code = response.getStatusLine().getStatusCode();
**if** (code == 200) {
result = response.getEntity().getContent();
} **else** {
Log.*e*("CN", "Non 200 Status Code " + code);
}
} **catch** (ClientProtocolException e) {
// **TODO** Auto-generated catch block
e.printStackTrace();
} **catch** (IOException e) {
// **TODO** Auto-generated catch block
e.printStackTrace();
}
**return** result;
}
}
从主活动开始,您可以看到有一个按钮和一个文本视图,我们将使用它进行用户交互。当用户点击我们的按钮时,我们启动我们的 CommNotifier 线程。这个线程将执行我们的 Comms 文件中的 HTTP get 请求,并返回一个从网站上收集的 Chuck Norris 事实列表中随机选取的引用。 CommNotifier 然后触发 onTextReceived(字符串文本)函数。我们的主活动实现了 CommEvent 接口。因此,每当这个方法被触发时,我们需要访问文本参数来接收我们的报价。当我们执行应用并点击按钮时,我们会看到类似于图 9-7 所示的输出。查克·诺里斯确实很吓人。
现在我们已经有了自己的应用,让我们看看如何使用 LVL 来保护它。
图 9-7 查克·诺里斯事实应用 ?? 在行动
我将在 Android 模拟器上运行这个演示。这涉及到一个额外的步骤,因为 Android 模拟器没有预先打包到 Android Market 应用中。我需要下载 Google API 插件 平台,它提供了 Android Market 的基本后台实现。它实现了我们测试 LVL 所需的许可服务。不过,我有点言过其实了。让我们从准备我们的开发环境开始。我将假设您使用 Eclipse 进行开发,并且您已经下载并安装了 API 级别至少为 8 的 Android SDK。我们出发了!
下载谷歌 API 插件
如果您使用 Eclipse,我将描述获得 Google API 附加组件所需的步骤。首先,打开 Android SDK 管理器。选择窗口 Android SDK 管理器。接下来,导航到您计划使用的 API 级别,并勾选谷歌公司的谷歌 API(参见图 9-8 )。在点击安装按钮之前,再次导航到 Extras 文件夹并勾选 Google Market 许可包(参见图 9-9 )。现在点击安装按钮。对于这个应用,我使用 2.3.3 版本的 Android API level 10,所以这是我选择的。
图 9-8 为 Android 版本 2.3.3 安装 Google APIs
图 9-9 安装市场许可包
就是这样。Eclipse 会将您的 API 下载并安装到 SDK 目录中。要找到 LVL 源代码,请从 Android SDK 目录导航到/extras/Google/market _ licensing/library。在这里,你会看到一个类似于图 9-10 所示的目录结构。让我们进入下一组步骤,即导入、修改和构建 LVL。
图 9-10T3。LVL 来源
将 LVL 源文件复制到单独的目录下
现在我们已经有了 LVL 源代码,让我们把它移到另一个工作目录中。这样做的主要原因是,如果我们继续从原始的源目录开始工作,每当我们进行更新时,我们所有的更改都可能被覆盖。因此,我们需要将我们的 LVL 源文件保存在一个单独的目录中,这样就不会被覆盖。这很简单。将库目录以及所有子目录和文件复制到您的开发目录中。
导入 LVL 源作为库项目
我们现在将建立 LVL 图书馆。为此,我们必须创建一个新的 Eclipse Android 项目,并将该项目标记为库项目。库项目没有活动,也不直接与最终用户交互。相反,它的存在是为了让其他应用可以在它们的代码中使用它的功能。创建一个新的 Eclipse 项目,选择文件 新建
其他,打开 Android 文件夹,选择 Android 项目(参见图 9-11 )。命名您的项目(参见图 9-12 ,并选择您计划开发项目的正确 API 版本(参见图 9-13 )。您需要将您的包命名为与 LVL 源代码相同的名称,即 com . Android . vending . licensing(参见图 9-14 )。
图 9-11T3。Android 项目
图 9-12T3。命名您的项目
图 9-13T3。选择 API 版本
图 9-14T3。指定包名。它应该与 LVL 源代码包相同
完成后,让我们将 LVL 源代码导入到我们的项目中。但在此之前,我们先把项目设定为库项目。在项目资源管理器窗口中右键单击项目名称,然后选择 Properties。在左侧窗格中选择 Android 选项,在右侧窗格的下半部分,您会看到一个标记为 Is Library 的勾选框。勾选此选项并点击确定按钮(参见图 9-15 )。
图 9-15 。将项目标记为库
现在我们可以导入我们的源代码了。在“项目资源管理器”窗口中右键单击项目名称,然后选择“导入”。在出现的窗口中,选择文件系统(见图 9-16 )并点击下一步按钮。在下一个窗口中,单击 Browse 按钮并导航到作为 Android LVL 源代码一部分的 library 文件夹。在左中窗格中,您应该会看到目录出现。勾选库目录并点击完成按钮,将 LVL 源文件导入到您的项目中(参见图 9-17 )。如果要求您覆盖 AndroidManifest.xml 文件,选择 Yes。您的 LVL 源现在是项目的一部分。
图 9-16T3。导入文件系统
图 9-17T3。找到并导入源代码
在我们的 app 中建立并包含 LVL
**我们先把 Google 提供的基础版 LVL 集成到我们的 app 中。在此之后,我将解释一些可能的地方,您可以修改 LVL 源代码,使之成为您自己的。我强烈推荐这种方法,因为正如我前面提到的,您编写的 LVL 修改过的源代码不会广为人知,因此攻击者会花更长的时间来破坏您的许可模块。
要在您的应用中包含 LVL,请在 Eclipse 的 Project Explorer 视图中导航到您的应用名称,右键单击并选择 Properties。从左侧窗口窗格中选择 Android 选项,然后在右下方的窗口页面中,单击“Add”按钮。然后提示您选择一个库项目(参见图 9-18 )。选择我们刚刚创建的 Android LVL 库项目。完成后,你会看到库项目包含在你的应用的项目中(见图 9-19 )。
图 9-18 。选择 Android LVL 库项目
图 9-19T3。LVL 库项目包含在 app 项目中
现在让我们把我们的 ChuckNorrisFactsActivity.java 文件改成清单 9-8 所示的文件。你可以看到我们添加了一个新的私有类,叫做 LicCallBack 。这实现了 LVL 的 LicenseCheckerCallBack 类。当许可证检查完成时,以及当许可证服务器有肯定或否定的响应时,调用此类。分别调用 allow() 或 don 牛油()方法。
清单 9-8 。 修改后的 ChuckNorrisFactsActivity.java 文件
**package** net.zenconsult.android.chucknorris;
**import** java.util.UUID;
**import** com.android.vending.licensing.AESObfuscator;
**import** com.android.vending.licensing.LicenseChecker;
**import** com.android.vending.licensing.LicenseCheckerCallback;
**import** com.android.vending.licensing.ServerManagedPolicy;
**import** android.app.Activity;
**import** android.content.Context;
**import** android.os.Build;
**import** android.os.Bundle;
**import** android.provider.Settings.Secure;
**import** android.view.View;
**import** android.view.Window;
**import** android.widget.Button;
**import** android.widget.TextView;
**import** android.widget.Toast;
**public class** ChuckNorrisFactsActivity **extends** Activity **implements** CommsEvent {
**private** Button button;
**private** TextView view;
**private** Activity activity;
**private** CommsEvent event;
**private** LicCallBack lcb;
**private static** final String*PUB_KEY* = "MIIBI...";// Add your Base64 Public
// key here
**private** staticfinal byte[]*SALT* = **new byte**[] { −118, -112, 38, 124, 15,
-121, 59, 93, 35, -55, 14, -15, -52, 67, -53, 54, 111, -28,
-87, 12 };
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
**super**.onCreate(savedInstanceState);
requestWindowFeature(Window.*FEATURE_INDETERMINATE_PROGRESS*);
setContentView(R.layout.*main*);
event = **this**;
activity = **this**;
view = (TextView) findViewById(R.id.*editText1*);
// Click Button
button = (Button) findViewById(R.id.*button1*);
button.setOnClickListener(**new** View.OnClickListener() {
public void onClick(View v) {
// Do License Check before allowing click
// Generate a Unique ID
String deviceId = Secure.*getString*(getContentResolver(),
Secure.*ANDROID_ID*);
String serialId = Build.*SERIAL*;
UUID uuid = **new** UUID(deviceId.hashCode(),
serialId.hashCode());
String identity = uuid.toString();
Context ctx = activity.getApplicationContext();
// Create an Obfuscatorand a Policy
AESObfuscator obf = **new** AESObfuscator(*SALT*,
get**Package**Name(),
identity);
ServerManagedPolicy policy = **new**
ServerManagedPolicy(ctx, obf);
// Create the LicenseChecker
LicenseChecker lCheck = **new** LicenseChecker(ctx,
policy,*PUB_KEY*);
// Do the license check
lcb = **new** LicCallBack();
lCheck.checkAccess(lcb);
}
});
}
@Override
public void onTextReceived(final String text) {
runOnUiThread(**new** Runnable() {
public void run() {
setProgressBarIndeterminateVisibility(**false**);
view.setText(text);
button.setEnabled(**true**);
}
});
}
**public class** LicCallBack implements LicenseCheckerCallback {
@Override
public void allow() {
if (isFinishing()) {
return;
}
Toast toast = Toast.*makeText*(getApplicationContext(),
"Licensed!",
Toast.*LENGTH_LONG*);
toast.show();
button.setEnabled(**false**);
setProgressBarIndeterminateVisibility(**true**);
view.setText("Fetching fact...");
CommsNotifier c = **new** CommsNotifier(event);
c.start();
}
@Override
public void dontAllow() {
if (isFinishing()) {
return;
}
Toast toast = Toast.*makeText*(getApplicationContext(),
"Unlicensed!", Toast.*LENGTH_LONG*);
toast.show();
}
@Override
public void applicationError(ApplicationErrorCode errorCode) {
// TODO Auto-generated method stub
}
}
}
下一件你会注意到的事情是,我们没有在我们的按钮点击上做任何活动。相反,我们做执照检查。这意味着我们将我们的报价获取活动移到了 LicenseCallBack 类的 allow() 部分。要使用来自 LVL 的许可证检查,您必须调用 LicenseChecker 类的 checkAccess() 方法。您必须使用以下参数构建 LicenseChecker:
- 应用上下文
- 许可政策
- 您的公钥
对于应用上下文,您可以使用当前的应用上下文。如果您的 LicenseChecker 在另一个类中被实例化,那么您需要将应用上下文对象传递给这个类。您的 Base 64 编码公钥将位于您的在线发布者配置文件页面中。要访问它,登录 market.android.com/publish/Hom… 的,点击编辑个人资料,然后向下滚动到名为许可&应用内计费的部分。名为公钥的文本区域保存您的密钥(参见图 9-20 )。将此复制并粘贴到您的应用中。许可策略需要更多的解释,所以我将在下一节描述它。
同样,请看下面的代码:
AESObfuscator obf = new AESObfuscator(*SALT*, get**Package**Name(),identity);
当你的应用收到来自 Android 许可服务器的响应时,它需要在本地设备上存储关于这个响应的信息。将响应数据保持为纯文本形式只会意味着攻击者可以读取和篡改这些信息。为了防止这种情况发生,LVL 允许我们在将信息存储在设备上之前对其进行模糊处理。 AESObfuscator 类就是这样做的。它需要一个 salt 值(只是一个随机的 20 字节)和一个唯一的设备标识。唯一标识确保只能从具有该匹配标识的设备读取数据。在您自己的代码中,您将希望从尽可能多的信息来源中构建这个标识字符串。在这种情况下,我使用的是 ANDROID_ID 和 OS 构建序列号。
另请注意,您的应用必须请求新的权限。为了能够通过 Android Market 验证许可证,请确保将以下权限添加到 AndroidManifest.xml 文件中:
<用途-权限 Android:name =【com . Android . vending . check _ LICENSE】>
图 9-20T3。Base64 编码的公钥
您的 publisher 仪表板有一个标有测试响应的下拉菜单(参见图 9-20 )。您可以通过将该值设置为许可或非许可来测试您的应用。Google API 和 LVL 将联系 Android Market 服务器,并向您的应用提供此响应。将测试响应值设置为 NOT_LICENSED 可让您看到未经授权的用户试图使用您的应用时,您的应用会如何运行(参见图 9-21 )。相应地,您可以进行更改,或者显示一条消息(我用一个词来表示该应用是否获得许可),或者将用户重定向到 Android Market,以便她可以购买您的应用。
图 9-21T3。一个未经许可的用户收到一个否定的回应,应用无法运行
许可政策
可以用来定制许可过程的一个关键机制是许可策略。Android LVL 附带两个默认策略 :
- 严格的政策
- 服务器管理策略
Google 建议您使用 ServerManagedPolicy,因为它还处理服务器响应的缓存。这通常很有用,因为 Google 对应用查询其服务器的次数进行了限制。StrictPolicy 将始终查询服务器;虽然这可以防止本地设备数据被篡改,从而更加安全,但如果谷歌服务器因为您达到了极限而拒绝给您响应,它可能会将您的最终用户锁定在外。
这两个策略对象都提供了您将会关注的两个基本方法: allowAccess() 和 processServerResponse() 。 allowAccess() 方法必须返回一个布尔值。当被调用时,如果选择允许访问,则返回 true;否则,返回假。请看清单 9-9 中的示例实现。
***清单 9-9 。***server managed policy 对象中的 allowAccess()方法 (由 Google 提供)
public boolean allowAccess() {
long ts = System.currentTimeMillis();
if (mLastResponse == LicenseResponse.LICENSED) {
// Check if the LICENSED response occurred within the validity timeout.
if (ts < = mValidityTimestamp) {
// Cached LICENSED response is still valid.
return true;
}
} else if (mLastResponse == LicenseResponse.RETRY &&
ts < mLastResponseTime + MILLIS_PER_MINUTE) {
// Only allow access if we are within the retry period or we haven't
used up our
// max retries.
return (ts < = mRetryUntil || mRetryCount < = mMaxRetries);
}
return false;
}
您可以看到,如果该函数接收到 LicenseResponse,它将返回 true 。许可作为其回应。该函数首先检查收到的最后一个响应是否表明该应用已获得许可。然后,它检查该日期是否仍在有效期内。如果是,那么它返回真。如果日期大于有效期,则返回假。该函数还检查服务器是否要求我们继续重试,并且它在合理的重试限制和时间间隔内这样做。响应对象 mLastResponse 是从 processserveresponse()方法 中派生出来的,如清单 9-10 所示。您可以看到这个函数检查三个响应:
- 许可证响应。瑞莉
- 许可证响应。得到许可的
- 许可证响应。未获得许可
相应地,它随后设置参数, allowAccess() 方法可以读取这些参数。你会注意到另一件事。 processServerResponse() 对象中的最后一行是一个 commit() 操作。这是一种缓存功能,在该功能中,响应被混淆,然后存储在设备的共享首选项中。StrictPolicy 中不存在此部分,因为没有缓存任何数据。
***清单 9-10 。***ServerManagedPolicy 对象中的 processServerResponse()方法
**public void** processServerResponse(LicenseResponse response, ResponseData rawData) {
// Update retry counter
if (response != LicenseResponse.*RETRY*) {
setRetryCount(0);
} **else** {
setRetryCount(mRetryCount + 1);
}
**if** (response == LicenseResponse.*LICENSED*) {
// Update server policy data
Map < String, String > extras = decodeExtras(rawData.extra);
mLastResponse = response;
setValidityTimestamp(extras.get("VT"));
setRetryUntil(extras.get("GT"));
setMaxRetries(extras.get("GR"));
} **else if** (response == LicenseResponse.*NOT_LICENSED*) {
// Clear out stale policy data
setValidityTimestamp(*DEFAULT_VALIDITY_TIMESTAMP*);
setRetryUntil(*DEFAULT_RETRY_UNTIL*);
setMaxRetries(*DEFAULT_MAX_RETRIES*);
}
setLastResponse(response);
mPreferences.commit();
}
```*** *****LVL 的有效使用**
如果您修改了 LVL 源代码(即您的策略),使其成为您的应用独有的东西,那么这一努力是值得的。您可能犯的一个错误是使用 LVL 的普通实现,每个人都知道它的源代码。这使得有人更容易修补你的应用,绕过你的许可证检查程序。Justin Case 已经在 Android Police 网站上展示了这个漏洞。可以在[www . androidpolice . com/2010/08/23/exclusive-report-Google-Android-market-license-verification-easy-boxed-will-not-stop-pirates/](http://www.androidpolice.com/2010/08/23/exclusive-report-googles-android-market-license-verification-easily-circumvented-will-not-stop-pirates/)找到文章。诚然,这是一篇老文章,但它仍然展示了这样一个原则,即如果您知道源代码是什么样子,就可以很容易地理解和修改逆向工程代码。在这种情况下,Justin 在一个演示和一个实际的商业应用中演示了如何修补和绕过 LVL 检查。
Android 开发者博客的 Trevor Johns 为我们提供了另一套很好的指南。这篇文章非常值得一读,并列出了一些更有效地使用 LVL 的技巧。有一段代码非常有趣。看图 9-22 。Trevor 告诉我们,攻击者可以猜测许可和未 _ 许可常量值的响应,然后交换它们,以便未经许可的用户可以完全使用该应用。为了防止这种情况,Trevor 向我们展示了一些代码,这些代码将对响应运行 CRC32 检查;不检查常量,而是检查常量的 CRC32 检查结果。我想在这个问题上多谈一点。想象一下,如果您不运行固定值的检查,而是执行另一个 HTTP fetch 来从您自己的服务器检索响应。

图 9-22 。备用响应验证思路
考虑清单 9-11 中的代码。您向服务器发出一个额外的请求,并从那里检索响应代码,而不是直接与一个数字进行比较。这样做的一个好处是,你可以以任何你喜欢的方式设计你的 ServerVerifier 对象。您甚至可以设置它,使响应代码每次都发生变化。您甚至可以考虑在代码中使用质询响应来改变每次的响应。
***清单 9-11 。*** *修改验证功能*
```java
public void verify(PublicKey publicKey, int responseCode, String signedData, String
signature) {
// ... Response validation code omitted for brevity ...
// Compute a derivative version of the response code
// Rather than comparing to a static value, why not retrieve the value from
a server that you control?
java.util.zip.CRC32 crc32 = new java.util.zip.CRC32();
crc32.update(responseCode);
int transformedResponseCode = crc32.getValue();
ServerVerifier sv = new ServerVerifier(); // This class will make an
HTTP request to your server to fetch the code.
int serverResponse = sv.retrieveLicensedCode(); // There is no limit
to how you can create this routine.
// ... put unrelated application code here ...
// crc32(LICENSED) == 3523407757 But this part is calculated on your server.
if (transformedResponse == serverResponse) {
LicenseResponse limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
handleResponse(limiterResponse, data);
}
...
...
...
或者,这也可以发生在策略中(与检查硬编码的服务器响应相反):
**if** (response == LicenseResponse.*LICENSED*)
您可以通过以下方式从您信任的服务器之一检索响应来检查响应:
ServerVerifier sv = new ServerVerifier();
**if** (response == sv.getLicensedResponse())
```*** *****混淆视听**
混淆 是你需要考虑的另一个要点。它适用于软件盗版,以及知识产权盗窃。模糊处理是将源代码中的所有类名、变量名和方法名更改为随机的、不相关的名称的过程。你可能想知道为什么我的反编译应用在目录列表中有像 a.smali 、 b.smali 、 c.smali 等文件。当我使用 BakSmali 反编译我的应用时,我是在二进制文件的模糊版本上运行它的。代码混淆器确保将我的类名(如 Comms 、 CommsEvent 、 CommsNotifier 等)更改为不主动提供它们所做工作的信息的类名。此外,如果您查看这些文件,您会发现方法名和成员名都被混淆了。这对于试图对代码进行逆向工程的人来说是非常令人沮丧的,并且它可以作为对知识产权或代码盗窃的极好的威慑。
模糊处理不能保证你的代码不会被窃取或盗版。它只是让逆向工程的任务变得更加困难。Android SDK 附带了一个名为 ProGuard 的混淆器。您可以使用 ProGuard 来混淆您的任何 Java 代码。你可以在[`proguard.sourceforge.net/;`](http://proguard.sourceforge.net/;)下载它,它是免费的开源软件。Android 开发者文档强烈建议你在打包应用发布时对代码进行模糊处理。如果您使用 Eclipse,那么这是一项简单的任务。在您的项目中找到您的 project.properties 文件(参见图 9-23 )并添加这一行(参见图 9-24 ):
```java
proguard.config = proguard.cfg
注意,这一行假设您没有将 proguard.cfg 文件的位置从其默认位置移走。
图 9-23T3。项目属性文件
图 9-24T3。添加 proguard.config 属性
要导出已签名或未签名的 APK 文件,右键单击您的项目名称,选择 Android Tools,然后选择导出未签名的应用包或导出已签名的应用包(参见图 9-25 )。
图 9-25T3。导出混淆后的包
ProGuard 是一个免费的开源 Java 代码混淆器。除了混淆之外,ProGuard 还试图缩小、优化和预先验证您提供给它的代码。就缩短执行时间而言,预验证对于移动应用非常重要。预验证阶段确保 Java 类的注释方式允许 VM 更快地读取和执行一些运行时检查。在大多数情况下,使用默认的 proguard.cfg 文件就足够了。图 9-26 显示了反编译、混淆的类文件的输出。正如您所看到的,由于重命名的、看起来很神秘的类名和变量名,代码本身很难阅读。混淆并不意味着停止逆向工程;相反,它更像是一种威慑,因为重新构建被重命名的变量和类名可能需要很长时间。一些商业 Java 混淆器甚至混淆了类文件中的字符串。这使得代码更加难以逆向工程。
图 9-26T3。反编译、混淆的类文件*** ***总结
这一章专门讨论了你在应用货币化时将会面临的一些重要问题。虽然像苹果应用商店、黑莓应用世界和安卓市场这样的网站让你很容易获得收入,但你无疑将不得不面对软件盗版和知识产权盗窃等问题。你应该记住,本章讨论的主题不是灵丹妙药。它们不会完全保护您,但是它们会为您提供优势,使您的代码变得更难被攻击。在最好的情况下,攻击者会不动你的应用,因为他不想花力气对它进行逆向工程。
在这一章中,我们看了如果你的应用发现自己处于一个敌对的环境中,它会受到什么影响。我们研究了如何对你的应用进行逆向工程,并在修改后重新构建。我们展示了如何混淆你的源代码,使得攻击者更难读懂你的代码,即使是在逆向工程之后。然后,我们研究了如何检查应用中的许可,以确保最终用户不会盗版你的应用。我们通过使用 Android LVL 做到了这一点。要记住的一件事是,总是在许可检查库中编写自己的例程。这确保了你的代码是新鲜的,新的,不为人所知的。这使得逆向工程更加困难。
在发布应用之前,请记住这几个步骤。你可以在网上的developer . Android . com/guide/publishing/preparating . html找到它们的完整描述。
- 选好包名。它将在应用的整个生命周期中起作用。
- 关闭调试和日志记录。确保搜索调试跟踪并禁用它。
- 清除项目目录中的备份文件或开发和测试过程中创建的其他不必要的文件。
- 检查您的清单文件,并确保所有必需的权限都存在。确保设置了标签和图标值以及正确的版本代码和版本名称属性。
- 检查并优化您的应用,以获得正确的 Android 版本。确保您的应用适合在不同规格的设备上运行。
- 在你的应用中更新你的网址。这意味着删除任何本地 IP 地址和测试服务器。将它们更改为正确的生产 IP 地址。
- 在您的应用中实施许可。*****
十、恶意软件和间谍软件
像个人电脑一样,移动智能手机也容易受到各种恶意软件的攻击。在本章中,我将把恶意软件和间谍软件统称为恶意软件。尽管我这样做了,但了解这些类型的恶意应用之间的区别是非常重要的。
恶意软件被定义为 驻留在用户电脑或智能手机上的任何恶意软件,其唯一任务是破坏数据、窃取个人信息或访问系统资源,以获得对其所在设备的完全控制。编写恶意软件的唯一目的是造成伤害;通常,恶意软件作者会编写恶意软件来针对操作系统或平台中的特定弱点。通常,恶意软件作者会希望最大化其恶意软件的传播,并寻求实现一种机制,使其软件能够将自身复制到其他类似的设备上。
间谍软件 是一个术语,用来指从设备中访问和窃取个人或私人信息的恶意软件。例如,在手机恶意软件的情况下,应用可能会跟踪最终用户的电子邮件、联系人列表、SMS 消息,甚至照片。间谍软件通常需要隐蔽并长时间驻留在设备上。因此,间谍软件作者的目标是在设备上执行很少或没有破坏性的活动,以便最终用户不知道她的数据被盗。几乎任何人都可以使用恶意软件;不再要求您知道如何自己编写恶意软件代码。
许多公司向个人、大公司甚至政府出售恶意软件(参见本章后面的案例研究)。我见过两种出售恶意软件的公司:一种卖给大型组织或政府,另一种卖给个人零售消费者。正如我们将在本章后面回顾的那样,一家大型中东电信供应器被发现在监视其整个黑莓用户群。有助于做到这一点的软件是由一家著名的专门从事合法监听的美国公司出售的。原来,源代码完全是从头开始开发的,它的唯一目的是从受感染的设备上捕获和泄露电子邮件。
另一方面,你会发现恶意软件或间谍软件被打包出售给任何愿意监视她认识的人。在大多数情况下,销售这类软件的公司会宣称“抓住出轨的配偶!”显然,这对一些人来说很有吸引力!我还将更详细地看一下这些版本的零售恶意软件。
恶意软件的四个阶段
我们可以将恶意软件操作分为四个不同但截然不同的阶段。虽然不是正式的,但这些阶段在大多数在设备上发现恶意软件的情况下都是可见的。
感染
这是恶意软件被引入设备的阶段。感染的圣杯是不涉及终端用户交互的圣杯。当恶意软件可以通过一些无害的方式复制到设备上时,就会发生这种情况,如向用户发送 SMS 消息或在无线网络上损害设备。
第二种感染方式是通过部分辅助行为。用户被要求单击恶意网站中的链接。一旦他这样做了,恶意软件就会把自己复制到设备上。攻击者通过 SMS 或电子邮件将此链接发送给用户。虽然有效,但这需要用户干预;在大多数情况下,勤奋的用户总是对点击发送给他们的随机链接持怀疑态度。
最后一种感染形式是攻击者通过 USB 端口或浏览网站,将恶意软件物理复制到设备上。这发生在攻击者和终端用户彼此认识,或者攻击者可以物理访问终端用户的设备的情况下。如果用户对其设备进行了密码保护,并且需要密码才能使用设备或在设备上安装应用,则此技术无效。
妥协
大多数情况下,感染和妥协是相伴而生的。在这种情况下,我使用单词 compromise 来描述恶意软件如何能够获得对设备的超级用户访问。因此,恶意软件可以以它选择的任何方式对设备配置进行更改 — ,而不需要设备所有者的交互。
正如我们在前面章节中看到的,运行在 Android 上的程序需要用户明确授权才能访问互联网或阅读电子邮件。在危害阶段,恶意软件将利用操作系统中的弱点来规避权限授予过程,从而允许它在用户不知情的情况下执行任何功能。
传播
除非专门针对个人,否则恶意软件作者通常会想要感染大量用户。他可能想要控制一大批设备,或者只是从许多不同的人那里获取私人信息。Zeus 特洛伊木马(在个人计算机平台上发现)将利用操作系统中的弱点进行传播。它的唯一目的是收集用户的按键,并收集银行和社交网站的凭据。
最近,另一种流行的传播甚至感染机制是使用谷歌 Android 市场(作者可以在这里出售或免费分发他们的应用)。恶意软件作者可以将游戏或社交网络互动工具等看起来无害的应用上传到 Android Market。当最终用户购买或下载该应用时,她的设备就会被感染。
渗出
恶意软件通常以个人或机密信息为目标。它可能会记录击键,试图获取网上银行和电子邮件等网站的用户名或密码。然而,仅仅收集这些信息是不够的。攻击者需要访问这些信息,因此恶意软件会找到一种“呼叫总部”或与远程服务器通信的方式,要么接收新的指令,要么上传捕获的信息。这个阶段叫做渗出。让我们来看一个案例研究,说明这是如何工作的。
案例研究 1:政府批准的恶意软件
2009 年 7 月,阿拉伯联合酋长国(UAE)的电信供应器 Etisalat 向其所有黑莓手机用户发送了一条短信,要求下载并安装系统补丁。该补丁旨在提高手机 3G 功能的性能。原来,这个“补丁”只不过是一个恶意软件,旨在读取每个用户的外发电子邮件。
直到今天,该公司仍坚称该补丁旨在提高性能。大多数检查过该恶意软件的研究人员,包括我自己和 Research In Motion (RIM ),都可以看到它没有任何性能上的好处。相反,检查代码会发现有人故意试图捕获设备所有者的所有外发电子邮件,并将其副本发送到供应器的服务器进行检查。
这个案件的标题,政府批准的恶意软件,可能有点强,特别是当你考虑到没有确凿的证据已经从这个案件的调查具体化。我选择这个标题是基于我在阿联酋工作的 11 年(其中 5 年为 Etisalat 工作)、最近的媒体事件,以及我对政府和监管机构控制该国媒体和通信基础设施的密切程度的了解。
我提到的媒体事件发生在 2010 年 8 月左右,当时阿联酋政府宣布,如果 RIM 公司不提供监控用户信息的手段,包括电子邮件和 BlackBerry Messenger(允许黑莓用户相互发送信息的本地消息平台),它将关闭该国境内的所有黑莓服务。由于我不是在写间谍小说,我将为我的下一本书搁置我所有的理论,而是带你了解恶意软件本身的一些更真实的方面。在本案例研究中,我们将尝试唯一确定恶意软件感染的阶段。
感染
Etisalat 通过使用简单的 WAP-push 消息将恶意软件引入其用户的设备。这是一条出现在设备的 SMS 收件箱中的消息,它包含文本和 URL。WAP-push 消息的文本如下:
亲爱的 Etisalat BlackBerry 客户:
Etisalat 始终热衷于为您提供最佳的黑莓服务和终极体验,为此,我们将向您发送一个性能增强补丁,您需要将其安装在您的设备上。如需更多信息,请拨打 101
借助 Etisalat 的黑莓和移动解决方案提升您的业务
一旦用户点击附带的 URL,设备就会下载并安装一个名为 Registration 的应用。该设备将提示终端用户授予应用特定的权限。由于 WAP-push 消息来自看似合法的来源,大多数用户没有理由不信任该请求,并且通常授予应用完全权限。
妥协
在这种情况下,恶意软件没有依靠操作系统中的弱点来获取个人信息。用户认为应用是合法的,因此在安装阶段授予了所有必要的权限。
传播
Etisalat 发布的恶意软件旨在保留在设备上并收集信息。它不是为传播到其他设备而设计的。恶意软件依赖于 WAP-push 消息,而不是传播。安装将一次性完成,此后不会扩散。
渗出
这是 Etisalat 恶意软件最重要的阶段。它被设计成将自己附加到用户发送的电子邮件中,并向 Etisalat 内部的服务器发送每条外发邮件的副本。这由内置的 BlackBerry API 调用来完成。
图 10-1 中描述了一条真实的消息(恶意软件用来向服务器登记)。这是一条每小时发送到服务器的消息。然后,恶意软件系统的操作员可以看到哪些设备被恶意软件感染,包括哪些设备定期检查。
图 10-1T3。注册恶意软件使用的捕获的“心跳”消息
检测
这种特定的恶意软件之所以被检测出来,是因为它写得很糟糕。恶意软件一发布,本应接收泄漏数据的服务器就被信息淹没了。无法承受负载,服务器崩溃。这导致设备上的恶意软件不断重试连接到无响应的服务器。这种持续的连接尝试增加了设备本身的处理器使用率。
此时,最终用户开始注意到他们的设备性能缓慢,电池过早耗尽。一些用户甚至注意到他们的设备过热。这促使几名研究人员调查注册应用,于是他们发现这实际上是恶意软件。图 10-2 显示了恶意软件安装在设备上时如何运行的流程图。以下是注册恶意软件特征的详细列表:
- 它检查它是否在 BlackBerry 安装的应用中被列为可见。
- 如果它是可见的,它会隐藏自己,不让订阅者看到。这可以防止用户找到并删除它。
- 它遍历手持设备上的所有邮件帐户,并将自己附加到每个帐户上,查找收到的电子邮件和 PIN 消息。
- 它截取并监控手持设备的状态,以发现发生的网络事件。当这些事件发生时,它通知服务供应器的服务器。
- 它监听通过电子邮件或 BlackBerry PIN 从特定地址收到的消息。这些控制消息可以启用或禁用对用户消息的拦截。
- 它定期向预定义的服务供应器服务器报告。
- 如果启用,应用会将订户发出的电子邮件的副本转发到服务供应器服务器。
图 10-2T3。Etisalat 恶意软件操作流程图
案例研究 2:零售恶意软件—FlexiSPY
现在让我们看看第二个恶意软件应用:FlexiSPY,一种零售恶意软件。当攻击者在目标设备上安装 FlexiSPY 时,它会窃听所有通信。最新版本的 FlexiSPY Omni 为 Android 用户提供了以下功能:
- 捕获短信和电子邮件
- 捕获通话记录
- 通过 GPS 和手机信号塔信息发现 GPS 位置
- 把手机变成监听设备
- 拦截电话
- SIM 卡更换通知
对于监视任何人来说,这似乎已经足够了,我发现这个特性列表非常有趣,足以获得一个副本并对其进行分析。
注意本着全面披露的精神,我要提一下,在我评估 FlexiSPY 的时候,我查看了黑莓版本,因为那是我的主要手机。激活和启用设备的协议都是基于网络的,所以它们或多或少保持相同,不管支持的设备平台是什么(当然包括 Android)。
一旦买家支付了 349 美元,她就会收到一本用户手册,上面提供了如何在目标客户的手机上安装该应用的信息。当浏览用户手册时,首先映入我眼帘的是它提供了*。。。明确指示* 将黑莓手持设备的默认权限设置为允许所有 。
这意味着,不仅仅是 FlexiSPY,目标安装在手机上的每一个应用都可以获得对手持设备的完全控制(在编程接口或 API 的范围内)。显然,在这种情况下,用户保护并不是最重要的。类似地,查看 FlexiSPY 的 Android 手册,在您可以成功地在设备上安装恶意软件之前,设备本身必须是根。该网站以超级一键点击的形式提供了一个根设备的解决方案。除了这段文字,该网站没有提供直接链接。找到漏洞是客户的责任。
FlexiSPY 需要激活才能开始监视目标。为此,用户必须拨打号码 *#900900900 ,这将激活一个隐藏的屏幕。在此屏幕上,系统会提示用户输入激活码。从来没有一个离开家没有我最喜欢的网络数据包嗅探器,Wireshark,我嗅探了激活过程中通过的流量。以下是通过网络传递的信息:
- 邮件/t4l-mcli/cmd/productactivate?模式=【0】&【查看】【0302】&【PID =【FSP _ bb _ v 4.2】&【act code】【启用代码】&散列=)
此请求是向具有下列二级域的服务器发出的:
aabackup.info
它解析为与之前列出的主机 djp.cc 相同的 IP 地址。正如你所看到的,手机的 IMEI 被发送回 FlexiSPY 总部。还可以看到激活码,它返回一个哈希值。看起来手机计算了一个类似的算法,并等待一个匹配的散列。一旦收到正确的散列,应用就被激活。
从这一点上说,这是一个配置应用拦截短信,电子邮件,通话记录,等等。该应用有一个通过短信的命令通道。因此,您有一个包含八个命令的列表,它们执行以下操作:
- 开始捕获:开始捕获事件,如电子邮件、SMS、位置等。
- 停止捕获:停止已经开始的捕获。
- 立即发送:将所有收集的事件发送到中央日志记录主机。
- 发送诊断信息:发送诊断信息。
- 启动 SIM 卡监视器:观察任何改变 SIM 卡的企图。
- 停止 SIM 卡监控:停止监控 SIM 卡。
- 启动麦克风监听:等待触发号码的来电。
- 停止麦克风监听:停止监听来自该触发号码呼叫。
有趣的是,命令频道 SMS 消息不能被删除,所以手册建议用户选择像“早上好”或类似的短语来开始捕捉信息。措辞应该选择得不会引起目标的怀疑。
请记住,我在 BlackBerry 版本的 FlexiSPY 上执行了前面的检查。考虑到运行 Java 的每个平台的相似性,Android 也会以相似的方式运行。
反取证
目前,最广泛使用的检测机制 是基于签名的。这意味着任何反恶意软件公司编写删除或检测功能需要事先了解恶意软件*。如果它遇到它,那么它可以删除它。因此,不太可能检测到新的恶意软件。这是不幸的,因为如果反恶意软件公司无法跟上恶意软件的发展,那么从恶意软件发布到发现并解决它总是有一个滞后。在此滞后期间,所有用户都面临风险。*
*作为开发人员,您无法直接控制用户是否选择安装反恶意软件应用。您的责任在于确保您的应用安全地处理其数据。我们在前面的章节中已经介绍了这些技术中的大部分,但是我想强调另一个可用的、非正统的选项:反取证。
反取证是一种 技术,用于通过降低可收集信息的质量来挫败对计算机或移动设备的取证分析。法医分析包括检查这种设备的证据。大多数情况下,需要收集的证据非常脆弱。反取证试图通过使用定期运行的自动化工具来销毁这些信息。然后,当进行法医分析时,调查人员只会发现乱码或无用的数据。这大大降低了可检索信息的质量。我们可以使用类似的技术来阻止恶意软件的行为。
我将从一个简单的例子开始:假设您的应用读写设备的消息存储。既然可以接触到这些数据,就可以人为生成邮件信息,随意删除。假设在设备上安装了等待复制进入收件箱的消息的恶意软件应用。通过生成许多虚假消息,然后定期删除它们,您正在为恶意软件提供低质量、无用的数据。如果操作正确,这个过程会使恶意软件作者提取有效信息变得非常繁琐。这个概念在图 10-3 和图 10-4 中进行了说明。
图 10-3T3。恶意软件拦截邮件信息
图 10-4T3。生成假消息
这种技术可以被认为有点咄咄逼人;显然,最终用户应该同意您的应用的这种行为。我在这里提到它是作为另一种要考虑的技术。至于如何想出击败恶意软件的额外机制,我将留给你的想象力。然而,除非你的主要目标是开发这样的反恶意软件应用,否则你可以选择跳过它们。
摘要
在本章中,我们看了恶意软件和间谍软件以及它们是什么。我们还研究了恶意软件的各个阶段,以及我们如何将它们分成几大类。我们了解到恶意软件可以被任何人使用,并且有许多商业实体向个人和公司消费者提供恶意软件。我们的案例研究涉及 2009 年发生的真实世界恶意软件感染,当时阿联酋的电信供应器之一 Etisalat 将其整个黑莓用户群置于间谍软件应用之下。
我们已经看到,作为一名应用开发人员,您通常在控制什么样的恶意软件被引入设备方面能力有限。相反,您的目标是安全地处理应用的数据(和最终用户数据)。我们非常简要地讨论了一个主题,即如何使用一些反取证技术有目的地向恶意软件提供无用的数据,从而迫使恶意软件作者费力地通过这些消息找到真正的消息。虽然这绝不是一个万无一失的解决方案,但这种技术主要是作为一种威慑。除非恶意软件作者专门针对你,否则他不太可能浪费时间筛选无用的数据。相反,他会把注意力转移到下一个被他感染的人身上。*
十一、附录一:Android 权限常量
出于参考目的,本附录提供了 Android 权限常量 的完整列表。许可和它们的使用在整本书中都有讨论,尤其是在第三章。
| 许可常数 | 描述 |
|---|---|
| 访问签入属性 | 允许对签入数据库中的属性表进行读/写访问,从而能够更改上传的值 |
| 访问 _ 粗略 _ 位置 | 允许应用访问粗略位置(例如,蜂窝 ID、WiFi) |
| 访问 _ 精细 _ 位置 | 允许应用访问精确位置(例如 GPS) |
| 访问位置额外命令 | 允许应用访问额外的位置提供程序命令 |
| 访问模拟位置 | 允许应用创建模拟位置提供程序进行测试 |
| 访问网络状态 | 允许应用访问网络信息 |
| ACCESS_SURFACE_FLINGER | 允许应用使用 SurfaceFlinger 的底层特性 |
| 访问 _ WIFI _ 状态 | 允许应用访问有关 Wi-Fi 网络的信息 |
| 客户 _ 经理 | 允许应用调用帐户授权码 |
| 身份验证 _ 帐户 | 允许应用充当 AccountManager 的帐户验证者 |
| 电池状态 | 允许应用收集电池统计数据 |
| BIND_APPWIDGET | 允许应用告诉 AppWidget 服务哪个应用可以访问 AppWidget 的数据 |
| 绑定设备管理 | 设备管理接收器必须要求,以确保只有系统才能与之交互 |
| 绑定输入方法 | 必须是 InputMethodService 所要求的,以确保只有系统可以绑定到它 |
| 绑定 _ 远程视图 | 必须是 RemoteViewsService 所要求的,以确保只有系统可以绑定到它 |
| 绑定 _ 壁纸 | 必须是壁纸服务所要求的,以确保只有系统可以绑定到它 |
| 蓝牙技术 | 允许应用连接到配对的蓝牙设备 |
| 蓝牙 _ 管理 | 允许应用发现和配对蓝牙设备 |
| 砖 | 要求能够禁用设备(非常危险!) |
| 广播 _ 包 _ 已删除 | 允许应用广播应用包已被删除的通知 |
| 广播 _ 短信 | 允许应用广播短信回执通知 |
| 广播 _ 粘性 | 允许应用广播粘性意图 |
| 广播 _ WAP _ 推送 | 允许应用广播 WAP 服务信息回执通知 |
| 呼叫电话 | 允许应用发起电话呼叫,而无需通过拨号器用户界面让用户确认正在进行的呼叫 |
| 通话特权 | 允许应用呼叫任何电话号码,包括紧急号码,而无需通过拨号器用户界面让用户确认正在进行的呼叫 |
| 照相机 | 需要能够访问相机设备 |
| 更改组件启用状态 | 允许应用更改是否启用应用组件(而不是它自己的组件) |
| 更改配置 | 允许应用修改当前配置,如区域设置 |
| 改变网络状态 | 允许应用更改网络连接状态 |
| 更改 WIFI 多播状态 | 允许应用进入 Wi-Fi 多播模式 |
| 更改 WIFI 状态 | 允许应用更改 Wi-Fi 连接状态 |
| 清除应用缓存 | 允许应用清除设备上所有已安装应用的缓存 |
| 清除应用用户数据 | 允许应用清除用户数据 |
| 控制 _ 位置 _ 更新 | 允许从无线电启用/禁用位置更新通知 |
| 删除缓存文件 | 允许应用删除缓存文件 |
| 删除 _ 包 | 允许应用删除包 |
| 设备 _ 电源 | 允许对电源管理进行低级访问 |
| 诊断的 | 允许应用读写诊断资源 |
| 禁用 _ 键盘守卫 | 允许应用禁用键盘守卫 |
| 倾销 | 允许应用从系统服务中检索状态转储信息 |
| 展开状态栏 | 允许应用展开或折叠状态栏 |
| 工厂测试 | 作为制造商测试应用运行,作为根用户运行 |
| 手电筒 | 允许使用手电筒 |
| 强制返回 | 允许应用在顶层活动上强制执行 BACK 操作 |
| 获取 _ 帐户 | 允许访问帐户服务中的帐户列表 |
| 获取 _ 包 _ 大小 | 允许应用找出任何包使用的空间 |
| 获取 _ 任务 | 允许应用获取关于当前或最近运行的任务的信息:任务的缩略图、其中正在运行的活动等等 |
| 全局 _ 搜索 | 可用于内容供应器,以允许全球搜索系统访问他们的数据 |
| 硬件 _ 测试 | 允许访问硬件外围设备 |
| 注入 _ 事件 | 允许应用将用户事件(例如,按键、触摸和轨迹球)注入到事件流中,并将它们传递给任何窗口 |
| 安装位置供应器 | 允许应用将位置提供程序安装到位置管理器中 |
| 安装软件包 | 允许应用安装软件包 |
| 内部系统窗口 | 允许应用打开供部分系统用户界面使用的窗口 |
| 因特网 | 允许应用打开网络套接字 |
| 终止 _ 后台 _ 进程 | 允许应用调用 killBackgroundProcesses(String) |
| 管理 _ 账户 | 允许应用管理帐户管理器中的帐户列表 |
| 管理应用令牌 | 允许应用在窗口管理器中管理(例如,创建、销毁和 Z 顺序)应用令牌 |
| 主机 _ 清除 | |
| 修改 _ 音频 _ 设置 | 允许应用修改全局音频设置 |
| 修改电话状态 | 允许修改电话状态—开机、人机界面等 |
| 挂载格式文件系统 | 允许格式化可移动存储的文件系统 |
| 挂载卸载文件系统 | 允许安装和卸载可移动存储的文件系统 |
| 国家足球联盟 | 允许应用通过 NFC 执行 I/O 操作 |
| 持久 _ 活动 | 此常数已被否决。将来会删除此功能;请不要使用它。允许应用保持其活动的持久性。 |
| 处理 _ 呼出 _ 呼叫 | 允许应用监控、修改或中止呼出 |
| 阅读 _ 日历 | 允许应用读取用户的日历数据 |
| 阅读 _ 联系人 | 允许应用读取用户的联系人数据 |
| 读取帧缓冲区 | 允许应用获取屏幕截图,并且更一般地访问帧缓冲区数据 |
| 阅读 _ 历史 _ 书签 | 允许应用读取(但不写入)用户的浏览历史和书签 |
| 读取输入状态 | 允许应用检索按键和开关的当前状态 |
| 读取日志 | 允许应用读取低级系统日志文件 |
| 读取电话状态 | 允许对电话状态进行只读访问 |
| 阅读 _ 短信 | 允许应用读取短信 |
| 读取同步设置 | 允许应用读取同步设置 |
| 读取同步统计数据 | 允许应用读取同步统计数据 |
| 重新启动 | 需要能够重新启动设备 |
| 接收 _ 引导 _ 完成 | 允许应用接收系统完成引导后广播的 ACTION_BOOT_COMPLETED |
| 接收 _ 彩信 | 允许应用监控收到的彩信,并记录或执行处理 |
| 接收 _ 短信 | 允许应用监控传入的 SMS 消息,并记录或处理它们 |
| 接收 _WAP_PUSH | 允许应用监控传入的 WAP 服务信息 |
| 录音 _ 音频 | 允许应用录制音频 |
| 重新排序 _ 任务 | 允许应用更改任务的 Z 顺序 |
| 重启 _ 包 | 此常数已被否决。不再支持 restart package(String)API |
| 发送 _ 短信 | 允许应用发送短信 |
| 设置活动观察器 | 允许应用观察和控制活动如何在系统中全局启动 |
| 设置 _ 报警 | 允许应用广播为用户设置警报的意图 |
| 设置 _ 总是 _ 完成 | 允许应用控制活动是否在后台立即完成 |
| 设置 _ 动画 _ 缩放 | 修改全局动画比例因子 |
| SET_DEBUG_APP | 为调试配置应用 |
| 设置方向 | 允许设置屏幕方向(实际上是旋转)的低级访问 |
| 设定 _ 指针 _ 速度 | 允许设置指针速度的低级访问 |
| 设置 _ 首选 _ 应用 | 此常量已被弃用,不再有用;详见 addpackagetoppreferred(String) |
| 集合 _ 进程 _ 限制 | 允许应用设置可以运行的(不需要的)应用进程的最大数量 |
| 设置定时器 | 允许应用设置系统时间 |
| 设置时区 | 允许应用设置系统时区 |
| 设置 _ 壁纸 | 允许应用设置壁纸 |
| 设置 _ 壁纸 _ 提示 | 允许应用设置壁纸提示 |
| 信号 _ 持久 _ 进程 | 允许应用请求向所有持久进程发送信号 |
| 状态栏 | 允许应用打开、关闭或禁用状态栏及其图标 |
| 订阅 _ 订阅源 _ 阅读 | 允许应用允许访问订阅的提要内容提供者 |
| 订阅 _ 订阅源 _ 写入 | |
| 系统警报窗口 | 允许应用使用类型 TYPE_SYSTEM_ALERT 打开窗口,显示在所有其他应用的顶部 |
| 更新设备状态 | 允许应用更新设备统计数据 |
| 使用凭据 | 允许应用从 AccountManager 请求 authtokens |
| 使用 _SIP | 允许应用使用 SIP 服务 |
| 颤动 | 允许接触振动器 |
| 唤醒 _ 锁定 | 允许使用电源管理器唤醒锁来防止处理器休眠或屏幕变暗 |
| 写 _ APN _ 设置 | 允许应用写入 apn 设置 |
| 写日历 | 允许应用写入(但不读取)用户的日历数据 |
| 写联系人 | 允许应用写入(但不读取)用户的联系人数据 |
| 写 _ 外部 _ 存储 | 允许应用写入外部存储 |
| WRITE _ 服务 | 允许应用修改谷歌服务地图 |
| 写 _ 历史 _ 书签 | 允许应用写入(但不读取)用户的浏览历史和书签 |
| 写入 _ 安全 _ 设置 | 允许应用读取或写入安全系统设置 |
| 写入设置 | 允许应用读取或写入系统设置 |
| 写短信 | 允许应用编写 SMS 消息 |
内容供应器类别
| 类别名 | 描述 |
|---|---|
| 闹钟响了 | AlarmClock 提供程序包含一个意向动作和附加动作,可用于启动一个活动,在闹钟应用中设置一个新的闹钟 |
| 浏览器 | |
| 浏览器。书签栏 | 在书签 _URI 提供的混合书签和历史项目的列定义 |
| 浏览器。搜索列 | 搜索历史表的列定义,可从搜索 _URI 获得 |
| 呼叫日志 | 呼叫日志提供程序包含有关发出和接收呼叫的信息 |
| 通话记录。打电话 | 包含最近的通话 |
| 联系人联系人 | 联系人提供者和应用之间的合同 |
| 联系我们。聚合 xceptions | 联系人汇总例外表的常数,该表包含覆盖自动汇总所用规则的汇总规则 |
| 联系合同。通用数据类型 | 存储在 ContactsContract 中的通用数据类型定义的容器。数据表 |
| 联系合同。CommonDataKinds.Email | 代表电子邮件地址的数据类型 |
| 联系人联系人。CommonDataKinds.Event | 表示事件的数据类型 |
| 联系人联系人。common data kinds . group membership | 组成员关系 |
| ContactsContract.CommonDataKinds.Im | 表示 IM 地址的数据类型 |
| 您可以使用为 ContactsContract 定义的所有列。数据,以及以下别名 | |
| 联系合同。CommonDataKinds .昵称 | 代表联系人昵称的数据类型 |
| 联系人联系人。CommonDataKinds。注意 | 关于联系人的注释 |
| 联系人联系人。通用数据类型.组织 | 代表组织的数据类型 |
| 联系合同。CommonDataKinds.Phone | 代表电话号码的数据类型 |
| 联系合同。CommonDataKinds.Photo | 代表联系人照片的数据类型 |
| 联系人联系人。CommonDataKinds.Relation | 表示关系的数据类型 |
| 联系人联系人。CommonDataKinds.SipAddress | 代表联系人的 SIP 地址的数据类型 |
| 联系人联系人。CommonDataKinds.StructuredName | 表示联系人正确姓名的数据类型 |
| 联系合同。common data kinds . structured postal | 代表邮政地址的数据类型 |
| 联系人联系人。CommonDataKinds .网站 | 表示与联系人相关的网站的数据种类 |
| 联系我们。连络人 | contacts 表的常数,该表包含代表同一个人的每个原始联系人聚合的记录 |
| 联系合同。联系人。聚合建议 | 包含所有聚合建议(例如,其他联系人)的单个联系人聚合的只读子目录 |
| 联系人联系人。联系人.数据 | 单个联系人的子目录,包含所有组成的 raw contactContactsContract。数据行 |
| 联系合同。联系人.实体 | 联系人的子目录,包含其所有的 contacts contact。原始联系人,以及联系人。数据行 |
| 联系我们。联系人,照片 | 包含联系人主要照片的单个联系人的只读子目录 |
| 联系我们。日期 | 数据表的常数,其中包含与原始联系人相关的数据点 |
| 联系人联系人。目录 | 目录代表联系人语料库 |
| 联系人联系人。组 | 组表的常数 |
| 联系我们。试试看 | 包含用于创建或管理涉及联系人的意图的助手类 |
| 联系人联系人。意图。插入 | 包含用于创建联系意图的字符串常量的便利类 |
| 联系我们。PhoneLookup(电话查找) | 表示查找电话号码(例如,查找呼叫者 ID)的结果的表 |
| 联系我们。QuickContact(快速联系) | 帮助器方法显示 QuickContact 对话框,允许用户在特定的联系人条目上旋转 |
| 联系我们。拉瓦联系人 | 原始联系人表的常量,该表包含每个同步帐户中每个人的一行联系人信息 |
| 联系我们。RawContacts 日期 | 单个原始联系人的子目录,包含其所有的 contacts contact。数据行 |
| 联系我们。RawContacts.Entity .实体 | 单个原始联系人的子目录,包含其所有的 contacts contact。数据行 |
| 联系我们。rawcontactsentity(联系人信息) | 原始 contacts 实体表的常量,可以认为是数据表的 raw_contacts 表的外部连接 |
| 联系合同。设置 | 各种帐户的联系人特定设置 |
| 联系我们。状态更新 | 状态更新链接到一个 ContactsContract。Data row 并通过相应的源捕获用户的最新状态更新 |
| 联系我们。SyncState | 为同步适配器提供的用于存储专用同步状态数据的表 |
| 实时文件夹 | LiveFolder 是一个特殊的文件夹,其内容由 ContentProvider 提供 |
| 媒体库 | 媒体提供程序包含内部和外部存储设备上所有可用媒体的元数据 |
| 媒体商店。声音的 | 所有音频内容的容器 |
| 媒体商店。音频.相册 | 包含音频文件的艺术家 |
| 媒体商店。音频。艺术家 | 包含音频文件的艺术家 |
| MediaStore。音频,艺术家,专辑 | 每个艺术家的子目录,包含出现该艺术家歌曲的所有专辑 |
| 媒体商店。音频类型 | 包含所有类型的音频文件 |
| 媒体商店。音频.类型.成员 | 包含所有成员的每个流派的子目录 |
| 媒体商店。音频媒体 | |
| 媒体商店。音频.播放列表 | 包含音频文件的播放列表 |
| 媒体商店。音频.播放列表.成员 | 包含所有成员的每个播放列表的子目录 |
| 媒体商店。文件 | 包含媒体存储器中所有文件(包括非媒体文件)索引的媒体提供者表 |
| 媒体商店。形象 | 包含所有可用图像的元数据 |
| 媒体商店。图像.媒体 | |
| 媒体商店。图像.缩略图 | 这个类允许开发者查询并获取两种缩略图:MINI_KIND: 512 × 384 缩略图和 MICRO_KIND: 96 × 96 缩略图 |
| 媒体商店。录像 | |
| MediaStore。视频。媒体 | |
| 媒体商店。视频.缩略图 | 这个类允许开发者查询并获取两种缩略图:MINI_KIND: 512 × 384 缩略图和 MICRO_KIND: 96 × 96 缩略图 |
| SearchRecentSuggestions | 这是一个工具类,提供对 SearchRecentSuggestionsProvider 的访问 |
| 设置 | 设置提供程序包含全局系统级设备首选项 |
| 设置。名称值表 | 名称/值设置表的公共库 |
| 设置。安全的 | 安全系统设置,包含应用可以读取但不允许写入的系统偏好设置 |
| 设置。系统 | 系统设置,包含各种系统偏好设置 |
| SyncStateContract | 用于将数据与任何数据数组帐户相关联的 ContentProvider 协定 |
| SyncStateContract(同步状态合同)。常数值 | |
| SyncStateContract。助手 | |
| 用户词典 | 用于输入法的用户定义单词的提供者,用于预测文本输入 |
| 用户词典。话 | 包含用户定义的单词 |