有人会解ero新出的吗?

怎么感觉好多游戏的文本字幕都不在本地 比如梦鸡 龙少女

uint64_t Pyro_AssetBundleManager_AssetBundlesLoader__GetAssetBundleOffset(
        System_String_o *platform,
        System_String_o *path,
        const MethodInfo *method)
{
  System_Text_Encoding_o *ASCII; // x19
  System_String_o *v6; // x0
  __int64 v7; // x0
  __int64 v8; // x9
  unsigned __int8 v9; // w8
  _BYTE *v10; // x10
  __int64 v11; // x11
  __int64 v12; // x12

  ASCII = System_Text_Encoding__get_ASCII(0LL);
  v6 = System_String__Concat_121532840(platform, path, 0LL);
  if ( !ASCII
    || (v7 = ((__int64 (__fastcall *)(System_Text_Encoding_o *, System_String_o *, const MethodInfo *))ASCII->klass->vtable._18_GetBytes.methodPtr)(
               ASCII,
               v6,
               ASCII->klass->vtable._18_GetBytes.method)) == 0 )
  {
    sub_2DA6564();
  }
  v8 = *(_QWORD *)(v7 + 24);
  v9 = 0;
  v10 = (_BYTE *)(v7 + (int)v8 - 1 + 32);
  if ( (int)v8 >= 1 )
  {
    v11 = (unsigned int)*(_QWORD *)(v7 + 24);
    if ( (unsigned int)*(_QWORD *)(v7 + 24) )
    {
      v9 = 0;
      v12 = 0LL;
      while ( v11 != v12 )
      {
        v9 += *(_BYTE *)(v7 + 32 + v12);
        if ( (v12 & 3) == 0 )
          v9 += *v10;
        if ( v11 == ++v12 )
          goto LABEL_10;
      }
LABEL_12:
      sub_2DA656C();
    }
  }
LABEL_10:
  if ( !(_DWORD)v8 )
    goto LABEL_12;
  return (unsigned __int8)*v10 * (unsigned int)v9 % 0x3E8 + 1000;
}

这里贴一段代码是计算偏移量的应该是(FeakHead)
还是没有找到下载的MasterData和第一个ab包

https://zonenova.future-step.net:12000/ServerConfig/GetConfig?appVersion=1.01&customAppIdentifier=EROLAB_ANDROID_RELEASE

