Unity ships in two very different shapes, and identifying which one you have is the entire first decision. A Mono build hands you decompilable .NET assemblies. An IL2CPP build compiles C# to C++ to native code, and the C# metadata lives in a separate file that you must recombine with the binary before anything is readable.
When to Use
- The APK contains
assets/bin/Data/Managed/Metadata/global-metadata.datand
lib/arm64-v8a/libil2cpp.so
- The APK contains
assets/bin/Data/Managed/Assembly-CSharp.dll(Mono build) - The IPA contains
Frameworks/UnityFramework.framework jadxshows onlyUnityPlayerActivityand Unity plumbing- You need to recover game or app logic, anti-cheat behaviour, or the API layer
When NOT to Use
- Flutter or React Native — use
reversing-flutter-appsor
- Ordinary native apps — use
testing-mobile-applications - Desktop game binaries with no Unity markers — use
analyzing-binaries - Building cheats or bypassing anti-cheat in live multiplayer services —
out of scope; that is service abuse, not assessment
Identify the Build
unzip -l target.apk | rg 'global-metadata|libil2cpp|Assembly-CSharp|libmono'| Present | Build | Difficulty |
|---|---|---|
Assembly-CSharp.dll, libmono*.so | Mono | Easy — decompile the DLL directly |
global-metadata.dat + libil2cpp.so | IL2CPP | The main path below |
Neither, but UnityFramework | iOS IL2CPP | Same as IL2CPP; extract from the decrypted IPA |
Mono builds are a short job. Pull the assembly and open it:
unzip -j target.apk 'assets/bin/Data/Managed/Assembly-CSharp.dll' -d ./out
# dnSpy / dnSpyEx / ILSpy / dotPeek — full C# source recovery, and dnSpy can edit
ilspycmd ./out/Assembly-CSharp.dll -o ./decompiledEverything below is for IL2CPP.
Recombining Metadata with the Binary
IL2CPP splits the information: libil2cpp.so holds the compiled code, global-metadata.dat holds the C# type system — class names, method names, field names, string literals. Neither is useful alone. The tools' job is to match them and produce symbols.
unzip -j target.apk 'lib/arm64-v8a/libil2cpp.so' \
'assets/bin/Data/Managed/Metadata/global-metadata.dat' -d ./in
# Il2CppDumper: the standard first attempt
Il2CppDumper ./in/libil2cpp.so ./in/global-metadata.dat ./outTypical output and what it is for:
| Artifact | Use |
|---|---|
dump.cs | Every class, method, and field with addresses — read this first |
DummyDll/ | Stub .NET assemblies; open in dnSpy/ILSpy to browse the type system comfortably |
script.json | Symbol import for IDA/Ghidra — applies names to libil2cpp.so |
stringliteral.json | String constants: URLs, keys, messages |
Il2CppInspector is the alternative when Il2CppDumper fails; it supports a different range of Unity versions and emits Ghidra and IDA scripts plus a richer C++ header. Try both before concluding a build is unsupported.
Version mismatch is the usual failure. The metadata format changes across Unity releases; the tool asks for or infers the version. Read the Unity version first and feed it explicitly:
strings ./in/libil2cpp.so | rg -m3 '20[0-9]{2}\.[0-9]+\.[0-9]+'
strings ./in/global-metadata.dat | head -5Protected builds encrypt or scramble global-metadata.dat — the header magic will be wrong and the dumpers will refuse. Signs: the file does not start with the expected metadata magic, or entropy is uniformly high. The practical route is dynamic: let the app decrypt the metadata itself, then dump it from memory once loaded.
# Dump the decrypted metadata from a running process
frida -U -f com.target.app -l dump-metadata.js
# Then feed the dumped file to Il2CppDumper as normalThat pattern — let it unpack itself, dump from memory — is the same one used for packed malware, and it is almost always cheaper than defeating the protection statically. See analyzing-binaries.
Working the Recovered Code
- Read
dump.csfor the type map. Search for the classes that matter:
anything named *Manager, *Service, *Api, *Network, *Auth, *Purchase, *Config.
- Load
script.jsoninto IDA or Ghidra solibil2cpp.soshows C# method
names instead of sub_1A2B3C. Without this step the disassembly is unreadable; with it, it reads like decompiled C#.
- Check
stringliteral.jsonfor endpoints and keys. Same reasoning as
every other framework — string constants survive everything.
- Hook at runtime with Frida using the addresses from
dump.cs. Method
addresses are relative to the libil2cpp.so base, so resolve the module base and add the offset.
// Pattern: resolve base, add the RVA from dump.cs, intercept
const base = Module.findBaseAddress('libil2cpp.so');
Interceptor.attach(base.add(0x1A2B3C), {
onEnter(args) { console.log('called with', args[1]); },
onLeave(ret) { console.log('->', ret); }
});Security-Relevant Findings
For an assessment rather than a curiosity exercise, the recurring issues:
- Hardcoded credentials and endpoints in
stringliteral.json— backend
keys, analytics tokens, cloud storage credentials.
- Client-authoritative logic. Currency, entitlements, and validation done
in C# and trusted by the server. Verify by calling the API directly.
- Weak or absent certificate pinning. Unity's networking layer is often
configured permissively; check UnityWebRequest usage.
- Local save and config tampering.
PlayerPrefsand local save files
storing values the server trusts.
- Receipt validation done client-side for in-app purchases.
Rationalizations to Reject
- "jadx shows nothing, so it's protected." Unity apps have almost nothing in
the dex by design.
- "The dumper failed, so the build can't be analyzed." Try the other dumper,
supply the Unity version explicitly, and if metadata is encrypted, dump it from memory.
- "IL2CPP is compiled, so the logic is safe." The metadata ships alongside
it. That is the whole point of the format.
- "The client validates the purchase." Then the purchase is not validated.
- "I'll read the raw arm64." Apply
script.jsonfirst. The same function is
unreadable before and routine after.
References
testing-mobile-applications— platform storage, permissions, and native layertesting-apis— the backend, where client-authoritative logic becomes a findinganalyzing-binaries— memory dumping and anti-analysis for protected builds- Il2CppDumper, Il2CppInspector, dnSpy/ILSpy, Frida, IDA/Ghidra