Android APK Signature Scheme v2 渠道包生成方案

Google在Android7.0(Nougat)推出了新的签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。也就是说目前比较流行的渠道分包方案在APK Signature Scheme v2下将无法使用,虽然目前V2签名方案Google并不是强制要求使用,但和传统的签名方案对比它有更快的签名速度和更安全的保护,不排除新的签名方案会被强制要求使用的可能,所以我们需要适配V2签名。

目前在旧的签名方式下能够实现分包的方案有以下几种:

1、先解压apk,往assets目录或其他目录放置配置文件(也可以不需要解压)。这种方式是最简单,也是最安全的方式,别人不能篡改配置。缺点就是速度慢,渠道包一多需要等待很长一段时间。

2、在apk的meta-info文件夹下面放置一个配置文件,这种方式分包速度挺快,但读取配置文件效率不高,需要初始化zip才能读取。

3、第三种方式是 在apk的zip file comment 区域写入数据,这种方式是目前比较流行的,也是效率最高的一种。

APK Signature Scheme v2方式将会对以上几种方式产生什么影响了?我们先了解一下V2签名方式。
使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK Signing Block,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在APK签名方案中。

签名前和签名后的APK

整个APK(ZIP文件格式)会被分为以下四个区块:

1、Contents of ZIP entries(from offset 0 until the start of APK Signing Block)

2、 APK Signing Block

3、 ZIP Central Directory

4、ZIP End of Central Directory

签名后的各个 APK 部分

APK 签名方案 v2 负责保护第 1、3、4 部分的完整性,以及第 2 部分包含的“APK 签名方案 v2 分块”中的 signed data 分块的完整性。之前所列出来的分包方案都将会影响1、3、4的完整性。

通过以上分析我们知道区块1、3、4都是受完整性保护的,而区块2是部分区域是不受保护的,我们是否可以从区块2入手解决问题呢?那我们先看一下Google对区块2格式的描述:


偏移  字节数   描述


@+0    8    这个Block的长度(本字段的长度不计算在内)


@+8   n    一组ID-value


@-24   8    这个Block的长度(和第一个字段一样值)


@-16   16    魔数 “APK Sig Block 42”


区块2中APK Signing Block是由以上几部分组成,其中两个部分记录的是区块的长度,一个部分是魔数,这些都是用做验证,我们重点注意一下ID-value这部分,一组ID-value是由8字节长度标示+4字节ID+内容组成,Apk v2的签名信息的ID为0x7109871a。也就是说可以有若干组ID-value,那我们是不是可以加一组ID-value用于记录渠道信息呢?

我们先查看一下Android验证签名的机制:

APK 签名验证过程(新步骤以红色显示)

APK 验证签名信息步骤:

1、安装APK时先判断是否有v2签名块,如果有则验证,验证成功安装,验证失败拒绝安装。

2、未找到v2签名块,则走原有的v1验证机制。

那么Android系统是如何验证v2签名模块的呢?我们只能从源码入手,查看源码android.util.apk.ApkSignatureSchemeV2Verifier。从方法hasSignature开始查看

/**
     * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature.
     *
     * <p><b>NOTE: This method does not verify the signature.</b>
     */
    public static boolean hasSignature(String apkFile) throws IOException {
        try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
            findSignature(apk);
            return true;
        } catch (SignatureNotFoundException e) {
            return false;
        }
    }

这个方法只是提供了一个apk文件,再继续查看findSignature方法

/**
     * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
     * additional information relevant for verifying the block against the file.
     *
     * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
     * @throws IOException if an I/O error occurs while reading the APK file.
     */
    private static SignatureInfo findSignature(RandomAccessFile apk)
            throws IOException, SignatureNotFoundException {
        // Find the ZIP End of Central Directory (EoCD) record.
        Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
        ByteBuffer eocd = eocdAndOffsetInFile.first;
        long eocdOffset = eocdAndOffsetInFile.second;
        if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
            throw new SignatureNotFoundException("ZIP64 APK not supported");
        }

        // Find the APK Signing Block. The block immediately precedes the Central Directory.
        long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
        Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
                findApkSigningBlock(apk, centralDirOffset);
        ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
        long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;

        // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
        ByteBuffer apkSignatureSchemeV2Block = findApkSignatureSchemeV2Block(apkSigningBlock);

        return new SignatureInfo(
                apkSignatureSchemeV2Block,
                apkSigningBlockOffset,
                centralDirOffset,
                eocdOffset,
                eocd);
    }

读懂这段代码需要了解zip的格式,getEocd(apk)通过标识ox06054b50查找到Eocd的位移,从zip格式得知位移@+16 4个字节记录的是中央目录的起始位移,方法getCentralDirOffset就是通过该逻辑查找到中央目录的。紧挨着中央目录起始位移的就是APK Signing Block,再根据APK Signing Block区块格式就能找到APK Signing Block起始位移。方法findApkSignatureSchemeV2Block是用用来查找v2签名块的信息的,我们重点看下这个方法。

