kulokuro

wow!

Essays and Notes

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas non dictum orci, non feugiat ex. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi egestas quam in luctus aliquet. Integer orci eros, consequat a pretium at, suscipit in nulla. Etiam ut augue eu felis finibus tempor. Donec sollicitudin a felis sed ornare. Cras vel est lectus. Proin semper euismod posuere. Duis lacinia, ipsum nec facilisis cursus, lacus arcu condimentum risus, ac consequat justo risus ac ligula. Sed ac turpis nec dolor hendrerit hendrerit. Duis id volutpat diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vivamus ac ipsum nisl. Phasellus euismod et elit non rhoncus. Proin egestas eleifend enim a ultricies.
Integer semper imperdiet tortor, at rhoncus nisi vulputate tempor. Morbi sit amet lectus malesuada, molestie ipsum id, lobortis sem. Nam finibus risus et quam consequat, sed bibendum ante dignissim. Ut suscipit varius malesuada. Sed vulputate pulvinar magna at pulvinar. Maecenas sollicitudin faucibus nisl, eu iaculis lorem posuere id. Morbi porttitor mi vitae imperdiet tempus. Fusce malesuada, leo vitae iaculis accumsan, urna libero tristique diam, eu varius felis augue ac ligula. Ut bibendum rhoncus est in rutrum. In neque justo, faucibus ac orci in, facilisis pellentesque nunc. Vestibulum ultrices, turpis id faucibus tincidunt, justo nulla semper lorem, id sollicitudin lacus dui ac nibh. Aliquam quis enim ut ante pretium feugiat. Cras consequat ultrices nunc, ac viverra urna pellentesque sed. Integer auctor quam at nisl vulputate aliquam. Maecenas vitae laoreet quam.

Zero - Preface#

As a general rhythm game player with a strong addiction, I have seen various private servers and game modifications. I tried to learn a bit of reverse engineering skills, thinking I might understand some technology, but all attempts ended in failure!!!! Of course, it wasn't completely fruitless; at least I learned something.
Project Sekai, a game I played since its initial server testing, initially didn't attract me much, so I abandoned it. However, by chance, I returned to play and have been playing ever since. I never had any motivation for reverse analysis until last year when I ranked up for a character. While ranking, I thought about writing an API to check the score line in real-time, which led to the idea of analyzing the game's network packets. Recently, I remembered this and decided to give it a try.

One - Overview#

Reading Difficulty: Easy

Task: Decrypt global-metadata.dat for further analysis with Il2CppDumper.
Target Object: Project Sekai Japanese version 3.4.0 apk
Tools: IDA Pro 7.7; 010 Editor
Reference: https://katyscode.wordpress.com/2021/02/23/il2cpp-finding-obfuscated-global-metadata/

Two - Initial Exploration#

When reverse engineering Unity games, the analysis generally revolves around libil2cpp.so, which is the core logic of the game. Since this game is packaged with il2cpp, throwing it into IDA doesn't yield a symbol table, which is when global-metadata.dat becomes necessary. Through global-metadata.dat, we can use Il2CppDumper to generate a lot of useful information to assist our reverse analysis, such as function names and structures.
Find libil2cpp.so in \lib\arm64-v8a and global-metadata.dat in \assets\bin\Data\Managed\Metadata. Try throwing these two into Il2CppDumper, but as expected, it throws an error, indicating ERROR: Metadata file not found or encrypted. Open global-metadata.dat with 010 Editor, and the file header is not the iconic AF 1B B1 FA, which basically indicates that the file is encrypted.

image

Comparing with other games, no patterns were found, ruling out simple obfuscation methods like XOR. So, we have to manually analyze the source code.

Three - Source Code Analysis#

If you have zero knowledge in this area, it is recommended to read the articles in the reference material before continuing!

Throw libil2cpp.so into IDA and wait for the analysis to complete. Based on the experience summarized in the reference material, try searching for related strings.
Press Shift + F12 to open String View, search for global-metadata, and there is only one result:

image

