How to Decode Unity IL2CPP Memory Structures for Game Security Testing

This article explains the principles behind IL2CPP memory parsing, demonstrates how to use exported Unity functions via Frida to retrieve assemblies, classes, and methods, and shows techniques for identifying protobuf messages and handling version differences and anti‑tamper protections.

NetEase Smart Enterprise Tech+
NetEase Smart Enterprise Tech+
NetEase Smart Enterprise Tech+
How to Decode Unity IL2CPP Memory Structures for Game Security Testing

Introduction

GameSentry reduces the barrier for deep security testing by analyzing game protocols, function logic, address mapping, partial hot‑reloading, and automated Hooking. The tool assists testers in reverse‑engineering APKs and requires a basic understanding of Unity internals.

IL2CPP Parsing

Understanding how IL2CPP stores data in memory is essential. Below are key structures from the IL2CPP source.

typedef struct Il2CppImage {
    const char* name;
    const char* nameNoExt;
    Il2CppAssembly* assembly;
    TypeDefinitionIndex typeStart;
    uint32_t typeCount;
    TypeDefinitionIndex exportedTypeStart;
    uint32_t exportedTypeCount;
    CustomAttributeIndex customAttributeStart;
    uint32_t customAttributeCount;
    MethodIndex entryPointIndex;
    #ifdef __cplusplus
    mutable
    #endif
    Il2CppNameToTypeDefinitionIndexHashTable* nameToClassHashTable;
    const Il2CppCodeGenModule* codeGenModule;
    uint32_t token;
    uint8_t dynamic;
} Il2CppImage;
typedef struct Il2CppAssembly {
    Il2CppImage* image;
    uint32_t token;
    int32_t referencedAssemblyStart;
    int32_t referencedAssemblyCount;
    Il2CppAssemblyName aname;
} Il2CppAssembly;
typedef struct Il2CppClass {
    const Il2CppImage* image;
    void* gc_desc;
    const char* name;
    const char* namespaze;
    Il2CppType byval_arg;
    Il2CppType this_arg;
    Il2CppClass* element_class;
    Il2CppClass* castClass;
    Il2CppClass* declaringType;
    Il2CppClass* parent;
    Il2CppGenericClass* generic_class;
    const Il2CppTypeDefinition* typeDefinition;
    const Il2CppInteropData* interopData;
    Il2CppClass* klass;
    // ... many more fields ...
} Il2CppClass;
typedef struct MethodInfo {
    Il2CppMethodPointer methodPointer;
    InvokerMethod invoker_method;
    const char* name;
    Il2CppClass* klass;
    const Il2CppType* return_type;
    const ParameterInfo* parameters;
    ...
} MethodInfo;

The structures form a tree: assembly → class → method . Unity also provides exported functions to query these structures.

Key Exported Functions

DO_API(int, il2cpp_init, (const char* domain_name));
DO_API(const Il2CppImage*, il2cpp_get_corlib, ());
DO_API(void, il2cpp_add_internal_call, (const char* name, Il2CppMethodPointer method));
DO_API(Il2CppMethodPointer, il2cpp_resolve_icall, (const char* name));
// assembly
DO_API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly));
// class
DO_API(void, il2cpp_class_for_each, (void(*klassReportFunc)(Il2CppClass* klass, void* userData), void* userData));
DO_API(const Il2CppType*, il2cpp_class_enum_basetype, (Il2CppClass* klass));
DO_API(FieldInfo*, il2cpp_class_get_field_from_name, (Il2CppClass* klass, const char* name));
DO_API(const MethodInfo*, il2cpp_class_get_methods, (Il2CppClass* klass, void** iter));
DO_API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass* klass, const char* name, int argsCount));
// domain
DO_API(Il2CppDomain*, il2cpp_domain_get, ());
DO_API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain* domain, const char* name));
// method
DO_API(const Il2CppType*, il2cpp_method_get_return_type, (const MethodInfo* method));
DO_API(Il2CppClass*, il2cpp_method_get_declaring_type, (const MethodInfo* method));
DO_API(const char*, il2cpp_method_get_name, (const MethodInfo* method));

By invoking these exports from Frida JavaScript, we can enumerate assemblies, classes, and methods at runtime.

Frida Helper Functions

static r(exportName, retType, argTypes) {
    let exportPointer = null;
    if (Il2Cpp.module.findExportByName(exportName)) {
        exportPointer = Il2Cpp.module.findExportByName(exportName);
    } else {
        exportPointer = this.cModule[exportName];
    }
    if (exportPointer == null) {
        console.raise(`cannot resolve export ${exportName}`);
    }
    return new NativeFunction(exportPointer, retType, argTypes);
}
static get _classGetMethodFromName() {
    return this.r("il2cpp_class_get_method_from_name", "pointer", ["pointer", "pointer", "int"]);
}
static get _classGetMethods() {
    return this.r("il2cpp_class_get_methods", "pointer", ["pointer", "pointer"]);
}
static get _classGetName() {
    return this.r("il2cpp_class_get_name", "pointer", ["pointer"]);
}
class Il2CppClass {
    /** Gets the interfaces implemented or inherited by the current class. */
    get interfaces() {
        return Array.from(utils.nativeIterator(this, Il2Cpp.Api._classGetInterfaces, Il2Cpp.Class));
    }
    /** Gets the methods implemented by the current class. */
    get methods() {
        return Array.from(utils.nativeIterator(this, Il2Cpp.Api._classGetMethods, Il2Cpp.Method));
    }
    /** Gets the name of the current class. */
    get name() {
        return Il2Cpp.Api._classGetName(this).readUtf8String();
    }
}

Usage Notes

Unity versions differ; if an exported API changes or memory layout is modified, the tool may fail. In such cases, use pointer arithmetic with offsets as a fallback.

For versions below Unity 2018 lacking il2cpp_image_get_class_count, compute the count manually via ptr(image).add(p_size*2+4+4).readU32().

When dealing with anti‑Frida protections, ensure the exported functions are successfully registered before attempting hooks.

Protobuf Identification

All types that support protobuf serialization implement the IMessage interface. By iterating over every IMessage subclass in memory, we can intercept and hook protocol classes without needing to know whether the data is encrypted.

message = IMessage or IExtensible (IExtensible works similarly)
for (const assembly of base.assemblies()) {
    for (const clazz of assembly.image.classes) {
        const isSubIMessage = clazz.isSubclassOf(message, 1)
        if (isSubIMessage) {
            hook_proto_class(clazz, clazz.name, clazz.namespace, assembly.image.name)
        }
    }
}

Additional Considerations

Lua scripts used in games cannot be hooked in real time with the current tool; dumping Lua from memory and analyzing it is a more practical approach.

Conclusion

The article covered IL2CPP memory parsing fundamentals, protobuf class filtering, and scenarios where the tool may need adjustments. Future posts will address Lua dumping and live modification techniques.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ProtobufFridaUnityGame SecurityIL2CPPMemory Parsing
NetEase Smart Enterprise Tech+
Written by

NetEase Smart Enterprise Tech+

Get cutting-edge insights from NetEase's CTO, access the most valuable tech knowledge, and learn NetEase's latest best practices. NetEase Smart Enterprise Tech+ helps you grow from a thinker into a tech expert.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.