# Il2cpp

> 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.

- Skill: `purpleailab/il2cpp` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/il2cpp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/il2cpp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/il2cpp

---


# 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)

