可以在DMM game player上下到桌面版
和宝石姬re和神杀是一个团队的作品,ab包加密也同样是重复文件头+隐藏版本号
目前还是事前但是据说可以下到大部分资源了
catalog.bin那里有注明,但是资源下载路径似乎应该是经过了SHA256加密
找到了很久以前大佬写的参考文献,但似乎加密方法与神杀不一样。能力止步于此了
希望各位大佬能抽空解惑资源路径的获取方法。附上链接
虽然从一些地方来看传的差不多了,但我还是建议不要过分传播官方未公开内容
import os
import requests
from pathlib import Path
from hashlib import sha256
# https://github.com/anosu/AddressablesToolsPy/tree/7a0c5fF
from AddressablesToolsPy.src.AddressablesTools import parse_binary
from AddressablesToolsPy.src.AddressablesTools.Catalog.SerializedObjectDecoder import SerializedObjectDecoder
from loguru import logger
# 以下两值由服务器api下发,可能会变
abHashkey = '4ee2a4fb96258c7a9f4b430d8b2715fe'
resUrl = 'prev0.5.0_1274f73d451349ff34074783a2572797'
assetUrl = f'https://cdn.app.siprj.com/{resUrl}/Addressables/Windows/'
catalogName = "catalog_0.5.0.bin"
savePath = Path('resources')
savePath.mkdir(exist_ok=True)
session = requests.session()
catalogData = session.get(f"{assetUrl}{catalogName}").content
def patcher(s):
if s == "GeePlus.GPUL.AddressablesManager; GeePlus.GPUL.AddressablesManager.ResourceProviders.EncryptedAssetBundleRequestOptions":
return SerializedObjectDecoder.ABRO_MATCHNAME
else:
return s
cl = parse_binary(catalogData,patcher=patcher)
for nameKey,infos in cl.Resources.items():
if isinstance(nameKey,str) and nameKey.endswith('.bundle'):
info = infos[0]
abhash128 = info.Data.Object.Hash
reshash = sha256(f"{nameKey}+{abhash128}+{abHashkey}".encode()).hexdigest()
saveLoc = savePath / reshash
if not os.path.exists(saveLoc) or info.Data.Object.BundleSize != os.path.getsize(saveLoc):
logger.info(f"downloading {nameKey}")
res = session.get(f"{assetUrl}{reshash}")
with open(saveLoc, 'wb') as f:
f.write(res.content)
想问下这游戏cg上的白码有办法去掉吗,不修改贴图的情况下。感觉像是游戏里动态加码用的。我对spine有点了解,但对live2d不是很熟(完全不熟)
您好,现在开服了,问问资源相关的地址hashkey和url有没有变化,谢谢
(看了一下catalog相关bin变化到1.0.2了,自己试了一下没试出来(参考楼下删掉的部分))
resurl直接游戏请求链接中拿就行,hashkey没变
游戏会 GET https://osapi.dmm.com/gadgets/makeRequest 开头的链接拿p.txt,这个包含个人信息应该,每次有更新抓包就行,一堆参数懒得整了,有需要的可以自己研究,具体的解析代码如下
"""
需要传入 https://osapi.dmm.com/gadgets/makeRequest 返回的 p.txt
"""
import json
import base64
import struct
import argparse
from io import BytesIO
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from typing import Any, List, Dict
key_b64 = "dxVAV9m1wG2YY/GUiPmSHA=="
iv_b64 = "A1C3PyrC3EqOFDie97WijQ=="
key = base64.b64decode(key_b64)
iv = base64.b64decode(iv_b64)
def decrypt_aes_cbc_pkcs7(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
return unpad(plaintext, AES.block_size)
class MiniMessageUnpacker:
def __init__(self, buf: bytes):
self.stream = BytesIO(buf)
def _read(self, n: int) -> bytes:
data = self.stream.read(n)
if len(data) != n:
raise EOFError("unexpected end of stream")
return data
def _u8(self) -> int:
return struct.unpack(">B", self._read(1))[0]
def _i8(self) -> int:
return struct.unpack(">b", self._read(1))[0]
def _u16(self) -> int:
return struct.unpack(">H", self._read(2))[0]
def _i16(self) -> int:
return struct.unpack(">h", self._read(2))[0]
def _u32(self) -> int:
return struct.unpack(">I", self._read(4))[0]
def _i32(self) -> int:
return struct.unpack(">i", self._read(4))[0]
def _u64(self) -> int:
return struct.unpack(">Q", self._read(8))[0]
def _i64(self) -> int:
return struct.unpack(">q", self._read(8))[0]
def _f32(self) -> float:
return struct.unpack(">f", self._read(4))[0]
def _f64(self) -> float:
return struct.unpack(">d", self._read(8))[0]
def unpack(self) -> Any:
code = self._u8()
if code <= 0x7F: # positive fixint
return code
if 0x80 <= code <= 0x8F: # fixmap
return self._unpack_map(code & 0x0F)
if 0x90 <= code <= 0x9F: # fixarray
return self._unpack_array(code & 0x0F)
if 0xA0 <= code <= 0xBF: # fixstr
return self._unpack_str(code & 0x1F)
if code >= 0xE0: # negative fixint
return struct.unpack("b", bytes([code]))[0]
if code == 0xC0: # nil
return None
if code == 0xC2: return False # false
if code == 0xC3: return True # true
if code == 0xCC: return self._u8() # uint8
if code == 0xCD: return self._u16() # uint16
if code == 0xCE: return self._u32() # uint32
if code == 0xCF: return self._u64() # uint64
if code == 0xD0: return self._i8() # int8
if code == 0xD1: return self._i16() # int16
if code == 0xD2: return self._i32() # int32
if code == 0xD3: return self._i64() # int64
if code == 0xCA: return self._f32() # float32
if code == 0xCB: return self._f64() # float64
if code == 0xD9: # str8
return self._unpack_str(self._u8())
if code == 0xDA: # str16
return self._unpack_str(self._u16())
if code == 0xDB: # str32
return self._unpack_str(self._u32())
if code == 0xC4: # bin8
return self._unpack_bin(self._u8())
if code == 0xC5: # bin16
return self._unpack_bin(self._u16())
if code == 0xC6: # bin32
return self._unpack_bin(self._u32())
if code == 0xDC: # array16
return self._unpack_array(self._u16())
if code == 0xDD: # array32
return self._unpack_array(self._u32())
if code == 0xDE: # map16
return self._unpack_map(self._u16())
if code == 0xDF: # map32
return self._unpack_map(self._u32())
raise ValueError(f"unsupported code 0x{code:02X}")
def _unpack_str(self, length: int) -> str:
return self._read(length).decode("utf-8", errors="replace")
def _unpack_bin(self, length: int) -> bytes:
return self._read(length)
def _unpack_array(self, length: int) -> List[Any]:
return [self.unpack() for _ in range(length)]
def _unpack_map(self, length: int) -> Dict[str, Any]:
result = {}
for _ in range(length):
k = self.unpack()
v = self.unpack()
result[k] = v
return result
def _to_json_compatible(obj):
if isinstance(obj, bytes):
return {"__bytes__": base64.b64encode(obj).decode()}
if isinstance(obj, dict):
return {k: _to_json_compatible(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_to_json_compatible(x) for x in obj]
return obj
def main():
ap = argparse.ArgumentParser()
ap.add_argument("file")
args = ap.parse_args()
raw_text = Path(args.file).read_text(encoding="utf-8", errors="ignore")
json_start = raw_text.find("{")
wrapper_json = raw_text[json_start:]
wrapper = json.loads(wrapper_json)
first_val = next(iter(wrapper.values()))
body_b64 = first_val["body"]
mp_bytes = base64.b64decode(body_b64)
obj_lvl1 = MiniMessageUnpacker(mp_bytes).unpack()
encrypted = base64.b64decode(obj_lvl1["p"])
decrypted = decrypt_aes_cbc_pkcs7(encrypted, key, iv)
final_cfg = MiniMessageUnpacker(decrypted).unpack()
with open("Config.json", "w", encoding="utf-8") as f:
json.dump(final_cfg, f, ensure_ascii=False, indent=4)
if __name__ == "__main__":
main()
经过b64decode → MiniMessageUnpacker → b64decode → aes_cbc → MiniMessageUnpacker后拿到配置文件,resUrl就在这里面(不知道这游戏为啥要搞这么麻烦)
记得先去除多余头再填
这么多层怎么分析出来的,求教
这些都在一个函数里面来着,我躺床了,明天我会把详细步骤发下面
下回来的文件,导出来的资源路径命名都乱七八糟的,要咋还原处理呀
可以看看有没有列表文件或逆向一下看看怎么读取资源的。
分析过程,排版随意排的将就看
抓包catalog链接看看
结尾是bin所以直接搜.bin,跳转过去
一路向上xref到这里,有个
Polaris::Environment::get_RemoteCatalogURL返回值是SourceAddressablesRemoteURL_k__BackingField + RuntimePlatformName + / + RemoteCatalog_k__BackingField拼成的,所以能知道抓包的catalog是这么拼的:
https://cdn.app.siprj.com/v1.0.2_43cd1ae68faf9e6b5a19b4c04d11d8f4/Addressables/ +
Android + / +
catalog_1.0.2.bin
再搜https://cdn.app.siprj.com,跳转过去
继续向上xref到
Polaris::Common::OnSuccessAppInit, 从res_version拿的值再往上xref到
Polaris::DMMNativeSdkHandler::_RequestNativeInit_b__15_0那么很明显了,是从DMM那里拿的
过一遍抓包内容后就只能找到从这里拿的
对着
APIResult *result xref globally, 轻松找到Polaris::NetManager::ParseEncryptResponse看代码,首先是用MiniMessagePacker::Unpack解包(传入前提前base64 decode过了问题不大)
messagePacker = v5->_messagePacker;
v12 = GeePlus::GPUL::Network::MiniMessagePacker::Unpack(messagePacker, v10, 0LL);
解包后如果有字段 p,再base64 decode然后解密
v31 = System::Convert::FromBase64String(v30, 0LL);
v32 = GeePlus::GPUL::Network::EncryptUtil::Decrypt(
v5->_CryptKey_k__BackingField,
v5->_CryptIV_k__BackingField,
v31,
0LL);
然后再来一次MiniMessagePacker::Unpack就完事了
v34 = GeePlus::GPUL::Network::MiniMessagePacker::Unpack(v33, v32, 0LL);
剩下的找key,iv没啥好说的
原始路径要根据那个catalog.bin里记载的路径信息去还原
其实不用还原他的那个MiniMessagepacker实现,用标准的msgpack也能解
感谢分析过程,我回去试试。
hh,主要是github上正好有所以直接拿过来用了
我可以得到“catalog.bin”,但我可以解密它并检查资源吗?如果可以的话我想知道方法
我读了帖子,但我想了解更多











