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
74%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
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.mdUnity 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.
mobile/android/SKILL.md for pull methods).ghidra server) or IDA (host-side).mobile/android/SKILL.md
Frida setup section).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# 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# 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# 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 typesAfter symbol restore, navigate to IsPremiumUser, CheckLicense,
VerifyIAP, IsCheatDetected, etc. by name.
// 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# 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.apkSome apps (particularly heavily monetized games) encrypt or obfuscate
global-metadata.dat to frustrate IL2CPP reversing.
# 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| 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# 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.
# 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 modeskg_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",
},
)dump.cs excerpt showing IsPremiumUser with RVA comment.dump.cs may contain plaintext user-data class names that
reveal the developer's internal naming conventions — treat as
sensitive during an engagement.| 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 |
mobile/android/SKILL.md (Frida setup, APK pull)mobile/flutter/SKILL.md (Dart AOT — different toolchain)reverser/triage/SKILL.md (binary triage)31e1c8e
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.