Android高效进阶:从数据到AI【1.9】

3.2.3 Monkey 稳定性检测

Monkey 是 Android 系统中的一个命令行工具,可以运行在模拟器里或实际设备上。它向系统发送伪随机的用户事件流(如按键输入、触摸屏输入、手势输入等),可以对正在开发的App 进行压力测试。 Monkey 测试是一种测试软件稳定性和健壮性的快速、有效的方法。

Monkey 测试具备以下特性。

( 1)测试对象为应用程序包。

( 2) Monkey 测试使用的事件流、数据流是随机的。

( 3)可对 Monkey 测试的对象及事件数量、类型、发生频率等进行设置。

Monkey 的基本语法如下:

1. $ adb shell monkey [options]

如果不指定参数, Monkey 将以无反馈模式启动,并把事件任意发送到安装在目标环境中的全部包。下面是一个更典型的命令行示例,它启动指定的应用程序,并向其发送 500 个伪随机事件:

1. $ adb shell monkey -p your.package.name -v 500

3.2.4 自动化敏感权限检测

敏感权限检测属于安全性检测的范畴。敏感权限的申请或者不合理的申请都会给用户设备的安全带来隐患。

敏感权限检测的常规手段是对权限进行分类,具体是对 APK AndroidMenifest 进行权限扫描,将包含敏感权限或者特殊权限的应用程序发给用户以做报警。

3.2.5 面向游戏的真机检测

市面上很多游戏都会开发国外版本,国外版本基本都继承了对 Google Play 服务的支持,因此在针对游戏的真机检测中,其中很重要的一点就是如何自动化地进行游戏 Google Play 服务检测( Google Play Service Check,简称 GPC)。

目前比较常见的 Google Play 服务检测依赖静态扫描应用程序安装包文件,通过是否包含Google Play 服务框架组件来判断其是否依赖 Google Play 服务,这种判断无法准确地检测一个应用程序是否真正强依赖于 Google Play 服务框架,如某一 App 包含了 Google Play 服务框架静态组件,但是在无 Google Play 服务框架的设备上,它依然可以正常使用,那么其实这个 App没有强依赖 Google Play 服务来运行。因此, GPC 的目标就是在包含 Google Play 服务框架的应用安装包中找出正在强依赖 Google Play 服务框架的应用程序。

安装包的 Google Play 服务检测分为两个部分,即静态检测和动态运行检测。其中,静态检测只是对 APK 进行静态扫描,速度快、处理难度低,可以快速排除一些不需要 Google Play服务的 App。

1.静态检测

这部分使用包解析技术对 APK 进行扫描分析,主要扫描 AndroidManifest 中是否存在Google Play 服务框架必备的 com.google.android.gms.version 信息:

1. <meta-data android:name=”com.google.android.gms.version” android: value=”@integer/google_play_services_version”/>

如果存在该信息,则说明该 APK 引用了 Google Play 服务框架,需要对该 APK 进行动态运行检测。

2.动态运行检测

这部分通过将 APK 运行在无 Google Play 服务框架的手机上,监控该应用程序的运行情况来实现。主要的检测场景为无法启动,以及在运行过程中是否会提示需要 Google Play 服务框架以及需要升级等场景。

场景一:无法启动。在无 Google Play 服务的手机上,出现闪退现象。

场景二:提示需要 Google Play 服务框架。部分 App 在调用 Google Play 服务框架前,会先判断是否存在 Google Play 账户并给出提示,如图 3-5 所示。

场景三:需要升级。 App 不是最新版本的,需要升级才能运行,因此无法做出判断。游戏 Google Play 服务( Google Play Service)检测的主要流程如图 3-6 所示。

3.3 APK 信息一站式修改

一般一个 App 产品需要面向不同的渠道投放不同的渠道包,而这些渠道包的内容除了渠道信息差异外基本都是一样的,因此可以进行 APK 信息一站式修改。

3.3.1 APK 文件构成

图 3-7 是 APK 文件的构成示意图。

第一部分是内容块,所有的压缩文件都在这部分。每个压缩文件都有一个 local file header,主要记录了文件名、压缩算法、压缩前后的文件大小、修改时间、 CRC32 值等。

第二部分被称为中央目录,包含多个 central directory file header(与第一部分的 local file header 一一对应),每个中央目录文件头主要记录了压缩算法、注释信息、对应 local file header 的偏移量等,方便快速定位数据。

最后一部分是 EOCD( End Of Central Directory),主要记录了中央目录大小、偏移量和ZIP 注释信息等,其详细结构如表 3-1 所示。

