蜜桃乌托邦更新过后加密了,里面全是外冷内齁

有大手子吗

大致的解密方法需要你获取并解析清单
资产清单的解析方式参考

    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和几个小怪?)

我说怎么之前的几周搞的自动热更失效了,原来改了逻辑。