VasDolly对V2签名机制的支持

为啥需要V2签名?

v1有如下两个弊端:

  • MANIFEST.MF中的数据摘要是基于原始未压缩文件计算的。因此在校验时,需要先解压出原始文件,才能进行校验。
  • V1签名仅仅校验APK第一部分中的文件,缺少对APK的完整性校验。因此,如上面文章的分析,我们还是可以改apk文件的,而且不需要再签名。

而V2的出现就是为了解决上面的问题。提升速度,对apk本身进行数据摘要计算,这样就不用解压apk来校验了;增加安全性,对整个apk校验,因此任何的修改都无法通过签名的校验。

在开发中如何应对这种变化?

  • JDK中的jarsigner与V2签名是不兼容的
  • apksigner同时支持v1、v2的支持
  • Gradle2.2以上默认开启V2签名,所以如果想关闭V2签名,可将下面的v2SigningEnabled设置为false

    signingConfigs {
          release {
              ...
              v1SigningEnabled true
              v2SigningEnabled false
          }
    
          debug {
              ...
              v1SigningEnabled true
              v2SigningEnabled false
          }
      }
    

V2签名的apk的文件格式

V2签名的apk也是zip,不过不是标准的zip包。而是一种扩展的zip。

V2签名会生成一个单独的签名块,插入到apk中。

就是在文件内容块之后,目录中心之前插入。为了使得V2的apk依然是个zip文件,V2签名同时修改了EOCD中的中央目录的偏移量,使签名后的APK还符合ZIP结构。

ApkSignerV2源码分析

主要代码(也是签名的入口)是在ApkSignerV2.java#ByteBuffer[] sign()方法里。代码很长,我们只对关键的代码做下分析。

1. 获取apk的全部数据

inputApk.clear();
ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset);
ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset);
// Create a copy of End of Central Directory because we'll need modify its contents later.
byte[] eocdBytes = new byte[inputApk.remaining()];
inputApk.get(eocdBytes);
ByteBuffer eocd = ByteBuffer.wrap(eocdBytes);
eocd.order(inputApk.order());

通过定位,找出位置,从而可以分段读取zip文件里的每段的数据。

2. 计算内容的摘要

// Compute digests of APK contents.
Map<Integer, byte[]> contentDigests; // digest algorithm ID -> digest
try {
    contentDigests =
    computeContentDigests(
                        contentDigestAlgorithms,
                            new ByteBuffer[] {beforeCentralDir, centralDir, eocd});
} catch (DigestException e) {
    throw new SignatureException("Failed to compute digests of APK", e);
}

生成摘要信息是非常耗性能的,因此google也做了相应的优化,可以并行计算:

主要的思路是:

  1. 将上述APK中文件内容块、中央目录、EOCD按照1MB大小分割成一些小块。
  2. 计算每个小块的数据摘要,基础数据是0xa5 + 块字节长度 + 块内容。
  3. 计算整体的数据摘要,基础数据是0x5a + 数据块的数量 + 每个数据块的摘要内容。

2. 生成apk签名块

private static byte[] generateApkSigningBlock(
            List<SignerConfig> signerConfigs,
            Map<Integer, byte[]> contentDigests) throws InvalidKeyException, SignatureException {
        byte[] apkSignatureSchemeV2Block =
                generateApkSignatureSchemeV2Block(signerConfigs, contentDigests);
        return generateApkSigningBlock(apkSignatureSchemeV2Block);
    }

private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) {
        int resultSize =
                8 // size
                        + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair
                        + 8 // size
                        + 16 // magic
                ;
        ByteBuffer result = ByteBuffer.allocate(resultSize);
        result.order(ByteOrder.LITTLE_ENDIAN);
        long blockSizeFieldValue = resultSize - 8;
        result.putLong(blockSizeFieldValue);
        long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length;
        result.putLong(pairSizeFieldValue);
        result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
        result.put(apkSignatureSchemeV2Block);
        result.putLong(blockSizeFieldValue);
        result.put(APK_SIGNING_BLOCK_MAGIC);
        return result.array();
    }

内部代码逻辑太过复杂~~,只说明下签名块的格式:

对格式的进一步解释:

魔数的存在是为了方便确认apk签名分块的,因为它是固定的值,比较好定位到它;而当它被确定了,再又上面的签名块的大小,就能确定签名分块在文件中的起始位置。但是没搞懂为什么最上面需要记录签名块大小。

V2的验证

gradle2.2以上默认会同时走V1和V2签名(没有v1的签名,是可以编译通过的,但是在7.0上运行会报签名错误,所以会同时开启v1、v2签名)

主要流程简单的说:优先V2,没有V2,走V1的校验流程

V1的流程在签名已经介绍过,现在介绍下V2的流程。它的主要代码在ApkSignatureSchemeV2Verifier.java中。

1.

VasDolly对的多渠道支持

在上面的分析中,可以看到,上面的签名也是存在漏洞的,即Android系统只会关注ID为0x7109871a的V2签名块,并且忽略其他的ID-Value,同时V2签名只会保护APK本身,不包含签名块。

因此基于这个漏洞,可以在apk的签名块中添加一个ID-Value,存储渠道信息。

1. 读取V2签名的apk信息

