APK签名的作用
在安装Apk时,需要确保Apk来源的真实性,以及Apk没有被第三方篡改。如何解决这两个问题呢?方法就是开发者对Apk进行签名。什么叫做签名呢,签名就是在Apk中写入一个“指纹”。指纹写入以后,Apk中如果有任何修改,都会导致这个指纹无效,Android系统在安装Apk进行签名校验时就会不通过,从而保证了安全性。
APK格式介绍
Apk文件本质上就是一个zip文件, zip文件整体是由三个部分组成,分别是数据区,*目录区以及*目录结尾记录。
1.数据区(Contents of ZIP entries):此区块包含了zip中所有文件的记录,是一个列表,每条记录包含:文件名、压缩前后size、压缩后的数据等。
2.*目录区(Central Directory Header):存放目录信息,也是一个列表,每条记录包含:文件名、压缩前后size、本地文件头的起始偏移量等。通过本地文件头的起始偏移量即可找到压缩后的数据。
3.*目录结尾记录(End of Central Directory Record:ECDR):标识*目录结尾,包含:*目录条目数、size、起始偏移量、zip文件注释内容等存储zip文件的整体信息。
APK签名与检验
在介绍签名之前,需要先介绍一下数字摘要和数字证书:
数字摘要:数字摘要将任意长度的消息转换成固定长度的短消息,意思就是通过hash算法,将明文转换成一串固定长度的密文,经过Hash算法加密生成的密文也被称为数字指纹。
数字证书:数字证书是身份认证机构颁发的一种证书文件,文件中包含了证书颁发机构,颁发机构的签名,颁发机构的加密算法(非对称加密),算法的公钥等。
上文提到了通过签名的方式来确保apk包的可靠性和不可篡改性。而签名就是在数字摘要的基础上再进行一次加密,把对摘要加密后的数据当作数字签名。下面介绍一下对APK文件签名的过程:
首先,通过Hash算法计算出原始数据的摘要,使用非对称加密算法对摘要进行加密,得到密文,密文就是数据的签名信息;将签名信息写入到原始数据的签名区块中。
而验签的过程就是使用相同的Hash算法,计算出文件的摘要,使用私钥对签名信息进行解密,将解密出来的数据和摘要进行比对,如果两者相同,说明文件并没有被篡改,签名和验签的过程如下图所示:
APK签名协议之V1机制
签名工具介绍
Android应用的签名工具有两种,一种是jarsigner还有一种是signapk。这两者签名算法并没有什么不同,只是签名使用的文件不同。
jarsigner:jdk 自带的签名工具,可以对 jar 进行签名。使用 keystore 文件进行签名。生成的签名文件默认使用 keystore 的别名命名。
signAPK:Android sdk 提供的专门用于 Android 应用的签名工具。使用 pk8、x509.pem 文件进行签名。其中 pk8 是私钥文件,x509.pem 是含有公钥的文件。生成的签名文件统一使用“CERT”命名。
在对应用签名之后,apk压缩包中会增加一个名为META-INF的文件夹,在 META-INF 文件夹下有三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。它们就是签名过程中生成的文件,如下图所示:
MANIFEST.MF:该文件中保存的内容其实就是逐一遍历 APK 中的所有条目,如果是目录就跳过,如果是一个文件,就用 SHA1(或者 SHA256)消息摘要算法提取出该文件的摘要然后进行 BASE64 编码后,作为“SHA1-Digest”属性的值写入到 MANIFEST.MF 文件中的一个块中。该块有一个“Name”属性, 其值就是该文件在 APK 包中的路径,如下图所示:
CERT.SF:SHA1-Digest-Manifest-Main-Attributes:对 MANIFEST.MF 头部的块做 SHA1(或者SHA256)后再用 Base64 编码。
SHA1-Digest-Manifest:对整个 MANIFEST.MF 文件做 SHA1(或者 SHA256)后再用 Base64 编码。
SHA1-Digest:对 MANIFEST.MF 的各个条目做 SHA1(或者 SHA256)后再用 Base64 编码。
CERT.RSA:这里会把之前生成的 CERT.SF 文件,用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。这里要注意的是,Android APK 中的 CERT.RSA 证书是自签名的,并不需要这个证书是第三方权威机构发布或者认证的,用户可以在本地机器自行生成这个自签名证书。Android 目前不对应用证书进行 CA 认证。CERT.RSA是一个加密后的文件,需要使用openssl解密后才能查看明文,可以使用openssl 进行解密查看,结果如下所示:
所以一个完整的apk签名过程如下所示:
签名验证是发生在APK的安装过程中,一共分为三步:1、检查 APK 中包含的所有文件,对应的摘要值与 MANIFEST.MF 文件中记录的值一致。2、使用证书文件(RSA 文件)检验签名文件(SF 文件)没有被修改过。3、使用签名文件(SF 文件)检验 MF 文件没有被修改过。综上所述,一个完整的签名验证过程如下所示:
APK签名协议之V2机制
从 Android 7.0 开始,Android 支持了一套全新的 V2 签名机制,为什么要推出新的签名机制呢?通过前面的分析,可以发现 v1 签名有两个地方可以改进:
签名校验速度慢
校验过程中需要对apk中所有文件进行摘要计算,在 APK 资源很多、性能较差的机器上签名校验会花费较长时间,导致安装速度慢。
完整性保障不够
META-INF 目录用来存放签名,自然此目录本身是不计入签名校验过程的,可以随意在这个目录中添加文件,比如一些快速批量打包方案就选择在这个目录中添加渠道文件。
v2 签名模式
v2 签名模式在原先 APK 块中增加了一个新的块(签名块),新的块存储了签名,摘要,签名算法,证书链,额外属性等信息,apk文件经过v2签名前后的差异如下所示:
1、ZIP 条目的内容(从偏移量 0 处开始一直到“APK 签名分块”的起始位置)
2、APK 签名分块
3、ZIP *目录
4、ZIP *目录结尾
应用签名方案的签名信息会被保存在 区块 2(APK Signing Block)中,而区块 1(Contents of ZIP entries)、区块 3(ZIP Central Directory)、区块 4(ZIP End of Central Directory)是受保护的,在签名后任何对区块 1、3、4 的修改都逃不过新的应用签名方案的检查。看起来是APK签名分块替代了V1机制中META-INF的作用。
V2签名过程
将 APK 中文件 ZIP 条目的内容、ZIP *目录、ZIP *目录结尾按照 1MB 大小分割成一些小块;计算每个小块的数据摘要,数据内容是 0xa5 + 块字节长度 + 块内容;计算整体的数据摘要,数据内容是 0x5a + 数据块的数量 + 每个数据块的摘要内容。再将 APK 的摘要 + 数字证书 + 其他属性生成签名数据写入到 APK Signing Block 区块。
V2验签过程
v2 签名机制是在 Android 7.0 以及以上版本才支持。因此对于 Android 7.0 以及以上版本,在安装过程中,如果发现有 v2 签名块,则必须走 v2 签名机制,不能绕过。否则降级走 v1 签名机制。
V1签名校验绕过漏洞(CVE-2017-13156)介绍
漏洞原理
Android在4.4引入ART虚拟机,相比较于Dalvik虚拟机仅能运行包装于apk中的dex文件,ART还允许直接运行优化后的dex文件。ART虚拟机在加载并执行一个文件时,会首先判断这个文件的类型。如果这个文件是一个Dex文件,则按Dex的格式加载执行,如果是一个APK文件,则先抽取APK中的dex文件,然后再执行。而判断的依据是通过文件的头部魔术字(Magic Code)来判断。如果文件的头部魔术字是“dex”则判定该文件为Dex文件,如果文件头部的魔术字是“PK”则判定该文件为Apk文件。
但是ZIP文件的读取方式是读取文件末尾定位的central directory,Android在安装一个APK时会对APK进行签名验证,但却直接默认该APK就是一个ZIP文件(并不检查文件头部的魔术字),而ZIP格式的文件一般都是从尾部先读取,因此只要ZIP文件尾部的数据结构没有被破坏,并且在读取过程中只要没有碰到非正常的数据,那么整个读取就不会有任何问题。
因此只要构造一个APK,从其头部看是一个Dex文件,从其尾部看,是一个APK文件,就可以实施攻击。很容易想到,将原APK中的classes.dex抽取出来,改造或替换成攻击者想要执行的dex,并将这个dex和原APK文件拼起来,合成一个文件,就可以利用V1签名校验绕过漏洞。
当然仅仅简单地将恶意dex放在头部,原apk放在尾部合起来的文件还是不能直接用来攻击。需要稍作修正。对于头部dex,需要修改DexHeader中的file_size,将其调整为合并后文件的大小。另外需要修改尾部Zip,修正[end of central directory record]中[central directory]的偏移和[central directory]中各[local file header]的偏移。
漏洞利用
1.从设备上取出目标应用的APK文件,并构造用于攻击的DEX文件;
2.将攻击DEX文件与原APK文件简单拼接为一个新的文件;
3.修复这个合并后的新文件的ZIP格式部分和DEX格式部分,需要修复文件格式中的关键偏移值和数据长度值。
4. 最后,将修复后的文件,重命名为APK文件,覆盖安装设备上的原应用即可。
参考链接
https://github.com/xyzAsian/Janus-CVE-2017-13156