Wdream

Personal Website

The Sky's The Limit.


逆向

逆向工具

引路人

unity引擎基于Windows下的il2cpp逆向初探——以CTF赛题为例-先知社区

[原创]IL2CPP 逆向初探-软件逆向-看雪安全社区|专业技术交流与安全研究论坛

dnspy

反编译C#相关程序,反编译修改后需要保存模块以应用到目标程序。

获取so metadata

Il2cpp逆向:global-metadata解密-腾讯云开发者社区-腾讯云

Unity

打开APK安装包,自己找,只要是unity游戏一定会有,不过有可能被加密或者隐藏了。

  1. libil2cpp.so 所有的C#方法被编译成了C++
  2. global-metadata.dat 结构化的数据库,存放了定义

MT管理器左侧功能,提取套件(Extract APK)

image-20260207140719098

image-20260207140736492

image-20260207141909677

Cocos

  1. libcocos2dlua.so

没有元数据,可以直接导入IDA

Zygisk-Il2CppDumper

如果不想给so或者metadata脱壳,可以通过动态获取。

image-20260207142302987

比如这里的metadata就是套壳了。

IDA

下载

需要自行配备python3.8以上环境,IDA安装时会自动检测。

磁力链接下载IDA9.1

magnet:?xt=urn:btih:f24cfadb8a66b343bf1ff4f0c1386a5f6991c818&dn=ida91

激活方式,生成License

ThatNotEasy/IDA-Patcher: IDA Pro Patcher & License Generator

main第28行写错了,修改为

end_year = args.end_date or (now.year + 10)

执行命令参数为安装目录,也就是ida.exe的根目录

python main.py -p "D:\Tools\IDA"

成功激活后如下

image-20251111230515010

使用

F5反编译

G跳转地址

右键查看XREF引用

左侧函数列表Ctrl F 搜索,右侧文本搜索。

按住Ctrl点击文本视图函数,会新建视窗栏目,而不是覆盖上一个。

插件

  1. 可以配合il2Cppdumper使用

    对于global-metadata.dat和libil2cpp.so都正常的情况:会生成以下这些文件

    image-20260114214423024

    如果你没有指定目录,那么就会这些生成在Il2CppDumper安装目录

    在IDA中选择file > script file... 选中Il2CppDumper安装目录下的ida_with_struct_py3.py

    然后会继续弹框让你选择文件,依次选择script.json > il2cpp.h

    之后就等待IDA运行脚本了。

  2. IDA 9.0 安装Findcrypt插件踩坑分享 - 吾爱破解 - 52pojie.cn

  3. IDA Pro 9.2 python环境切换、插件配置和常用快捷键 - zhubayi - 博客园

Zygisk-Il2CppDumper

下载Magisk工具Release Magisk (f9f02c65) (30403) · ayasa520/Magisk

【安卓模拟器安装 Magisk30.4】 https://www.bilibili.com/video/BV1PYWHzCEjE/?share_source=copy_web&vd_source=05320767bb904def1ce7c1e88823462a

获取游戏包名称,以mumu模拟器为例,在这个路径的文件夹名称就是包名。

image-20251112114512032

Fork本体violet-wdream/Zygisk-Il2CppDumper: Using Zygisk to dump il2cpp data at runtime,不需要下载,然后在你Fork的分支actions里面输入包名,构建文件。

原仓库使用的actions太久了需要改成v4版本,修改了build.yml

- uses: actions/upload-artifact@v4

Workflow runs · violet-wdream/Zygisk-Il2CppDumper 这个是我改过的。

image-20251112114757145

输入包名jp.glee.girl构建结果模块

然后你点击这个Build,进去下载这个构建好的工件,下载后是一个压缩包(模块)。image-20260207143753003

安装这个构建的模块。

image-20251112115650744

启动游戏,会在/data/user/0/GamePackageName/files/或者/data/data/0/GamePackageName/files/目录下生成dump.cs,要是没有就重启模拟器再试一下。

image-20251112115318123

Il2CppDumper

和Zygisk-Il2CppDumper二选一即可。Perfare/Il2CppDumper: Unity il2cpp reverse engineer

使用方法Il2CppDumper/README.zh-CN.md at master · Perfare/Il2CppDumper

这里的放置少女的global-metadata.dat已被加密,所以不适用。

对于global-metadata.dat和libil2cpp.so都正常的情况:会生成以下这些文件

image-20260114214423024

如果你没有指定目录,那么就会这些生成在Il2CppDumper安装目录

在IDA中选择file > script file... 选中Il2CppDumper安装目录下的ida_with_struct_py3.py

然后会继续弹框让你选择文件,依次选择script.json > il2cpp.h

之后就等待IDA运行脚本了。

ADB

adb connect 127.0.0.1:5555
adb connect 127.0.0.1:16384
adb devices
adb root
adb shell

exit
#查找package,不建议自己查,不如MT管理器简单方便
pm path com.fknzj.qooapp
ls -l /data/data/com.fknzj.qooapp

Frida

#server - emulator
adb connect 127.0.0.1:16384
adb root
adb shell
cd /data/local/tmp
./frida
#client - PC
adb forward tcp:27042 tcp:27042
#conda activate py314
frida-ps -U
frida -U -f "jp.glee.girl" -l hook.js #重启注入
frida -U -n jp.glee.girl -l hook.js #附加注入

模拟器frida服务

APP逆向第五课: Frida安装&基础HOOK⽅法_哔哩哔哩_bilibili

MuMu模拟器开发者须知_MuMu模拟器_安卓模拟器

模拟器adb shell连接

查看模拟器adb端口

image-20251113135216351

找到adb路径D:\Games\Mumu\MuMuPlayer\nx_device\12.0\shell,getprop查询CPU架构为x86_64

adb.exe connect 127.0.0.1:5555
adb.exe shell
getprop ro.product.cpu.abi

image-20251113135346077

找到对应版本下载。https://github.com/frida/frida/releases/download/16.1.0/frida-server-16.1.0-android-x86_64.xz

解压xz文件后得到一个无拓展名文件。重新命名为frida

传入到模拟器中。这里使用mumu共享文件夹传输文件,然后在共享文件夹再次把文件移动到本机根目录

或者使用push指令一步到位

adb push ...\frida /data/local/tmp 

image-20251113142748911

打开root explorer或者任意root管理器,找到frida文件

image-20251113142910547

再次移动frida,移动到tmp目录,因为其他目录下不能修改这个文件的权限。

image-20251113143136570

修改文件权限为777,全部勾选即可。

image-20251113143206934

