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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
