求助 影之诗Beyond如何解包

嗯偶然间尝试了一下发现我之前抓到的一个居然已经是final key了 :joy:,回溯一下发现base mask了,感谢佬

请问下 这游戏的global-metadata.dat如何提取,只提取的CS文件,好奇global-metadata.dat到底藏哪里了,内存中查找头部 AF 1B B1 FA也没找到,可能把头部也改了,还有其他方法可以提取吗

大佬好,目前我已经找到了assetbundle manifest,但是AssetBundleBaseKey这个字节数组始终没能找到,GameAssembly快看完了,依旧没什么头绪,断点尝试了很多位置,都没能找到。请问大佬能给点提示吗?

可以参考这个 IL2CPP Tutorial: Finding loaders for obfuscated global-metadata.dat files | Katy's Code

结构没改 直接内存搜global-metadata.dat找引用

对着上面github的代码看就行了,一个静态常量

更新8:研究了一下0人在意的wem id到音效名的音频对应关系

  • 音频从Play_{XXX}到WWise ID通过固定的FNV-1A hash. szb还很贴心的帮你把对应表存下来了,头0x20为key 之后0x10是iv, RJ256解密
  • pck文件都是1个bnk + N个wem, WWise版本2023.1, 至少需要支持AK_SOUNDBANK_VERISON>=150 的工具来替换
  • bnk里面可以从WEM → CAkSound → CAkActionPlay → CAkEvent → FNV-1a(Play_{XXX}) 一级一级对应下去,折叠后可以得到wem到音效名的对应

脚本如下,需自行clone wwiser仓库

def decrypt_wwise_bytes(data: bytes) -> bytes:    
    key = data[:0x20]
    iv = data[0x20:0x30]
    ciphertext = data[0x30:]

    cipher = AES.new(key, AES.MODE_CBC, iv)
    return unpad(cipher.decrypt(ciphertext), AES.block_size)


def read_wwise_table(data: bytes) -> dict:
    stream = io.BytesIO(data)
    count = le_u32(stream.read(4))
    
    mapping = {}
    for i in range(count):
        wem_id = le_u32(stream.read(4))
        name_len = le_u32(stream.read(4))
        
        name = stream.read(name_len).decode('utf-8')
        mapping[wem_id] = name
    
    return mapping


def fnv1a_32(name: str, *, lower: bool = True) -> int:
    utf8_bytes = (name.lower() if lower else name).encode("utf-8")

    h = 0x811c9dc5
    for b in utf8_bytes:
        h = (h * 0x01000193) & 0xFFFF_FFFF
        h ^= b
    return h


def extract_banks(data: bytes,
                  out_dir: Path) -> int:
    """Scan *src_path* and dump every bank into *out_dir*.
       Returns the number of banks written."""
       
    CHUNK_IDS = {
        b'BKHD', b'HIRC', b'DATA', b'FXPR', b'ENVS',
        b'STID', b'STMG', b'DIDX', b'PLAT', b'INIT',
    }
    CHUNK_IDS_BE = {int.from_bytes(c, 'big') for c in CHUNK_IDS}
    
    limit = len(data)
    out_dir.mkdir(parents=True, exist_ok=True)

    pos   = 0
    bnk_mapping = []
    while True:
        idx = data.find(b'BKHD', pos)
        if idx == -1:
            break                                     # no more banks
        # ── quick sanity checks (match QuickBMS logic) ───────────────
        if idx + 16 > limit:
            break                                     # truncated file
        size_field = le_u32(data[idx + 4:idx + 8])
        if size_field > 0x10000:                      # unlikely bank size
            pos = idx + 4
            continue
        bank_id = le_u32(data[idx + 12:idx + 16])     # bank numeric ID

        # ── walk successive chunks to find total bank size ───────────
        total     = 0
        first_blk = True
        sub       = idx
        while sub + 8 <= limit:
            cid  = be_u32(data[sub:sub + 4])          # big-endian four-cc
            csz  = le_u32(data[sub + 4:sub + 8])      # little-endian size
            # next BKHD/STID → start of a new bank
            if not first_blk and cid in (0x424B4844, 0x53544944):
                break
            first_blk = False
            if cid not in CHUNK_IDS_BE:               # garbage, abort
                break
            total += csz + 8
            sub   += csz + 8
        if total == 0:                                # invalid candidate
            pos = idx + 4
            continue

        name = f"{bank_id:08X}.bnk"
        (out_dir / name).write_bytes(data[idx:idx + total])

        # print(f"Extracted {name} @ 0x{idx:X} size 0x{total:X}")
        
        pos = idx + total                             # keep scanning

        bnk_mapping.append(out_dir / name)
    return bnk_mapping


def get_wem_mapping(wwise_event_to_name: dict, bnk_path: Path):
    sys.path.append(Path('./wwiser/wwiser'))
    
    from wwiser.wwiser.parser.wparser import Parser
    parser = Parser()
    parser.parse_bank(bnk_path)
    
    bnk = parser.get_banks()[0]
    
    items = bnk.find(name='listLoadedItem')
    nodes = items.get_children()
    
    wem_to_sound = {}
    sound_to_action = {}
    action_to_event = {}
    
    wem_to_name = {}
    
    for node in nodes:
        
        if node.get_name() == 'CAkSound':
            # ulID
            sound_id = node.find(type='sid').get_attrs()['value']
            # SoundInitialValues.AkBankSourceData.AkMediaInformation.sourceID
            wem_id = node.find(name='AkMediaInformation').find(type='tid').get_attrs()['value']
            wem_to_sound[wem_id] = sound_id
            
        if node.get_name() == 'CAkActionPlay':
            # ulID
            action_id = node.find(type='sid').get_attrs()['value']
            # ActionInitialValues.idExt
            sound_id = node.find(name='ActionInitialValues').find(name='idExt').get_attrs()['value']
            sound_to_action[sound_id] = action_id
            
        if node.get_name() == 'CAkEvent':
            # ulID
            event_id = node.find(type='sid').get_attrs()['value']
            # EventInitialValues.actions[0].ulActionID
            action_id = node.find1(name='Action').find(type='tid').get_attrs()['value']
            action_to_event[action_id] = event_id

    for wid in wem_to_sound:
        wem_to_name[wid] = wwise_event_to_name[
            action_to_event[
                sound_to_action[
                    wem_to_sound[wid]
                ]
            ]
        ]

    return OrderedDict(sorted(wem_to_name.items()))
    
    
def rebuild_wem_name_mapping(mapping_file: Path, pck_file: Path):
    with open(mapping_file, 'rb') as f:
        wwise_id_mapping = read_wwise_table(decrypt_wwise_bytes(f.read()))
    
    with open(pck_file, 'rb') as f:
        bnk_path = extract_banks(f.read(), Path('./wwise_bnks'))[0]
        
    return get_wem_mapping(wwise_id_mapping, bnk_path)

国服的pck文件是下载到哪个文件夹?