Click on it, press X to find all references, and again, there is only one place:

image

Looking at this function, press F5 to generate pseudo code, and the string appears in this line of code

__int64 __fastcall sub_1A5E8BC(_DWORD *a1, int *a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  // ...
  result = sub_1AEC2AC((int)"global-metadata.dat", (int)a2, a3, a4, a5, a6, a7, a8, v21, v22, v23);
  qword_864DEB8 = result;
  if ( result )
  {
    // ...
  }
  return result;
}

According to the il2cpp source code, this piece of code is very likely MetadataCache::Initialize()

bool il2cpp::vm::MetadataCache::Initialize()
{
    s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile("global-metadata.dat");
    if (!s_GlobalMetadata)
        return false;
    // ...
}

Comparing, sub_1AEC2AC() is most likely MetadataLoader::LoadMetadataFile(). Let's take a look:

_BYTE *__fastcall sub_1AEC2AC(const char *a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, char a9, int a10, void *a11)
{
  // ... variables

  sub_1AF0D80();
  v29 = "Metadata";
  v30 = 8LL;
  v12 = (unsigned __int64)(unsigned __int8)v23 >> 1;
  if ( (v23 & 1) != 0 )
    v13 = (char *)p;
  else
    v13 = (char *)&v23 + 1;
  if ( (v23 & 1) != 0 )
    v12 = v24;
  v27[0] = (__int64)v13;
  v27[1] = v12;
  sub_1A81EEC(v27, &v29);
  if ( (v23 & 1) != 0 )
    operator delete(p);
  v14 = strlen(a1);
  if ( (v31 & 1) != 0 )
    v15 = v33;
  else
    v15 = v32;
  if ( (v31 & 1) != 0 )
    v16 = *(_QWORD *)&v32[7];
  else
    v16 = (unsigned __int64)v31 >> 1;
  v29 = (char *)a1;
  v30 = v14;
  v23 = (__int64)v15;
  v24 = v16;
  sub_1A81EEC(&v23, &v29);
  if ( (v27[0] & 1) != 0 )
    v17 = v28;
  else
    v17 = (char *)v27 + 1;
  v18 = open(v17, 0);
  if ( v18 == -1 )
    goto LABEL_25;
  if ( fstat(v18, (struct stat *)&v23) == -1 )
  {
    close(v18);
    goto LABEL_25;
  }
  v19 = len[0];
  v20 = mmap(0LL, len[0], 3, 2, v18, 0LL);
  close(v18);
  if ( v20 == (_BYTE *)-1LL )
  {
LABEL_25:
    v20 = 0LL;
    goto LABEL_26;
  }
  if ( v19 >= 1 )
  {
    v21 = 0LL;
    do
    {
      v20[v21] ^= byte_67C9101[v21 & 0x7F];
      ++v21;
    }
    while ( v19 != (_DWORD)v21 );
  }
LABEL_26:
  if ( (v27[0] & 1) != 0 )
    operator delete(v28);
  if ( (v31 & 1) != 0 )
    operator delete(v33);
  return v20;
}

Comparing with the source code of MetadataLoader::LoadMetadataFile():

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

There are many common points, both have MetaData strings and file operations, confirming that these two are the same thing. As for the large number of parameters defined, a simple analysis of the function call relationship shows that only const char *a1 is truly useful, representing the file name, which in this case is global-metadata.dat.
Now, let's start renaming the variables in the generated reverse pseudo code to make it slightly more readable:

