步高加密 APK 格式 BPK 研究 : 续

发布时间 2023-08-13 18:15:26作者: 20206675

上一篇: 步步高家教机加密安装包 BPK 研究 (已弃坑)

闲得蛋疼又开始研究这个了,主要是目前网上没搜到有人公开解密方法,心里还是痒痒的,虽然我菜,但是每次都能进步一点点嘛

这次继续从 Android ROM 上开刀,但是这次没有实体机了,只能慢慢摸索,没法调试咯

这次选了家教机 A6 开刀,因为官网上能下载到 ROM 的,A6 是最新的机型,基于 Android 11,紫光展锐平台

之前已经大概确定了 BPK 文件是通过魔改 Android 四大件实现对特殊加密格式的支持,所以这次直接就从 framework 下手了

值得一提的是,从步步高团队在 Github 上泄露的存储库上来看,BPK 的加密似乎是借由其他的脚本或者工具完成的,在 Android Studio 打包输出的时候,文件名中含有 "-ununencrypted" 字样,可惜并没有找到加密用的脚本

image.png

反编译 framework.jar,可以在 android.util.apk.ZipUtils 找到一些端倪

abstract class ZipUtils {
    private static final int UINT16_MAX_VALUE = 65535;
    private static final int ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER = 1347094023;
    private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
    private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
    private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
    private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
    private static final int ZIP_EOCD_REC_BBKSIG = 88821826;
    private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
    private static final int ZIP_EOCD_REC_SIG = 101010256;
    private static final byte[] xorCodeEOCD = "END_OF_CENTRAL_DIRECTORY_XOR_CODE_OF_BBK_APK_ENCRYPTION".getBytes();

    private ZipUtils() {
    }

    /* JADX INFO: Access modifiers changed from: package-private */
    public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip) throws IOException {
        long fileSize = zip.length();
        if (fileSize < 22) {
            return null;
        }
        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
        if (result != null) {
            return result;
        }
        return findZipEndOfCentralDirectoryRecord(zip, 65535);
    }

    private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip, int maxCommentSize) throws IOException {
        if (maxCommentSize < 0 || maxCommentSize > 65535) {
            throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
        }
        long fileSize = zip.length();
        if (fileSize < 22) {
            return null;
        }
        ByteBuffer buf = ByteBuffer.allocate(((int) Math.min(maxCommentSize, fileSize - 22)) + 22);
        buf.order(ByteOrder.LITTLE_ENDIAN);
        long bufOffsetInFile = fileSize - buf.capacity();
        zip.seek(bufOffsetInFile);
        zip.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
        int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
        if (eocdOffsetInBuf == -1) {
            return null;
        }
        buf.position(eocdOffsetInBuf);
        ByteBuffer eocd = buf.slice();
        eocd.order(ByteOrder.LITTLE_ENDIAN);
        return Pair.create(eocd, Long.valueOf(eocdOffsetInBuf + bufOffsetInFile));
    }

    private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
        assertByteOrderLittleEndian(zipContents);
        int archiveSize = zipContents.capacity();
        if (archiveSize < 22) {
            return -1;
        }
        int maxCommentLength = Math.min(archiveSize - 22, 65535);
        int eocdWithEmptyCommentStartPosition = archiveSize - 22;
        for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; expectedCommentLength++) {
            int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
            if (zipContents.getInt(eocdStartPos) == 101010256) {
                int actualCommentLength = getUnsignedInt16(zipContents, eocdStartPos + 20);
                if (actualCommentLength == expectedCommentLength) {
                    return eocdStartPos;
                }
            } else if (zipContents.getInt(eocdStartPos) == 88821826) {
                for (int i = 4; i < 22; i++) {
                    byte tmp = zipContents.get(eocdStartPos + i);
                    byte[] bArr = xorCodeEOCD;
                    zipContents = zipContents.put(eocdStartPos + i, (byte) (bArr[(i - 4) % bArr.length] ^ tmp));
                }
                int i2 = eocdStartPos + 20;
                int actualCommentLength2 = getUnsignedInt16(zipContents, i2);
                if (actualCommentLength2 == expectedCommentLength) {
                    return eocdStartPos;
                }
            } else {
                continue;
            }
        }
        return -1;
    }

    public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(RandomAccessFile zip, long zipEndOfCentralDirectoryPosition) throws IOException {
        long locatorPosition = zipEndOfCentralDirectoryPosition - 20;
        if (locatorPosition < 0) {
            return false;
        }
        zip.seek(locatorPosition);
        return zip.readInt() == 1347094023;
    }

    public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
        return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + 16);
    }

    public static void setZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory, long offset) {
        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
        setUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + 16, offset);
    }

    public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
        assertByteOrderLittleEndian(zipEndOfCentralDirectory);
        return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + 12);
    }

    private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
        if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
            throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
        }
    }

    private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
        return buffer.getShort(offset) & 65535;
    }

    private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
        return buffer.getInt(offset) & 4294967295L;
    }

    private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
        if (value < 0 || value > 4294967295L) {
            throw new IllegalArgumentException("uint32 value of out range: " + value);
        }
        buffer.putInt(buffer.position() + offset, (int) value);
    }
}

