Example 2: Decoy global-metadata.dat file (Tale of Immortal / 鬼谷八荒)

A quick glance at global-metadata.dat reveals a bunch of non-sensical ASCII data. Initially, we suspect some form of encryption.

We search for the string global-metadata.dat in the application binary but come up blank, so we once again trace the code path from il2cpp_init to MetadataCache::Initialize. In this sample I have already renamed some symbols:

char il2cpp::MetadataCache::Initialize()
{
  

if ( qword_182A02CC0 && (qword_182A02CC8 = (__int64 (__fastcall *)(_QWORD))qword_182A02CC0(107i64), v0 = (__int64 (__fastcall *)(_QWORD))qword_182A02CC0(108i64), qword_182A02CD0 = v0, qword_182A02CC8) && v0 ) { strcpy((char *)&v60, "game.dat"); l_metadataFilePath = (const char *)&v60; } else { l_metadataFilePath = "../../resources.resource.resdata"; }

v2 = il2cpp::vm::MetadataLoader::LoadMetadataFile(l_metadataFilePath); s_GlobalMetadata = v2; v3 = 0; if ( v2 || (v2 = il2cpp::vm::MetadataLoader::LoadMetadataFile("../../resources.resource.resdata"), s_GlobalMetadata = v2, qword_182A02CD0 = 0i64, v2) ) { s_GlobalMetadataHeader = (Il2CppGlobalMetadataHeader *)v2; s_TypeInfoTable = IL2CPP_CALLOC(*(int *)(qword_182A03588 + 0x30), 8i64); s_TypeInfoDefinitionTable = IL2CPP_CALLOC(s_GlobalMetadataHeader->typeDefinitionsCount / 0x5Cui64, 8i64); s_MethodInfoDefinitionTable = IL2CPP_CALLOC((unsigned __int64)s_GlobalMetadataHeader->methodsCount >> 5, 8i64); s_GenericMethodTable = IL2CPP_CALLOC(*(int *)(qword_182A03588 + 0x40), 8i64); s_ImagesCount = s_GlobalMetadataHeader->imagesCount / 0x28ui64; s_ImagesTable = IL2CPP_CALLOC(s_ImagesCount, 0x50i64);

The highlighted code has been inserted by the developer. Without bothering to figure out what all the calls at the top do, we use our intuition to assume that development builds of the game use game.dat as the metadata file, and production builds use resources.resource.resdata – the provided global-metadata.dat file is a sneaky lie!

When we check this file in the game’s data folder, we find it does not resemble a global-metadata.dat file, however it is clearly being loaded, therefore we know there is additional encryption.

We take a look at MetadataLoader::LoadMetadataFile. Let’s first recap the important part of this function from the original source code:

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;
}

Line 1 retrieves a handle to the file, lines 2-6 check that the file has been opened correctly, line 8 maps the file into memory, line 10 closes the file handle, and lines 11-16 undo the mapping if there has been an error.

Now let’s look at Tale of Immortal’s spin on this part of the same function, which again I have partially annotated:

handle = il2cpp::os::File::Open(resourceFilePath, 3, 1, 1u, 0, &error);
    v28 = handle;
    if ( error )
    {
      v29 = (const char *)resourceFilePath;
      if ( v51 >= 0x10 )
        v29 = (const char *)resourceFilePath[0];
      sub_1800CA220("ERROR: Could not open %s", v29);
      l_pDecryptedMetadata = 0i64;
    }
    else
    {
      hMappedFile = il2cpp::utils::MemoryMappedFile::Map(handle);
      il2cpp::os::File::Close(v28, &error);
      if ( error )
      {
        sub_1800C9DA0((__int64)hMappedFile);
        l_pDecryptedMetadata = 0i64;
      }
      

else { metadataLength = calculateDecryptedMetadataLength((unkStruct *)hMappedFile); pMetadata = (char *)j_allocBytes((unsigned __int64)metadataLength); probably_memcpy(pMetadata, hMappedFile, (size_t)metadataLength); do ++l_lengthOfFirstKey; // 21 while ( metadataFirstDecryptionKey[l_lengthOfFirstKey] ); metadataLength_1 = (int)metadataLength; numBytesToCopy = (int)metadataLength - (int)l_lengthOfFirstKey; pDestBytes = j_securityMemoryAllocator((int)numBytesToCopy);

The developers have added an else clause to the file mapping error check which performs decryption (these symbols are my interpretation, in a real-world session you will have to figure out their meanings for yourself – start by naming the known variables to match those in the source code and proceed to pick it apart from there).