明日方舟游戏资源解压提取指南

前言

解密方法不是我发现的,我仅对已知的解密方法进行整合与利用。

省流:https://github.com/jiellll1219/assetstudio/releases/ 里有做好解密适配的简体中文版 AssetStudio,项目适用 Deepseek V4 Pro 进行魔改,测试中没有发现问题。

代码

def _read_extra_length(data: bytearray, cur_pos: int, max_pos: int) -> Tuple[int, int]:
    l = 0
    while cur_pos < max_pos:
        b = data[cur_pos]
        l += b
        cur_pos += 1
        if b != 0xFF:
            break
    return l, cur_pos

def decompress_lz4ak(compressed_bytes: bytes, uncompressed_length: int) -> bytes:
    ip = 0
    op = 0
    fixed_compressed_data = bytearray(compressed_bytes)
    compressed_size = len(compressed_bytes)
    
    while ip < compressed_size:
        literal_length = fixed_compressed_data[ip] & 0xF
        match_length = (fixed_compressed_data[ip] >> 4) & 0xF
        fixed_compressed_data[ip] = (literal_length << 4) | match_length
        ip += 1
        
        if literal_length == 0xF:
            l, ip = _read_extra_length(fixed_compressed_data, ip, compressed_size)
            literal_length += l
        ip += literal_length
        op += literal_length
        
        if op >= uncompressed_length:
            break
            
        offset = (fixed_compressed_data[ip] << 8) | fixed_compressed_data[ip + 1]
        fixed_compressed_data[ip] = offset & 0xFF
        fixed_compressed_data[ip + 1] = (offset >> 8) & 0xFF
        ip += 2
        
        if match_length == 0xF:
            l, ip = _read_extra_length(fixed_compressed_data, ip, compressed_size)
            match_length += l
        match_length += 4
        op += match_length
    
    return lz4.block.decompress(fixed_compressed_data, uncompressed_length)

用法如下

UnityPy.helpers.CompressionHelper.DECOMPRESSION_MAP[
    UnityPy.enums.BundleFile.CompressionFlags.LZHAM
] = decompress_lz4ak

技术解析

前置基础知识

在正式解析前,需要先知道标准LZ4压缩数据流的结构

LZ4的压缩数据由多个序列组成,每个序列包含:

  • 令牌字节(Token):1字节,编码初始的字面量长度和匹配长度
  • 可选的额外字面量长度:如果字面量长度 ≥ 15,则此处有额外字节
  • 字面量数据:原始未压缩字节
  • 偏移量(Offset):2字节,小端序,表示向后查找的距离
  • 可选的额外匹配长度:如果匹配长度 ≥ 15(存储时加基数4),则此处有额外字节

令牌字节结构如下

  7  6  5  4  3  2  1  0
┌──┬──┬──┬──┬──┬──┬──┬──┐
│  字面量长度│  匹配长度  │
│  (高4位)  │  (低4位)   │
└──┴──┴──┴──┴──┴──┴──┴──┘

高4位:字面量长度(0-15,值为15表示有额外长度)
低4位:匹配长度(0-15,实际匹配长度 = 存储值 + 4)

偏移量使用小端序(Little-Endian)存储,即低字节在前、高字节在后。当字面量长度或匹配长度字段值为15(0xF)时,会从后续字节继续读取长度值。额外长度采用变长编码:每个字节的低7位表示数值,最高位(第8位)为标志位(1表示还有后续字节,0表示结束)

正式解析

lz4ak 格式对令牌字节进行了高低半字节交换,并且对偏移量字段进行了高低字节交换

ip = 0 # 输入指针,遍历压缩数据  
op = 0 # 输出指针,估计已解码的数据大小,用于判断循环终止  
fixed_compressed_data = bytearray(compressed_bytes) # 创建可变副本用于原地修正

while ip < compressed_size:
    # 步骤1: 读取并修正令牌字节
    literal_length = fixed_compressed_data[ip] & 0xF
    match_length = (fixed_compressed_data[ip] >> 4) & 0xF
    fixed_compressed_data[ip] = (literal_length << 4) | match_length
    ip += 1

    # 步骤2: 处理额外字面量长度
    if literal_length == 0xF:
        l, ip = _read_extra_length(fixed_compressed_data, ip, compressed_size)
        literal_length += l

    # 跳过字面量数据,更新指针
    ip += literal_length
    op += literal_length # 估算已解压数据量增加

    # 检查是否已经解压完毕(最后一个序列可能只有字面量,无匹配)
    if op >= uncompressed_length:
        break

    # 步骤3: 读取并修正偏移量
    # 注:此时 ip 指向偏移量字段
    offset = (fixed_compressed_data[ip] << 8) | fixed_compressed_data[ip + 1]
    # 写回修正后的小端序
    fixed_compressed_data[ip] = offset & 0xFF
    fixed_compressed_data[ip + 1] = (offset >> 8) & 0xFF
    ip += 2

    # 步骤4: 处理额外匹配长度
    if match_length == 0xF:
        l, ip = _read_extra_length(fixed_compressed_data, ip, compressed_size)
        match_length += l
    match_length += 4

    # 更新估计的输出大小
    op += match_length

在所有序列的令牌和偏移量都被修正为小端序标准格式后,内存中的 fixed_compressed_data 已经是一个完全合法的标准 LZ4 压缩块,直接调用 lz4 标准库进行解压即可

_read_extra_length 函数用于读取 LZ4 中的可变长度编码。如果令牌中的长度字段为 15(0xF),则表示实际长度是 15 + 后续字节的累加和。后续每个字节如果为 255(0xFF),则继续累加并读取下一个字节,直到遇到一个小于 255 的字节为止。此函数读取时,直接操作内存中的数据,因为在步骤1 令牌修正后,紧跟的额外长度字段本身无需修正