在这个类中,发现了两个比较可疑的东西

  • 常量

    private static final byte[] xorCodeEOCD = "END_OF_CENTRAL_DIRECTORY_XOR_CODE_OF_BBK_APK_ENCRYPTION".getBytes();
    
  • 方法

    findZipEndOfCentralDirectoryRecord()
    

这个常量的内容让人非常眼熟,因为 BPK 中含有大量由它组成的内容,只是在有些地方它的顺序,或者说字节是被打乱的

image.png

在 BPK 文件的末尾也可以找到它

image.png

常量 xorCodeEOCD 被方法 findZipEndOfCentralDirectoryRecord 所使用,那么就继续看这个方法吧

先从它的名字看起,Find ZIP End of Central Directory Record,寻找 ZIP 中的中心目录标识,那是什么?

从这里开始,就涉及到 ZIP 格式的特性和文件结构了,先看一张图

img

一个 ZIP 文件可以分为以上三个区域,而刚才方法的用处就是寻找最下面的中心目录标识 —— EOCD

img

EOCD 的结构如上表所示,在一个没有注释的 ZIP 文件中,EOCD 的长度为 22 字节,也就是一个 ZIP 文件末尾的 22 个字节。EOCD 将告诉我们 Central Directory 的偏移、长度等信息

EOCD 的头 4 个字节是 50 4B 05 06 (因为 ZIP 使用小端序存储,所以这里是反着写的),有没有很熟悉?回顾一下上次找到的 BPK 与 ZIP 之间文件头的规律

APK (ZIP) 文件头 BPK 文件头
50 4B 01 02 42 50 4B 01
50 4B 03 04 42 50 4B 03
50 4B 05 06 42 50 4B 05
50 4B 07 08 42 50 4B 07

我们随便找一个 BPK,看看文件末尾的 22 个字节

image.png

这 22 个字节以 42 50 4B 05 开头,正好对应上了 ZIP 的 50 4B 05 06

但是很显然,这个 EOCD 并不是常规的 ZIP 的 EOCD,它也经过了一定程度的加密,我们继续看代码

方法 findZipEndOfCentralDirectoryRecordandroid.util.apk.ApkSigningBlockUtils.getEocd 所引用

static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk) throws IOException, SignatureNotFoundException {
    Pair<ByteBuffer, Long> eocdAndOffsetInFile = ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
    if (eocdAndOffsetInFile == null) {
        throw new SignatureNotFoundException("Not an APK file: ZIP End of Central Directory record not found");
    }
    return eocdAndOffsetInFile;
}

这个方法判断了传入的文件能不能被找到 EOCD,也就是说,它是不是一个 ZIP 文件。而方法 getEocd 继续被 android.util.apk.ApkSigningBlockUtils 所引用

public static SignatureInfo findSignature(RandomAccessFile apk, int blockId) throws IOException, SignatureNotFoundException {
    Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
    ByteBuffer eocd = eocdAndOffsetInFile.first;
    long eocdOffset = eocdAndOffsetInFile.second.longValue();
    if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
        throw new SignatureNotFoundException("ZIP64 APK not supported");
    }
    long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
    Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile = findApkSigningBlock(apk, centralDirOffset);
    ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
    long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second.longValue();
    ByteBuffer apkSignatureSchemeBlock = findApkSignatureSchemeBlock(apkSigningBlock, blockId);
    return new SignatureInfo(apkSignatureSchemeBlock, apkSigningBlockOffset, centralDirOffset, eocdOffset, eocd);
}

这个方法的用处是寻找签名,而这个类中的另一个方法,verifyIntegrity 又给我们提供了一点信息