接下来cd进这个目录,启动frida

cd /data/local/tmp;
./frida

然后这里会显示权限不够。

`Cetus:/data/local/tmp $ ./frida
Unable to save SELinux policy to the kernel: Permission denied`

需要提升权限root,输入exit返回

exit
adb root
adb shell
cd /data/local/tmp
./frida

然后如果命令行一动不动,说明模拟器frida服务成功开启了

image-20251113145652991

物理机连接frida服务

接下来用物理机连接模拟器的frida服务,新建一个终端,还是在D:\Games\Mumu\MuMuPlayer\nx_device\12.0\shell这个路径下,进行端口转发。

adb forward tcp:27042 tcp:27042 

需要python环境,物理机下载fridafrida-tools,参照这个表找到对应版本thelicato/frida-compatibility-matrix: Compatible versions of the frida package for each version of the frida-tools package. Automatically updated through CI/CD

因为我们下载的是16.1.0版本,所以tools下载12.3.0,如果版本下载太高了就无法识别模拟器的frida服务。

pip uninstall frida-tools frida -y #如果下载错了需要先卸载frida和依赖
pip install frida==16.1.0 frida-tools==12.3.0

两个包都需要指定版本,不然会自动下载默认版本,可能会出问题。

如果一切顺利,查询模拟器进程,可以查到PID和对应进程。

frida-ps -U

image-20251113153704169

hook

模拟器frida服务容易掉线,如果出现了问题可以检查一下是不是frida服务掉线了。

最后一个参数是包名,获取方式在Zygisk-Il2CppDumper这个章节,这里是目标游戏放置少女的包名。

在物理机下输入以下指令,会自动启动,同时附加到进程。

frida -U -f "jp.glee.girl"

image-20251113154533916

这里就完成了Frida功能的初步验证。

Frida-il2cpp-bridge

当前使用很多很多问题,难以解决。

git clone https://github.com/oleavr/frida-agent-example.git
cd frida-agent-example/
npm install -g typescript
npm install
npm install --save-dev frida-il2cpp-bridge

index.ts

import "frida-il2cpp-bridge";
console.log("Rebuilded")

Il2Cpp.perform(() => {
	console.log(Il2Cpp.unityVersion);  
});

测试使用

npm run watch
frida -Uf jp.glee.girl -l _agent.js

更多用法Snippets · vfsfitvnm/frida-il2cpp-bridge Wiki

CE

模拟器链接CE如何在MuMu模拟器上使用CheatEngine_MuMu模拟器_安卓模拟器

Unity 可以参考哥特少女勇闯恶魔城2 Sinisistar2

加密算法

UnityCN

加密:AssetBundle.SetAssetBundleEncryptKey();
解密:AssetBundle.SetAssetBundleDecryptKey();

雷索纳斯

搜索SetAssetBundleDecryptKey

image-20260129193440305

image-20260129195036875

image-20260129214644106

ResonanceRebornR String -> Hex

十六进制转字符串,文本转16进制-ME2在线工具

得到5265736F6E616E63655265626F726E52

image-20260129184109816

黑色信标 RC4

下面找一下key是怎么来的。

搜索SetAssetBundleDecryptKey

查看引用发现GameAOT_AOT$$Awake

搜索GameAOT_AOT$$Awake发现Game_GenerateData$$GenerateSetAssetBundleDecryptKey提供了参数。

image-20260201215248183

查看Game_GenerateData$$Generate定义,发现一个可疑的string

image-20260201215341782

得到原始key,后续得知这个其实是RC4的seed

