前言
解密方法不是我发现的,我仅对已知的解密方法进行整合与利用。
省流: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 令牌修正后,紧跟的额外长度字段本身无需修正