public static void verifyIntegrity(Map<Integer, byte[]> expectedDigests, RandomAccessFile apk, SignatureInfo signatureInfo) throws SecurityException, EncryptedByEEBBKException {
    if (expectedDigests.isEmpty()) {
        throw new SecurityException("No digests provided");
    }
    if (signatureInfo.eocd.getInt(0) == 88821826) {
            throw new EncryptedByEEBBKException("is bbk encrypted apk, check with v1 signed");
    }
    
    ...
    
}

如果 EOCD 为 88821826,也就是 42 50 4B 05,则判断为 "is bbk encrypted apk, check with v1 signed"。看来步步高加密包使用了 V1 签名

在这边先打住一下,因为再按照引用关系上去就是验证签名的方法了。回去继续看 findZipEndOfCentralDirectoryRecord 方法

public static Pair < ByteBuffer, Long > findZipEndOfCentralDirectoryRecord(RandomAccessFile zip) throws IOException
{
    long fileSize = zip.length();
    if(fileSize < 22)
    {
        return null;
    }
    Pair < ByteBuffer, Long > result = findZipEndOfCentralDirectoryRecord(zip, 0);
    if(result != null)
    {
        return result;
    }
    return findZipEndOfCentralDirectoryRecord(zip, 65535);
}

private static Pair < ByteBuffer, Long > findZipEndOfCentralDirectoryRecord(RandomAccessFile zip, int maxCommentSize) throws IOException
{
    if(maxCommentSize < 0 || maxCommentSize > 65535)
    {
        throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
    }
    long fileSize = zip.length();
    if(fileSize < 22)
    {
        return null;
    }
    ByteBuffer buf = ByteBuffer.allocate(((int) Math.min(maxCommentSize, fileSize - 22)) + 22);
    buf.order(ByteOrder.LITTLE_ENDIAN);
    long bufOffsetInFile = fileSize - buf.capacity();
    zip.seek(bufOffsetInFile);
    zip.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
    int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
    if(eocdOffsetInBuf == -1)
    {
        return null;
    }
    buf.position(eocdOffsetInBuf);
    ByteBuffer eocd = buf.slice();
    eocd.order(ByteOrder.LITTLE_ENDIAN);
    return Pair.create(eocd, Long.valueOf(eocdOffsetInBuf + bufOffsetInFile));
}

private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)
{
    assertByteOrderLittleEndian(zipContents);
    int archiveSize = zipContents.capacity();
    if(archiveSize < 22)
    {
        return -1;
    }
    int maxCommentLength = Math.min(archiveSize - 22, 65535);
    int eocdWithEmptyCommentStartPosition = archiveSize - 22;
    for(int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; expectedCommentLength++)
    {
        int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
        if(zipContents.getInt(eocdStartPos) == 101010256)
        {
            int actualCommentLength = getUnsignedInt16(zipContents, eocdStartPos + 20);
            if(actualCommentLength == expectedCommentLength)
            {
                return eocdStartPos;
            }
        }
        else if(zipContents.getInt(eocdStartPos) == 88821826)
        {
            for(int i = 4; i < 22; i++)
            {
                byte tmp = zipContents.get(eocdStartPos + i);
                byte[] bArr = xorCodeEOCD;
                zipContents = zipContents.put(eocdStartPos + i, (byte)(bArr[(i - 4) % bArr.length] ^ tmp));
            }
            int i2 = eocdStartPos + 20;
            int actualCommentLength2 = getUnsignedInt16(zipContents, i2);
            if(actualCommentLength2 == expectedCommentLength)
            {
                return eocdStartPos;
            }
        }
        else
        {
            continue;
        }
    }
    return -1;
}

这个方法被重载了 3 次,它的作用是输入一个 ZIP 文件,返回 Pair 形式的变量,Pair.first 为 ByteBuffer 类型,内容为 EOCD 的 22 个字节,Pair.second 为 Long 类型,内容为 EOCD 的偏移

这并不是步步高单独定义的方法,而是魔改了 AOSP 的代码,为了审计方便,先删掉了注释

private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
private static final int ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER = 0x504b0607;
private static final int UINT16_MAX_VALUE = 0xffff;

static Pair < ByteBuffer, Long > findZipEndOfCentralDirectoryRecord(RandomAccessFile zip)
throws IOException
{
    long fileSize = zip.getChannel().size();
    if(fileSize < ZIP_EOCD_REC_MIN_SIZE)
    {
        return null;
    }

    Pair < ByteBuffer, Long > result = findZipEndOfCentralDirectoryRecord(zip, 0);
    if(result != null)
    {
        return result;
    }
    return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
}