https://asset.lecisxqw.com/amgameasset/Stg/AssetBundles/Android/9A188D7BF0BF119DE38B57A44DF3A917.ab
的解密逻辑(恼

这个偏移量计算函数在

void Pyro_AssetBundleManager_AssetBundlesLoader__LoadAssetBundleFlow_d__41__MoveNext(
        Pyro_AssetBundleManager_AssetBundlesLoader__LoadAssetBundleFlow_d__41_o *this,
        const MethodInfo *method)

被调用(使用1.0版本做的分析新版1.01懒得再重新分析了)

从代码中可以看到对文件名进行了md5加密而下载的文件命名也确实是md5作为文件名


图随便截的函数中一个获取MD5的地方这个函数足足有715行代码。。。应该是AssetBundle加载的核心

看到master-data.bytes这文件名就觉得是不是GitHub - Cysharp/MasterMemory: Source Generator based Embedded Typed Readonly In-Memory Document Database for .NET and Unity. (CySharp真王朝了吧)

看了下还真是, DummyDll里的数据表都继承MasterMemory.TableBase, 但是文件名全混淆了, 不过可以看annotation知道表名.

从MemoryDatabase.ctor反推, 看了下解密master-data.bytes的逻辑大概是这个样子, 需要下断点拿Salt, ghidra这里只能decompile到Field$<PrivateImplementationDetails>

ResourceManager$$FGDEMJDNGHM -> FEAPPKCFGPC$$BIHCOBDLBKP(input, key = "zVGyaQ6h9tLzqiD5HJQF")
ResourceManager$$FGDEMJDNGHM -> AOBPAOAMPDA (MemoryDatabase.ctor)

FileName = MD5.ComputeHash(String.Concat(int-based-filename, salt="Fikv88qzPb")) + ".ab"

MasterData = Load<TextAsset>("master-data", FileName)

FEAPPKCFGPC$$BIHCOBDLBKP:
      using (PasswordDeriveBytes pdb = new PasswordDeriveBytes("zVGyaQ6h9tLzqiD5HJQF", "下断点拿Salt")
      using (AesManaged aes = new AesManaged())
      {
            aes.Key = pdb.GetBytes((aes.KeySize + 7) / 8);
            aes.IV = pdb.GetBytes((aes.BlockSize + 7) / 8);

            using (ICryptoTransform encryptor = aes.CreateEncryptor())
            using (MemoryStream memoryStream = new MemoryStream())
            using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
            {
                  cryptoStream.Write(input, 0, input.max_length);
                  cryptoStream.Close();
                  return memoryStream.ToArray();
            }
      }
1 个赞

淦只能动态了找到了你说的这个函数,对生成密钥的那个函数交叉引用找不到其他函数了只能动态调试。。。不是专门搞安卓的寄。。。

我看这个PrivateImplementationDetails是可以直接从global-metadata.dat里拿的, 也就是下面的salt, 但是我这样解出来不是正常的msgpack, 估计哪里搞错了

private static byte[] Decrypt(byte[] input)
{
    byte[] salt = { 0x93, 0x83, 0x33, 0x72 };
    Array.Reverse(salt);

    using (PasswordDeriveBytes pdb = new PasswordDeriveBytes("zVGyaQ6h9tLzqiD5HJQF", salt))
    using (MemoryStream memoryStream = new MemoryStream())
    using (AesManaged aes = new())
    {
        
        aes.Key = pdb.GetBytes(aes.KeySize / 8);
        aes.IV = pdb.GetBytes(aes.BlockSize / 8);

        using (ICryptoTransform encryptor = aes.CreateEncryptor())
        using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
        {
            cryptoStream.Write(input, 0, input.Length);
            cryptoStream.Close();
            return memoryStream.ToArray();
        }
    }
}
 Key.length    : 32
 Mode          : CBC
 IV.length     : 16
 BlockSize     : 128
 FeedbackSize  : 128
 Padding       : PKCS7
 TransformMode : Decrypt
 Key  (hex)    : 2c 3a 1b d0 62 9a 26 9c f5 39 b3 5d 01 af 63 17 d5 b6 30 25 76 33 1a 1a d6 e7 af 64 14 bf c7 19
 IV   (hex)    : f5 39 b3 5d 01 af 63 17 c1 9d 9f 69 c7 f4 d1 fc

解密后

简单解析了下,看起来没啥大问题?

import msgpack
import json
import base64
import struct
import lz4.block
from typing import Dict, List, Any


INPUT_FILE = 'master-data.decrypted'
OUTPUT_FILE = 'master-data.json'

class MasterMemoryExtTypeHandler:
    @staticmethod
    def decode_ext_type(code: int, data: bytes) -> Any:
        if code == 99 and len(data) >= 5 and data[0] == 0xd2:
            # ExtType 99: 0xd2 + 4字节大小 + LZ4压缩数据
            uncompressed_size = struct.unpack('>I', data[1:5])[0]
            compressed_data = data[5:]

            try:
                decompressed = lz4.block.decompress(compressed_data, uncompressed_size=uncompressed_size)
                return msgpack.unpackb(decompressed, raw=False, strict_map_key=False)
            except Exception:
                pass

class MasterMemoryJSONEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, bytes):
            try:
                return obj.decode('utf-8')
            except UnicodeDecodeError:
                return base64.b64encode(obj).decode('ascii')
        elif isinstance(obj, msgpack.ExtType):
            return MasterMemoryExtTypeHandler.decode_ext_type(obj.code, obj.data)
        elif obj.__class__.__name__ == 'Timestamp':
            return {
                "__type__": "Timestamp",
                "__seconds__": getattr(obj, 'seconds', None),
                "__nanoseconds__": getattr(obj, 'nanoseconds', None)
            }
        return super().default(obj)

def extract_table_data(header: Dict[str, List[int]], raw_data: bytes) -> Dict[str, Any]:
    tables = {}
    
    for table_name, table_info in header.items():
        if not isinstance(table_info, list) or len(table_info) < 2:
            continue
            
        offset, length = table_info[0], table_info[1]
        
        try:
            table_data = raw_data[offset:offset + length]
            
            if len(table_data) == 0:
                tables[table_name] = {"objects_count": 0, "data": []}
                continue
            
            try:
                result = msgpack.unpackb(
                    table_data, 
                    raw=False, 
                    strict_map_key=False,
                    ext_hook=MasterMemoryExtTypeHandler.decode_ext_type
                )
                objects = result if isinstance(result, list) else [result]
            except Exception:
                unpacker = msgpack.Unpacker(
                    raw=False, 
                    strict_map_key=False,
                    ext_hook=MasterMemoryExtTypeHandler.decode_ext_type
                )
                unpacker.feed(table_data)
                objects = [obj for obj in unpacker]
            
            tables[table_name] = {
                "objects_count": len(objects),
                "data": objects
            }
            
        except Exception as e:
            tables[table_name] = {
                "error": str(e),
                "offset": offset,
                "length": length
            }
    
    return tables

def convert_master_memory_to_json():
    try:
        with open(INPUT_FILE, 'rb') as f:
            raw_data = f.read()
        print(f"文件读取成功,大小: {len(raw_data)} 字节")
    except FileNotFoundError:
        print(f"错误: 找不到文件 {INPUT_FILE}")
        return

    try:
        unpacker = msgpack.Unpacker(
            raw=False, 
            strict_map_key=False,
            ext_hook=MasterMemoryExtTypeHandler.decode_ext_type
        )
        unpacker.feed(raw_data)
        all_objects = [obj for obj in unpacker]
        print(f"解析成功!总共 {len(all_objects)} 个对象")
    except Exception as e:
        print(f"解析失败: {e}")
        return
    
    result = {"tables": {}, "compressed_data": {}}
    
    if all_objects and isinstance(all_objects[0], dict):
        header = all_objects[0]
        tables = extract_table_data(header, raw_data)
        result["tables"] = tables
        

    compressed_count = 0
    for i, obj in enumerate(all_objects[1:], 1):
        if isinstance(obj, (dict, list)) and not isinstance(obj, str):
            result["compressed_data"][f"data_{i}"] = obj
            compressed_count += 1
    
    if compressed_count > 0:
        print(f"提取了 {compressed_count} 个压缩数据块")
    
    try:
        with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
            json.dump(result, f, indent=2, ensure_ascii=False, cls=MasterMemoryJSONEncoder)
        print(f"✓ 转换完成: {OUTPUT_FILE}")
    except Exception as e:
        print(f"保存失败: {e}")

if __name__ == "__main__":
    convert_master_memory_to_json()
2 个赞

感谢大佬的解密
看了一眼应该是多语言的文本字幕
不过第一个ab文件还是无法解密
这个ab包在首次打开游戏未下载资源时会请求
https://asset.lecisxqw.com/amgameasset/Stg/AssetBundles/Android/9A188D7BF0BF119DE38B57A44DF3A917.ab
之后就会弹出

      [
        "DOWNLOAD_BUNDLE",
        "需要下載{0}MB更新資料",
        "Requires downloading {0}MB of update data",
        "{0}MBの更新データをダウンロードする必要があります",
        "需要下载{0}MB更新资料"
      ],

估计就是资源清单文件

1 个赞

请问一下AES decrypt部分怎么写的, 我发现key, iv, padding, mode都一样, 但是按照我上面的C#代码跑还原不出这个

AES有多种模式设置对了?

似乎。。。
你在加密

:joy:我去 还真是, 完全没发现

改完就行了

其实第一个bundle就是

MD5("Android" + "version.json").toUpper() -> "9A188D7BF0BF119DE38B57A44DF3A917"

然后花里胡哨的又是一段解密

def decrypt_version_json(content):
    output = [0] * len(content)
        
    for i in range(len(content)):
        v5 = i * 0x4104105 >> 0x20
        offset = (v5 + ((i - v5) >> 1) >> 5) * -0x3F
        
        output[i] = content[i] ^ i + offset

看着应该是索引了

decrypted_version.zip (16.6 KB)

然后单独文件的下载依旧是按照MD5("Android" + filename), 比如MD5("Androidprefab/characterspine/cg/3/4012") -> C53246A36360BC766381E4B9F196552B

3 个赞

2点还在整啊,强的 :joy:
不过也算是从资产到数据表都搞定了

感谢大佬们
看到是ab文件就只考虑是unity文件类型没往json方向想。。。一直在分析ab文件的加载逻辑 :clown_face:

能从那么多代码中看出来属实厉害,我看一会头都快晕了。

密碼一直錯誤 怎麼辦

大哥1.02了更新了两个版本怎么解包也写了我也写了自动资源下载工具

手动提取就好了很简单的
如果这都解决不了那你拿到资源也不会用不会播放