_BYTE *__fastcall sub_1AEC2AC(char *fileName, ...)
{
  unsigned __int64 v12; // x8
  char *v13; // x9
  __int64 fileNameLength; // x0
  _BYTE *v15; // x8
  unsigned __int64 v16; // x9
  const char *resourceFilePath; // x0
  int fileHandle; // w21
  int fileSize; // w20
  _BYTE *fileBuffer; // x19
  __int64 i; // x8
  __int64 maybe_resourcesDirectory; // [xsp+0h] [xbp-E0h] BYREF
  unsigned __int64 v24; // [xsp+8h] [xbp-D8h]
  void *p; // [xsp+10h] [xbp-D0h]
  size_t len[2]; // [xsp+30h] [xbp-B0h]
  __int64 maybe_dataDictionary[2]; // [xsp+80h] [xbp-60h] BYREF
  char *v28; // [xsp+90h] [xbp-50h]
  char *resourcesFolder; // [xsp+98h] [xbp-48h] BYREF
  __int64 resourcesFolderNameLength; // [xsp+A0h] [xbp-40h]
  unsigned __int8 v31; // [xsp+A8h] [xbp-38h]
  _BYTE v32[15]; // [xsp+A9h] [xbp-37h] BYREF
  _BYTE *v33; // [xsp+B8h] [xbp-28h]

  utils::Runtime::GetDataDir((__int64)&maybe_resourcesDirectory);
  resourcesFolder = "Metadata";
  resourcesFolderNameLength = 8LL;
  v12 = (unsigned __int64)(unsigned __int8)maybe_resourcesDirectory >> 1;
  if ( (maybe_resourcesDirectory & 1) != 0 )
    v13 = (char *)p;
  else
    v13 = (char *)&maybe_resourcesDirectory + 1;
  if ( (maybe_resourcesDirectory & 1) != 0 )
    v12 = v24;
  maybe_dataDictionary[0] = (__int64)v13;
  maybe_dataDictionary[1] = v12;
  utils::PathUtils::Combine(maybe_dataDictionary, &resourcesFolder);
  if ( (maybe_resourcesDirectory & 1) != 0 )
    operator delete(p);
  fileNameLength = strlen(fileName);
  if ( (v31 & 1) != 0 )
    v15 = v33;
  else
    v15 = v32;
  if ( (v31 & 1) != 0 )
    v16 = *(_QWORD *)&v32[7];
  else
    v16 = (unsigned __int64)v31 >> 1;
  resourcesFolder = fileName;
  resourcesFolderNameLength = fileNameLength;
  maybe_resourcesDirectory = (__int64)v15;
  v24 = v16;
  utils::PathUtils::Combine(&maybe_resourcesDirectory, &resourcesFolder);
  if ( (maybe_dataDictionary[0] & 1) != 0 )
    resourceFilePath = v28;
  else
    resourceFilePath = (char *)maybe_dataDictionary + 1;
  fileHandle = open(resourceFilePath, 0);
  if ( fileHandle == -1 )
    goto fileHandleError;
  if ( fstat(fileHandle, (struct stat *)&maybe_resourcesDirectory) == -1 )
  {
    close(fileHandle);
    goto fileHandleError;
  }
  fileSize = len[0];
  fileBuffer = mmap(0LL, len[0], 3, 2, fileHandle, 0LL);
  close(fileHandle);
  if ( fileBuffer == (_BYTE *)-1LL )
  {
fileHandleError:
    fileBuffer = 0LL;
    goto LABEL_26;
  }
  if ( fileSize >= 1 )
  {
    i = 0LL;
    do
    {
      fileBuffer[i] ^= byte_67C9101[i & 0x7F];
      ++i;
    }
    while ( fileSize != (_DWORD)i );
  }
LABEL_26:
  if ( (maybe_dataDictionary[0] & 1) != 0 )
    operator delete(v28);
  if ( (v31 & 1) != 0 )
    operator delete(v33);
  return fileBuffer;
}

Four - Capture The Flag#

It should be quite obvious now; the clue lies in this piece of code:

if ( fileSize >= 1 )
{
  i = 0LL;
  do
  {
    fileBuffer[i] ^= byte_67C9101[i & 0x7F];
    ++i;
  }
  while ( fileSize != (_DWORD)i );
}

It is suspected that here a key table is used to XOR decrypt the read fileBuffer, and the 0x7F indicates that the key table size is 128, which is used in a loop with the index i. Clicking into byte_67C9101 confirms our suspicion.

image

