PokerFate 解包

PokerFate 解包(精简版)

声明:本文仅用于个人技术学习与资料归档,请勿用于任何商业用途或二次分发原作素材,请支持正版。所涉解密/转换脚本只处理你本人合法获取的本地文件。

引擎 Unity 2022.3(IL2CPP),bundle 外层加密。能拿 Spine 动效 / 立绘 / CG / 视频。 环境:pip install UnityPy lz4 Pillow,7-Zip,Spine 查看器。

适用版本(务必核对)

  • 游戏:PokerFate(QOO / 日服 Android APK,解包时间 2026-10)
  • 引擎:Unity 2022.3.62f3(IL2CPP)
  • Spine 骨骼二进制版本:4.1.24
  • 外层加密常数:S=0xAF3E40196CB07413、C=0x61C8864680B583EB(从该版本 GameAssembly.dll 逆出)
  • 规模参考:529 个 .bundle、约 172,517 个对象、180 套 Spine

:warning: 版本不符 = 解密会失败。

:warning: 仅供个人技术学习与资料归档,请勿商用或二次分发原作素材,请支持正版。所有脚本只处理你本人合法获取的本地文件。

能拿到:Spine 三件套(skel/atlas/png,可加载)、立绘/CG 贴图、动态表现视频。 环境:Python 3.10+,pip install UnityPy==1.25.3 lz4 Pillow;7-Zip;Spine 查看器(SpineViewer / 2DWaifuLooker,需支持 4.1)。

总流程:解APK → 解密 → 导出Spine → 导出视频/贴图 → 转atlas → 查看(可选皮肤合并)。

文件来源扑克命运


1. 解密 XENC

assets 下的 bundle 开头是 XENC 而不是 UnityFS,AssetStudio / UnityPy 打不开。容器结构:XENC(4B) + nonce(16B) + 密文。keystream 每 64 字节重新派生:seed 走 splitmix64,块常数用减法,xorshift(13,7,17) 逐字节取低位,明文 = 密文 XOR keystream。

存成 decrypt.py,python decrypt.py ./bundle ./plain:

import struct, sys, os, glob
MASK = (1 << 64) - 1
S = 0xAF3E40196CB07413
C = 0x61C8864680B583EB

def mix(z):
    z &= MASK
    z ^= z >> 30; z = (z * 0xBF58476D1CE4E5B9) & MASK
    z ^= z >> 27; z = (z * 0x94D049BB133111EB) & MASK
    return z

def keystream(nonce, ln):
    n64 = struct.unpack_from('<Q', nonce, 0)[0] ^ struct.unpack_from('<Q', nonce, 8)[0]
    m = mix(n64)
    seed = (m ^ (m >> 31) ^ S) & MASK
    ks = bytearray(); b = 0
    while len(ks) < ln:
        state = (seed - b * C) & MASK
        for _ in range(64):
            state ^= (state << 13) & MASK
            state ^= state >> 7
            state ^= (state << 17) & MASK
            ks.append(state & 0xFF)
        b += 1
    return bytes(ks[:ln])

def dec(src, dst):
    d = open(src, 'rb').read()
    if d[:4] != b'XENC':
        open(dst, 'wb').write(d); return
    open(dst, 'wb').write(bytes(a ^ b for a, b in zip(d[20:], keystream(d[4:20], len(d) - 20))))

src, dst = sys.argv[1], sys.argv[2]
os.makedirs(dst, exist_ok=True)
fs = [src] if os.path.isfile(src) else glob.glob(os.path.join(src, '**', '*.bundle'), recursive=True)
for f in fs: dec(f, os.path.join(dst, os.path.basename(f)))
print("decrypted", len(fs))

跑完看 ./plain 里任一文件开头是不是 UnityFS:是就成了;还是乱码,基本是你包版本和我这份不同、常数对不上,得自己逆 AesCryptoUtil.GenKeyBlock / Mix(IL2CPP 里文件偏移 = RVA − 0x1000)。

2. atlas 格式转换

本作 atlas 是自定义写法,标准 Spine 不认,直接打开会缺胳膊少腿:

部件名 bounds:x,y,w,h offsets:ox,oy,origW,origH rotate:90

要转成标准的 xy / size / orig / offset / index。存成 convert_atlas.py,python convert_atlas.py ./out/spine:

import os, re, sys
B = re.compile(r"^bounds:\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(\d+)\s*,\s*(\d+)$", re.I)
O = re.compile(r"^offsets:\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(\d+)\s*,\s*(\d+)$", re.I)
R = re.compile(r"^rotate:\s*(\d+)$", re.I)
H = ("size", "format", "filter", "repeat", "pma", "scale")