3.3.2 APK 签名校验流程

APK 签名校验的核心是如何处理 V1 和 V2 的签名校验。 PackageParser 类负责 V1 签名的具体校验,流程如图 3-8 所示。

对于校验流程,如何保证 APK 文件信息不被篡改?下面进行介绍。

1. V1 签名是怎么保证 APK 文件不被篡改的

首先,如果破坏者修改了 APK 中的任何文件,那么被篡改文件的数据摘要的 Base64 编码就和 MANIFEST.MF 文件的记录值不一致,这将导致校验失败。

其次,如果破坏者同时修改了对应文件在 MANIFEST.MF 文件中的 Base64 值,那么MANIFEST.MF 中对应数据块的 Base64 值就和 CERT.SF 文件中的记录值不一致,这将导致校验失败。

最后,如果破坏者更进一步,同时修改了对应文件在 CERT.SF 文件中的 Base64 值,那么CERT.SF 的数字签名就和 CERT.RSA 文件中记录的签名不一致,这也将导致校验失败。

那有没有可能继续伪造 CERT.SF 的数字签名呢?理论上是不可能的,因为破坏者没有开发者的私钥。

尽管看起来很安全,但 V1 签名的设计缺陷也导致了一些安全隐患,如 ZIP 元数据的保护等。

2.为什么要引入 V2 签名方案

首先, V1 签名不保护 APK 的某些部分,如 ZIP 元数据。

其次, APK 验证程序需要处理大量不可信(尚未经过验证)的数据结构,然后会舍弃不受签名保护的数据。这会导致相当大的受攻击面。

最后, APK 验证程序必须解压所有已压缩的条目,而这需要花费更多的时间和内存。

3.3.3 V1 与 V2 签名

上面我们看到了如何保证 APK 信息不被篡改的机制,那么到底什么是 V1、 V2 签名呢? 1. V1 签名V1 签名是 Android APK 最初的签名方案,基于 JDK( jarsigner),对 ZIP 压缩包的每个文件进行验证,在 META-INF 存放签名文件( MANIFEST.MF、 CERT.SF、 CERT.RSA 等文件),其中 MANIFEST.MF 文件保存了所有文件的 SHA1 指纹,但 ZIP 元数据不受签名保护。

2. V2 签名

V2 签名是一种全文件签名方案,该方案能够发现对 APK 受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。使用 APK 签名方案 V2 进行签名时,会在 APK 文件中插入一个 APK 签名分块(图 3-9 中的 APK Signing Block),该分块位于“ ZIP 中央目录”部分之前并紧邻该部分。在 APK 签名分块内, V2 签名和签名者身份信息会被存储在APK 签名方案 V2 分块中。

3.3.4 如何打造渠道包

上面介绍了 APK 文件的构成以及签名相关内容,下面介绍如何打造渠道包的内容。打造不同渠道包最关键的就是如何在保持签名一致的情况下批量实现不同渠道包中的渠道号不一样。1.基于 V1 签名的 APK 信息动态修改方案

原理:由于 V1 签名校验不包含 ZIP 注释字段,因此可以在 APK 文件的注释字段中添加动态信息。

步骤(如图 3-10 所示):

( 1)添加动态信息。

( 2)在末尾添加动态信息长度。

( 3)把 Comment length 长度修改为动态信息长度加 2。

将动态信息长度添加到末尾是为了方便向前读取数据、定位动态信息,其中 Comment 的最大长度为 65 535 字节。

基于 V1 签名的 APK 信息动态修改方案的核心代码逻辑如下:
 

1. zipFile = new ZipFile();
2. String zipComment = zipFile.getComment();
3. // 判断是否包含 Comment 信息
4. if (zipComment == null) {
5. byte[] bytecomment = comment.getBytes();
6. outputStream = new ByteArrayOutputStream();
7. // 写入 Comment 和长度
8. outputStream.write(bytecomment);
9. outputStream.write(short2Byte((short)bytecomment.length));
10. byte[] commentdata = outputStream.toByteArray();
11.
12. accessFile = new RandomAccessFile(file, s:"rw");
13. // 定位到 Comment length
14. accessFile.seek(l:file.length()-2);
15. // 写入动态信息长度+2 记录位
16. accessFile.write(short2Byte((short)commentdata.length));
17. accessFile.write(commentdata);
18. accessFile.close();
19. }

2.基于 V2 签名的 APK 信息动态修改方案

