Metadata loader code path

First, the Unity player (Android: libunity.so, PC: UnityPlayer.dll) calls the function il2cpp_init on the main application binary, which in almost all cases is a regular export (Android: libil2cpp.so, PC: GameAssembly.dll – note these files can be renamed by the developers). The following code path is then followed:

il2cpp_init
  -> il2cpp::vm::Runtime::Init
    -> il2cpp::vm::MetadataCache::Initialize
      -> il2cpp::vm::MetadataLoader::LoadMetadataFile

These last three functions do not usually have their symbols included in the production binary, so you will need to trace a path through to them. By looking at the IL2CPP source code, we can get an idea of what we’re looking for (there is slight variation between versions). Don’t worry about reading all of this code immediately – I’ve provided sizeable snippets because we can use the lines of code around the calls as context – waypoints or milestones if you like – to help us find the correct call chain.

il2cpp_init (located in il2cpp-api.cpp, comments elided):

int il2cpp_init(const char* domain_name)
{
    setlocale(LC_ALL, "");
    return Runtime::Init(domain_name, "v4.0.30319");
}

il2cpp::vm::Runtime::Init (located in vm/Runtime.cpp):

bool Runtime::Init(const char* filename, const char *runtime_version)
{
    SanityChecks();
 
    os::Initialize();
    os::Locale::Initialize();
    MetadataAllocInitialize();
 
    s_FrameworkVersion = framework_version_for(runtime_version);
 
    os::Image::Initialize();
    os::Thread::Init();
    il2cpp::utils::RegisterRuntimeInitializeAndCleanup::ExecuteInitializations();
    

if (!MetadataCache::Initialize())

return false; Assembly::Initialize(); gc::GarbageCollector::Initialize(); Thread::Initialize(); Reflection::Initialize(); register_allocator(il2cpp::utils::Memory::Malloc); memset(&il2cpp_defaults, 0, sizeof(Il2CppDefaults)); const Il2CppAssembly* assembly = Assembly::Load("mscorlib.dll"); il2cpp_defaults.corlib = Assembly::GetImage(assembly); DEFAULTS_INIT(object_class, "System", "Object"); DEFAULTS_INIT(void_class, "System", "Void"); DEFAULTS_INIT_TYPE(boolean_class, "System", "Boolean", bool); DEFAULTS_INIT_TYPE(byte_class, "System", "Byte", uint8_t); DEFAULTS_INIT_TYPE(sbyte_class, "System", "SByte", int8_t); DEFAULTS_INIT_TYPE(int16_class, "System", "Int16", int16_t); DEFAULTS_INIT_TYPE(uint16_class, "System", "UInt16", uint16_t); DEFAULTS_INIT_TYPE(int32_class, "System", "Int32", int32_t); DEFAULTS_INIT_TYPE(uint32_class, "System", "UInt32", uint32_t); DEFAULTS_INIT(uint_class, "System", "UIntPtr"); DEFAULTS_INIT_TYPE(int_class, "System", "IntPtr", intptr_t); DEFAULTS_INIT_TYPE(int64_class, "System", "Int64", int64_t); DEFAULTS_INIT_TYPE(uint64_class, "System", "UInt64", uint64_t); DEFAULTS_INIT_TYPE(single_class, "System", "Single", float); DEFAULTS_INIT_TYPE(double_class, "System", "Double", double); DEFAULTS_INIT_TYPE(char_class, "System", "Char", Il2CppChar); DEFAULTS_INIT(string_class, "System", "String"); // ...

il2cpp::vm::MetadataCache::Initialize (located in vm/MetadataCache.cpp, comments elided):

bool il2cpp::vm::MetadataCache::Initialize()
{
    

s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile("global-metadata.dat");

if (!s_GlobalMetadata) return false; s_GlobalMetadataHeader = (const Il2CppGlobalMetadataHeader*)s_GlobalMetadata; IL2CPP_ASSERT(s_GlobalMetadataHeader->sanity == 0xFAB11BAF); IL2CPP_ASSERT(s_GlobalMetadataHeader->version == 24); s_TypeInfoTable = (Il2CppClass**)IL2CPP_CALLOC(s_Il2CppMetadataRegistration->typesCount, sizeof(Il2CppClass*)); s_TypeInfoDefinitionTable = (Il2CppClass**)IL2CPP_CALLOC(s_GlobalMetadataHeader->typeDefinitionsCount / sizeof(Il2CppTypeDefinition), sizeof(Il2CppClass*)); s_MethodInfoDefinitionTable = (const MethodInfo**)IL2CPP_CALLOC(s_GlobalMetadataHeader->methodsCount / sizeof(Il2CppMethodDefinition), sizeof(MethodInfo*)); s_GenericMethodTable = (const Il2CppGenericMethod**)IL2CPP_CALLOC(s_Il2CppMetadataRegistration->methodSpecsCount, sizeof(Il2CppGenericMethod*)); s_ImagesCount = s_GlobalMetadataHeader->imagesCount / sizeof(Il2CppImageDefinition); s_ImagesTable = (Il2CppImage*)IL2CPP_CALLOC(s_ImagesCount, sizeof(Il2CppImage)); s_AssembliesCount = s_GlobalMetadataHeader->assembliesCount / sizeof(Il2CppAssemblyDefinition); s_AssembliesTable = (Il2CppAssembly*)IL2CPP_CALLOC(s_AssembliesCount, sizeof(Il2CppAssembly)); // ...

il2cpp::vm::MetadataLoader::LoadMetadataFile (located in vm/MetadataLoader.cpp):

void* il2cpp::vm::MetadataLoader::LoadMetadataFile(const char* fileName)
{
    std::string resourcesDirectory = utils::PathUtils::Combine(utils::Runtime::GetDataDir(), utils::StringView<char>("Metadata"));
 
    std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));
 
    int error = 0;
    os::FileHandle* handle = os::File::Open(resourceFilePath, kFileModeOpen, kFileAccessRead, kFileShareRead, kFileOptionsNone, &error);
    if (error != 0)
    {
        utils::Logging::Write("ERROR: Could not open %s", resourceFilePath.c_str());
        return NULL;
    }
 
    void* fileBuffer = utils::MemoryMappedFile::Map(handle);
 
    os::File::Close(handle, &error);
    if (error != 0)
    {
        utils::MemoryMappedFile::Unmap(fileBuffer);
        fileBuffer = NULL;
        return NULL;
    }
 
    return fileBuffer;
}

All we usually have to do is bust out our decompiler and navigate through these functions. The latter two are of most interest: il2cpp::vm::MetadataLoader::LoadMetadataFile takes the filename of the metadata file and maps it into memory, while il2cpp::vm::MetadataCache::Initialize calls this function and stores the pointer to the mapped file in a static global variable, then begins reading data structures from it. Decryption and deobfuscation typically occurs in one or both of these functions, so we will want to compare them closely with the original source code for changes.