通过ZIP文件格式的多渠道打包技术

原理

由于Android apk包使用的是压缩方式是zip。在zip中有一个区域,可以存放数据。若正确的修改这个部分,就可以在不破坏包同时不用重新打包的前提下,给apk写入数据。

在每一个zip文件的结尾,都有这样一组数据[资料来源wiki](https://en.wikipedia.org/wiki/Zip_(file_format))

End of central directory record (EOCD)

OffsetBytesDescription[25]
04End of central directory signature = 0x06054b50
42Number of this disk
62Disk where central directory starts
82Number of central directory records on this disk
102Total number of central directory records
124Size of central directory (bytes)
164Offset of start of central directory, relative to start of archive
202Comment length (n)
22nComments

倒数第一个是,注释的内容,倒数第二个是注释内容的长度。

核心读写过程

写入过程

定义一个流,将需要的数据按照写入RandomAccessFile.因为无法读知道写入内容的长度,因此需要写入长度。其次是定义一个Flag,方便程序在读取的时候找到数据。

private static void write(File path, byte[] content, String password) throws Exception {
        
        ZipFile zipFile = new ZipFile(path);
        boolean isIncludeComment = zipFile.getComment() != null;
        zipFile.close();
        if (isIncludeComment) {
            throw new IllegalStateException("Zip comment is exists, Repeated write is not recommended.");
        }
        
        boolean isEncrypt = password != null && password.length() > 0;
        byte[] bytesContent = isEncrypt ? encrypt(password, content) : content;
        byte[] bytesVersion = VERSION_1_1.getBytes(CHARSET_NAME);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(bytesContent); // 写入内容;
        baos.write(short2Stream((short) bytesContent.length)); // 写入内容长度;
        baos.write(isEncrypt ? 1 : 0); // 写入是否加密标示;
        baos.write(bytesVersion); // 写入版本号;
        baos.write(short2Stream((short) bytesVersion.length)); // 写入版本号长度;
        baos.write(SIG.getBytes(CHARSET_NAME)); // 写入SIG标记;
        byte[] data = baos.toByteArray();
        baos.close();
        if (data.length > Short.MAX_VALUE) {
            throw new IllegalStateException("Zip comment length > 32767.");
        }
        
        // Zip文件末尾数据结构:{@see java.util.zip.ZipOutputStream.writeEND}
        RandomAccessFile raf = new RandomAccessFile(path, "rw");
        raf.seek(path.length() - 2); // comment长度是short类型
        raf.write(short2Stream((short) data.length)); // 重新写入comment长度,注意Android apk文件使用的是ByteOrder.LITTLE_ENDIAN(小端序);
        raf.write(data);
        raf.close();
    }

读取过程

读取的过程可以直接使用Java的getComment()非常的方便,但是实际测试发现,这个方法只支持Java7以上的,因此对于Android 4.4.x之前是无法支持的。所以,先要去读取到最后面咋们加入的comment注释的长度,根据这个长度才能得到真真的内容。

private static byte[] read(File path, String password) throws Exception {
        byte[] bytesContent = null;
        byte[] bytesMagic = SIG.getBytes(CHARSET_NAME);
        byte[] bytes = new byte[bytesMagic.length];
        RandomAccessFile raf = new RandomAccessFile(path, "r");
        Object[] versions = getVersion(raf);
        long index = (long) versions[0];
        String version = (String) versions[1];
        if (VERSION_1_1.equals(version)) {
            bytes = new byte[1];
            index -= bytes.length;
            readFully(raf, index, bytes); // 读取内容长度;
            boolean isEncrypt = bytes[0] == 1;

            bytes = new byte[2];
            index -= bytes.length;
            readFully(raf, index, bytes); // 读取内容长度;
            int lengthContent = stream2Short(bytes, 0);

            bytesContent = new byte[lengthContent];
            index -= lengthContent;
            readFully(raf, index, bytesContent); // 读取内容;

            if (isEncrypt && password != null && password.length() > 0) {
                bytesContent = decrypt(password, bytesContent);
            }
        }
        raf.close();
        return bytesContent;
    }

使用方法

用法:java -jar MCPTool.jar [-path] [arg] [-contents] [arg] [-password] [arg]
-path APK文件路径
-outdir 输出路径(可选),默认输出到APK文件同一目录
-contents 写入内容集合,多个内容之间用“#”分割,如:googleplay#0_1039_1039008
-password 加密密钥(可选),长度8位以上,如果没有该参数,不加密
例如:
写入:java -jar MCPTool.jar -path D:/test.apk -outdir ./ -contents googleplay#0_1039_1039008 -password 12345678
读取:java -jar MCPTool.jar -path D:/test.apk -password 12345678

前端使用

通过Python,写一个简单的读取文件,将其变成参数后,通过引入os包调用改方法。

后续可以的改进

  • 现在的参数password是比较简单的,没有做校验的,再后面可以优化的这一部分
  • Python脚本目前的地址写的是绝对地址
    原文作者:Domon_Lee
    原文地址: https://www.jianshu.com/p/565d3891dee6
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