`?dZqfdo|&pM@Js^

但还不是真正的结果,发现还有一层处理

image-20260201220642925

System_Byte_array *Game_Helper__Encrypt(System_Byte_array *pwd, System_Byte_array *data, const MethodInfo *method)
{
  __int64 v5; // r15
  __int64 v6; // rdx
  __int64 v7; // rax
  __int64 v8; // rdx
  __int64 v9; // r13
  __int64 v10; // rax
  il2cpp_array_size_t max_length; // rcx
  __int64 v12; // r8
  __int64 max_length_low; // rdi
  unsigned __int64 i; // rsi
  __int64 v15; // rdx
  unsigned __int64 v16; // r9
  unsigned __int64 v17; // rax
  int v18; // ebx
  int v19; // ebx
  unsigned int v20; // r9d
  int v21; // r10d
  unsigned __int64 v22; // r11
  int v23; // ecx

  if ( !byte_4E3A8A8 )
  {
    sub_D96BF5(&byte___TypeInfo, data);
    sub_D96BF5(&int___TypeInfo, data);
    byte_4E3A8A8 = 1;
  }
  v5 = sub_D96C5F(int___TypeInfo, 256, method);
  v7 = sub_D96C5F(int___TypeInfo, 256, v6);
  if ( !data || (v9 = v7, v10 = sub_D96C5F(byte___TypeInfo, LODWORD(data->max_length), v8), !pwd) )
LABEL_29:
    sub_D96D1C();
  v12 = v10;
  max_length_low = LODWORD(pwd->max_length);
  for ( i = 0; i != 256; ++i )
  {
    v15 = (unsigned int)((int)i >> 31);
    LODWORD(v15) = (int)i % (int)max_length_low;
    if ( (int)i % (int)max_length_low >= (unsigned int)max_length_low )
      goto LABEL_27;
    if ( !v5 )
      goto LABEL_29;
    v16 = *(unsigned int *)(v5 + 24);
    if ( i >= v16 )
      goto LABEL_27;
    *(_DWORD *)(v5 + 4 * i + 32) = pwd->m_Items[(int)v15];
    if ( !v9 )
      goto LABEL_29;
    v17 = *(unsigned int *)(v9 + 24);
    if ( i >= v17 )
      goto LABEL_27;
    *(_DWORD *)(v9 + 4 * i + 32) = i;
  }
  v15 = 0;
  v18 = 0;
  do
  {
    if ( (unsigned int)v15 >= (unsigned int)v17 )
      goto LABEL_27;
    if ( (unsigned int)v15 >= (unsigned int)v16 )
      goto LABEL_27;
    max_length_low = *(unsigned int *)(v9 + 4 * v15 + 32);
    v19 = *(_DWORD *)(v5 + 4 * v15 + 32) + max_length_low + v18;
    i = (v19 + ((unsigned int)(v19 >> 31) >> 24)) & 0xFFFFFF00;
    v18 = v19 % 256;
    if ( v18 >= (unsigned int)v17 )
      goto LABEL_27;
    i = v18;
    max_length = *(unsigned int *)(v9 + 4LL * v18 + 32);
    *(_DWORD *)(v9 + 4 * v15 + 32) = max_length;
    *(_DWORD *)(v9 + 4LL * v18 + 32) = max_length_low;
    ++v15;
  }
  while ( (_DWORD)v15 != 256 );
  max_length = data->max_length;
  if ( (int)max_length > 0 )
  {
    if ( (unsigned int)v17 >= 2 )
    {
      v20 = data->max_length;
      LODWORD(v15) = 0;
      v21 = 1;
      v22 = 0;
      do
      {
        max_length_low = v21;
        max_length = *(unsigned int *)(v9 + 4LL * v21 + 32);
        v15 = (unsigned int)(((int)max_length + (int)v15) % 256);
        if ( (unsigned int)v15 >= (unsigned int)v17 )
          break;
        *(_DWORD *)(v9 + 4LL * v21 + 32) = *(_DWORD *)(v9 + 4LL * (int)v15 + 32);
        *(_DWORD *)(v9 + 4LL * (int)v15 + 32) = max_length;
        v23 = *(_DWORD *)(v9 + 4LL * v21 + 32) + max_length;
        i = (v23 + ((unsigned int)(v23 >> 31) >> 24)) & 0xFFFFFF00;
        max_length = (unsigned int)(v23 % 256);
        if ( (unsigned int)max_length >= (unsigned int)v17 || v22 >= v20 )
          break;
        if ( !v12 )
          goto LABEL_29;
        if ( v22 >= *(unsigned int *)(v12 + 24) )
          break;
        *(_BYTE *)(v12 + v22 + 32) = data->m_Items[v22] ^ *(_DWORD *)(v9 + 4LL * (int)max_length + 32);
        ++v22;
        v20 = data->max_length;
        if ( (__int64)v22 >= (int)v20 )
          return (System_Byte_array *)v12;
        LODWORD(v17) = *(_DWORD *)(v9 + 24);
        i = (v21 + ((unsigned int)((v21 + 1) >> 31) >> 24) + 1) & 0xFFFFFF00;
        max_length = (unsigned int)(v21 + 1 - i);
        v21 = max_length;
      }
      while ( (unsigned int)max_length < (unsigned int)v17 );
    }
LABEL_27:
    sub_D96D22(max_length_low, i, v15, max_length, v12);
  }
  return (System_Byte_array *)v12;
}

反正就是 RC4 的 KSA + PRGA,有点复杂,可以找现成的python复现或者找相关工具实现。

def rc4(key, data):
    S = list(range(256))
    j = 0
    out = []

    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) & 0xFF
        S[i], S[j] = S[j], S[i]

    i = j = 0
    for b in data:
        i = (i + 1) & 0xFF
        j = (j + S[i]) & 0xFF
        S[i], S[j] = S[j], S[i]
        out.append(b ^ S[(S[i] + S[j]) & 0xFF])

    return bytes(out)

完整流程

public static byte[] Generate()
{
    // 1. ASCII 编码器
    var ascii = new System.Text.ASCIIEncoding();

    // 2. 固定明文字符串
    string plain = "`?dZqfdo|&pM@Js^";

    // 3. 转成 ASCII 字节
    byte[] data = ascii.GetBytes(plain);

    // 4. 使用静态密码进行加密
    // pwd 存在于 Game.GenerateData  static fields
    byte[] encrypted = Game.Helper.Encrypt(pwd, data);

    // 5. 再次用 ASCIIEncoding  byte[] 转回 byte[]
    return ascii.GetBytes(encrypted);
}

这里还需要RC4的key,然后通过这个算法进一步处理

为了获取RC4的key,还需要查看上一级调用的函数的构造函数,也就是Game_Helper__Encrypt的调用者Game_GenerateData$$Generate的构造函数cctor。

查找Game_GenerateData相关函数,找到Game_GenerateData$$_cctor

可以看到这里创建一个256字节的数组,明显是RC4相关的数据。接着应该是使用常量Field_eval_p_eval_b来初始化。

image-20260201223818094

跳转到定义

image-20260201223949229

到这里还是有点迷惑,并没有给出实质性的数据,只有System_RuntimeFieldHandle_o <<800002F1h>>这个信息,gpt说这个800002F1h是一个token,需要查global-metadata.dat得出结果。

Token → metadata → default value

但是查metadata是需要偏移量的

接下来查找script.json,搜索eval_p.eval_b得到

{
    "Address": 81565432,
    "Name": "Field$eval_p.eval_b",
    "Signature": null
}

这里只给一个RVA地址,没什么用。

接下来查找dump.cs相关定义,成功找到偏移量

[CompilerGenerated]
internal sealed class eval_p // TypeDefIndex: 12761
{
	// Fields
	internal static readonly eval_p.eval_a eval_a /*Metadata offset 0x65D840*/; // 0x0
	internal static readonly eval_p.eval_a eval_b /*Metadata offset 0x65D948*/; // 0x100
}

查metadata导出数据

dat = r"global-metadata.dat"
with open(dat, "rb") as f:
    f.seek(0x65D948)
    eval_b = f.read(0x100)
print(eval_b.hex())

得到RC4的key为eval_b

eval_b = bytes.fromhex( "ca2c93b9b371dc2b9d91303bd6fc9836c246a8f4970481f2c6abd4635f996fb324eb4f62a450d693237315468e3fbc9e4f80c41a412b422690db81fac7c5dd24e3ac74989a1823d3b76ceccfc8033c52e0fc2066c2e6f37634de8cebc2686ab6c184f9b2d253673aacc519ee7da24702a378c7f40ca48e39f824ed91783e1aa15ed3018df351f11f3c1226440b42b566abf6f066769ab4c70c5ec2d07fd097204143d290082db6e0882bc5f838cb0740f4c2f6e5c2c3af86838e5317cf6d3678d9c1dea986c554e69b33d2bc35f0625b63715f261be571e05ef80d56ce2bf2f88187bd4f4cc310a167924ec7982e9786c3ddd5610f8d9249812b860d46e86ff5"
)

再次回到此前的流程

public static byte[] Generate()
{
    // 固定明文string
    string plain = "`?dZqfdo|&pM@Js^";

    // ASCII Encode: string(UTF16) -> Bytes
    byte[] data = ascii.GetBytes(plain);

    // 使用静态密码进行RC4加密
    byte[] encrypted = Game.Helper.Encrypt(pwd, data);

    // 再次用 ASCIIEncoding  byte[] 转回 byte[]
    return ascii.GetBytes(encrypted);
}

所以最终结果

