Android10.0应用安装白名单---添加签名校验

1,127 阅读6分钟

背景

为了避免系统被安装上各种各样的app,客户要求系统需要有个安装白名单的功能。

思路

白名单功能主要是通过确认要安装的应用是否在白名单上,如果不在,则不允许安装。筛选的标准可以通过包名进行判断。但单纯包名进行判断还是不够安全,这里是想再加个签名校验的机制,毕竟每个签名都是独一无二的。

这个过程,主要难点在于如何获取各个apk签名。在说明如何在代码中获取到系统签名之前,先大概说下Android应用的签名机制。

Android应用签名

签名的基本目的就是不允许apk内部任意一个文件的内容被篡改。所以得保证每个文件都被加密到。 将任意一个签过名的apk进行解压,都可以发现一个叫META-INF的文件夹,该文件夹包括但不限于如下三个文件:

CERT.RSA CERT.SF MANIFEST.MF

1.MANIFEST.MF文件。对APK包中每个文件(文件夹和签名文件除外)进行遍历,使用SHA1或者SHA256生成摘要信息。然后再用base64进行编码,截取MANIFEST.MF文件中某一段:

Name: AndroidManifest.xml
SHA1-Digest: D2yOY7wstlBC3AbjQznUDa6/8Xw=

这里我们可以自己对AndroidManifest.xml这个文件通过SHA1生成摘要信息,有在线的SHA1:

http://www.metools.info/code/c92.html

image

拿到摘要后,可以通过在线base64编码,将此摘要进行编码:

http://tomeko.net/online_tools/hex_to_base64.php?lang=en

image

可以看到这里获取到的base64编码结果跟MANIFEST.MF文件描述的一样。

其余文件的base64编码也是如此计算得来。从这里就知道,如果apk中的文件被篡改了,则最终得到的base64编码跟MANIFEST.MF文件描述不一样,就会导致安装出错。当然,也可以手动计算出所修改文件的base64编码,然后更新到MANIFEST.MF文件中,但也会功亏一篑,因为还有CERT.RSA和CERT.SF。

2.CERT.SF文件。截取部分该文件:

Signature-Version: 1.0
SHA1-Digest-Manifest: iaeBE2KJWElTbmMNGnjxretMvz8=
Created-By: 1.0 (Android SignApk)

Name: kotlin/collections/MapWithDefault.kotlin_metadata
SHA1-Digest: je8WuIzQjM5yX2bkDmjjyviMxOE=

Name: kotlin/coroutines/intrinsics/CoroutinesIntrinsicsHKt.kotlin_meta
 data
SHA1-Digest: reRpbwjfyR7tladgIdLzjpREduA=

Name: res/interpolator/btn_checkbox_checked_mtrl_animation_interpolato
 r_0.xml
SHA1-Digest: 9Lo7vzAcNsmM/qCg9/iamygexzk=

这个文件是对MANIFEST.MF文件中的每一项再做一次SHA1计算,然后再进行base64编码。另外还对MANIFEST.MF文件的内容进行SHA1计算,然后进行编码,将该值写到SHA1-Digest-Manifest中:

SHA1-Digest-Manifest: iaeBE2KJWElTbmMNGnjxretMvz8=

这里对MANIFEST.MF文件进行了一层篡改保护,只要MANIFEST.MF文件被修改过,那最后得到的base64编码肯定对不上。当然,到此为止,如果只是做到这一步,那还是可以被篡改的。关键看看CERT.RSA文件。

3.CERT.RSA文件。该文件保存了公钥,所采用的加密算法等信息。还有比较重要的是,对CERT.SF文件的内容用私钥进行加密之后的值。也就是说,即使手动篡改了MANIFEST.MF文件和CERT.SF文件,CERT.RSA文件还是会对应不上,安装就会失败。

apk安装白名单进行签名校验

上面所说是签名的过程,而安装则是反过来。

