iOS Unity reverse engineering guide
Overview
Unity iOS apps that use IL2CPP compile C# code into native code inside UnityFramework. Static analysis usually starts with the IPA, global-metadata.dat, and the Unity framework binary. Dynamic analysis then maps IL2CPP metadata back to runtime addresses so functions can be inspected while the app runs.
This workflow covers:
- Extracting the IPA and required IL2CPP files.
- Processing IL2CPP metadata to recover method names and signatures.
- Using Radare2 for static analysis of Unity logic.
- Using r2frida for runtime inspection on a jailbroken device.
The goal is to map compiled code back to useful C# context, identify functions of interest, and inspect runtime state without losing track of the binary’s loaded base address.
Process
Use this high-level process before moving into the detailed steps:
- Initial setup
- Acquire the IPA from the device.
- Extract the files required by Il2CppDumper.
- Process the metadata and generate
script.json.
- Function selection
- Review
script.jsonfor security-relevant or behaviorally important functions. - Use method names, signatures, and addresses to choose analysis targets.
- Examine candidate functions statically before attaching to the live process.
- Review
- Code review
- Compare disassembly, decompiler output, metadata names, and nearby string references.
- Identify the function’s purpose, inputs, return value, and side effects.
- Record assumptions before testing them dynamically.
- Dynamic analysis
- Set breakpoints on selected functions.
- Use
:dr.to read register values during execution. - Treat register writes and memory patches as explicit experiments that should be documented.
- Runtime experiments
- Patch memory when a controlled test requires a behavior change.
- Use
:dxcto call functions with specific parameters. - Use Frida scripts to create objects, call methods, and observe runtime behavior.
Requirements
Hardware requirements
- Jailbroken iOS device
Software requirements
- frida-ios-dump (for downloading IPA from device)
- radare2
- r2frida
IL2CPP analysis workflow
Step 1: IPA extraction
- Download and extract the IPA file from the iPhone.
Step 2: extract required files
- Extract
Data/Managed/Metadata/global-metadata.datfrom the IPA. - Extract the Unity framework from
Frameworks/UnityFramework.framework/UnityFramework.
Note:
UnityFrameworkcontains the compiled Unity logic. UseUnityFrameworkandglobal-metadata.dattogether to recover function addresses, names, and signatures. The metadata file provides the mapping between IL2CPP functions and their original C# context.
Step 3: IL2CPP processing
- Process both extracted files with Il2CppDumper.
- The output includes
script.json. - Since Il2CppDumper targets .NET, run it in Ubuntu with Mono:
- Ubuntu provides the Linux environment.
- Mono runs the .NET executable on Linux.
- The container makes the process repeatable across host platforms.
# Dockerfile FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y \ wget \ curl \ unzip \ mono-complete \ xvfb \ && rm -rf /var/lib/apt/lists/* WORKDIR /app RUN wget -O Il2CppDumper.zip "https://github.com/Perfare/Il2CppDumper/releases/download/v6.7.32/Il2CppDumper-v6.7.32.zip" && \ unzip Il2CppDumper.zip -d /app && \ rm Il2CppDumper.zip VOLUME /data RUN mkdir -p /app/output && chmod -R 777 /app COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh ENTRYPOINT ["/app/entrypoint.sh"]
# entrypoint.sh #!/bin/bash set -e Xvfb :99 -screen 0 1024x768x16 & export DISPLAY=:99 if [ $# -eq 0 ]; then echo "Il2CppDumper Docker Container" echo "Usage: docker run -v /path/to/your/files:/data il2cppdumper [executable] [metadata] [output-dir]" echo "Example: docker run -v \$(pwd):/data il2cppdumper GameAssembly.dll global-metadata.dat output" exec bash else EXE_FILE="$1" METADATA_FILE="$2" OUTPUT_DIR="$3" if [[ ! "$EXE_FILE" =~ ^/data/ ]]; then EXE_FILE="/data/$EXE_FILE" fi if [[ ! "$METADATA_FILE" =~ ^/data/ ]]; then METADATA_FILE="/data/$METADATA_FILE" fi if [[ ! "$OUTPUT_DIR" =~ ^/data/ ]]; then OUTPUT_DIR="/data/$OUTPUT_DIR" fi mkdir -p "$OUTPUT_DIR" cd /data mono /app/Il2CppDumper.exe "$EXE_FILE" "$METADATA_FILE" "$OUTPUT_DIR" chmod -R 777 "$OUTPUT_DIR" fiTo use the Docker container:
- Build the image:
docker build -t il2cppdumper . - Run the container with your files:
docker run -v $(pwd):/data il2cppdumper UnityFramework global-metadata.dat output
- The
il2cpp.r2.jsscript is used to process the metadata:(function() { function flagName(s) { return r2.call('fD ' + s).trim(); } const baddr = r2.cmd("?vx il.baddr 2> /dev/null"); const script = JSON.parse(r2.cmd("cat script.json")); const commands = []; console.error("Using il.baddr = " + baddr); console.error("Loading methods..."); for (const method of script.ScriptMethod) { const fname = flagName(method.Name); const faddr = method.Address + baddr; commands.push("f sym.il." + fname + " = " + faddr); } console.error("Loading strings..."); for (const str of script.ScriptString) { const fname = flagName(str.Value); const faddr = str.Address + baddr; commands.push("f str.il." + fname + " = " + faddr); } console.error("Loading IL metadata..."); for (const meta of script.ScriptMetadata) { const fname = flagName(meta.Name) + (meta.Address & 0xfff); const faddr = meta.Address + baddr; commands.push("f il.meta." + fname + " = " + faddr); } console.error("Loading IL methods metadata..."); for (const meta of script.ScriptMetadataMethod) { const fname = flagName(meta.Name) + (meta.Address & 0xfff); const faddr = meta.Address + baddr; commands.push("f il.meta.method." + fname + " = " + faddr); } console.error("Importing flags..."); for (const cmd of commands) { r2.cmd0(cmd); } })();The script performs several functions:
- Base address resolution:
- Reads the IL2CPP binary base address from
il.baddr. - Uses that base address to calculate runtime addresses.
- Reads the IL2CPP binary base address from
- Method symbolication:
- Reads all methods from
script.json. - Creates Radare2 flags for each method using the format
sym.il.<methodName>. - Adds the base address to each method address.
- Reads all methods from
- String resolution:
- Processes strings from the IL2CPP metadata.
- Creates flags for each string using the format
str.il.<stringValue>. - Helps locate string references in the code.
- Metadata processing:
- Handles IL metadata that contains type information.
- Creates flags for metadata entries using
il.meta.<name>. - Uses
& 0xfffto reduce flag-name collisions for metadata entries.
- Method metadata processing:
- Processes additional method metadata.
- Creates flags using
il.meta.method.<name>. - Helps connect methods to signatures and related metadata.
- Flag import:
- Imports all created flags into Radare2.
- Enables navigation by recovered IL2CPP names.
- Makes address-level analysis easier to correlate with original code concepts.
This script matters because it:
- Connects IL2CPP native code to recovered C# method names.
- Adds stable Radare2 flags for navigation.
- Helps correlate addresses, strings, metadata, and method signatures.
- Base address resolution:
Step 4: Radare2 analysis
- Load
UnityFrameworkin Radare2.$ r2 UnityFramework [0x100000000]> - Run the
aaecommand.[0x100000000]> aae [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for objc references [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information [x] Use -AA or aaaa to perform a complete analysis - Execute
. ./il2cpp.r2.jsto symbolicate the binary.[0x100000000]> . ./il2cpp.r2.js [*] Loading IL2CPP metadata... [*] Symbolicated 1234 functions [0x100000000]>
Step 5: function analysis
- Review
script.jsonto identify functions of interest.{ "functions": [ { "name": "GameManager.Update", "address": "0x100123456", "signature": "void GameManager.Update()" } ] } - Navigate to the function with
s <address>.[0x100000000]> s 0x100123456 [0x100123456]> - Run
pdto view symbolicated disassembly.[0x100123456]> pd ;-- GameManager.Update: 0x100123456 55 push rbp 0x100123457 4889e5 mov rbp, rsp 0x10012345a 4883ec20 sub rsp, 0x20 0x10012345e 488b05f3ffffff mov rax, qword [0x100123458] 0x100123464 488b00 mov rax, qword [rax]
Step 6: dynamic analysis setup
- Use r2frida to attach to the process running on the phone, then calculate the base address.
$ r2 frida://launch/usb//APP [0x100000000]> [0x100000000]> :dm~Unity~r-x 0x0000000102534000 - 0x000000010fa40000 r-x /private/var/containers/Bundle/Application/74E092E6-144F-4D01-86F9-0AB6A87708AA/CashmanCasino.app/Frameworks/UnityFramework.framework/UnityFramework [0x100000000]> f baseaddr = 0x0000000102534000
Note:
:switches to the Frida command context.
Step 7: calculate runtime address
- Use the base address from Step 6 to calculate a runtime address.
[0x100000000]> ?v 0x100123456 + <function address from script.json> 0x200123456 # This is our runtime address of the function in the UnityFramework.
Step 8: dynamic analysis operations
- Set breakpoints:
:db <calculated-address>[0x100000000]> :db 0x200123456 - Write to memory:
[0x100000000]> wx 90909090 @ 0x200123456 [0x100000000]> pd 5 @ 0x200123456 0x200123456 90 nop 0x200123457 90 nop 0x200123458 90 nop 0x200123459 90 nopNote: The single quote prevents the REPL from interpreting special characters such as
;. - Call functions with parameters:
dcx <function-address> <parameter-address>[0x100000000]> dcx GameManager.Update [0x100000000]> - Use Frida script constructs:
[0x100000000]> :. ./<scriptname>.js ObjC.perform(() => { var GameManager = ObjC.classes.GameManager; var originalUpdate = GameManager['- update'].implementation; GameManager['- update'].implementation = function() { console.log('Update called'); originalUpdate.call(this); }; });
Credits
- @as0ler (murphy)
- Hitchhiker’s guide for Unity: reversing iOS games - Murphy’s presentation on IL2CPP analysis techniques