(测试版)适用于AssetBundle的ArchiveVersion结构适配脚本

[又测试一下,对于注入版本号应该是有问题,刚想起来应该至少有两处要写入UnityRevision,有时间再改]
调试AssetBundle时,Vibe Coding了个小玩意[1],支持根据AssetBundle的ArchiveVersion对包结构进行适配调整,使得修改后的AssetBundle可以直接在AssetStudio加载。

顺便又增加了反向检查调整ArchiveVersion的功能和降低ArchiveVersion的功能(\x08→\x06)。

获取脚本

Python脚本:1.zip (2.9 KB)

代码内容
#!/usr/bin/env python3
"""
根据AssetBundle的ArchiveVersion对包结构进行适配调整,可以反向检查调整ArchiveVersion的功能和降低ArchiveVersion的功能(\x08→\x06)。
"""
import os
import sys
import argparse

def inject_unity_revision(input_path, output_path, new_revision=None, downgrade=False):
    """
    参数:
        input_path: 输入文件路径
        output_path: 输出文件路径
        new_revision: 要注入的新UnityRevision字符串(不含结尾\x00)
    """
    with open(input_path, 'rb') as f:
        data = bytearray(f.read())
    
    # 1. 查找signature "UnityFS\x00",并验证UnityVersion是否为5.x.x\x00
    signature = b'UnityFS\x00'
    signature_pos = -1
    search_start = 0
    # 避免假饵捕获:反复查找符合UnityVersion条件的signature
    while True:
        signature_pos = data.find(signature, search_start)
        if signature_pos == -1:
            raise ValueError("UnityFS\x00不被期待")

        # 检查UnityVersion是否为 b'5.x.x\x00'
        temp_pos = signature_pos + len(signature)
        if temp_pos + 4 + 6 > len(data):
            search_start = signature_pos + 1
            continue

        unity_version_start = temp_pos + 4
        unity_version = data[unity_version_start:unity_version_start+6]

        if unity_version == b'5.x.x\x00':
            # 找到符合条件的signature
            data = data[signature_pos:]
            signature_pos = 0
            break
        else:
            search_start = signature_pos + 1

    pos = signature_pos + len(signature)  # signature之后的位置
    
    # 2. 读取ArchiveVersion (4字节, 大端)
    ArchiveVersion = int.from_bytes(data[pos:pos+4], 'big')
    pos += 4

    # 3. 读取UnityVersion (6字节)
    unity_version = data[pos:pos+6]
    pos += 6
    
    # 4. 找到UnityRevision (从pos开始到第一个\x00, 包含\x00)
    null_idx = data.find(b'\x00', pos)
    if null_idx == -1:
        raise ValueError("未找到UnityRevision的终止符")
    revision_start = pos
    revision_len = null_idx - pos + 1  # 包含\x00
    original_revision = data[revision_start:revision_start+revision_len]

    # 5. 处理UnityRevision
    if new_revision is not None:
        # 确保末尾有\x00
        if not new_revision.endswith('\x00'):
            new_revision += '\x00'
        new_revision_bytes = new_revision.encode('ascii')
        new_len = len(new_revision_bytes)

        # 6. 计算长度差,移动数据
        delta = new_len - revision_len
        after_revision = data[revision_start + revision_len:]
        data = data[:revision_start] + new_revision_bytes + after_revision

        # 更新pos到新revision结束位置
        pos = revision_start + new_len
    else:
        # 不替换,直接跳过原revision
        pos = revision_start + revision_len

    # 7. 读取FileSize (8字节)
    file_size = int.from_bytes(data[pos:pos+8], 'big')
    pos += 8

    # 8. 读取compressedBlockInfoSize (4字节)
    compressed_block_info_size = int.from_bytes(data[pos:pos+4], 'big')
    pos += 4

    # 9. 读取decompressedBlockInfoSize (4字节)
    decompressed_block_info_size = int.from_bytes(data[pos:pos+4], 'big')
    pos += 4

    # 10. 读取flags (4字节)
    flags = int.from_bytes(data[pos:pos+4], 'big')
    pos += 4  # 此时pos指向flags结束后的位置

    # 11. 对齐检查及调整(version >= 7 或反向检查触发时执行)
    if ArchiveVersion >= 7 and downgrade:
        print("-----🦀·🦀-----")
        # 降版本模式:修改ArchiveVersion为6
        ArchiveVersion = 6
        # 回写version到data(signature后第4字节开始)
        data[signature_pos+len(signature):signature_pos+len(signature)+4] = ArchiveVersion.to_bytes(4, 'big')
        
        # 修改 flags:保留最后一个字节,其余置0
        last_byte = data[pos-1]  # flags的最后一个字节(大端下最低位)
        new_flags = (0 << 24) | (0 << 16) | (0 << 8) | last_byte
        data[pos-4:pos] = new_flags.to_bytes(4, 'big')
        flags = new_flags

        # 反向对齐:删除 flags_end 后的填充
        flags_end = pos
        # 找到第一个非零字节
        first_nonzero = None
        for i in range(flags_end, len(data)):
            if data[i] != 0:
                first_nonzero = i
                break
        if first_nonzero is not None:
            fill_len = first_nonzero - flags_end
            if fill_len > 0:
                data = data[:flags_end] + data[first_nonzero:]
                # 更新 pos(后续数据前移)
                # pos 仍指向 flags_end,但后面内容已变
                # 注意:compressedBlockInfo 起始位置现在是 flags_end

        # 对 compressed data 起始位置做同样反向对齐
        cd_start = flags_end + compressed_block_info_size
        first_nonzero2 = None
        for i in range(cd_start, len(data)):
            if data[i] != 0:
                first_nonzero2 = i
                break
        if first_nonzero2 is not None:
            fill_len2 = first_nonzero2 - cd_start
            if fill_len2 > 0:
                data = data[:cd_start] + data[first_nonzero2:]

        # 降级完成后,跳过后续对齐调整
        need_align = False
    else:
        need_align = False
        if ArchiveVersion >= 7:
            need_align = True
        elif new_revision is None and ArchiveVersion <= 7:
            # 反向检查:检查是否有对齐填充迹象
            flags_end = pos
            X_check = (16 - (flags_end % 16)) % 16
            first_nonzero = None
            for i in range(flags_end, len(data)):
                if data[i] != 0:
                    first_nonzero = i
                    break
            if first_nonzero is not None:
                Y_check = first_nonzero - flags_end
                if Y_check > 0:  # 存在填充,说明实际应为高版本
                    ArchiveVersion = 8
                    need_align = True
                    # 回写修改后的ArchiveVersion到data
                    data[signature_pos+len(signature):signature_pos+len(signature)+4] = ArchiveVersion.to_bytes(4, 'big')

        if need_align:
            flags_end = pos
            X = (16 - (flags_end % 16)) % 16

            first_nonzero = None
            for i in range(flags_end, len(data)):
                if data[i] != 0:
                    first_nonzero = i
                    break
            if first_nonzero is None:
                raise ValueError("未找到compressedBlockInfo起始字节")
            Y = first_nonzero - flags_end

            data = data[:flags_end] + b'\x00' * X + data[first_nonzero:]
            pos = flags_end + X

        # 12. 读取compressedBlockInfo (不解析,仅确定结束位置)
        compressed_block_info_end = pos + compressed_block_info_size

        # 13. 对compressed data起始做对齐调整
        cd_start = compressed_block_info_end
        # 计算X2
        X2 = (16 - (cd_start % 16)) % 16
        # 找到从cd_start开始的第一个非\\x00字节
        first_nonzero2 = None
        for i in range(cd_start, len(data)):
            if data[i] != 0:
                first_nonzero2 = i
                break
        if first_nonzero2 is None:
            raise ValueError("未找到compressed data起始字节")
        Y2 = first_nonzero2 - cd_start

        # 移动数据
        data = data[:cd_start] + b'\x00' * X2 + data[first_nonzero2:]

    # 14. 写入输出文件
    with open(output_path, 'wb') as f:
        f.write(data)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('input_file', help='输入文件路径')
    parser.add_argument('new_revision', nargs='?', default=None, help='新UnityRevision(可选)')
    parser.add_argument('--OnoManami', action='store_true', help='降版本模式(新锐测试中)')
    args = parser.parse_args()

    input_file = args.input_file
    new_rev = args.new_revision
    downgrade_mode = args.OnoManami

    dir_name = os.path.dirname(input_file) or '.'
    base_name = os.path.splitext(os.path.basename(input_file))[0]
    ext = os.path.splitext(input_file)[1]
    output_file = os.path.join(dir_name, f"[Test]_{base_name}{ext}")

    inject_unity_revision(input_file, output_file, new_rev, downgrade=downgrade_mode)
    print("修改完成。感谢您参与测试!")

简单使用:在终端打开

python 1.py <输入文件> [<新UnityRevision>]
测试样本

测试样本中有些就是真实样本,有些是基于真实样本修改的。侵删。
1.test1.zip (3.7 MB)
修改了ArchiveVersion的ab包,用于测试结构修复。
2.test2.zip (5.2 KB)
用于测试FakeHeader的功能、降低ArchiveVersion加载到AssetStudio v0.16.47的功能。

注:

基于实践经验设计了一下代码逻辑的,不推荐使用随意编造的样本测试功能。代码中也特地写了一些对于实现目前的功能基本无用的标注变量内容,如果想print(f"变量值:{<变量>}")一下,还是方便的。再者精简一下 Vibe Coding 个图形化界面应该还是容易的。


  1. 主要体验一下创建新话题的功能 :+1:,这个是捎带的 ↩︎