public static ApkSectionInfo getApkSectionInfo(File baseApk) throws IOException, ApkSignatureSchemeV2Verifier.SignatureNotFoundException {
        RandomAccessFile apk = new RandomAccessFile(baseApk, "r");
        //1.find the EOCD and offset
        Pair<ByteBuffer, Long> eocdAndOffsetInFile = ApkSignatureSchemeV2Verifier.getEocd(apk);
        ByteBuffer eocd = eocdAndOffsetInFile.getFirst();
        long eocdOffset = eocdAndOffsetInFile.getSecond();

        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
            throw new ApkSignatureSchemeV2Verifier.SignatureNotFoundException("ZIP64 APK not supported");
        }

        //2.find the APK Signing Block. The block immediately precedes the Central Directory.
        long centralDirOffset = ApkSignatureSchemeV2Verifier.getCentralDirOffset(eocd, eocdOffset);//通过eocd找到中央目录的偏移量
        Pair<ByteBuffer, Long> apkSchemeV2Block =
                ApkSignatureSchemeV2Verifier.findApkSigningBlock(apk, centralDirOffset);//找到V2签名块的内容和偏移量

        //3.find the centralDir
        Pair<ByteBuffer, Long> centralDir = findCentralDir(apk, centralDirOffset, (int) (eocdOffset - centralDirOffset));
        //4.find the contentEntry
        Pair<ByteBuffer, Long> contentEntry = findContentEntry(apk, (int) apkSchemeV2Block.getSecond().longValue());

        ApkSectionInfo apkSectionInfo = new ApkSectionInfo();
        apkSectionInfo.mContentEntry = contentEntry;
        apkSectionInfo.mSchemeV2Block = apkSchemeV2Block;
        apkSectionInfo.mCentralDir = centralDir;
        apkSectionInfo.mEocd = eocdAndOffsetInFile;

        System.out.println("baseApk : " + baseApk.getAbsolutePath() + " , ApkSectionInfo = " + apkSectionInfo);
        return apkSectionInfo;
    }

2. 从V2签名块中取出所有的Id-Value对

public static Map<Integer, ByteBuffer> getAllIdValue(ByteBuffer apkSchemeBlock) {
    ... ...
}

3. 生成新的V2签名块

public static ByteBuffer generateApkSigningBlock(Map<Integer, ByteBuffer> idValueMap) {
        if (idValueMap == null || idValueMap.isEmpty()) {
            throw new RuntimeException("getNewApkV2SchemeBlock , id value pair is empty");
        }

        // FORMAT:
        // uint64:  size (excluding this field)
        // repeated ID-value pairs:
        //     uint64:           size (excluding this field)
        //     uint32:           ID
        //     (size - 4) bytes: value
        // uint64:  size (same as the one above)
        // uint128: magic

        long length = 16 + 8;//length is size (excluding this field) , 24 = 16 byte (magic) + 8 byte (length of the signing block excluding first 8 byte)
        for (Map.Entry<Integer, ByteBuffer> entry : idValueMap.entrySet()) {
            ByteBuffer byteBuffer = entry.getValue();
            length += 8 + 4 + (byteBuffer.remaining());
        }

        ByteBuffer newApkV2Scheme = ByteBuffer.allocate((int) (length + 8));
        newApkV2Scheme.order(ByteOrder.LITTLE_ENDIAN);
        newApkV2Scheme.putLong(length);//1.write size (excluding this field)

        for (Map.Entry<Integer, ByteBuffer> entry : idValueMap.entrySet()) {
            ByteBuffer byteBuffer = entry.getValue();
            //2.1 write length of id-value
            newApkV2Scheme.putLong(byteBuffer.remaining() + 4);//4 is length of id
            //2.2 write id
            newApkV2Scheme.putInt(entry.getKey());
            //2.3 write value
            newApkV2Scheme.put(byteBuffer.array(), byteBuffer.arrayOffset() + byteBuffer.position(), byteBuffer.remaining());
        }

        newApkV2Scheme.putLong(length);//3.write size (same as the one above)
        newApkV2Scheme.putLong(ApkSignatureSchemeV2Verifier.APK_SIG_BLOCK_MAGIC_LO);//4. write magic
        newApkV2Scheme.putLong(ApkSignatureSchemeV2Verifier.APK_SIG_BLOCK_MAGIC_HI);//4. write magic
        if (newApkV2Scheme.remaining() > 0) {
            throw new RuntimeException("generateNewApkV2SchemeBlock error");
        }
        newApkV2Scheme.flip();
        return newApkV2Scheme;
    }

4. 将新的签名块写入到apk中

渠道的读取

public static String getChannelByV2(Context context) {
        String apkPath = getApkPath(context);
        String channel = ChannelReader.getChannel(new File(apkPath));
        Log.i(TAG, "getChannelByV2 , channel = " + channel);
        return channel;
    }

public static String getStringValueById(File channelFile, int id) {
        if (channelFile == null || !channelFile.exists() || !channelFile.isFile()) {
            return null;
        }

        byte[] buffer = getByteValueById(channelFile, id);
        try {
            if (buffer != null && buffer.length > 0) {
                return new String(buffer, ChannelConstants.CONTENT_CHARSET);
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        return null;
    }

public static ByteBuffer getByteBufferValueById(File channelFile, int id) {
        if (channelFile == null || !channelFile.exists() || !channelFile.isFile()) {
            return null;
        }

        Map<Integer, ByteBuffer> idValueMap = getAllIdValueMap(channelFile);
        System.out.println("getByteBufferValueById , destApk " + channelFile.getAbsolutePath() + " IdValueMap = " + idValueMap);
        if (idValueMap != null) {
            return idValueMap.get(id);
        }

        return null;
    }

可以看出,对于V2签名的apk,读取channel的值是先解析出整个的Id-value块,然后根据id得到具体的值。

results matching ""

    No results matching ""