Next, it's time to write the decryption script.
XOR is symmetric, so the same code can be used for both encryption and decryption. We right-click to select Convert - Convert to C/C++ Array to export the key table, then write a C++ script:

// Generated by New Bing
#include <iostream>
#include <fstream>
#include <vector>

unsigned char KeyTable[128] = {
    0xF7, 0xA9, 0x40, 0x2F, 0x25, 0x46, 0xC2, 0xF3, 0x2C, 0xC3, 0x5C, 0x8B, 0x04, 0x15, 0xA9, 0x7E, 
    0x7A, 0xDE, 0xB6, 0x16, 0x05, 0xD7, 0xE8, 0x50, 0x44, 0x6E, 0x8B, 0x9F, 0xB7, 0xFD, 0x93, 0xEF, 
    0x5C, 0x7C, 0x2A, 0x3A, 0xD3, 0x07, 0x1B, 0x3E, 0x0E, 0xC3, 0x10, 0xB9, 0x0E, 0x93, 0x09, 0xEF, 
    0xD7, 0x7F, 0x56, 0x93, 0xFB, 0x44, 0x16, 0x1F, 0xC1, 0xF0, 0xEB, 0xB7, 0xBA, 0x22, 0x3B, 0x2E, 
    0x2F, 0x6C, 0x2E, 0x88, 0xEA, 0x6E, 0xAA, 0x72, 0xC2, 0x77, 0x08, 0x42, 0xC7, 0x2E, 0x42, 0xB0, 
    0x65, 0x9C, 0xC8, 0x38, 0xA7, 0xFF, 0xF4, 0x17, 0x65, 0x0B, 0x79, 0xE3, 0x1C, 0x63, 0x83, 0xEB, 
    0x88, 0xE1, 0x89, 0xA1, 0x65, 0x80, 0x04, 0x68, 0xD4, 0xE3, 0xD0, 0x83, 0x3C, 0xD4, 0x57, 0x92, 
    0xAE, 0x3A, 0x52, 0x1E, 0x5A, 0x02, 0x86, 0x30, 0x77, 0x16, 0x23, 0xDF, 0xF5, 0xC7, 0x55, 0x5F
};

int main()
{
    std::ifstream inputFile("global-metadata.dat", std::ios::binary);

    inputFile.seekg(0, std::ios::end);
    size_t fileSize = inputFile.tellg();
    inputFile.seekg(0, std::ios::beg);

    std::vector<uint8_t> mmapPtr(fileSize);
    inputFile.read(reinterpret_cast<char*>(mmapPtr.data()), fileSize);
    inputFile.close();

    for (size_t i = 0; i < fileSize; ++i)
    {
        mmapPtr[i] ^= KeyTable[i & 0x7F];
    }

    std::ofstream decryptedFile("DecryptedMetadata.dat", std::ios::binary);
    decryptedFile.write(reinterpret_cast<char*>(mmapPtr.data()), fileSize);
    decryptedFile.close();

    std::cout << "Finish Decrypt" << std::endl;

    return 0;
}

Run it, and open the decrypted file:

image

You can see that the file header has changed to the familiar AF 1B B1 FA, and it can also be successfully read with MetaDataStringEditor.

image

Thus, we have completed the cracking of global-metadata.dat.

Postscript:#

In fact, that pseudo code didn't need so many renamings; once I figured out fileBuffer, I didn't bother much. This method of encrypting global-metadata.dat is relatively simple and can be broken by beginners with some time spent on it.
Speaking of which, I had just cracked version 3.3.1, and the next day it updated to 3.4.0, which was a bit frustrating...
The new version's key table is different from the old one, but fortunately, the game itself doesn't update frequently; otherwise, having to redo everything every time there’s an update while IDA is chugging away in the background for over an hour would be quite torturous (.
If I have time, I might try to see if I can read libil2cpp.so directly to get the key table without needing IDA, but for someone who doesn't even understand assembly, that would probably be quite a challenge (.

The next article will be about decrypting network communication encryption.

image

Loading...
Ownership of this page data is guaranteed by blockchain and smart contracts to the creator alone.