December 28, 2017

CVE-2017-13156漏洞分析(中)

承接上篇,本篇主要介绍一下Android的签名机制,并从原理上分析一下为什么使用APK Signature Scheme v2签名的应用在Android 7.0以上不受漏洞影响。Android提供了两种签名方式,一种是v1的基于JAR签名的方式,另一种就是APK Signature Scheme v2。

签名验证时机

首先看一下PackageManagerService的代码,看看是什么时候进行签名验证的。

PMS的installPackageLI方法会构造一个PackageParser对象,并调用collectCertificates方法来验证签名。

这里可以看到,首先要对apk文件的entry做遍历,如果遇到entry是目录,或者是META-INF文件名,或者是AndroidManifest.xml则跳过,否则把entry加入一个List,这个List中的每一个entry将在下一步进行签名验证。验证签名的过程后面会说,这一节只是说明,在apk安装的时候,PMS会进行签名验证。

V1签名

Android官方文档有说,在APK Signature Scheme v2签名之前,签名使用的是基于JAR签名的方式。关于JAR签名,oracle的官方文档在这里。官方文档有点冗长,这里用一个例子来说明一下这种签名。
签名完的JAR包会多出一个目录,META-INF,里面会多出来这么几个文件:

  1. MANIFEST.MF,这个文件只会有一个,是zip entry的清单文件。
  2. *.SF,这个后缀结尾的文件,命名可以任意,因为JAR允许多重签名,所以这个类型的文件可能存在多个,每一个SF文件对应一个签名者。
  3. *.RSA,这个后缀结尾的文件,命名也是任意的,但是必须和SF文件一一对应,每一个RSA文件也对应着一个签名者。

先看一下前两个文件的内容:
MANIFEST.MF
CERT.SF
截得不全,但是这一部分已经可以足够分析这两个文件的作用了。对比两个文件的头部,我们发现,SF文件比MF文件要多两个属性,SHA1-Digest-Manifest-Main-AttributesSHA1-Digest-Manifest
,而其他内容格式是一致的。这里看一下jarsigner的源码可以很方便地理解这两个文件内容。

这是main方法,可以看到,是按顺序写入MANIFEST.MF, CERT.SF和CERT.RSA的,先看看写入MANIFEST.MF的方法addDigestsToManifest()。

这个方法会遍历当前zip包内所有entry,并针对每一项生成一个SHA1的摘要,和文件名一起写入到MANIFEST.MF中,META-INF下的文件是例外的,不会生成摘要信息。MANIFEST.MF保存了每一个zip entry Base64编码后的SHA1摘要
再看看写入CERT.SF的方法writeSignatureFile()

CERT.SF文件存储的是MANIFEST.SF文件的摘要信息,首先看头部SHA1-Digest-Manifest,看代码很容易知道,这是MANIFEST.SF整个文件的SHA1摘要,接下来的循环,把原本MANIFEST.MF里每一项再做一次SHA1摘要,以同样的格式写入CERT.SF。所以表面上看起来CERT.SF和MANIFEST.SF格式差不多,但其实在证书校验中的作用完全不同。
最后来看一下CERT.RSA文件,这是个二进制文件,所以我们需要借助OpenSSL来解析。
CERT.RSA
显然,这包含了X509的证书,有公钥,加密算法,有效期,签发者等信息。让我们看看生成这个文件的代码:

writeSignatureBlock方法会将用私钥加密后的CERT.SF,以及包含了公钥的数字证书一起写入到CERT.RSA文件,这个文件符合PKCS7格式。

V1签名的验证

有了上面的背景知识,再来看Android对于apk对V1签名的验证,就不难理解了,贴代码:

