CtrlK
BlogDocsLog inGet started
Tessl Logo

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.

62

Quality

74%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Critical

Do not install without reviewing

Fix and improve this skill with Tessl

tessl review fix ./packages/decepticon/decepticon/skills/standard/mobile/android/il2cpp/SKILL.md
SKILL.md
Quality
Evals
Security

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

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:

FileContent
dump.csAll C# class/method/field definitions with offsets
script.jsonMachine-readable symbol map (used by IDA/Ghidra scripts)
il2cpp.hC-style struct definitions for IL2CPP internals
stringliteral.jsonAll 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

PatternDetectionCounter
XOR with static keyFirst 4 bytes XOR'd from AF 1B B1 FABrute short key or key in libil2cpp.so strings
Custom header / prepended garbageFile larger than expected; magic at offset NScan for \xAF\x1B\xB1\xFA pattern in file
RC4/AES at initlibil2cpp.so contains crypto init before metadata loadFrida 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

  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

BugSeverity
Client-side IAP bypass (server trusts client result)High 8.1
License check entirely client-sideHigh 7.5
Anti-cheat only client-side (game balance impact)Medium 5.5
Encrypted metadata recovered via runtime dumpInformational (enables further bugs)
Hardcoded API key / secret in dump.cs string literalsCritical 9.0

References

Repository
PurpleAILAB/Decepticon
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.