Skip to content

Commit 3e47730

Browse files
sunnamed434claude
andcommitted
feat(il2cpp): productionize #276 metadata encryption (native plugin + auto-encrypt)
The crypto half of #276 already encrypted global-metadata.dat; this delivers the decryptor into GameAssembly.dll automatically and wires it to a build toggle, so the protection works end to end without editing the user's Unity install (which Unity's license forbids anyway). - native/global_metadata_decrypt.cpp is now a self-installing Unity source plugin: a static initializer IAT-hooks CreateFileW (by name, so it survives kernel32/kernelbase/ apiset) before il2cpp_init; when IL2CPP opens an encrypted global-metadata.dat it serves the decrypted bytes. No-op on a plain build. Ships in the Unity package (Plugins/BitMono) and Unity compiles the loose .cpp straight into GameAssembly.dll. - BitMonoMetadataProtection (IPostprocessBuildWithReport) runs --encrypt-metadata on the built global-metadata.dat, gated by the new BitMonoConfig.EncryptIl2CppMetadata toggle. Windows x64 only for now. - bitmono_native_build.bat compile-checks the three native modes and runs the data round-trip when a built player is present. Validated end to end on Unity 6000.2.6f2 (metadata v31): the loose .cpp compiles into GameAssembly.dll, the player boots from the BMIL2CPP-encrypted metadata, and a corrupted-ciphertext control crashes at GlobalMetadata::Initialize - so the boot depends on the hook's decrypt. Blocks static dumpers (Il2CppDumper/Cpp2IL); a memory dumper still wins, as always when the key ships in the binary. Follow-ups: per-build key (currently a fixed default), other platforms (the hook is Win32-only), compile-gate the plugin when the feature is off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8fa867f commit 3e47730

9 files changed

Lines changed: 1060 additions & 9 deletions

File tree

docs/source/developers/il2cpp-compatibility.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,46 @@ protection's output ends up decides this:
4242
``[IL2CPPIncompatible]`` on top of them as well.
4343
4444
See :doc:`../protection-list/unity` for which built-in protections run on IL2CPP and which are skipped.
45+
46+
Encrypting the IL2CPP output (global-metadata.dat)
47+
--------------------------------------------------
48+
49+
Everything above is about the *managed* side, renaming and string encryption that land inside
50+
``global-metadata.dat`` because BitMono runs before ``il2cpp.exe``. That already cloaks your names, but the
51+
``global-metadata.dat`` file itself is still a normal, parseable file, so tools like Il2CppDumper and Cpp2IL
52+
happily read its structure straight off disk.
53+
54+
The IL2CPP metadata encryption protection (issue #276) closes that. When you turn it on, BitMono encrypts
55+
``global-metadata.dat`` in the built player so static dumpers can't parse it at all, and a tiny native
56+
decryptor that BitMono compiles into ``GameAssembly.dll`` restores it in memory at startup. The game boots
57+
exactly as before; the dumpers just see noise.
58+
59+
It's a separate, independent layer from the managed obfuscation, you can use either or both. Turn it on in
60+
Unity from the **BitMonoConfig** asset: tick **Encrypt IL2CPP Metadata**. Windows x64 builds only for now.
61+
62+
.. code-block:: text
63+
64+
BitMonoConfig
65+
[x] Enable Obfuscation (managed renaming/strings, runs before il2cpp.exe)
66+
[x] Encrypt IL2CPP Metadata (encrypts global-metadata.dat, decrypts in GameAssembly.dll)
67+
68+
Under the hood it's two halves that share one key:
69+
70+
- **Offline:** after the player is built, BitMono runs ``BitMono.CLI --encrypt-metadata global-metadata.dat``.
71+
That XXTEA-encrypts the whole file behind a small header and self-checks the round-trip. You can run it by
72+
hand for CI builds too.
73+
- **Runtime:** the source plugin ``global_metadata_decrypt.cpp`` (shipped in the Unity package, compiled into
74+
``GameAssembly.dll``) hooks the file read of ``global-metadata.dat`` and hands IL2CPP the decrypted bytes.
75+
It's a no-op on a plain build, so it only does anything when the file is actually encrypted.
76+
77+
.. note::
78+
79+
This stops **static** dumping, the shipped ``global-metadata.dat`` is unreadable, so anything that parses
80+
the file off disk is dead in the water. It does **not** stop a **memory** dumper that reads the already
81+
decrypted bytes out of the running process; nothing that ships the key in the binary can. Treat it as one
82+
more wall on top of the managed renaming, not a magic bullet. The key ships inside ``GameAssembly.dll``, so
83+
it's obfuscation strength, not a secret.
84+
85+
Validated end to end on a real Unity 6000.2 IL2CPP build (metadata version 31). The encryption is whole-file,
86+
so it doesn't care about the per-version metadata layout; only the decryptor's file hook is platform-specific
87+
(Windows x64 today).

src/BitMono.IL2CPP/native/global_metadata_decrypt.cpp

Lines changed: 324 additions & 9 deletions
Large diffs are not rendered by default.

src/BitMono.Unity/Editor/BitMonoConfig.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ public class BitMonoConfig : ScriptableObject
1414
[Tooltip("Enable BitMono obfuscation during Unity builds")]
1515
public bool EnableObfuscation = true;
1616

17+
[Header("IL2CPP Metadata Protection (#276)")]
18+
[Tooltip("On IL2CPP Windows x64 builds, encrypt the output global-metadata.dat so static dumpers " +
19+
"(Il2CppDumper, Cpp2IL) can't parse it. The native decryptor compiled into GameAssembly.dll " +
20+
"restores it in memory at startup. Independent of the managed obfuscation above.")]
21+
public bool EncryptIl2CppMetadata = false;
22+
1723
[Header("Configuration Path")]
1824
[Tooltip("Custom path to BitMono configuration files (leave empty for auto-detection)")]
1925
public string ConfigPath = "";

src/BitMono.Unity/Editor/BitMonoConfigInspector.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ public override void OnInspectorGUI()
2929
"When disabled, obfuscation will be skipped entirely."),
3030
config.EnableObfuscation);
3131

