.nvldata的读取与提取笔记

逆向工程 实用技术 算法.nvldata
浏览数 - 58发布于 - 2026-05-18 - 10:58

重新编辑于 - 2026-05-18 - 10:59

Tiny Snow .nvldata 完整逆向分析笔记

说明:这份文档整理的是可复现的分析过程、证据链和结论。内部逐字思考链不会记录;这里用“调查日志/判断依据/排除项”的形式替代,方便学习和复盘。

0. 目标

分析以下文件如何被游戏读取,以及如何离线提取:

text
TinySnow_Data/StreamingAssets/StandaloneWindows/TinySnow/scripts.nvldata
TinySnow_Data/StreamingAssets/StandaloneWindows/TinySnow/textures.nvldata
TinySnow_Data/StreamingAssets/StandaloneWindows/TinySnow/audios.nvldata

最终产物:

text
tools/nvldata_extract.py
docs/nvldata.md
docs/nvldata_full_analysis.md
extracted/nvldata_decrypted/*.assetbundle

1. 最终结论

.nvldata 是 NVLUnity 引擎使用的加密 AssetBundle。

它不是 ZIP,不是普通 UnityFS,也不是压缩包套壳。真实结构是:

  1. 原始数据本质是 UnityFS AssetBundle。
  2. 文件前 0x20 字节的 UnityFS 头被抹掉/替换。
  3. 从第 0x20 字节之后的数据,用固定 12 字节 key 按文件绝对偏移循环 XOR。
  4. 解密后可被 UnityPy/AssetStudio/AssetRipper 正常识别。

Tiny Snow 参数如下:

text
修复用 UnityFS header, 32 bytes:
55 6E 69 74 79 46 53 00 00 00 00 06 35 2E 78 2E
78 00 32 30 31 38 2E 34 2E 32 36 66 31 00 00 00

ASCII:
UnityFS\0 .... 5.x.x\0 2018.4.26f1\0\0\0

XOR key, 12 bytes:
61 BB 79 A8 62 D0 7E 7D EA 6B 76 E4

伪代码:

text
for offset in file:
    if offset < 0x20:
        output[offset] = fixed_unityfs_header[offset]
    else:
        output[offset] = input[offset] ^ key[offset % 12]

2. 项目结构观察

初始目录:

text
GameAssembly.dll
TinySnow.exe
UnityPlayer.dll
TinySnow_Data/
Dump/

关键文件:

text
TinySnow_Data/StreamingAssets/StandaloneWindows/TinySnow/audios.nvldata
TinySnow_Data/StreamingAssets/StandaloneWindows/TinySnow/scripts.nvldata
TinySnow_Data/StreamingAssets/StandaloneWindows/TinySnow/textures.nvldata
TinySnow_Data/il2cpp_data/Metadata/global-metadata.dat
GameAssembly.dll

文件大小:

text
audios.nvldata    247,510,833 bytes
scripts.nvldata     1,304,712 bytes
textures.nvldata 287,715,185 bytes

这些大小符合 Unity AssetBundle 资源包的体量分布:脚本包较小,音频/图片包较大。

3. 第一轮源码线索

Dump/ExportedProject/Assets/Scripts/Assembly-CSharp/NVLMaker 里找到相关类名:

text
PackageManager.cs
LoadingManager.cs
Utils.cs
AutoPathResolver.cs
AutoPathResolverGeneral.cs
AutoPathResolverPrecache.cs
IResource.cs
ProjectHelper.cs

关键线索:

csharp
private AutoPathResolver _textures;
private AutoPathResolver _scripts;
private AutoPathResolver _audios;

public static AssetBundle LoadBundle(string filename)
public static AssetBundleCreateRequest LoadBundleAsync(string filename)
public static string BundlePath(string entry, string name, string target)
public static string LocalBundlePath(string entry, string name)

Dump/ExportedProject 里的 C# 方法体大多是空桩:

csharp
public static AssetBundle LoadBundle(string filename)
{
    return null;
}

这说明这个 Dump 更像是类型/资源导出,不能直接作为真实逻辑依据。需要回到 IL2CPP 的 GameAssembly.dll + global-metadata.dat

4. 文件头证据

直接读 .nvldata 前 0x80 字节。

scripts.nvldata

text
4C 01 00 00 A4 28 00 00 4A EC 04 00 57 9D 98 00
FC 0D 7B 12 F4 B1 E6 3C FA 8C EF 5F A7 12 02 9E
EA 6B 76 F7 89 33 79 A8 62 31 7E 7D EB F0 76 E4
61 F8 67 A8 63 D0 4E 5C EA 69 71 E4 23 35 CE A8

textures.nvldata

text
4D 01 00 00 C7 28 00 00 8D F0 04 00 87 21 99 00
59 0F 8B 12 C7 DB D6 3E 19 9D 04 9C 07 06 8F E4
EA 6B 67 C2 4E CA 79 A8 E8 6A 7E 7C 61 36 76 E4

audios.nvldata

text
3A 01 00 00 75 26 00 00 A1 A8 04 00 F3 6B 90 00
CE 12 7D 11 61 47 25 1E 35 A5 83 A6 DF 01 F1 29
EA 6B 78 24 D6 8A 79 A8 64 A5 7E 7D A0 DC 76 E4

观察:

  1. 文件不是 UnityFS 开头。
  2. 前 16 字节像小端整数。
  3. 从 0x20 附近开始反复出现与 key 很接近的字节序列:
text
61 BB 79 A8 62 D0 7E 7D EA 6B 76 E4

这个重复结构非常像循环 XOR key 泄漏:如果明文在某些位置是 00,密文就会直接等于 key。

5. IDA 与 IL2CPP 证据

5.1 当前 IDA 数据库状态

IDA MCP 当前打开的是 TinySnow.exe,而不是 GameAssembly.dll

TinySnow.exe 只看到少量启动器/Unity 壳字符串:

text
C:\Workspace\Build\TinySnow\TinySnowRetail\build\bin\x86\Master\TinySnow_x86_Master_il2cpp.pdb
TinySnow.exe

判断:

text
TinySnow.exe 不是主要逻辑位置。
Unity 项目 IL2CPP 真实业务逻辑在 GameAssembly.dll。

因此主要静态分析目标应切到:

text
GameAssembly.dll
TinySnow_Data/il2cpp_data/Metadata/global-metadata.dat

5.2 Il2CppDumper 证据

使用 Il2CppDumper 对 GameAssembly.dllglobal-metadata.dat 生成:

text
Dump/Il2CppDumper/dump.cs
Dump/Il2CppDumper/script.json
Dump/Il2CppDumper/il2cpp.h
Dump/Il2CppDumper/DummyDll/

Il2CppDumper 输出:

text
Metadata Version: 24.1
Il2Cpp Version: 24.1
CodeRegistration     : 10b2c0d0
MetadataRegistration : 10b2c140

script.json 中关键方法地址:

text
NVLMaker.Utils$$LoadBundle              RVA 0x18C8B0, VA 0x1018C8B0
NVLMaker.Utils$$LoadBundleAsync         RVA 0x18C880, VA 0x1018C880
NVLMaker.Utils$$BundlePath              RVA 0x18AD60, VA 0x1018AD60
NVLMaker.Utils$$LocalBundlePath         RVA 0x18C8E0, VA 0x1018C8E0
NVLMaker.Utils$$StreammingFile          RVA 0x18D440, VA 0x1018D440
NVLMaker.Utils$$StreammingProjectFile   RVA 0x18D4E0, VA 0x1018D4E0
NVLMaker.Utils$$AssetBundleFolder       RVA 0x18AC50, VA 0x1018AC50
NVLMaker.PackageManager$$InitResolver   RVA 0x270CD0, VA 0x10270CD0
NVLMaker.LoadingManager$$LoadBytesFromBundle RVA 0x26C9B0, VA 0x1026C9B0
NVLMaker.LoadingManager$$ToBundlePath   RVA 0x26D920, VA 0x1026D920

5.3 LoadBundle 反汇编证据

NVLMaker.Utils$$LoadBundle

asm
1018c8b0  push ebp
1018c8b1  mov  ebp, esp
1018c8b3  push 0
1018c8b5  push dword ptr [ebp+8]      ; filename
1018c8b8  call 10422200               ; 本地文件存在/可读取检查
1018c8bd  add  esp, 8
1018c8c0  test al, al
1018c8c2  jne  1018c8c8
1018c8c4  xor  eax, eax               ; 文件不可用则返回 null
1018c8c6  pop  ebp
1018c8c7  ret
1018c8c8  mov  dword ptr [ebp+0c], 0
1018c8cf  pop  ebp
1018c8d0  jmp  108e0bf0               ; 跳到 UnityEngine.AssetBundle.LoadFromFile_Internal 包装

GameAssembly.dll 字符串区存在 Unity 绑定名:

text
UnityEngine.AssetBundle::LoadFromFile_Internal(System.String,System.UInt32,System.UInt64)
UnityEngine.AssetBundle::LoadFromFileAsync_Internal(System.String,System.UInt32,System.UInt64)
UnityEngine.AssetBundle::Contains(System.String)
UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)
UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)
UnityEngine.AssetBundle::GetAllAssetNames()

判断依据:

text
LoadBundle(filename) 检查 filename 后跳到 Unity AssetBundle.LoadFromFile_Internal。
所以游戏最终把 .nvldata 路径直接交给 Unity 的 AssetBundle 加载接口。

这也解释了为什么解密后必须变成标准 UnityFS AssetBundle。

5.4 LoadBundleAsync 反汇编证据

asm
1018c880  push ebp
1018c881  mov  ebp, esp
1018c883  push 0
1018c885  push dword ptr [ebp+8]
1018c888  call 10422200
1018c88d  add  esp, 8
1018c890  test al, al
1018c892  jne  1018c898
1018c894  xor  eax, eax
1018c896  pop  ebp
1018c897  ret
1018c898  mov  dword ptr [ebp+0c], 0
1018c89f  pop  ebp
1018c8a0  jmp  108e0b50

这与同步读取相同,只是最终跳到 async 版本。

5.5 .nvldata 扩展名证据

script.json 的字符串表里存在:

text
Address: 0x00C04DB4
Value  : .nvldata

该字符串附近还存在:

text
StandaloneWindows
/Resources/
Assets
Assets/Games

这说明 .nvldata 是游戏逻辑显式使用的 BundleExt,而不是随意扩展名。

5.6 资源路径推断

根据 Utils 方法名和字符串表:

text
AssetBundleFolder(RuntimePlatform.WindowsPlayer) -> StandaloneWindows
BundleExt -> .nvldata
ProjectName(entry) -> TinySnow
StreammingProjectFile(entry, filename)
StreammingFile(filename)
BundlePath(entry, name, target)

组合路径:

text
Application.streamingAssetsPath/StandaloneWindows/TinySnow/scripts.nvldata
Application.streamingAssetsPath/StandaloneWindows/TinySnow/textures.nvldata
Application.streamingAssetsPath/StandaloneWindows/TinySnow/audios.nvldata

实际磁盘路径与此一致。

6. 排除过程

6.1 排除 ZIP

.nvldata 文件头不是:

text
50 4B 03 04

也没有可直接识别的 ZIP central directory。

6.2 排除普通 UnityFS

普通 UnityFS 应该以:

text
55 6E 69 74 79 46 53 00
UnityFS\0

开头。

但原始 .nvldata 开头是:

text
4C 01 00 00 ...
4D 01 00 00 ...
3A 01 00 00 ...

因此不是未加密 AssetBundle。

6.3 排除单字节 XOR

在前几 MB 中搜索 UnityFSUnityWebUnityRawCAB- 的单字节 XOR 变体,没有稳定命中。

这说明不是“整个文件统一 XOR 一个字节”。

6.4 指向周期 XOR

0x20 附近出现明显重复:

text
61 BB 79 A8 62 D0 7E 7D EA 6B 76 E4

如果明文有大量 0 字节,循环 XOR 密文就会泄露 key。这与 UnityFS 头部附近经常出现多个 0 字节吻合。

7. 外部工具线索验证

检索 .nvldataNVLUnity 后找到 CNGALTools 的 NVLUnityDecryptor:

text
https://github.com/YeLikesss/CNGALTools

关键源码:

text
001.NVL/NVLUnity/NVLUnityDecryptor/NvlUnityDecrypt/NvlUnity/ArchiveCrypto.cs
001.NVL/NVLUnity/NVLUnityDecryptor/NvlUnityDecrypt/NvlUnity.V1/NVLFilterV1.cs
001.NVL/NVLUnity/NVLUnityDecryptor/NvlUnityDecrypt/NvlUnity.V1/GameDBV1.cs

NVLFilterV1.Decrypt 核心逻辑:

csharp
if (offset < headerLen)
{
    int copyLen = (int)Math.Min(headerLen - offset, dataLen);
    header[(int)offset..copyLen].CopyTo(data[(int)offset..copyLen]);
    dataPos += copyLen;
    offset += copyLen;
}

Span<byte> key = this.mFilterKey.XorKey;
int keyLen = key.Length;
int keyPos = (int)(offset % keyLen);

while (dataPos < dataLen)
{
    data[dataPos] ^= key[keyPos];
    ++keyPos;
    if (keyPos == keyLen)
    {
        keyPos = 0;
    }
    ++dataPos;
    ++offset;
}

Tiny Snow 条目:

csharp
internal class TinySnow : NVLUnityV101
{
    public override byte[] XorKey { get; } = new byte[]
    {
        0x61, 0xBB, 0x79, 0xA8, 0x62, 0xD0,
        0x7E, 0x7D, 0xEA, 0x6B, 0x76, 0xE4
    };
}

NVLUnityV101 header:

csharp
55 6E 69 74 79 46 53 00 00 00 00 06 35 2E 78 2E
78 00 32 30 31 38 2E 34 2E 32 36 66 31 00 00 00

这与本地文件泄露出的 key 特征、Unity 版本字符串 2018.4.26f1 完全匹配。

8. 本项目 Python 实现

脚本位置:

text
tools/nvldata_extract.py

默认解密命令:

powershell
python tools\nvldata_extract.py

输出:

text
extracted/nvldata_decrypted/audios.assetbundle
extracted/nvldata_decrypted/scripts.assetbundle
extracted/nvldata_decrypted/textures.assetbundle

继续提取 Unity 对象:

powershell
python -m pip install UnityPy
python tools\nvldata_extract.py --extract

输出:

text
extracted/nvldata_assets/scripts/
extracted/nvldata_assets/textures/
extracted/nvldata_assets/audios/

9. 解密验证

执行:

powershell
python tools\nvldata_extract.py

结果:

text
decrypted: audios.nvldata   -> extracted/nvldata_decrypted/audios.assetbundle
decrypted: scripts.nvldata  -> extracted/nvldata_decrypted/scripts.assetbundle
decrypted: textures.nvldata -> extracted/nvldata_decrypted/textures.assetbundle

解密后文件头:

text
audios.assetbundle   b'UnityFS\x00\x00\x00\x00\x065.x.x\x002018.4.26f1\x00\x00\x00'
scripts.assetbundle  b'UnityFS\x00\x00\x00\x00\x065.x.x\x002018.4.26f1\x00\x00\x00'
textures.assetbundle b'UnityFS\x00\x00\x00\x00\x065.x.x\x002018.4.26f1\x00\x00\x00'

UnityPy 验证:

text
scripts.assetbundle:
  objects    = 106
  containers = 105

textures.assetbundle:
  objects    = 2811
  containers = 2810

audios.assetbundle:
  objects    = 3058
  containers = 3057

部分资源名:

text
assets/games/tinysnow/data/scenario/start.bkscr.compiled.bytes
assets/games/tinysnow/main.bkscr.compiled.bytes
assets/games/tinysnow/data/fgimage/scfigrong_a_nol_031.png
assets/games/tinysnow/data/voice/cvrong14005.ogg
assets/games/tinysnow/data/sound/shuidibreak.ogg

这证明 .nvldata 解密后确实是正常 AssetBundle。

10. scripts 包为什么不是明文脚本

scripts.assetbundle 里大多是:

text
*.bkscr.compiled.bytes
*.tjs.compiled.bytes
*.bkpsr.compiled.bytes
assets.bytes
macro.bkscr.compiled.bytes
main.bkscr.compiled.bytes

这些是 NVLMaker 编译后的脚本 bytecode,不是 .bkscr 明文。

所以提取层级分两步:

  1. .nvldata -> UnityFS AssetBundle。
  2. AssetBundle -> *.compiled.bytes 等 Unity TextAsset。
  3. 如果需要可读脚本,还要继续分析 NVLMaker bytecode 格式。

CNGALTools 里另有 NVLUnityScriptDumper,它的用途就是动态 dump/还原脚本。那是下一阶段目标。

11. IDA 复现建议

如果要在 IDA 中复现证据,建议打开:

text
GameAssembly.dll

然后加载 Il2CppDumper 生成的:

text
Dump/Il2CppDumper/ida_with_struct_py3.py

或至少使用 script.json 里的 RVA 跳转。

关键地址:

text
ImageBase: 通常为 0x10000000

LoadBundle:
  VA 0x1018C8B0
  RVA 0x0018C8B0

LoadBundleAsync:
  VA 0x1018C880
  RVA 0x0018C880

BundlePath:
  VA 0x1018AD60
  RVA 0x0018AD60

LocalBundlePath:
  VA 0x1018C8E0
  RVA 0x0018C8E0

PackageManager.InitResolver:
  VA 0x10270CD0
  RVA 0x00270CD0

LoadingManager.LoadBytesFromBundle:
  VA 0x1026C9B0
  RVA 0x0026C9B0

建议在 IDA 中标注:

text
0x1018C8B0 -> NVLMaker.Utils.LoadBundle
0x1018C880 -> NVLMaker.Utils.LoadBundleAsync
0x1018AD60 -> NVLMaker.Utils.BundlePath
0x1018C8E0 -> NVLMaker.Utils.LocalBundlePath
0x10270CD0 -> NVLMaker.PackageManager.InitResolver
0x1026C9B0 -> NVLMaker.LoadingManager.LoadBytesFromBundle

可查字符串:

text
.nvldata
StandaloneWindows
UnityEngine.AssetBundle::LoadFromFile_Internal
UnityEngine.AssetBundle::LoadFromFileAsync_Internal
UnityEngine.AssetBundle::GetAllAssetNames

12. 学习用调查流程总结

推荐以后遇到类似 Unity/IL2CPP 封包时按这个顺序走:

  1. 列文件:确认资源目录、扩展名、大小分布。
  2. 看文件头:先判定是不是 ZIP/UnityFS/UnityWeb/UnityRaw。
  3. 搜源码:在 Dump C# 中找 AssetBundleStreamingAssets、扩展名、LoadFromFile
  4. 如果 C# 是空桩,转 IL2CPP:用 GameAssembly.dll + global-metadata.dat
  5. 用 Il2CppDumper/IDA 对齐方法名和地址。
  6. LoadBundleBundlePathPackageManagerLoadingManager
  7. 搜字符串表:扩展名、平台目录、Unity 内部 API 名。
  8. 观察密文重复:周期性泄漏通常指向 XOR key。
  9. 找外部同引擎工具验证:不要盲信,要和本地文件头/IDA证据交叉验证。
  10. 写最小解密器,然后用 UnityPy/AssetStudio 验证对象数量和资源名。

13. 参考

text
CNGALTools / NVLUnityDecryptor
https://github.com/YeLikesss/CNGALTools

本地关键文件:

text
Dump/CNGALTools/001.NVL/NVLUnity/NVLUnityDecryptor/NvlUnityDecrypt/NvlUnity.V1/NVLFilterV1.cs
Dump/CNGALTools/001.NVL/NVLUnity/NVLUnityDecryptor/NvlUnityDecrypt/NvlUnity.V1/GameDBV1.cs
Dump/Il2CppDumper/script.json
Dump/Il2CppDumper/dump.cs
tools/nvldata_extract.py

本文版权遵循 CC BY-NC 协议 本站版权政策

(。>︿<。) 已经一滴回复都不剩了哦~