seed = b"`?dZqfdo|&pM@Js^" #string -> bytes
encrypted = rc4(eval_b, seed)
final_key = encrypted.decode("ascii", errors="replace")
print(encrypted.hex())
print(final_key)
print(final_key.encode("ascii", errors="replace").hex())

输出得到

encrypted = 5f6c4efe3ae2238d9ab28666a81acbe8
final_key = _lN:�#����f���
UnityCN key = 5f6c4e3f3a3f233f3f3f3f663f1a3f3f //跟表格里的数据一致。

但是为什么先解码再编码后结果出现了一些差异?

5f
6c
4e
fe #3f
3a
e2 #3f
23
8d #3f
9a #3f
b2 #3f
86 #3f
66
a8 #3f
1a
cb #3f
e8 #3f

发现有很多个字节应该是3f,但是却不对。其实final_key = _lN�:�#����f��� 这里出现的问号已经解释了原因:一些字节无法进行ASCII解码:

  1. 0x00–0x7F:合法,原样保留
  2. ≥ 0x80:无法表示 → 替换为 ?(0x3F)

然后再次ASCII编码的时候,这些原先的非法字节已经都被替换了,原先合法的字节就原封不动了。

这也就解释了最后一步到底在干嘛。

return ascii.GetBytes(encrypted);
先看下这个方法的原型[ASCIIEncoding.GetBytes 方法 (System.Text) Microsoft Learn](https://learn.microsoft.com/zh-cn/dotnet/api/system.text.asciiencoding.getbytes?view=net-8.0)

ASCIIEncoding.GetBytes接受的参数只能是字符数组/字符串,这里输入的encryptedbyte[],应该是不合法的操作,但是为什么会发生呢?应该需要先进行数据类型转换。

其实这段是gpt反编译的,IDA反编译这一段失败了。

image-20260202201906554

但是大致可以推测是因为il2cpp编译的时候优化了这个部分。从汇编的角度来看就只是操作寄存器了,也就是直接移动内存数据进行计算。

实际上应该是这样

string tmp = ascii.GetString(encrypted);
return ascii.GetBytes(tmp);

也就是byte[] -> string -> byte[]的一个过程。这里的两个映射规则遵从ASCII的解码/编码。

通俗地讲就是把目标数据进行了一次ASCII洗筛,把不能够通过ASCII解码的数据清洗为合法数据,然后复原。

总结

总结一下,大概就是

  1. 明文string 作为RC4的 seed
  2. metadata中取出一个约定的256b数据eval_b作为key
  3. 计算密文encrypted = rc4(eval_b, seed)
  4. 对密文进行ASCII洗筛得到 UnityCN的key

花亦山心之月

SetABEncryption <- SetAssetBundleDecryptKey <- SetAssetBundleKey

image-20260212235856117

image-20260213000034219

dump.cs

image-20260213000114713

Unity内部的虚拟路径。

image-20260213000216478

点击mono后需要导入DummyDLL还原数据

image

image-20260213000345862

string EncryptKey = "INHJnhdypqk547xd"

string -> hex

494e484a6e68647970716b3534377864

###

Cocos

XXTEA

libcocos2djs.so搜索Cocos Game 后面跟着的第一个串(如果有)就是keywf-game-card

AppDelegate::applicationDidFinishLaunching(void)
cocos2d::Image::initWithImageData
FileUtils::fullPathForFilename

幻想名将录 - astc

key

这里得到的key并不重要,因为后续也用不到,只是用来反编译jsc使用。如果只需要获取spine,这里可以跳过

参考

关于Cocos2dx-js游戏的jsc文件解密(二) - 吾爱破解 - 52pojie.cn

群英风华录解密有教的吗 - 讨论 - Live2DHub

关于Cocos2dx打包游戏的jsc文件解密(一) - 知乎

cocos引擎,xxtea加密

libcocos2djs.so搜索Cocos Game 后面跟着的第一个串就是keywf-game-card

77 66 2D 67 61 6D 65 2D 63 61 72 64 00 00 00 00 #自动填充到16 bytes
wf-game-card

image-20260117172729244

或者IDA里面搜索xxtea相关的函数也能找到。

验证了论坛里的一个说法applicationDidFinishLaunching,大多都是在这里可以找到xxtea的key

AppDelegate::applicationDidFinishLaunching(void)

image-20260117173020279

还原astc为png

从散落的文件里看到了atlas文件,所以是spine无疑了。然后bin文件就是二进制skel骨骼,直接改为skel后缀就能用了。

绝大部分文件都是astc,压缩且加密,没法直接转换,解压astc发现文件头变成了beeplay,说明第一步是解压,然后再去掉beeplay进行XOR解密

逆向libcocos2djs.so搜索astc以及image相关函数

cocos2d::Image::initWithImageData

发现检测文件头7bytes,就是检测beeplay签名

if ( !memcmp(cocos2d::Image::initWithImageData(unsigned char const*,long)::ENCRYPT_SIGNATURE, p[0], 7u) )
{
    v8 = v4 - 7;
    v9 = (unsigned __int8 *)malloc(v4 - 7);
    memcpy(v9, v7 + 7, v4 - 7);
    if ( v4 - 7 >= 1 )
    {
        if ( v8 < 0x20 )
        {
            v10 = 0;
            do
                LABEL_15:
            v9[v10++] ^= 0x17u;
            while ( v8 != v10 );
            goto LABEL_16;
        }
        v11 = 0;
        v10 = v8 & 0xFFFFFFFFFFFFFFE0LL;
        v12.n128_u64[0] = 0x1717171717171717LL;
        v12.n128_u64[1] = 0x1717171717171717LL;
        do
        {
            v13 = (int8x16_t *)&v9[v11];
            v14 = *(int8x16_t *)&v9[v11];
            v15 = *(int8x16_t *)&v9[v11 + 16];
            v11 += 32;
            *v13 = veorq_s8(v14, v12);
            v13[1] = veorq_s8(v15, v12);
        }
        while ( v10 != v11 );
        if ( v8 != v10 )
            goto LABEL_15;
    }
}

这里大概就是说把beeplay文件头去掉后,直接对所有的字节XOR 0x17

astc文件  
  └─ GZIP解压
       └─ beeplay ASTC
             └─  7 字节头
                  └─ XOR 解密
                      └─ 标准 ASTC
                          └─ astcenc 解码 = 正常astc文件

astc处理为正常格式后,还需要分类,

image-20260117134749840

上面提到了很多种图片格式,这里统一处理为png格式的(spine的贴图是png的),这里只会处理astc

文件,skel 和 atlas不会处理,可以放心使用。后文会给出脚本。

资产分类/名称还原

所有文件的名称都是没什么规律的,没法进行分类,所以也没法通过名称找到同一个spine的三个文件。尽管可以尝试用atlas里面记载的图片名称和尺寸进行匹配,但是skel的匹配却别无他法。所以最优解法还是找到映射表。

根据上文找到的key,可以通过jsc反编译器jsc解加密工具-Orange.zip - 蓝奏云来查看源码index.jsc

image-20260117174828082

这里得到index.jsc,目前看来没什么用。推测是引擎自己使用的逻辑,有兴趣可以自行了解一下。

理论上来说通过config.json应该就可以搞定了,但是这里uuid里记载的都是22位的,实际文件基本都是36位,有少数是9位的。在config里面都是22位的根本找不到,应该还做了其他处理。

通过抓包发现,下载的本地manifest里面记载的也是22位数据,同时远程的manifest也是22位数据,所以说要处理的话只能在本地处理了。要去找到游戏内部的处理逻辑。

# 热更新
http://hotter-hxmjljp.wengames.com/japan/cardjapan-update/1.0.17.73/ios_bundle/assets/resources/import/51/5127c5e3-3e3e-4150-b8bf-904386ffe7fc.json?md5=b120aa6f54754ce2834e617c2ddc4e7f
# 本地manifest
http://hotter-hxmjljp.wengames.com/japan/cardjapan-update/1.0.17.73/ios_bundle/project.manifest

经过一天的瞎折腾,终于找到了答案:

不懂就问:UUID的压缩算法是怎么样的呢? - Creator 3.x - Cocos中文社区

有什么方法能从cocos creator构建的游戏里还原出live2d或spine动画资源? - 讨论 - Live2DHub

config.json里记载的uuid是22位的被压缩的uuid,而文件名称都是36位的uuid (不是hash)

处理顺序:读取config.json建表(path的key对应uuid的index)dictionary<uuid22, path>,遍历所有文件(uuid36 -> uuid22 -> path)找到对应path,重命名并移动。后文会给出处理脚本。


处理有点慢,要个5s吧。但是处理完发现还有大约900张图片是没有分类的,因为config里面根本没有对应的路径,只能找到uuid。

小人spines/panel/ 立绘spines/beauty 预览图icon/heroHead

到这里基本上就搞定了,接下来就是把spine相关文件的名称改为模型名称。

从马后炮的角度来看,其实path里面记载的路径比如"3": ["icon/heroHead/11068", 1],

最后一部分11068其实是文件名,icon/heroHead才是路径。这样可以理解为什么会有这样的"spines/panel/SG_SHU_OR_pangtong/SG_SHU_OR_pangtong"后面两段重复的路径了。

发现还缺了挺多东西的,不知道是不是鉴权,下面这几个角色有一大半没有。

image-20260118124502024

从头像数量来看,应该有211个的spine,config的路径里面spines/beauty/spine/开头的有168个,分类后实际42个。

我找了一个没有的角色头像,同时也是未被分类的,计算出来的path应该是8790,而这里刚好没有。这就产生了一个疑点。

image-20260118130301744

搜索武则天,在官网也能找到对应的立绘,但是本地只能找到小人,config里面也只有小人的相关文件,并没有立绘的。

图像

实测游戏内可以看到立绘,而且是动态的。

image-20260118143258827

在游戏里面切换立绘十分流畅,几乎可以肯定都是本地资源。

又经过了一番探索,发现了问题的端倪,APK里的只有500MB,而游戏体积占大头的一定是资产,所以这1.69G里面肯定有资产。

image-20260118145735093

/data/data/包名/这个路径下,remote目录

image-20260118145852589

这里还有个很重要的表cacheList.json,一并保存下来。

打开remote发现有两个特别大的json文件,果不其然是config。为什么会有两个?

猜测因为把native和import放一起了,所以既有atsc bin atlas 还有json文件。

但是这俩玩意里面很多重复的条目,这个也无从分辨,不知是怎么考虑的,当前的策略是第一个config为主体,如何没有查到,就查另一个config。不过从结果来看,用那个size更大的config好像就行了,因为这个查不到的话,另一个也还是查不到。

注意到文件的命名都不是36位的uuid,而是类似17686595069780的一串数字。这里cacheList.json就发挥作用了,这里又是一层映射关系,url -> uuid36

image-20260118153929977

然后接下来的处理方式跟之前一致。

三国云梦录 - XXTEA

cocos2d::HGameFileZip::getFileData
cocos2d::HGameFileZip::getFileDataEx
cocos2d::HGamePackFile::readFile
cocos2d::abc_decrypt
cocos2d::abc

得到

sub_E43DD4(src, 256, "k=%ds=%d%s", (_DWORD)this, a2, "9s07s26cs7r2449487rr8085sx7xa1c5");
v2 = (char *)calloc(1u, 0x21u);
md5_init(v5);
__strlen_chk(src, 0x100u);
md5_append((int)v5, src);
md5_finish(v5, v4);
sub_E43DD4(v2, 33, "%02x", v4[0]);
sub_E43DD4(v2 + 2, 31, "%02x", v4[1]);
sub_E43DD4(v2 + 4, 29, "%02x", v4[2]);
sub_E43DD4(v2 + 6, 27, "%02x", v4[3]);
sub_E43DD4(v2 + 8, 25, "%02x", v4[4]);
sub_E43DD4(v2 + 10, 23, "%02x", v4[5]);
sub_E43DD4(v2 + 12, 21, "%02x", v4[6]);
sub_E43DD4(v2 + 14, 19, "%02x", v4[7]);
sub_E43DD4(v2 + 16, 17, "%02x", v4[8]);
sub_E43DD4(v2 + 18, 15, "%02x", v4[9]);
sub_E43DD4(v2 + 20, 13, "%02x", v4[10]);
sub_E43DD4(v2 + 22, 11, "%02x", v4[11]);
sub_E43DD4(v2 + 24, 9, "%02x", v4[12]);
sub_E43DD4(v2 + 26, 7, "%02x", v4[13]);
sub_E43DD4(v2 + 28, 5, "%02x", v4[14]);
sub_E43DD4(v2 + 30, 3, "%02x", v4[15]);
return v2;

9s07s26cs7r2449487rr8085sx7xa1c5是salt

key生成流程:

def gen_key_hex(this_ptr: int, a2: int) -> str:
    s = f"k={this_ptr}s={a2}9s07s26cs7r2449487rr8085sx7xa1c5"
    return hashlib.md5(s.encode("ascii")).hexdigest()

xxtea也魔改了,太难了。

三国杀名将录跟这个基本一样。

对称加密/非对称加密

decrypt
IV
Key

哥特少女勇闯恶魔城2 Sinisistar2 - AES - Rijindael

其实隔壁已经给出答案了,但我还是挺好奇是怎么来的,花了几天看了下。仅供参考。

参考文献

https://live2dhub.com/t/topic/4420/13?u=twistzz

https://learn.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.aes?view=net-8.0


电报的资源

https://t.me/huangyou_A/99826

都是序列帧动画,然后基本都是拆分成好几个部分了(比如表情和脸),要看的话只能自己去Unity拼图了。

建议用Raz版AS导出,会跳过重复的资产。

虽然解决的不是很干净利索,但也算是完工了,抛砖引玉下吧。

stand_base

stand_normal


确定加密

搜索decrypt可以发现有很多个加密/解密相关的信息。

有DES RC2 3DES Rijindael AES这几种加密算法的信息,前面这三个都过时了,很显然应该使用的是后面这两个算法,不过也得看引用情况,说不准是混淆之类的。

image-20260204112211250

这里区分一下Rijindael和AES的关系:

项目 Rijndael AES
算法本体 原始算法 Rijndael 子集
分组长度bit 128–256 可变 仅 128
密钥长度bit 128–256 可变 128 / 192 / 256
轮数 依参数变化 固定(10/12/14)

这些Decryptor都是一些工具方法,而不是实际进行数据处理过程,所以需要看System_Security_Cryptography_RijndaelManagedTransform$$DecryptData以及Mono_Security_Cryptography_SymmetricTransform$$FinalDecrypt

这个FinalDecrypt不难猜到应该是对最后一个块的padding进行处理。

DecryptData 反编译后就有将近900行了,看不了一点,大致可以确定是标准的Rijindael算法,还需要判断一下工作模式,填充处理。

看到输入的参数里有paddingMode,以及fLase (应该是判断是否为Last,也就是最后一块)

image-20260204205837048

接下来寻找工作模式,值得一提的是,既然需要选择padding的模式,也就说明工作模式肯定是需要padding的,对应的也就是CBC、ECB、PCBC这三种,根据经验来看,默认是CBC了。

接下来观察到这样的两个分支,很显然对2和4两个模式做了专门的处理,是PKCS7和ANSI X923。

名称 说明
ANSIX923 4 ANSIX923 填充字符串由一个字节序列组成,此字节序列的最后一个字节填充字节序列的长度,其余字节均填充数字零。
ISO10126 5 ISO10126 填充字符串由一个字节序列组成,此字节序列的最后一个字节填充字节序列的长度,其余字节填充随机数据。
None 1 不填充。
PKCS7 2 PKCS #7 填充字符串由一个字节序列组成,每个字节填充该字节序列的长度。
Zeros 3 填充字符串由设置为零的字节组成。
if ( paddingMode != 1 )
   {
    if ( paddingMode == 2 )
       { ... }
//...
if ( paddingMode != 3 )
    {
      this = (System_Security_Cryptography_RijndaelManagedTransform_o *)(unsigned int)(paddingMode - 4);
      if ( paddingMode == 4 )
         { ... }

接下来只要获取IV和Key就可以尝试破解了。搜索关键词key

img

发现这个keyExpansion出现的很频繁,但是找不到IV相关的定义,可能是由其他变量名替代了。

System_Security_Cryptography_RijndaelManagedTransform_o *v11; // rbx
v11 = this;
//...
m_decryptKeyExpansion = v11->fields.m_decryptKeyExpansion;

跳转到fields定义

struct System_Security_Cryptography_RijndaelManagedTransform_o // sizeof=0x80
00000000 {
00000000     System_Security_Cryptography_RijndaelManagedTransform_c *klass;
00000008     void *monitor;
00000010     System_Security_Cryptography_RijndaelManagedTransform_Fields fields;
00000080 };

struct __declspec(align(8)) System_Security_Cryptography_RijndaelManagedTransform_Fields // sizeof=0x70
00000000 {                                       // XREF: System_Security_Cryptography_RijndaelManagedTransform_o/r
00000000     int32_t m_cipherMode;
00000004     int32_t m_paddingValue;
00000008     int32_t m_transformMode;
0000000C     int32_t m_blockSizeBits;
00000010     int32_t m_blockSizeBytes;
00000014     int32_t m_inputBlockSize;
00000018     int32_t m_outputBlockSize;
0000001C     // padding byte
0000001D     // padding byte
0000001E     // padding byte
0000001F     // padding byte
00000020     struct System_Int32_array *m_encryptKeyExpansion;
00000028     struct System_Int32_array *m_decryptKeyExpansion;
00000030     int32_t m_Nr;
00000034     int32_t m_Nb;
00000038     int32_t m_Nk;
0000003C     // padding byte
0000003D     // padding byte
0000003E     // padding byte
0000003F     // padding byte
00000040     struct System_Int32_array *m_encryptindex;
00000048     struct System_Int32_array *m_decryptindex;
00000050     struct System_Int32_array *m_IV;
00000058     struct System_Int32_array *m_lastBlockBuffer;
00000060     struct System_Byte_array *m_depadBuffer;
00000068     struct System_Byte_array *m_shiftRegister;
00000070 };

发现了IV的信息。接下来只要找到对应的赋值语句了。

最简单的方式就是去构造函数里面看看

搜搜RijndaelManagedTransform 找到ctor 和 cctor,然后依次向上寻找调用点以及对应的ctor,尽量找相关联的,比如这里有encrypt和decrypt,肯定优先看decrypt

依旧只是调用,不能直接找到赋值。

继续向上。。。XREF为o以及行数是1的不用看

运气不太好,这些相关的函数都没有实质性的信息。

其实也没有太多固定的方法,大多数时候要点运气,后来在dump.cs里面搜到了一个Util类

img

然后可以看下Util类的方法

System_Security_Cryptography_RijndaelManaged_o *Util__get_aesManaged(const MethodInfo *method)
{
  Util_c *v1; // rcx
  System_Security_Cryptography_RijndaelManaged_o *v2; // rbx
  struct Util_StaticFields *static_fields; // rdx
  void *aes; // rcx
  struct System_Security_Cryptography_RijndaelManaged_o *v5; // rbx
  System_Text_Encoding_o *UTF8; // rax
  __int64 v7; // rax
  struct System_Security_Cryptography_RijndaelManaged_o *v8; // rbx
  System_Text_Encoding_o *v9; // rax
  __int64 v10; // rax

  if ( !byte_1830E4F1F )
  {
    sub_18038C080(&System_Security_Cryptography_RijndaelManaged_TypeInfo);
    sub_18038C080(&Util_TypeInfo);
    byte_1830E4F1F = 1;
  }
  v1 = Util_TypeInfo;
  if ( !Util_TypeInfo->static_fields->aes )
  {
    Util__Update(0);
    v2 = (System_Security_Cryptography_RijndaelManaged_o *)sub_18033E930(System_Security_Cryptography_RijndaelManaged_TypeInfo);
    System_Security_Cryptography_RijndaelManaged___ctor(v2, 0);
    Util_TypeInfo->static_fields->aes = v2;
    sub_18038B400(&Util_TypeInfo->static_fields->aes, v2);
    aes = Util_TypeInfo->static_fields->aes;
    if ( !aes )
      goto LABEL_14;
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 552LL))(
      aes,
      256,
      *(_QWORD *)(*(_QWORD *)aes + 560LL));
    aes = Util_TypeInfo->static_fields->aes;
    if ( !aes )
      goto LABEL_14;
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 424LL))(
      aes,
      256,
      *(_QWORD *)(*(_QWORD *)aes + 432LL));
    aes = Util_TypeInfo->static_fields->aes;
    if ( !aes )
      goto LABEL_14;
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 584LL))(
      aes,
      1,
      *(_QWORD *)(*(_QWORD *)aes + 592LL));
    v5 = Util_TypeInfo->static_fields->aes;
    UTF8 = System_Text_Encoding__get_UTF8(0);
    if ( !UTF8 )
      goto LABEL_14;
    v7 = ((__int64 (__fastcall *)(System_Text_Encoding_o *, struct System_String_o *, const MethodInfo *))UTF8->klass->vtable._17_GetBytes.methodPtr)(
           UTF8,
           Util_TypeInfo->static_fields->IV,
           UTF8->klass->vtable._17_GetBytes.method);
    if ( !v5 )
      goto LABEL_14;
    ((void (__fastcall *)(struct System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v5->klass->vtable._10_set_IV.methodPtr)(
      v5,
      v7,
      v5->klass->vtable._10_set_IV.method);
    v8 = Util_TypeInfo->static_fields->aes;
    v9 = System_Text_Encoding__get_UTF8(0);
    aes = Util_TypeInfo;
    static_fields = Util_TypeInfo->static_fields;
    if ( !v9
      || (v10 = ((__int64 (__fastcall *)(System_Text_Encoding_o *, struct System_String_o *, const MethodInfo *))v9->klass->vtable._17_GetBytes.methodPtr)(
                  v9,
                  static_fields->K,
                  v9->klass->vtable._17_GetBytes.method),
          !v8)
      || (((void (__fastcall *)(struct System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v8->klass->vtable._12_set_Key.methodPtr)(
            v8,
            v10,
            v8->klass->vtable._12_set_Key.method),
          (aes = Util_TypeInfo->static_fields->aes) == 0) )
    {
LABEL_14:
      sub_18038C2D0(aes, static_fields);
    }
    (*(void (__fastcall **)(void *, __int64, _QWORD))(*(_QWORD *)aes + 616LL))(
      aes,
      2,
      *(_QWORD *)(*(_QWORD *)aes + 624LL));
    v1 = Util_TypeInfo;
  }
  return v1->static_fields->aes;
}

这里的几个数字很有趣

img

aes的数据类型一路溯源可以找到

struct __declspec(align(8)) System_Security_Cryptography_SymmetricAlgorithm_Fields // sizeof=0x38
00000000 {                                       // XREF: System_Security_Cryptography_SymmetricAlgorithm_o/r
00000000                                         // System_Security_Cryptography_Aes_Fields/r ...
00000000     int32_t BlockSizeValue;
00000004     int32_t FeedbackSizeValue;
00000008     struct System_Byte_array *IVValue;
00000010     struct System_Byte_array *KeyValue;
00000018     struct System_Security_Cryptography_KeySizes_array *LegalBlockSizesValue;
00000020     struct System_Security_Cryptography_KeySizes_array *LegalKeySizesValue;
00000028     int32_t KeySizeValue;
0000002C     int32_t ModeValue;
00000030     int32_t PaddingValue;
00000034     // padding byte
00000035     // padding byte
00000036     // padding byte
00000037     // padding byte
00000038 };

按照偏移量也就是顺序来看:

256只能是Keysize或者是BlockSize,得出的信息是BlockSize = 256 bit ` Keysize = 256bit `

因为CipherMode.CBC = 1,所以对应工作模式CBC

名称
CBC 1
CFB 4
CTS 5
ECB 2
OFB 3

所以Rijndael-CBC-256

img

aes.IV = Encoding.UTF8.GetBytes(Util.IV);
aes.Key = Encoding.UTF8.GetBytes(Util.K);

IV 和 Key 来源是 字符串 ,使用 UTF-8编码把ASCII字符转为字节

所以可以确定原始的IV和KEY的长度跟转化后是一致的,32B也就是32个字符,这里需要记住长度,后面会用到。

然后最后这个2毫无疑问对应的是padding了

PaddingMode.PKCS7 = 2所以填充方式是PKCS7

完整的加密方式为:Rijndael-256-CBC-PKCS7


寻找 IV 和 KEY

接下来只需要找出静态域的Key和IV了

继续寻找Util相关函数,发现没有ctor,但是有一个Update完成了初始化数据。

void Util__Update(const MethodInfo *method)
{
  Il2CppObject *object; // rax
  __int64 v2; // rdx
  __int64 v3; // rcx
  Il2CppObject *v4; // rbx
  struct System_String_o *v5; // rax
  struct System_String_o *v6; // rax

  if ( !byte_1830E4F20 )
  {
    sub_18038C080(&Method_UnityEngine_Resources_Load_Unique___);
    sub_18038C080(&Util_TypeInfo);
    sub_18038C080(&StringLiteral_6649);
    sub_18038C080(&StringLiteral_10860);
    sub_18038C080(&StringLiteral_6614);
    byte_1830E4F20 = 1;
  }
  if ( System_String__IsNullOrEmpty(Util_TypeInfo->static_fields->IV, 0) )
  {
    object = UnityEngine_Resources__Load_object_(StringLiteral_10860, Method_UnityEngine_Resources_Load_Unique___);
    v4 = object;
    if ( !object )
      sub_18038C2D0(v3, v2);
    v5 = System_String__Concat_6464675520((System_String_o *)object[2].klass, StringLiteral_6614, 0);
    Util_TypeInfo->static_fields->IV = v5;
    sub_18038B400(Util_TypeInfo->static_fields, v5);
    v6 = System_String__Concat_6464675520((System_String_o *)v4[2].monitor, StringLiteral_6649, 0);
    Util_TypeInfo->static_fields->K = v6;
    sub_18038B400(&Util_TypeInfo->static_fields->K, v6);
  }
}

查看汇编视图直接得到了这几个常量的字符值。其中两个数字和字母组成的串已经在dump.cs里面看过了。

image-20260205005153917

可以初步得到的信息:

object = UnityEngine_Resources__Load_object_("Manager/unique", Method_UnityEngine_Resources_Load_Unique___);
K  = object[2].monitor + "06789412023ED45";
IV = object[2].klass + "01127802CDEF00BC";

# Il2CppObject是某一个没有确定的类型,所以取了一个笼统的名字object
00000000 struct Il2CppObject // sizeof=0x10
00000000 {                                       // XREF: MidasTouch_State_array/r
00000000                                         // UnityEngine_GameObject_array/r ...
00000000     Il2CppClass *klass;                 // XREF: sub_180248F60+48/w
00000008     void *monitor;                      // XREF: sub_180248F60+4D/w
00000010 };

object应该是从这个路径"Manager/unique"上加载的一个文件,同时Method_UnityEngine_Resources_Load_Unique___这里说明了类型是<Unique>

以及拼接使用的字符值偏移量20h,28h

img

可以在dump.cs找到Unique定义,发现这里的偏移量也是20和28,正好对应上了。

// Namespace: 
public class Unique : MonoBehaviour // TypeDefIndex: 673
{
	// Fields
	public string x; // 0x20
	public string y; // 0x28

	// Methods

	// RVA: 0x3FA3D0 Offset: 0x3F97D0 VA: 0x1803FA3D0
	public void .ctor() { }
}

x -> klass

y -> monitor

可以进一步细化结果

K  = y + "06789412023ED45"; # y 32-15 = 17个字符
IV = x + "01127802CDEF00BC"; # x 32-16 = 16个字符

所以接下来只需要找到这个unique文件即可。

但是对于Unity而言Resources资源存储在resources.assetsresources.assets.resSsharedassets0.assetssharedassets0.assets.resS等文件中,不确定最好全部导AS里面,然后查找。

大概就是这些玩意

image-20260205170130643

设置里需要打开Display all assets然后导入文件,不然看不到unique。

image


https://live2dhub.com/t/topic/5678/7?u=twistzz

这里之前没有x和y的值,因为之前选择Monobehaviour时候AS会弹出一个框让你选择assembly加载dll还原数据,之前没想太多就关了

image 顺利导入后就会正常显示x和y

image

或者导出原始数据查看。 https://live2dhub.com/t/topic/5678/9?u=twistzz

image

image

image-20260213001806180


CE搜索内存

用IDA调试的时候游戏里一读档就一直报错卡异常,无语了。

突然发现可以用CE硬搜,前文已经得出了两个关键的拼接串,所以可以用CE直接搜索内存,但是不知道为什么这个CE没有搜索子串的方法(也可能是我没找到),只能搜索精确值。

然后可以枚举一下,因为这里的IV和KEY都是 32个字符

所以IV需要枚举前16位,KEY需要枚举前17位

其实最多也就 16 * 16 + 16 * 17 = 528 次就能搞定。

比如这里搜01127802CDEF00BC会显示有50个地址。(注意搜索的值类型)

image-20260205163729880

然后向前添加一位,尝试0~F,发现添加0之后还是可以搜索到50多个地址,但是添加1就什么都没有,可以说明添加0是对的。

如果我们搜到了同一个串,那么每次添加一个位,地址应该是邻近的。

这里保存下前三个搜索结果,然后重新进行搜索:

image-20260205164420816

发现地址确实跟假设的一样,是相邻的。不妨大胆一点,直接算一下地址,这里已经找到了1位,所以还剩15位,也就是地址偏移0x0F

0x1CA76F2607F - 0x0F = 0x1CA76F26070

手动添加一下地址0x1CA76F26070 ,这里自动读出来了内容。

image-20260205165239219

成功得到了IVD9AB89AA56F5673001127802CDEF00BC

同理可得KEY0BFAB106A793DCA7F06789412023ED45

0x1C98502A9E0 - Ox10 = 0x1C98502A9D0

image-20260205165803706

虽然有点丑陋,不过好在没有烂尾。

CE-Mono

CE对Unity的程序有额外支持,附加之后在顶部会额外出现一个mono选项

img

img

202602071130784.png (761×622)

可以选择实例

202602071150927.png (1096×297)

双击ctor,进入内存视图然后CTRL + F 搜索数值。

202602071138191.png (1618×939)


总结

  1. 确定加密方式Rijndael-256-CBC-PKCS7
  2. 找到IV和KEY的生成方式
  3. CE

Rijndael-256-CBC-PKCS7

IVD9AB89AA56F5673001127802CDEF00BC

KEY0BFAB106A793DCA7F06789412023ED45

注意这里的IV和KEY需要用UTF-8编码

aes.IV = Encoding.UTF8.GetBytes(Util.IV);
aes.Key = Encoding.UTF8.GetBytes(Util.K);

之前用py3rijindael来算,发现太慢了这也,byd满载跑了一晚上快8个小时,最后一个50M的文件跑了一个小时还没跑出来,不建议使用。

Rijindael.pypythonnet甚至不要10分钟🤡🤡🤡

其他

Spine合并

Spine文件怎么把文件和背景合二为一 - 讨论 - Live2DHub

image

Spine骨骼解析

Spine: JSON导出文件格式

  1. skeleton
  2. bones
  3. slots
  4. skins
  5. animations
"skeleton": {
   "hash": "5WtEfO08B0TzTg2mDqj4IHYpUZ4", //类似 MD5,用于检测数据是否变化
   "spine": "3.8.24",
   "x": -17.2, //AABB盒原点坐标(x,y)
   "y": -13.3,
   "width": 470.86, //AABB盒宽高
   "height": 731.44,
   //"fps": 30
   //"images": "./images/",
   //"audio": "./audio/"
},

Spine.Animation$$Deserialize

AseetStudio Fork开发

环境设置

IDE为Rider

使用.NET8.0 开发

MSbuild 选择IDE自带的17.0版本。

image-20251106203527957

配置选择GUI,设置为.NET8.0 后启动。

image-20251106203928618

项目架构

Comments