Unity IL2CPP Game Reversing Playbook
Unity IL2CPP compiles C# to C++ then to native
libil2cpp.so. The managed bytecode is stripped —jadxandapktoolexpose only the thin Java bootstrap and reveal nothing of game logic. This playbook recovers readable symbols and hooks runtime methods for license/IAP bypass and vulnerability assessment.
Prerequisites
- APK obtained (see
mobile/android/SKILL.mdfor pull methods). - Il2CppDumper (Windows .NET or mono CLI): https://github.com/Perfare/Il2CppDumper
- Il2CppInspector (alternative with plugin support): https://github.com/djkaty/Il2CppInspector
- Ghidra (via MCP
ghidraserver) or IDA (host-side). - Frida + Objection for runtime hooking (see
mobile/android/SKILL.mdFrida setup section). - zygisk-il2cpp-dumper for runtime metadata on protected apps: https://github.com/Perfare/Zygisk-Il2CppDumper
Step 1: Identify Unity IL2CPP App
unzip -o base.apk -d /tmp/apk-out/
# Confirm IL2CPP backend
ls /tmp/apk-out/lib/arm64-v8a/
# Must contain: libil2cpp.so
ls /tmp/apk-out/assets/bin/Data/Managed/Metadata/
# Must contain: global-metadata.dat
# If only libmono.so present → Mono backend (smali/jadx works; this skill N/A)
# If libil2cpp.so present but no global-metadata.dat → encrypted/obfuscated (go to Step 5)
file /tmp/apk-out/lib/arm64-v8a/libil2cpp.so
# output: ELF 64-bit LSB shared object, ARM aarch64
Step 2: Recover Symbols with Il2CppDumper
# Extract libs from APK
cp /tmp/apk-out/lib/arm64-v8a/libil2cpp.so /tmp/
cp /tmp/apk-out/assets/bin/Data/Managed/Metadata/global-metadata.dat /tmp/
# Run Il2CppDumper (mono CLI on Linux/macOS)
mono Il2CppDumper.exe /tmp/libil2cpp.so /tmp/global-metadata.dat /tmp/dump-output/
# Windows .NET:
# Il2CppDumper.exe <libil2cpp.so> <global-metadata.dat> <output-dir>
Output files:
| File | Content |
|---|---|
dump.cs |
All C# class/method/field definitions with offsets |
script.json |
Machine-readable symbol map (used by IDA/Ghidra scripts) |
il2cpp.h |
C-style struct definitions for IL2CPP internals |
stringliteral.json |
All managed string literals with addresses |
# Quick scan of dump.cs for interesting classes
grep -i "licen\|premium\|iap\|purchase\|unlock\|cheat\|anti\|integrity" /tmp/dump-output/dump.cs | head -30
# Find method offsets for hooks
grep -A2 "IsPremium\|CheckLicense\|VerifyReceipt\|IsSubscribed" /tmp/dump-output/dump.cs
# Output: // RVA: 0x<offset> — this is the function RVA in libil2cpp.so
Step 3: Apply Symbols in Ghidra / IDA
Ghidra (via MCP ghidra server — batch mode)
# 1. Import libil2cpp.so into Ghidra project
# 2. Run auto-analysis (aarch64)
# 3. Execute the Il2CppDumper Ghidra script:
# Script: ghidra_with_struct.py (from Il2CppDumper/tools/)
# Input: script.json + il2cpp.h
# 4. All methods now have their managed C# names
# Command-line Ghidra headless analysis + script
"$GHIDRA_HOME/support/analyzeHeadless" /tmp/ghidra-project IL2CPP \
-import /tmp/libil2cpp.so \
-postScript ghidra_with_struct.py /tmp/dump-output/script.json \
-processor AARCH64:LE:64:v8A \
-noanalysis
IDA (host-side)
# In IDA scripting console (Python):
# Run ida_with_struct_py3.py from Il2CppDumper/tools/
# File → Script File → ida_with_struct_py3.py
# Provide path to script.json when prompted
# IDA applies all function names + struct types
After symbol restore, navigate to IsPremiumUser, CheckLicense,
VerifyIAP, IsCheatDetected, etc. by name.
Step 4: Frida Runtime Hooking
Hook via RVA from dump.cs
// Read RVA from dump.cs comment line: // RVA: 0x<hex>
// Base address of libil2cpp.so changes per run; use Module.findBaseAddress
var il2cpp_base = Module.findBaseAddress("libil2cpp.so");
// Example: hook IsPremiumUser at RVA 0x1A4F80
var RVA = 0x1A4F80;
var isPremium = il2cpp_base.add(RVA);
Interceptor.attach(isPremium, {
onEnter: function(args) {
console.log("[+] IsPremiumUser called");
},
onLeave: function(retval) {
console.log("[+] Original return:", retval.toInt32());
retval.replace(ptr(1)); // return true
console.log("[+] Replaced with: 1");
}
});
# Load hook script
frida -U -f com.unity.targetgame -l hook-il2cpp.js --no-pause
Static libil2cpp.so patch (persistent, no Frida needed)
# Patch return value of IsPremiumUser at computed file offset
python3 - <<'EOF'
import struct
RVA = 0x1A4F80
LOAD_OFFSET = 0x0 # verify with readelf -l libil2cpp.so
with open("/tmp/libil2cpp.so", "r+b") as f:
file_offset = RVA - LOAD_OFFSET
f.seek(file_offset)
# AArch64: MOV W0, #1 (0x20008052) + RET (0xC003_5FD6)
f.write(b"\x20\x00\x80\x52\xC0\x03\x5F\xD6")
print(f"[+] Patched at file offset 0x{file_offset:X}")
EOF
# Repack APK
apktool b /tmp/apk-smali/ -o /tmp/patched.apk
# Replace libs/arm64-v8a/libil2cpp.so with patched version
zip -u /tmp/patched.apk lib/arm64-v8a/libil2cpp.so
uber-apk-signer.jar --allowResign -a /tmp/patched.apk -o /tmp/
adb install /tmp/patched-aligned-signed.apk
Step 5: Encrypted / Obfuscated global-metadata.dat
Some apps (particularly heavily monetized games) encrypt or obfuscate
global-metadata.dat to frustrate IL2CPP reversing.
Detect obfuscation
# Check magic bytes — valid global-metadata starts with: AF 1B B1 FA
xxd /tmp/global-metadata.dat | head -2
# If first 4 bytes ≠ AF 1B B1 FA → encrypted/custom header
Common obfuscation patterns
| Pattern | Detection | Counter |
|---|---|---|
| XOR with static key | First 4 bytes XOR'd from AF 1B B1 FA | Brute short key or key in libil2cpp.so strings |
| Custom header / prepended garbage | File larger than expected; magic at offset N | Scan for \xAF\x1B\xB1\xFA pattern in file |
| RC4/AES at init | libil2cpp.so contains crypto init before metadata load |
Frida hook on il2cpp_codegen_initialize_method |
# Search libil2cpp.so for crypto key material near metadata init
r2 -qc 'iz~metadata\|iz~global' /tmp/libil2cpp.so | head -20
strings /tmp/libil2cpp.so | grep -iE "meta|key|init" | head -20
zygisk-il2cpp-dumper (runtime dump, bypasses all static obfuscation)
# Install Zygisk-Il2CppDumper module via Magisk Manager
# Flash zip: ZygiskIl2CppDumper-v<version>.zip
# Configure target package in /data/adb/modules/zygisk_il2cpp_dumper/config.json
cat /data/adb/modules/zygisk_il2cpp_dumper/config.json
# { "package_name": "com.unity.targetgame" }
# Launch the target app
adb shell am start -n com.unity.targetgame/.MainActivity
# Dumped files appear in /data/local/tmp/il2cpp_dump/
adb pull /data/local/tmp/il2cpp_dump/
ls il2cpp_dump/
# global-metadata.dat libil2cpp.so (decrypted at runtime)
Feed the runtime-dumped files to Il2CppDumper per Step 2.
Step 6: Il2CppInspector (Alternative — Richer Output)
# Il2CppInspector CLI mode
mono Il2CppInspector.exe \
--select-outputs Frida \
--output /tmp/frida-hooks.js \
/tmp/libil2cpp.so /tmp/global-metadata.dat
# Produces a ready-to-load Frida script with all class/method stubs
# Load and customize the method of interest
# Also supports IDA, C# pseudo-code, and Roslyn output modes
Evidence
kg_add_node(
kind="finding",
label="Unity IL2CPP client-side IAP bypass",
props={
"key": f"il2cpp-iap-bypass::{package_id}",
"severity": "high",
"cvss": 8.1,
"package": package_id,
"hooked_method": "IsPremiumUser / VerifyReceipt",
"rva": "0x<from-dump.cs>",
"bypass_proof": "Frida hook returns true; premium features unlocked",
},
)
kg_add_node(
kind="finding",
label="Unity IL2CPP anti-cheat bypass",
props={
"key": f"il2cpp-anticheat-bypass::{package_id}",
"severity": "medium",
"method": "IsCheatDetected",
"details": "Client-only check; server-authoritative validation absent",
},
)
ZFP
dump.csexcerpt showingIsPremiumUserwith RVA comment.- Screenshot/screen-recording of the patched/hooked app with premium features unlocked or anti-cheat bypassed.
- Frida console output showing hook fired + return value replaced.
OPSEC Notes
- Il2CppDumper runs entirely offline on extracted APK files. No network activity required for analysis.
- Static patching changes the APK signature; Play Integrity / SafetyNet will flag it. Use Frida hooks on a rooted device for non-persistent testing.
- zygisk-il2cpp-dumper requires Zygisk (Magisk Delta or native Zygisk). It runs in the app process at startup and can be detected by some anti-cheat engines (EAC, BattlEye mobile). Use only in scope.
- Dumped
dump.csmay contain plaintext user-data class names that reveal the developer's internal naming conventions — treat as sensitive during an engagement.
Severity Table
| Bug | Severity |
|---|---|
| Client-side IAP bypass (server trusts client result) | High 8.1 |
| License check entirely client-side | High 7.5 |
| Anti-cheat only client-side (game balance impact) | Medium 5.5 |
| Encrypted metadata recovered via runtime dump | Informational (enables further bugs) |
Hardcoded API key / secret in dump.cs string literals |
Critical 9.0 |
References
- Il2CppDumper: https://github.com/Perfare/Il2CppDumper
- Zygisk-Il2CppDumper: https://github.com/Perfare/Zygisk-Il2CppDumper
- Il2CppInspector: https://github.com/djkaty/Il2CppInspector
- Cross-ref:
mobile/android/SKILL.md(Frida setup, APK pull) - Cross-ref:
mobile/flutter/SKILL.md(Dart AOT — different toolchain) - Cross-ref:
reverser/triage/SKILL.md(binary triage)