中间过程过于冗长,这里直接给结论,Android对于V1签名的验证,主要分以下几个步骤:

  1. 对apk文件中每个file entry做SHA1摘要,Base64编码后与MANIFEST.MF中内容比对。
  2. 对RSA和SF文件进行校验,因为RSA文件中,有私钥加密后的签名,还有公钥,所以可以根据公钥解签名,这里应该能得到CERT.SF的SHA1摘要,跟当前CERT.SF做比较就可以知道CERT.SF有没有被篡改。
  3. 比对MANIFEST.MF整个文件的SHA1摘要,看是否和CERT.SF头部记录的一致,然后再逐条比较MANIFEST.MF数据项的摘要是否和CERT.SF对应条目一致。这样可以确定MANIFEST.MF有没有被篡改。

这里可以看出,基于JAR签名的方案,是有缺陷的,V1签名方案是对zip文件的entry做校验,并没有校验整个apk是否被篡改。所以就有了很多利用这一点的黑科技,比如zipalign对齐,美团的多渠道打包方案,在apk使用V1签名完后还是可以做一些修改,而不影响签名的验证。CVE-2017-13156漏洞也是一样,因为并没有修改原来apk文件zip entry的数据,所以签名验证是可以通过的。

V2签名

因为V1签名有以上的缺点,所以Android在7.0之后加入了对V2签名支持。SDK目录下的apksigner工具可以完成对APK的V2签名。V2签名并不是apk文件中的一个entry,而是在Central Directory之前的一段block,这里借鉴一下Android官方文档的图片:
SigV2
可以很明显看出,签名之后,多出了红色的部分,紧邻Central Directory之前。从源码来看一下这个块的格式:

签名块分为以下几个部分:

  1. uint64类型,8个字节,表示整个block长度

  2. 多个ID/Value的键值对

  3. 和第一个字段一样的,8字节的block长度

  4. uint128类型,16字节的magic number,内容为“APK Sig Block 42”
    V2签名block的ID为0x7109871a,这段代码只从键值对pairs中读取V2签名块的内容,其余部分不作处理。
    V2签名块的内容主要包含了签名,公钥和证书,生成V2签名块的代码比较冗长,可以查看源码,这里总结了一下步骤,先来看一张图:
    Apk Sections
    这里把整个APK文件分为了4个部分,APK Signing Block是对1,3,4的校验,并不包括本身。校验值的计算步骤如下:

  5. 把1,3,4分别分成1M大小的连续数据块

  6. 对每一块分别计算摘要信息,摘要的计算方法是把0xa5,数据块大小(小端字节序)和数据块内容拼接起来根据传入的算法做摘要。

  7. 把0xa5,数据块数量和数据块的摘要拼接,再做一次摘要

  8. 最后计算出的摘要私钥加密后即为签名
    Apk integrity protection

显然可以看出,整个过程是很容易并行的,因为数据块之前没有关联性,V2签名同时保护了zip entry和Central Directory,任何一个字节的改变都将导致签名校验失败。因此CVE-2017-13156漏洞这种在头部拼接dex文件的方式无法通过签名验证。

V2签名的验证

这里可以看一下PackageParser.collectCertificates方法在Android 7.0以上的实现:

可以看到,PMS会先尝试验证V2版本的签名,只有V2版本签名找不到时,才会降级为验证V1版本的签名,因此同时使用了V1和V2签名的APK,是无法通过降级策略绕过V2签名的校验的。
这里再看一下校验V2签名的代码

findSignature方法通过找到EoCD的位置来确定Central Directory的位置,从而找到签名块的位置,再通过之前贴过的findApkSignatureSchemeV2Block方法找到V2签名块,并获取其中的签名,证书等信息。verify方法则是完成真正的签名校验,签名校验过程无非是非对称解密那一套,这里不再赘述。
V2签名对APK完整性提供了更全面的保证,因此对于APK同时使用V1和V2签名可以在保证兼容性的同时提高安全性。
这里多说两句,美团之前的多渠道打包方案在V2签名机制下也失效了,对于V2的多渠道打包,只能在签名块中写入新的ID/Value键值对来实现,因为目前对于V2签名的校验,只识别了ID为0x7109871a下的value,不认识的ID一律不作处理。