def conv(t):
    ls = t.splitlines(); out = []; i = 0
    while i < len(ls):
        s = ls[i].strip()
        if s.lower().endswith(".png") and not s.lower().startswith(H) \
           and not s.lower().startswith(("bounds", "offsets", "rotate", "index")):
            size = None; ex = {}; k = i + 1
            while k < len(ls):
                x = ls[k].strip()
                if not x: k += 1; continue
                m = re.match(r"^([A-Za-z]+)\s*:\s*(.*)$", x)
                if not m or m.group(1).lower() not in H: break
                if m.group(1).lower() == "size": size = m.group(2).strip()
                else: ex[m.group(1).lower()] = m.group(2).strip()
                k += 1
            out.append(s)
            if size: out.append("size:" + size)
            out += ["format:RGBA8888", "filter:" + ex.get("filter", "Linear,Linear"), "repeat:" + ex.get("repeat", "none")]
            if "pma" in ex: out.append("pma:" + ex["pma"])
            if "scale" in ex: out.append("scale:" + ex["scale"])
            i = k; continue
        mb = B.match(s)
        if mb:
            x, y, w, h = (int(mb.group(j)) for j in (1, 2, 3, 4))
            offs = None; rot = 0; k = i + 1
            while k < len(ls):
                xx = ls[k].strip(); mo = O.match(xx); mr = R.match(xx)
                if mo: offs = tuple(int(mo.group(j)) for j in (1, 2, 3, 4)); k += 1; continue
                if mr: rot = int(mr.group(1)); k += 1; continue
                break
            if rot: out.append("rotate:%d" % rot)
            out += ["xy:%d,%d" % (x, y), "size:%d,%d" % (w, h)]
            ox, oy, ow, oh = offs if offs else (0, 0, w, h)
            out += ["orig:%d,%d" % (ow, oh), "offset:%d,%d" % (ox, oy), "index:-1"]
            i = k; continue
        out.append(ls[i]); i += 1
    return "\n".join(out) + "\n"

for dp, _, fns in os.walk(sys.argv[1] if len(sys.argv) > 1 else "./out/spine"):
    for fn in fns:
        if fn.lower().endswith(".atlas"):
            p = os.path.join(dp, fn)
            open(p, "w", encoding="utf-8", newline="\n").write(conv(open(p, encoding="utf-8-sig").read()))


常见坑对照

现象 原因 解决
解密后开头不是 UnityFS 版本不符,常数变了 重新逆向拿常数,或找同版本包
bundle 打不开 没解密(开头是 XENC) 跑第 2 步
skel/atlas 是乱码 用了 get_raw_data() 改用 m_Script
打开缺胳膊少腿/错位 atlas 没转 / 转时交换了 size / 抹了 180 按第 4 步两条规则重转
模型打不开、找不到页图 平铺导出导致同名覆盖/页名对不上 用第 3 步引用链版,每模型独立目录
打开是素体没脸 部件在别的 skin 勾皮肤或合并进 default
视频/立绘取不到 流式纹理/资源 按 offset+size 从 .resource 切

附录:加密原理(可跳过)

外层是"每 64 字节重派生密钥流"的 XOR 流加密:

  • seed = Mix(nonce64) ^ (Mix(nonce64) >> 31) ^ S,Mix 为 splitmix64 两步乘法,nonce64 = LE64(nonce[0:8]) ^ LE64(nonce[8:16])。
  • 每块 state = seed - 块号*C(减),跑 xorshift(13,7,17),每字节取低位作密钥流;明文 = 密文 XOR 密钥流。
  • 常数来自反汇编 GameAssembly.dll 的 AesCryptoUtil.GenKeyBlock/Mix + GF(2) 线性求解。

验证教训:解密"对不对"必须校验整个明文结构(能出 UnityFS 头 + 块信息能解压 + 头里 size 字段 == 真实 payload 长度)。只校验前 8 字节会被多个"假解"骗到(xorshift 对 seed 是 GF(2) 仿射,8 字节约束秩只有 57/64,有上百个候选常数都满足)。

欣赏

因为是让ai解压的,所以本人不是很懂,如果你有问题我会尽力解决的,如果打扰到各位大佬我道歉。感谢论坛里各位大佬的帖子对我的帮助。

2 个赞

这游戏的资源包什么时候加密的?:thinking: 最近吗?

先申明这是ai回的,我这份是 2026-10 的安卓包,assets 下 529 个 bundle 头 4 字节全是 XENC(不是 UnityFS),AssetStudio / UnityPy 直接打不开,得先拿 nonce 派生 keystream 异或还原成 UnityFS 才能喂给现成工具。至于哪个版本开始加的、是不是最近——我没翻更新日志,不确定,不乱说。你可以 hexdump 一下你手上包的开头:是 XENC 就是加了,是 UnityFS 那就是你那份没加密(版本/渠道不同)。

另外怎么说呢我其实发这个帖子主要就是想解这个游戏,但是失败了好多次,所以想发一下。我其实不会解包,也不懂,我只是想欣赏游戏文件,而不是玩个游戏去充钱抽卡去被恶心。如果因为ai脏了你的眼我道歉。

另外我也很感谢大佬发布的资源,我也下了,感谢大佬。

没什么纯好奇问问,因为早几个月每个月都会处理steam端的,一直都没有加密我记得。我看看steam端是不是也更新了加密来。