CERT.RSA这个文件中的包含的公钥对数字签名(自己本身)进行解密,将解密后的结果与CERT.SF文件hash运算后的结果进行比对,一致的话就返回证书链信息,并将证书链保存在certificates对象中,同时说明CERT.SF文件没有被篡改。如果这里对比通过,则下一步就验证MANIFEST.MF是否被篡改。在这一步,会遍历每个文件(entry),对非文件夹非签名文件的文件,逐个生成SHA1的数字签名信息,再用Base64进行编码,并与MANIFEST.MF中对内容进行比对,若一一对应,则表示文件没被篡改。到这里校验APK所有文件是否有被篡改,也已完成。

显然,如果我们想要在安装白名单上实现签名校验,那可以对比RSA文件上的证书签名是否与apk的签名一致即可。

获取apk签名的证书指纹

拿到app后,通过解压apk,获取到CERT.RSA文件,执行如下指令:

keytool -printcert -file CERT.RSA

会可以得到签名的证书指纹:

Certificate fingerprints:
 MD5:  0E:BA:50:A4:5C:15:B3:5D:97:7D:04:D8:43:79:B3:58
 SHA1: 41:79:1C:9B:8F:AF:15:E1:AC:D5:AA:F5:92:10:FD:42:46:7D:82:70
 SHA256: 2D:37:0C:21:F5:DF:D5:53:D2:A7:96:31:4B:70:92:5F:B3:8A:DE:EF:90:86:4C:92:0B:BB:BB:12:88:7D:35:20

每个签名的证书指纹都是唯一的,如此一来,就可以通过该证书指纹的唯一性来进行签名校验了。

android源码中获取证书指纹

要想获取apk的签名证书指纹,就需要先获取其签名。app安装时通过PackageParser类来解析安装包的信息,而签名也包含其中。我们可以利用这个来获取到签名的证书指纹。具体的安装流程这里不多介绍,直接定位到PackageManagerService类的preparePackageLI方法,该方法会去new出一个PackageParser对象来做apk包的解析:

private PrepareResult preparePackageLI(InstallArgs args, PackageInstalledInfo res)
        throws PrepareFailure {
        
    ...
    PackageParser pp = new PackageParser();
    pp.setSeparateProcesses(mSeparateProcesses);
    pp.setDisplayMetrics(mMetrics);
    pp.setCallback(mPackageParserCallback);

    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage");
    final PackageParser.Package pkg;
    try {
        pkg = pp.parsePackage(tmpPackageFile, parseFlags);
        DexMetadataHelper.validatePackageDexMetadata(pkg);
    } catch (PackageParserException e) {
        throw new PrepareFailure("Failed parse during installPackageLI", e);
    } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    }
    ...
    
    try {
        // either use what we've been given or parse directly from the APK
        if (args.signingDetails != PackageParser.SigningDetails.UNKNOWN) {
            pkg.setSigningDetails(args.signingDetails);
        } else {
            PackageParser.collectCertificates(pkg, false /* skipVerify */);
        }
    } catch (PackageParserException e) {
        throw new PrepareFailure("Failed collect during installPackageLI", e);
    }
    ...
}

这里列出部分代码,pkg是一个Pakage对象,属于PackageParser内部类,该类用于保存apk相关的信息,如包名,apk路径,签名等。

签名拿到后,就可以拿到它的证书指纹了,如下:

String sha1 = getFingerprint(pkg.mSigningDetails.signatures[0], "SHA1");

private String getFingerprint(Signature signature, String hashAlgorithm) {
    if (signature == null) {
        return null;
    }
    try {
        MessageDigest digest = MessageDigest.getInstance(hashAlgorithm);
        return toHexadecimalString(digest.digest(signature.toByteArray()));
    } catch(NoSuchAlgorithmException e) {
        // ignore
    }
    return null;
}

MessageDigest 类为应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法。信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值。 这里是根据SHA算法获取摘要,即证书指纹。

签名校验

用该指纹和使用keytool得出的SHA1值进行比对,如果符合,则签名校验通过,允许应用安装。具体实现也比较单,可以将keytool得出的SHA1值保存到白名单中,然后在apk安装过程中,再读出进行比对即可。

结语

每个签名的证书指纹,即消息摘要,是哪些内容经过SHA1、MD5、SHA256算法得到的呢?私钥加上其他的一些内容?

微信公众号

我在微信公众号也有写文章,更新比较及时,有兴趣者可以微信搜索【Android系统实战开发】,关注有惊喜哦!