private static Pair < ByteBuffer, Long > findZipEndOfCentralDirectoryRecord(RandomAccessFile zip, int maxCommentSize) throws IOException
    {
        if((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE))
        {
            throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
        }

        long fileSize = zip.getChannel().size();
        if(fileSize < ZIP_EOCD_REC_MIN_SIZE)
        {
            return null;
        }

        maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
        ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
        buf.order(ByteOrder.LITTLE_ENDIAN);
        long bufOffsetInFile = fileSize - buf.capacity();
        zip.seek(bufOffsetInFile);
        zip.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
        int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
        if(eocdOffsetInBuf == -1)
        {
            // No EoCD record found in the buffer
            return null;
        }
        // EoCD found
        buf.position(eocdOffsetInBuf);
        ByteBuffer eocd = buf.slice();
        eocd.order(ByteOrder.LITTLE_ENDIAN);
        return Pair.create(eocd, bufOffsetInFile + eocdOffsetInBuf);
    }

private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)
{
    assertByteOrderLittleEndian(zipContents);
    int archiveSize = zipContents.capacity();
    if(archiveSize < ZIP_EOCD_REC_MIN_SIZE)
    {
        return -1;
    }
    int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
    int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
    for(int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; expectedCommentLength++)
    {
        int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
        if(zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG)
        {
            int actualCommentLength = getUnsignedInt16(zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
            if(actualCommentLength == expectedCommentLength)
            {
                return eocdStartPos;
            }
        }
    }
    return -1;
}

不同之处在于最后一个 findZipEndOfCentralDirectoryRecord,步步高的代码多出了一块

image.png

if (zipContents.getInt(eocdStartPos) == 101010256) {
    ...
}

else if (zipContents.getInt(eocdStartPos) == 88821826) {
    for (int i = 4; i < 22; i++) {
        byte tmp = zipContents.get(eocdStartPos + i);
        byte[] bArr = xorCodeEOCD;
        zipContents = zipContents.put(eocdStartPos + i, (byte) (bArr[(i - 4) % bArr.length] ^ tmp));
     }
     int i2 = eocdStartPos + 20;
     int actualCommentLength2 = getUnsignedInt16(zipContents, i2);
     if (actualCommentLength2 == expectedCommentLength) {
            return eocdStartPos;
     }
}

这段代码终于到了区别 ZIP 和 BPK 的地方,第一个 if 判断 EOCD 的首 4 个字节是否为 50 4B 05 06 (依旧是十进制 + 小端序),而第二个 else-if 则判断 EOCD 的首 4 个字节是否为 50 4B 05 06,也就是 BPK 的 EOCD。如果是 BPK,则从 EOCD 的第 5 个字节开始进行异或解密

运行这段程序,并输出 Pair.second,我们得到了一个 22 字节大小的文件,使用十六进制编辑器打开

image.png

因为刚才的解密操作并没有操作 EOCD 的前 4 个字节,所以我们手动把它替换为 ZIP 文件的 50 4B 05 06,使用 010 Editor 的模板功能,我们发现这就是 BPK 文件所对应的 EOCD 了,EOCD 的几个参数也如数呈现

最让人关心的,自然是 Central Directory 了

img

用 EOCD 提供的偏移跳转,我们来到位置 30343168

image.png

这里的 APK Sig Block 42 是从 V2 签名开始,在 Central Directory 区块之前的单独小区块,用于存储签名信息,看来这里正是 Central Directory,中心目录区

img

到了这一步之后,就不再有涉及 Central Directory 的部分了,因为 Android 的目的只是获得 APK Signing Block,所以只需要获得 Central Directory 的偏移即可,至于 Central Directory 部分的数据要如何处理,它并不关心

目前已经知道了通过异或解密 EOCD 部分的方法,那么 CD (为了少打几个字,Central Directory 下面统一简写为 CD) 部分呢?我猜步步高依旧是魔改了 Android 的某个库来实现对加密格式的兼容。用 Everything 在 system 分区中搜索 ZIP 相关库

image.png

发现了一个 libziparchive.so 库,这个库正是 Android 中用来处理 ZIP 文件的。使用 IDA32 打开它,因为之前已经摸清了 EOCD 的套路,那么 CD 应该也是用一个字符串 + 异或的方式实现加密的,我们直接 Shift + F12 查看字符串,搜索 BBK

image.png

果然,除了 END_OF_CENTRAL_DIRECTORY_XOR_CODE_OF_BBK_APK_ENCRYPTION 之外,还有一个 CENTRAL_DIRECTORY_XOR_CODE_OF_BBK_APK_ENCRYPTION,显然这个字符串是用来异或 CD 区域的,按 X 查看交叉引用

image.png

这段文本被 OpenArchiveFd 函数调用,双击进入这个函数

image.png

看伪代码是一件非常痛苦的事情,不过好在也不需要完全看懂

v56 = v54;
dest[0] = '\0';
dest[1] = '\0';
*(_QWORD *)((char *)v105 + 6) = '\0';
*(_QWORD *)((char *)&v105[1] + 6) = '\0';
dest[2] = '\0';
v105[0] = '\0';
v57 = *(_DWORD *)v54;
v98 = v54;
if ( *((_BYTE *)v45 + 72) ) {
	v58 = 0;
	LODWORD(dest[0]) = *(_DWORD *)v54;
	do {
		*((_BYTE *)dest + v58 + 4) = *((_BYTE *)v54 + v58 + 4) ^ aCentralDirecto[v58 + -48 * (v58 / 0x30)];
		++v58;
	}
	while ( v58 != 42 );
	v57 = dest[0];
	v59 = 21712962; // 42 50 4B 01
	v60 = 1;
	v56 = dest;
} else {
	v59 = 33639248; // 50 4B 01 02
	v60 = 0;
}

上面这段代码中又发现了异或语句,同时 v59 的数值也很重要。看来这里又是区分 ZIP 和 BPK 的地方

乍一看,异或的算法是一样的,只是异或用的字典有些不同,我们尝试直接把 CENTRAL_DIRECTORY_XOR_CODE_OF_BBK_APK_ENCRYPTION 替换到 Java 实现的版本中

public class Main {

    public static final byte[] xorCodeEOCD = "CENTRAL_DIRECTORY_XOR_CODE_OF_BBK_APK_ENCRYPTION".getBytes();

    public static void main(String[] args) throws IOException {

        System.out.println("Hello World");

        File file = new File("E:\\SSDBACK\\Awsl\\BPK\\BPackageInstaller.CD.BPK");

        try {
            FileInputStream fis = new FileInputStream(file);
            byte[] data = new byte[(int) file.length()];
            fis.read(data);
            fis.close();

            ByteBuffer zipContents = ByteBuffer.wrap(data);

            for (int i = 4; i < zipContents.capacity(); i++) {
                byte tmp = zipContents.get(i);
                byte[] bArr = xorCodeEOCD;
                zipContents = zipContents.put(i, (byte) (bArr[(i - 4) % bArr.length] ^ tmp));
            }

            File outputFile = new File("E:\\SSDBACK\\Awsl\\BPK\\BPackageInstaller.CD.APK");
            FileOutputStream fos = new FileOutputStream(outputFile);
            byte[] byteArray = new byte[zipContents.remaining()];
            zipContents.get(byteArray);
            fos.write(byteArray);
            fos.close();


        } catch (IOException e) {
            e.printStackTrace();
        }



    }
}

现在我们得到了一个可以把 BPK 的 CD 部分通过异或还原为 APK 的方法

首先,我们先从一个 BPK 文件中提取一段 CD 区域,42 50 4B 01 在 BPK 中用来标记一个 CD 区域的起始,在 ZIP 中则是 50 4B 01 02

image.png

单独提取这段 CD 区域到新的文件,保存为 BPackageInstaller.CD.BPK。现在运行 Java 程序,输出的内容在 BPackageInstaller.CD.APK

image.png

打开两个文件,惊喜地发现 CD 区域的内容已经被解密出来了,把头部 42 50 4B 01 替换为 ZIP 的 50 4B 01 02,执行 010 Editor 的 ZIP 模板

image.png

现在 CD 区域的内容已经被如数还原

BPK ZIP 解释
42 50 4B 01 50 4B 01 02 Central directory file header 标记
42 50 4B 03 50 4B 03 04 File Record 标记
42 50 4B 05 50 4B 05 06 End of Central Directory Record (EOCD) 标记

一个完整的 ZIP 包还需要 File Record 区域的数据,这点官方的 BPK 和第三方制作的 "直装包" 有点区别,官方包是加密了这块的,但是第三方包没有加密,只是替换了标记

image.png

所以我猜这块不加密也可以,替换标记就行。不过目前手上没有机器,还有待验证 (2023/08/13)