32+
config.EncryptIl2CppMetadata = EditorGUILayout.Toggle(
33+
new GUIContent("Encrypt IL2CPP Metadata",
34+
"On IL2CPP Windows x64 builds, encrypt the output global-metadata.dat so static dumpers " +
35+
"(Il2CppDumper, Cpp2IL) can't parse it. GameAssembly.dll decrypts it in memory at startup. " +
36+
"Independent of the managed obfuscation - you can use either or both. See issue #276."),
37+
config.EncryptIl2CppMetadata);
38+
3239
EditorGUILayout.Space();
3340
EditorGUILayout.LabelField("Configuration Path", EditorStyles.boldLabel);
3441

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#if UNITY_EDITOR
2+
using System;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using UnityEditor;
7+
using UnityEditor.Build;
8+
using UnityEditor.Build.Reporting;
9+
using Debug = UnityEngine.Debug;
10+
11+
namespace BitMono.Unity.Editor
12+
{
13+
// Post-build half of #276: after an IL2CPP Windows player is built, encrypt its global-metadata.dat in
14+
// place so static dumpers (Il2CppDumper, Cpp2IL) can't parse it. The native decryptor shipped as a source
15+
// plugin (Plugins/BitMono/global_metadata_decrypt.cpp) is compiled into GameAssembly.dll and restores the
16+
// metadata in memory at startup. Opt-in via BitMonoConfig.EncryptIl2CppMetadata.
17+
public class BitMonoMetadataProtection : IPostprocessBuildWithReport
18+
{
19+
// "BMIL2CPP" little-endian - the marker GlobalMetadataEncryptor writes; lets us stay idempotent.
20+
private const ulong EncryptedSanity = 0x505043324C494D42UL;
21+
22+
public int callbackOrder => 1000; // run last, once the player (and its metadata) is fully written
23+
24+
public void OnPostprocessBuild(BuildReport report)
25+
{
26+
var config = LoadConfig();
27+
if (config == null || !config.EncryptIl2CppMetadata)
28+
{
29+
return;
30+
}
31+
// Don't gate on result == Succeeded: OnPostprocessBuild runs before the report is finalized, so the
32+
// result is usually BuildResult.Unknown here. Only bail on an explicit failure/cancel.
33+
if (report.summary.result == BuildResult.Failed || report.summary.result == BuildResult.Cancelled)
34+
{
35+
return;
36+
}
37+
// The native decryptor is Windows x64 only (CreateFileW hook); only encrypt where it can decrypt.
38+
if (report.summary.platform != BuildTarget.StandaloneWindows64)
39+
{
40+
Debug.LogWarning("[BitMono] Encrypt IL2CPP Metadata is on, but it currently supports Windows x64 " +
41+
"only. Leaving global-metadata.dat unencrypted for this platform.");
42+
return;
43+
}
44+
45+
var metadata = FindMetadata(report.summary.outputPath);
46+
if (metadata == null)
47+
{
48+
// No metadata = not an IL2CPP build (Mono backend). Nothing to do.
49+
return;
50+
}
51+
if (IsEncrypted(metadata))
52+
{
53+
return; // already done (idempotent for incremental builds)
54+
}
55+
56+
var cli = FindCli();
57+
if (cli == null)
58+
{
59+
Debug.LogError("[BitMono] Encrypt IL2CPP Metadata is on but BitMono.CLI.exe wasn't found. " +
60+
"global-metadata.dat is left UNENCRYPTED.");
61+
return;
62+
}
63+
64+
try
65+
{
66+
// --encrypt-metadata writes <path>.enc and self-verifies the round-trip.
67+
var psi = new ProcessStartInfo
68+
{
69+
FileName = cli,
70+
Arguments = $"--encrypt-metadata \"{metadata}\"",
71+
UseShellExecute = false,
72+
RedirectStandardOutput = true,
73+
RedirectStandardError = true,
74+
CreateNoWindow = true,
75+
};
76+
using var process = Process.Start(psi);
77+
var stdout = process.StandardOutput.ReadToEnd();
78+
var stderr = process.StandardError.ReadToEnd();
79+
process.WaitForExit();
80+
if (process.ExitCode != 0)
81+
{
82+
Debug.LogError($"[BitMono] --encrypt-metadata failed (exit {process.ExitCode}); " +
83+
$"global-metadata.dat left UNENCRYPTED.\n{stdout}\n{stderr}");
84+
return;
85+
}
86+
87+
var enc = metadata + ".enc";
88+
if (!File.Exists(enc))
89+
{
90+
Debug.LogError("[BitMono] --encrypt-metadata reported success but produced no .enc file; " +
91+
"global-metadata.dat left UNENCRYPTED.");
92+
return;
93+
}
94+
File.Copy(enc, metadata, overwrite: true);
95+
File.Delete(enc);
96+
Debug.Log("[BitMono] Encrypted IL2CPP global-metadata.dat (#276): static dumpers are blocked; " +
97+
"GameAssembly.dll decrypts it at startup.");
98+
}
99+
catch (Exception ex)
100+
{
101+
Debug.LogError($"[BitMono] IL2CPP metadata encryption failed: {ex}. " +
102+
"global-metadata.dat left UNENCRYPTED.");
103+
}
104+
}
105+
106+
// outputPath is the .exe; the metadata sits at <Name>_Data/il2cpp_data/Metadata/global-metadata.dat.
107+
private static string FindMetadata(string outputPath)
108+
{
109+
var dir = Path.GetDirectoryName(outputPath);
110+
if (string.IsNullOrEmpty(dir))
111+
{
112+
return null;
113+
}
114+
var name = Path.GetFileNameWithoutExtension(outputPath);
115+
var expected = Path.Combine(dir, name + "_Data", "il2cpp_data", "Metadata", "global-metadata.dat");
116+
if (File.Exists(expected))
117+
{
118+
return expected;
119+
}
120+
// Fallback for unusual output layouts.
121+
try
122+
{
123+
return Directory.GetFiles(dir, "global-metadata.dat", SearchOption.AllDirectories).FirstOrDefault();
124+
}
125+
catch
126+
{
127+
return null;
128+
}
129+
}
130+
131+
private static bool IsEncrypted(string path)
132+
{
133+
try
134+
{
135+
using var fs = File.OpenRead(path);
136+
var head = new byte[8];
137+
return fs.Read(head, 0, 8) == 8 && BitConverter.ToUInt64(head, 0) == EncryptedSanity;
138+
}
139+
catch
140+
{
141+
return false;
142+
}
143+
}
144+
145+
private static string FindCli()
146+
{
147+
var dataPath = UnityEngine.Application.dataPath;
148+
var paths = new[]
149+
{
150+
// BitMono.CLI~ is Unity-ignored (~) so its DLLs never ship into the player; the .exe still runs.
151+
Path.Combine(dataPath, "BitMono.Unity", "BitMono.CLI~", "BitMono.CLI.exe"),
152+
Path.Combine(dataPath, "BitMono.Unity", "BitMono.CLI", "BitMono.CLI.exe"),
153+
Path.Combine(dataPath, "..", "BitMono.CLI", "BitMono.CLI.exe"),
154+
Path.Combine(dataPath, "..", "..", "src", "BitMono.CLI", "bin", "Release", "net462", "BitMono.CLI.exe"),
155+
};
156+
return paths.FirstOrDefault(File.Exists);
157+
}
158+
159+
private static BitMonoConfig LoadConfig()
160+
{
161+
var guids = AssetDatabase.FindAssets("t:BitMonoConfig");
162+
if (guids.Length == 0)
163+
{
164+
return null;
165+
}
166+
return AssetDatabase.LoadAssetAtPath<BitMonoConfig>(AssetDatabase.GUIDToAssetPath(guids[0]));
167+
}
168+
}
169+
}
170+
#endif

src/BitMono.Unity/Editor/PackageExporter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@ public static void ExportPackage()
5959
var coreFiles = new[]
6060
{
6161
"Editor/BitMonoBuildProcessor.cs",
62+
"Editor/BitMonoMetadataProtection.cs",
6263
"Editor/BitMonoConfig.cs",
6364
"Editor/BitMonoConfigInspector.cs",
6465
"Editor/BitMono.Unity.Editor.asmdef",
66+
// #276 native decryptor, compiled into GameAssembly.dll on IL2CPP Win64 builds.
67+
"Plugins/BitMono/global_metadata_decrypt.cpp",
6568
"package.json",
6669
"BitMonoConfig.asset"
6770
};

0 commit comments

Comments
 (0)