private static ByteBuffer findApkSignatureSchemeV2Block(ByteBuffer apkSigningBlock)
            throws SignatureNotFoundException {
        checkByteOrderLittleEndian(apkSigningBlock);
        // FORMAT:
        // OFFSET       DATA TYPE  DESCRIPTION
        // * @+0  bytes uint64:    size in bytes (excluding this field)
        // * @+8  bytes pairs
        // * @-24 bytes uint64:    size in bytes (same as the one above)
        // * @-16 bytes uint128:   magic
        ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);

        int entryCount = 0;
        while (pairs.hasRemaining()) {
            entryCount++;
            if (pairs.remaining() < 8) {
                throw new SignatureNotFoundException(
                        "Insufficient data to read size of APK Signing Block entry #" + entryCount);
            }
            long lenLong = pairs.getLong();
            if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount
                                + " size out of range: " + lenLong);
            }
            int len = (int) lenLong;
            int nextEntryPos = pairs.position() + len;
            if (len > pairs.remaining()) {
                throw new SignatureNotFoundException(
                        "APK Signing Block entry #" + entryCount + " size out of range: " + len
                                + ", available: " + pairs.remaining());
            }
            int id = pairs.getInt();
            if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
                return getByteBuffer(pairs, len - 4);
            }
            pairs.position(nextEntryPos);
        }

        throw new SignatureNotFoundException(
                "No APK Signature Scheme v2 block in APK Signing Block");
    }

这个方法是在遍历APK Signing Block中ID-value,当查找到id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID时候就返回内容,而APK_SIGNATURE_SCHEME_V2_BLOCK_ID的值是0x7109871a,也就是说查找到签名信息后其余未知的ID-value选择忽略。谷歌官方文档APK 签名方案 v2也有描述“在解译该分块时,应忽略 ID 未知的“ID-值”对。”,至此我们可以放心大胆的在该区域增加一组ID-value了。

接下来我们就往apk签名块中插入一组ID-value,以下是步骤:

1、根据标识(0x06054b50)找到EOCD位移。

2、EOCD起始位移16字节,找到记录中央目录的起始位移。

3、根据插入ID-value的大小修改EOCD中记录中央目录的位移。

4、根据中央目录起始位移-24找到记录签名块大小。

5、修改前后记录签名块大小的值。

以下为插入一组渠道id的代码:

    private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
    private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
    private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
    private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
    
    private final static int CHANNEL_FLAG = 0x12345678;   //渠道id标识
    private static void insertChannelId(RandomAccessFile apk,int adChannelId) {
        try{
            
            byte[] channelIdBuff = intToBytes2(adChannelId);
            int contentSize = channelIdBuff.length;
            
            //根据标识(0x06054b50)找到EOCD
            Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
            ByteBuffer eocd = eocdAndOffsetInFile.first;
            long eocdOffset = eocdAndOffsetInFile.second;
            
            if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
                throw new SignatureNotFoundException("ZIP64 APK not supported");
            }
            int size = 8 + 4 + contentSize;
            long neweocdOffset = eocdOffset + size;
            
            //查找中央目录位移
            long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
            long newCentralDirOffset = centralDirOffset + size;
            
            //查找签名块位移
            Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile = findApkSigningBlock(apk, centralDirOffset);
            long newSigningBlockSize = apkSigningBlockAndOffsetInFile.first.capacity()-8 + size;
            
            
            //插入一组渠道 格式为[大小:标识:内容]
            int pos = (int) (apkSigningBlockAndOffsetInFile.second + 8);
            File tmp = File.createTempFile("tmp", null);//创建一个临时文件存放数据;
            FileInputStream fis = new FileInputStream(tmp);
            FileOutputStream fos = new FileOutputStream(tmp);
            apk.seek(pos);//把指针移动到指定位置
            byte[] buf = new byte[1024];
            int len = -1;
            //把指定位置之后的数据写入到临时文件
            while((len = apk.read(buf)) != -1){
                fos.write(buf, 0, len);
            }
            apk.seek(pos);//再把指针移动到指定位置,插入追加的数据
            ByteBuffer buffer = ByteBuffer.allocate(size);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            buffer.putLong(size-8);  //大小
            buffer.putInt(CHANNEL_FLAG); //标识
            buffer.putInt(adChannelId); //内容
            apk.write(buffer.array());
            //再把临时文件的数据写回
            while((len = fis.read(buf)) > 0){
                apk.write(buf, 0, len);
            }
            
            apk.seek(neweocdOffset+ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
            buffer = ByteBuffer.allocate(4);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            buffer.clear();
            buffer.putInt((int) newCentralDirOffset); 
            apk.write(buffer.array());//修改eocd中央目录位移
            
            apk.seek(apkSigningBlockAndOffsetInFile.second);//移到签名块头
            buffer = ByteBuffer.allocate(8);
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            buffer.clear();
            buffer.putLong(newSigningBlockSize);
            apk.write(buffer.array()); //修改签名头大小
            
            apk.seek(newCentralDirOffset-24);
            buffer.clear();
            buffer.putLong(newSigningBlockSize);
            apk.write(buffer.array()); //修改签名尾大小
            
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

读取插入的ID-value原理也是一样,代码就不贴出来了。

参考:

新一代开源Android渠道包生成工具Walle
APK 签名方案 v2
Android Apk 动态写入数据方案,用于添加渠道号,数据倒流等
Zip (file format)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容