原理: Android 系统只会关注 ID 为 0x7109871a 的 V2 签名块,并且忽略其他的 IDValue,同时 V2 签名只会保护 APK 本身,而不包含签名块。这样就可以在 APK 签名块中添加一个 ID-Value 并赋值。

步骤:

( 1)找到 APK 的 EOCD 块。

( 2)找到 APK 签名块。

( 3)获取已有的 ID-Value 对。

( 4)添加包含动态信息的 ID-Value。

( 5)基于所有的 ID-Value 生成新的签名块。

( 6)修改 EOCD 的中央目录的偏移量。

( 7)用新的签名块替代旧的签名块,生成带有动态信息的 APK。

基于 V2 签名的 APK 信息动态修改方案的核心代码逻辑如下:

1. public static void addIdValueByteBufferMap(ApkSectionInfo apkSectionInfo,
File destApk, Map<Integer, ByteBuffer> idValueMap) {
2. if (idValueMap == null || idValueMap.isEmpty()) {
3. throw new RuntimeException("addIdValueByteBufferMap, id
value pair is empty");
4. }
5.
6. // 不能与系统 V2 签名块的 ID 冲突
7. if (idValueMap.containsKey(ApkSignatureSchemeV2Verifier.APK_
SIGNATURE_SCHEME_V2_BLOCK_ID)) {
8. idValueMap.remove(ApkSignatureSchemeV2Verifier.APK_SIGNATURE_
SCHEME_V2_BLOCK_ID);
9. }
10.
11. Map<Integer, ByteBuffer> existentIdValueMap = V2SchemeUtil.
getAllIdValue(apkSectionInfo.schemeV2Block.getFirst());
12. // 必须包含 V2 签名块
13. if (!existentIdValueMap.containsKey(ApkSignatureSchemeV2Verifier.
APK_SIGNATURE_SCHEME_V2_BLOCK_ID)) {
14. throw new ApkSignatureSchemeV2Verifier.SignatureNotFoundExcetion
("No Apk V2 Signature Scheme block in APK Signing Block");
15. }
16. // 添加动态信息
17. existentIdValueMap.putAll(idValueMap);
18.
19. // 生产新的签名块
20. ByteBuffer newApkSigningBlock = V2SchemeUtil.generateApkSigningBlock
(existentIdValueMap);
21. }
1. // 生成新的签名块
2. ByteBuffer newApkSigningBlock = V2SchemeUtil.generateApkSigningBlock
(existentIdValueMap);
3.
4. ByteBuffer centralDir = apkSectionInfo.centralDir.getFirst();
5. ByteBuffer eocd = apkSectionInfo.eocd.getFirst();
6. long centralDirOffset = apkSectionInfo.centralDir.getSecond();
7. int apkChangeSize = newApkSigningBlock.remaining()-
apkSectionInfo.schemeV2Block.getFirst().remaining();
8. // 修改了 EOCD 中保存的中央目录偏移量
9. ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset:centralDirOffset+
apkChangeSize);
1. fIn = new RandomAccessFile(destApk, s:"rw");
2. if (apkSectionInfo.lowMemory) {
3. fIn.seek(apkSectionInfo.schemeV2Block.getSecond());
4. } else {
5. ByteBuffer contentEntry = apkSectionInfo.contentEntry.getFirst();
6. fIn.seek(apkSectionInfo.contentEntry.getSecond());
7. // 1, write real content Entry block
8. fIn.write(contentEntry.array, i:contentEntry.arrayOffset()+
contentEntry.position(),contentEntry.remaining());
9. }
10.
11. // 2, write new apk v2 scheme block
12. fIn.write(newApkSigningBlock.array(), i:newApkSigningBlock arrayOffset()
+newApkSigningBlock.position(), newApkSigningBlock.remaining());
13.
14. // 3, write central dir block
15. fIn.write(centralDir.array(),i:centralDir.arrayOffset()+central.
position(),centralDir.remaining());
16. // 4, write eocd block
17. fIn.write(eocd.array(), i:eocd.arrayOffset()+eocd.position(), eocd.
remaining());
18. // 5, modify the length of apk file
19. if (fIn.getFilePointer() != apkLength) {
20. throw new RuntimeException("after addIdValueByteBufferMap, file
size wrong, FilePointer:"+fIn.getFilePointer()+", apkLength:"+apkLength);
21. }
22. fIn.setLength(apkLength);

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
米途栗然的头像 - 宋马
评论 抢沙发

请登录后发表评论

    暂无评论内容