- name
- il2cpp
- description
- Unity IL2CPP game reversing — Il2CppDumper metadata recovery, global-metadata.dat decryption, IDA/Ghidra symbol restore via generated scripts, Frida method hooking, IAP/license bypass, and zygisk-il2cpp-dumper for obfuscated metadata.
- allowed-tools
- Bash Read Write
- metadata
- {"subdomain":"mobile","when_to_use":"unity il2cpp libil2cpp.so global-metadata.dat il2cppdumper il2cppinspector mono game reversing anti-cheat license iap inapp purchase ghidra ida symbol restore zygisk","tags":"unity, il2cpp, game, reverse-engineering, il2cppdumper, global-metadata, ghidra, frida, iap, anti-cheat","mitre_attack":"T1635, T1406, T1407"}
# Unity IL2CPP Game Reversing Playbook
> Unity IL2CPP compiles C# to C++ then to native `libil2cpp.so`. The
> managed bytecode is stripped — `jadx` and `apktool` expose 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.md` for 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 `ghidra` server) or IDA (host-side).
- Frida + Objection for runtime hooking (see `mobile/android/SKILL.md`
Frida setup section).
- **zygisk-il2cpp-dumper** for runtime metadata on protected apps:
https://github.com/Perfare/Zygisk-Il2CppDumper
## Step 1: Identify Unity IL2CPP App
```bash
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
```bash
# 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 |
```bash
# 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
```
```bash
# 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)
```python
# 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
```javascript
// 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");
}
});
```
```bash
# Load hook script
frida -U -f com.unity.targetgame -l hook-il2cpp.js --no-pause
```
### Static libil2cpp.so patch (persistent, no Frida needed)
```bash
# 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
```bash
# 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` |
```bash
# 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)
```bash
# 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)
```bash
# 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
```python
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
1. `dump.cs` excerpt showing `IsPremiumUser` with RVA comment.
2. Screenshot/screen-recording of the patched/hooked app with
premium features unlocked or anti-cheat bypassed.
3. 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.cs` may 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)
在 GitHub 查看