有大手子吗
大致的解密方法需要你获取并解析清单
资产清单的解析方式参考
class BinaryReader:
def __init__(self, data: bytes):
self.stream = BytesIO(data)
def tell(self):
return self.stream.tell()
def remaining(self):
current = self.stream.tell()
self.stream.seek(0, 2)
end = self.stream.tell()
self.stream.seek(current)
return end - current
def read_bytes(self, size):
data = self.stream.read(size)
if len(data) != size:
raise EOFError(
f"读取数据失败,需要 {size} 字节,"
f"当前位置 {self.tell()},剩余 {self.remaining()} 字节"
)
return data
def read_u8(self):
return struct.unpack("<B", self.read_bytes(1))[0]
def read_bool(self):
return self.read_u8() != 0
def read_i16(self):
return struct.unpack("<h", self.read_bytes(2))[0]
def read_u16(self):
return struct.unpack("<H", self.read_bytes(2))[0]
def read_i32(self):
return struct.unpack("<i", self.read_bytes(4))[0]
def read_u32(self):
return struct.unpack("<I", self.read_bytes(4))[0]
def read_i64(self):
return struct.unpack("<q", self.read_bytes(8))[0]
def read_string(self):
"""
格式:
[2 bytes] 字符串长度
[N bytes] UTF-8字符串
"""
length = self.read_u16()
if length == 0:
return ""
raw = self.read_bytes(length)
try:
return raw.decode("utf-8")
except UnicodeDecodeError:
return raw.decode("utf-8", errors="replace")
def parse_manifest(data: bytes):
reader = BinaryReader(data)
manifest = {}
# ============================================================
# PackageManifest_DefaultPackage
# ============================================================
# sign : [4 bytes, bytes]
manifest["sign"] = reader.read_bytes(4)
# FileVersion : [2 bytes : str]
manifest["FileVersion"] = reader.read_string()
# EnableAddressable : [1 bytes, bool]
manifest["EnableAddressable"] = reader.read_bool()
# LocationToLower : [1 bytes, bool]
manifest["LocationToLower"] = reader.read_bool()
# IncludeAssetGUID : [1 bytes, bool]
manifest["IncludeAssetGUID"] = reader.read_bool()
# OutputNameStyle : [4 bytes, int]
manifest["OutputNameStyle"] = reader.read_i32()
# PackageName : [2 bytes, str]
manifest["PackageName"] = reader.read_string()
# PackageVersion : [2 bytes, str]
manifest["PackageVersion"] = reader.read_string()
asset_count = reader.read_i32()
manifest["AssetCount"] = asset_count
manifest["Assets"] = []
for i in range(asset_count):
asset = {}
# Address : [2 bytes : str]
asset["Address"] = reader.read_string()
# AssetPath : [2 bytes : str]
asset["AssetPath"] = reader.read_string()
# AssetGUID : [2 bytes : str]
asset["AssetGUID"] = reader.read_string()
# AssetTags : [2 bytes : int]
asset_tag_count = reader.read_u16()
asset["AssetTags"] = []
for _ in range(asset_tag_count):
# [2 bytes : str]
asset["AssetTags"].append(reader.read_string())
# BundleID : [4 bytes, int]
asset["BundleID"] = reader.read_i32()
# DependIDs : [read 2 bytes : int]
depend_count = reader.read_u16()
asset["DependIDs"] = []
for _ in range(depend_count):
# [4 bytes, int]
asset["DependIDs"].append(reader.read_i32())
manifest["Assets"].append(asset)
bundle_count = reader.read_i32()
manifest["BundleCount"] = bundle_count
manifest["Bundles"] = []
for i in range(bundle_count):
bundle = {}
# BundleName : [2 bytes : str]
bundle["BundleName"] = reader.read_string()
# UnityCRC : [4 bytes : uint]
bundle["UnityCRC"] = reader.read_u32()
# FileHash : [2 bytes : str]
bundle["FileHash"] = reader.read_string()
# FileCRC : [2 bytes : str]
bundle["FileCRC"] = reader.read_string()
# FileSize : [8 bytes : int]
bundle["FileSize"] = reader.read_i64()
# IsRawFile : [1 bytes, bool]
bundle["IsRawFile"] = reader.read_bool()
# LoadMethod : [1 bytes, int]
bundle["LoadMethod"] = reader.read_u8()
# Tags : [2 bytes : int]
tag_count = reader.read_u16()
bundle["Tags"] = []
for _ in range(tag_count):
# [2 bytes : str]
bundle["Tags"].append(reader.read_string())
# ReferenceIDs : [read 2 bytes : int]
reference_count = reader.read_u16()
bundle["ReferenceIDs"] = []
for _ in range(reference_count):
# [4 bytes, int]
bundle["ReferenceIDs"].append(reader.read_i32())
manifest["Bundles"].append(bundle)
manifest["RemainingBytes"] = reader.remaining()
return manifest
拿到 BundleName 和 FileHash
然后向服务器请求获取下载的url带sign参数不然直接请求是没法下载的
之后参考代码
import base64
import hashlib
import hmac
import os
import struct
import tempfile
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
MAGIC = b"IDOLAB2"
HEADER_SIZE = 0x80
CHUNK_SIZE = 0x10000
FULL_RECORD_SIZE = 0x10040
KDF_INFO = b"IdolTime/AssetBundle/v2/AES256CBC-HMACSHA256"
MASTER_KEY = "B3A755E19FD88CB176FAA283BF0839FFD4FBC0A0F81211F9FADC5C86DB4CDC91"
def hmac_sha256(key: bytes, data: bytes) -> bytes:
return hmac.new(key, data, hashlib.sha256).digest()
def parse_master_key(value: str) -> bytes:
value = value.strip()
if len(value) == 64:
try:
return bytes.fromhex(value)
except ValueError as exc:
raise ValueError("Invalid master key hex") from exc
try:
decoded = base64.b64decode(value, validate=True)
if len(decoded) == 32:
return decoded
except Exception:
pass
raise ValueError("master key must be a 32-byte value as 64-char hex or canonical Base64")
def derive_keys(master_key: bytes, salt: bytes):
print("[DEBUG] derive_keys")
print(f" master_key = {master_key.hex()}")
print(f" salt = {salt.hex()}")
prk = hmac_sha256(salt, master_key)
print(f" prk = {prk.hex()}")
encryption_key = hmac_sha256(prk, KDF_INFO + b"\x01")
authentication_key = hmac_sha256(prk, encryption_key + KDF_INFO + b"\x02")
print(f" enc_key = {encryption_key.hex()}")
print(f" auth_key = {authentication_key.hex()}")
return encryption_key, authentication_key
def round_up_16(value: int) -> int:
return (value + 15) & ~15
def cipher_length(plain_length: int, add_full_padding_block: bool = True) -> int:
result = round_up_16(plain_length)
if add_full_padding_block:
result += 16
return result
def encrypted_length(plain_length: int, final_chunk_has_extra_block: bool = True) -> int:
if plain_length < 0:
raise ValueError("plain length must be non-negative")
if plain_length == 0:
return HEADER_SIZE
total = HEADER_SIZE
remaining = plain_length
chunk_index = 0
while remaining > 0:
current_plain = min(CHUNK_SIZE, remaining)
if chunk_index < (plain_length // CHUNK_SIZE):
total += FULL_RECORD_SIZE
else:
total += 16 + cipher_length(current_plain, add_full_padding_block=final_chunk_has_extra_block) + 32
remaining -= current_plain
chunk_index += 1
return total
def read_u32_le(data: bytes, offset: int) -> int:
return struct.unpack_from("<I", data, offset)[0]
def read_u64_le(data: bytes, offset: int) -> int:
return struct.unpack_from("<Q", data, offset)[0]
def verify_header(blob: bytes, bundle_name: str, master_key: bytes):
print("[DEBUG] verify_header")
print(f" bundle_name = {bundle_name}")
print(f" bundle_hash = {hashlib.sha256(bundle_name.encode('utf-8')).digest().hex()}")
if len(blob) < HEADER_SIZE:
raise ValueError("file is too small for header")
header = blob[:HEADER_SIZE]
if header[:7] != MAGIC:
raise ValueError(f"Invalid magic: {header[:7]!r}")
print(" magic ok")
if header[8] != 2:
raise ValueError(f"Unsupported format version: {header[8]}")
print(" version ok")
key_id = read_u32_le(header, 0x0C)
print(f" key_id = {key_id}")
if key_id != 1:
raise ValueError(f"Unsupported key_id: {key_id}, expected 1")
chunk_size = read_u32_le(header, 0x10)
print(f" chunk_size = {chunk_size:#x}")
if chunk_size != CHUNK_SIZE:
raise ValueError(f"Unexpected chunk size: 0x{chunk_size:x}, expected 0x{CHUNK_SIZE:x}")
reserved = read_u32_le(header, 0x14)
print(f" reserved = {reserved:#x}")
if reserved != 0:
raise ValueError("Header reserved field is not zero")
plain_length = read_u64_le(header, 0x18)
print(f" plain_length = {plain_length}")
if plain_length & (1 << 63):
raise ValueError("Invalid plaintext length")
salt = header[0x20:0x40]
stored_name_hash = header[0x40:0x60]
stored_header_tag = header[0x60:0x80]
print(f" stored_name_hash = {stored_name_hash.hex()}")
expected_name_hash = hashlib.sha256(bundle_name.encode("utf-8")).digest()
print(f" expected_name_hash = {expected_name_hash.hex()}")
if not hmac.compare_digest(stored_name_hash, expected_name_hash):
raise ValueError("bundle_name hash mismatch; bundle name is wrong")
print(" bundle name hash ok")
encryption_key, authentication_key = derive_keys(master_key, salt)
expected_header_tag = hmac_sha256(authentication_key, header[:0x60])
print(f" expected_header_tag = {expected_header_tag.hex()}")
print(f" stored_header_tag = {stored_header_tag.hex()}")
if not hmac.compare_digest(stored_header_tag, expected_header_tag):
raise ValueError("Header authentication failed; master key may be wrong")
print(" header tag ok")
standard_size = encrypted_length(plain_length, final_chunk_has_extra_block=True)
compat_size = encrypted_length(plain_length, final_chunk_has_extra_block=False)
print(f" actual_size = {len(blob)}")
print(f" standard = {standard_size}")
print(f" compat = {compat_size}")
if len(blob) == standard_size:
final_chunk_has_extra_block = True
elif len(blob) == compat_size:
final_chunk_has_extra_block = False
else:
raise ValueError(
f"File size mismatch: actual={len(blob)}, expected standard={standard_size}, compat={compat_size}"
)
return {
"plain_length": plain_length,
"header_tag": stored_header_tag,
"encryption_key": encryption_key,
"authentication_key": authentication_key,
"final_chunk_has_extra_block": final_chunk_has_extra_block,
}
def decrypt_bundle(blob: bytes, bundle_name: str, master_key_hex_or_b64: str) -> bytes:
master_key = parse_master_key(master_key_hex_or_b64)
info = verify_header(blob, bundle_name, master_key)
plain_length = info["plain_length"]
header_tag = info["header_tag"]
encryption_key = info["encryption_key"]
authentication_key = info["authentication_key"]
final_chunk_has_extra_block = info["final_chunk_has_extra_block"]
print(f"[DEBUG] decrypting {plain_length} bytes of plaintext")
print(f"[DEBUG] final_chunk_has_extra_block = {final_chunk_has_extra_block}")
out = bytearray()
file_offset = HEADER_SIZE
chunk_index = 0
while len(out) < plain_length:
remaining = plain_length - len(out)
plain_chunk_len = min(CHUNK_SIZE, remaining)
is_final_chunk = remaining <= CHUNK_SIZE
if is_final_chunk and not final_chunk_has_extra_block:
encrypted_chunk_len = cipher_length(plain_chunk_len, add_full_padding_block=False)
else:
encrypted_chunk_len = cipher_length(plain_chunk_len, add_full_padding_block=True)
record_length = 16 + encrypted_chunk_len + 32
print(f"\n[DEBUG] chunk {chunk_index}")
print(f" remaining = {remaining}")
print(f" plain = {plain_chunk_len}")
print(f" enc = {encrypted_chunk_len}")
print(f" record= {record_length}")
if file_offset + record_length > len(blob):
raise ValueError(f"chunk {chunk_index} exceeds file boundary")
iv = blob[file_offset:file_offset + 16]
file_offset += 16
ciphertext = blob[file_offset:file_offset + encrypted_chunk_len]
file_offset += encrypted_chunk_len
stored_chunk_tag = blob[file_offset:file_offset + 32]
file_offset += 32
print(f" iv = {iv.hex()}")
print(f" ciphertext_head = {ciphertext[:16].hex()}")
print(f" stored_chunk_tag = {stored_chunk_tag.hex()}")
# HMAC = HMAC(authKey, headerTag || u64_le(chunkIndex) || iv || ciphertext)
auth_input = header_tag + struct.pack("<Q", chunk_index) + iv + ciphertext
expected_chunk_tag = hmac_sha256(authentication_key, auth_input)
print(f" expected_chunk_tag = {expected_chunk_tag.hex()}")
if not hmac.compare_digest(stored_chunk_tag, expected_chunk_tag):
raise ValueError(f"Chunk {chunk_index} authentication failed")
print(" chunk auth ok")
aes = AES.new(encryption_key, AES.MODE_CBC, iv)
padded_plain = aes.decrypt(ciphertext)
try:
plain_chunk = unpad(padded_plain, AES.block_size)
except ValueError as exc:
raise ValueError(f"chunk {chunk_index} invalid PKCS7 padding") from exc
print(f" plaintext len = {len(plain_chunk)}")
if len(plain_chunk) != plain_chunk_len:
raise ValueError(
f"Chunk {chunk_index} plaintext length mismatch: "
f"actual={len(plain_chunk)}, expected={plain_chunk_len}"
)
out.extend(plain_chunk)
chunk_index += 1
if len(out) != plain_length:
raise ValueError(
f"Final plaintext length mismatch: actual={len(out)}, expected={plain_length}"
)
print(f"\n[DEBUG] decryption completed: total plaintext = {len(out)}")
return bytes(out)
def decrypt_ab_file(encrypted_ab_path: str, bundle_name: str, master_key_hex_or_b64: str, output_path: str | None = None) -> bytes:
if not os.path.exists(encrypted_ab_path):
raise FileNotFoundError(encrypted_ab_path)
blob = Path(encrypted_ab_path).read_bytes()
print(f"[DEBUG] opening file: {encrypted_ab_path}")
plaintext = decrypt_bundle(blob, bundle_name, master_key_hex_or_b64)
if output_path is not None:
Path(output_path).write_bytes(plaintext)
print(f"[DEBUG] wrote decrypted file to {output_path}")
return plaintext
def decryptf(file_path: str) -> str:
source = Path(file_path)
if not source.is_file():
raise FileNotFoundError(f"文件不存在: {source}")
local_name = source.name
if "-_-" in local_name:
bundle_name = local_name.split("-_-", 1)[0]
else:
bundle_name = local_name
print("[decryptf] 源文件:", source)
print("[decryptf] BundleName:", bundle_name)
encrypted_data = source.read_bytes()
decrypted_data = decrypt_bundle(
blob=encrypted_data,
bundle_name=bundle_name,
master_key_hex_or_b64=MASTER_KEY,
)
print("[decryptf] 解密完成,明文大小:", len(decrypted_data))
print("[decryptf] 明文头:", decrypted_data[:32])
temp_path = None
try:
with tempfile.NamedTemporaryFile(
mode="wb",
prefix=f".{source.name}.",
suffix=".tmp",
dir=str(source.parent),
delete=False,
) as temp_file:
temp_path = Path(temp_file.name)
temp_file.write(decrypted_data)
temp_file.flush()
os.fsync(temp_file.fileno())
# 原子替换源文件
os.replace(temp_path, source)
temp_path = None
finally:
if temp_path is not None and temp_path.exists():
temp_path.unlink()
print("[decryptf] 已覆盖源文件:", source)
return str(source)
decryptf(r"assets_res_audio_lang_chinesesimplified_avg_1_31002601_chinesesimplified.bundle-_-21b4ddc8ba727e3f90ad0bd117c52300")
完成解密
注意我decryptf传入的文件名格式为{BundleName}-_-{FileHash} 或者你可以修改代码用你自己的格式
这种又臭又长的逆向还得是copilot牛逼…
3 个赞
给一个提示,清单位于yoo文件,后缀为byts,按大小第一个就是(抓清单用),如果你跟我一样没什么技术了,那么请关掉模拟器root和卸掉mt直接按下下载数据就行了,然后直接用大佬给出的第二个脚本的解密逻辑进行解密进行(还有这个图片和atlas并不在同一个文件内,但是因为有多个同名文件,所以还是需要按文件路径导出,然后将同名文件夹合并就ok,其次图片角色仍然未出定位大概为10039,这个只是战斗动画?大概新增一个hcg和几个小怪?)
我说怎么之前的几周搞的自动热更失效了,原来改了逻辑。




