Get busy

Enough foreplay, crack open your IDAs.

The natural first place to look for accesses to Il2CppGlobalMetadataHeader is right after the file has been loaded. We already looked at the loader – il2cpp::vm::MetadataLoader::LoadMetadataFile – in part 1, so if we click on the function name and search for cross-references we’ll find the call site, il2cpp::vm::MetadataCache::Initialize. We also navigate directly to this in our empty project (which has symbols already).

The test project:

void il2cpp::vm::MetadataCache::Initialize(void)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  v0 = (const Il2CppGlobalMetadataHeader *)il2cpp::vm::MetadataLoader::LoadMetadataFile("global-metadata.dat");
  s_GlobalMetadata = (void *)v0;
  s_GlobalMetadataHeader = v0;
  s_TypeInfoTable = (Il2CppClass **)il2cpp::utils::Memory::Calloc(s_Il2CppMetadataRegistration->typesCount, 8ui64);
  s_TypeInfoDefinitionTable = (Il2CppClass **)il2cpp::utils::Memory::Calloc(
                                                s_GlobalMetadataHeader->typeDefinitionsCount / 0x68ui64,
                                                8ui64);
  s_MethodInfoDefinitionTable = (MethodInfo **)il2cpp::utils::Memory::Calloc(
                                                 s_GlobalMetadataHeader->methodsCount / 0x38ui64,
                                                 8ui64);
  s_GenericMethodTable = (const Il2CppGenericMethod **)il2cpp::utils::Memory::Calloc(
                                                         s_Il2CppMetadataRegistration->methodSpecsCount,
                                                         8ui64);
  s_ImagesCount = (unsigned __int64)s_GlobalMetadataHeader->imagesCount >> 5;
  s_ImagesTable = (Il2CppImage *)il2cpp::utils::Memory::Calloc(s_ImagesCount, 0x40ui64);
  s_AssembliesCount = s_GlobalMetadataHeader->assembliesCount / 0x44ui64;
  s_AssembliesTable = (Il2CppAssembly *)il2cpp::utils::Memory::Calloc(s_AssembliesCount, 0x60ui64);
  v1 = s_GlobalMetadataHeader;
  v2 = (char *)s_GlobalMetadata;
  v3 = (int *)((char *)s_GlobalMetadata + s_GlobalMetadataHeader->imagesOffset);
  v4 = 0;
  v5 = 0;
  v6 = s_ImagesCount;
  if ( s_ImagesCount > 0 )
// ...

Honkai Impact:

void sub_7FFF41E81660()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  qword_7FFF43D74AD0 = il2cpp::vm::MetadataLoader::LoadMetadataFromFile();
  qword_7FFF43D74AD8 = qword_7FFF43D74AD0;
  v0 = (qword_7FFF43D74AD0 + *(qword_7FFF43D74AD0 + 0x78i64));
  v46 = (qword_7FFF43D74AD0 + *(qword_7FFF43D74AD0 + 0x78i64));
  v1 = 0;
  v2 = 0;
  if ( *(qword_7FFF43D74AD0 + 0x7Ci64) / 68ui64 )
  {
    v3 = 0i64;
    do
    {
      sub_7FFF41ECDEF0(&v0[17 * v3]);
      v3 = ++v2;
    }
    while ( v2 < *(qword_7FFF43D74AD8 + 124) / 68ui64 );
  }
  qword_7FFF43D747E8 = sub_7FFF41EE0390(*(qword_7FFF43D74A38 + 48), 8i64);
  qword_7FFF43D747F0 = sub_7FFF41EE0390(*(qword_7FFF43D74AD8 + 84) / 0x68ui64, 8i64);
  qword_7FFF43D747F8 = sub_7FFF41EE0390(*(qword_7FFF43D74AD8 + 300) >> 6, 8i64);
  qword_7FFF43D74808 = sub_7FFF41EE0390(*(qword_7FFF43D74A38 + 64), 8i64);
  dword_7FFF43D74810 = *(qword_7FFF43D74AD8 + 0x74) >> 5;
  v5 = sub_7FFF41EE0390(dword_7FFF43D74810, 0x38i64);
  qword_7FFF43D74818 = v5;
  v8 = qword_7FFF43D74AD0;
  v9 = (qword_7FFF43D74AD0 + *(qword_7FFF43D74AD8 + 0x70));
  v10 = 0;
  if ( dword_7FFF43D74810 > 0 )
// ...

Clearly, by comparing these we can quickly pick off some low-hanging fruit and rename some obvious symbols, like this:

void il2cpp::vm::MetadataCache::Initialize()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  s_GlobalMetadata = il2cpp::vm::MetadataLoader::LoadMetadataFromFile();
  s_GlobalMetadataHeader = s_GlobalMetadata;
  v0 = (s_GlobalMetadata + *(s_GlobalMetadata + 0x78i64));
  v46 = (s_GlobalMetadata + *(s_GlobalMetadata + 0x78i64));
  v1 = 0;
  v2 = 0;
  if ( *(s_GlobalMetadata + 0x7Ci64) / 68ui64 )
  {
    v3 = 0i64;
    do
    {
      sub_7FFF41ECDEF0(&v0[17 * v3]);
      v3 = ++v2;
    }
    while ( v2 < *(s_GlobalMetadataHeader + 124) / 68ui64 );
  }
  qword_7FFF43D747E8 = il2cpp::utils::Memory::Calloc(*(qword_7FFF43D74A38 + 48), 8i64);
  qword_7FFF43D747F0 = il2cpp::utils::Memory::Calloc(*(s_GlobalMetadataHeader + 84) / 0x68ui64, 8i64);
  qword_7FFF43D747F8 = il2cpp::utils::Memory::Calloc(*(s_GlobalMetadataHeader + 300) >> 6, 8i64);
  qword_7FFF43D74808 = il2cpp::utils::Memory::Calloc(*(qword_7FFF43D74A38 + 64), 8i64);
  s_ImagesCount = *(s_GlobalMetadataHeader + 0x74) >> 5;
  local_ImagesTable = il2cpp::utils::Memory::Calloc(s_ImagesCount, 0x38i64);
  s_ImagesTable = local_ImagesTable;
  v8 = s_GlobalMetadata;
  v9 = (s_GlobalMetadata + *(s_GlobalMetadataHeader + 0x70));
  v10 = 0;
  if ( s_ImagesCount > 0 )

Right now all of the fields in s_GlobalMetadataHeader (the static location of Il2CppGlobalMetadataHeader) are referenced as pointer offsets. To progress further we should create an Il2CppGlobalMetadataHeader struct of our own and assign it as s_GlobalMetadataHeader‘s type.

You can use IDA’s struct editor to do this but it’s fiddly. An easier way is to just paste in a C type declaration in the Local Types window. We create an initial struct with the following definition:

typedef struct Il2CppGlobalMetadataHeader
{
    int32_t unknown00;
    int32_t unknown04;
    int32_t unknown08;
    int32_t unknown0C;
        // ...
    int32_t unknown14C;
    int32_t unknown150;
    int32_t unknown154;
} Il2CppGlobalMetadataHeader;

We then return to the decompilation and assign the type Il2CppGlobalMetadataHeader * to both s_GlobalMetadata and s_GlobalMetadataHeader. Make sure you include the “*” pointer address-of operator!

Now our code starts to look more readable:

void il2cpp::vm::MetadataCache::Initialize()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  s_GlobalMetadata = il2cpp::vm::MetadataLoader::LoadMetadataFromFile();
  s_GlobalMetadataHeader = s_GlobalMetadata;
  v0 = (&s_GlobalMetadata->unknown00 + s_GlobalMetadata->unknown78);
  v46 = s_GlobalMetadata + s_GlobalMetadata->unknown78;
  v1 = 0;
  v2 = 0;
  if ( s_GlobalMetadata->unknown7C / 68ui64 )
  {
    v3 = 0i64;
    do
    {
      sub_7FFF41ECDEF0(&v0[17 * v3]);
      v3 = ++v2;
    }
    while ( v2 < s_GlobalMetadataHeader->unknown7C / 68ui64 );
  }
  qword_7FFF43D747E8 = il2cpp::utils::Memory::Calloc(*(qword_7FFF43D74A38 + 48), 8i64);
  qword_7FFF43D747F0 = il2cpp::utils::Memory::Calloc(s_GlobalMetadataHeader->unknown54 / 0x68ui64, 8i64);
  qword_7FFF43D747F8 = il2cpp::utils::Memory::Calloc(s_GlobalMetadataHeader->unknown12C >> 6, 8i64);
  qword_7FFF43D74808 = il2cpp::utils::Memory::Calloc(*(qword_7FFF43D74A38 + 64), 8i64);
  s_ImagesCount = s_GlobalMetadataHeader->unknown74 >> 5;
  local_ImagesTable = il2cpp::utils::Memory::Calloc(s_ImagesCount, 0x38i64);
  s_ImagesTable = local_ImagesTable;
  v8 = s_GlobalMetadata;
  v9 = (s_GlobalMetadata + s_GlobalMetadataHeader->unknown70);
  v10 = 0;
  if ( s_ImagesCount > 0 )

We can now start renaming the struct fields one by one as we discover their meanings. In the code above, for example:

s_ImagesCount = s_GlobalMetadataHeader->unknown74 >> 5;

We can see both by looking at the name of the assigned variable and at the empty project code that unknown74 is really imagesCount. We rename this struct field – which we can do directly from the decompilation by clicking on the field symbol – and continue working our way through the function. There is no need to understand every line, and things that don’t match the empty project code can be ignored for now. Look for lines of code that are identical in both DLLs, or places where the table lengths are divided or right-shifted to get the item counts, where the code in both DLLs divide by the same amount (indicating a matching table entry size).

Sometimes you might want to redefine an item as an array, for example to change this:

*(v11 + local_ImagesTable) = v12;

into this:

local_ImagesTable[v11] = v12;

To do so, just change the variable type (local_ImagesTable in this case) to a pointer.

Don’t try to analyze the code line by line in order. It’s often easier to skip around picking off obvious things one by one, and then when you come back to earlier code the renamed symbols will make it easier to understand.

When it comes to divisions, remember that the compiler will often substitute division for right-shift when the divisor is a power of 2. Shifting right by 5 (>> 5) is the same as dividing by 32 (0x20), shifting right by 6 (>> 6) is the same as dividing by 64 (0x40) and so on. The compiler performs this optimization because bit-shift operators execute much faster than divide instructions at the CPU level.

An address-of operator to the first field of a struct, eg. &s_GlobalMetadata->unknown00 is equivalent to s_GlobalMetadata (it takes the address of the first member of the struct, which is the address of the struct).

Not everything will be in the same place, and this is highly dependent on compiler optimizations such as inlining and holding temporary placeholder variables. Consider this code on line 16 of the test project decompilation:

s_AssembliesCount = s_GlobalMetadataHeader->assembliesCount / 0x44ui64;

This does not exist in Honkai Impact’s decompilation. However, scrolling down a hundred lines or so in the empty project reveals:

if ( v6 > 0 )
  {
    v19 = 0i64;
    v20 = &v2[v1->assembliesOffset + 24];
    while ( 1 )
    {
      v21 = &s_AssembliesTable[v19];
      v22 = *(v20 - 6);
      v23 = v22 == -1 ? 0i64 : &s_ImagesTable[v22];
      v21->image = v23;
// ...

In Honkai Impact:

for ( i = v38; v21 < v18->unknown7C / 0x44ui64; v46 += 68 )
  {
    v23 = *v0;
    if ( v23 == -1 )
      v24 = 0i64;
    else
      v24 = s_ImagesTable + 56 * v23;
    for ( j = 0i64; j < *(v24 + 24); ++j )
    {

Although this code looks very different, the key is in the division by 0x44. In the test code, the dividend field refers to assembliesCount, and so that is likely to be the case for unknown7C in Honkai Impact.

If you’re not sure about the meaning of a field, give it a name anyway, but prefix it with something like maybe. This will help you with more named symbols as you look at other code, and you may be able to find out that it’s wrong further down the line, rather than just staring at v150 and not remembering where you last saw it.

Bear in mind that data obfuscation may periodically lead to a situation where a table entry in the test code has a different size to that in the target code (meaning that the divisor will be different), including potentially the same size as an entry in a different table. Be mindful of this if a particular table doesn’t seem to make sense.

If you find a for or while loop that iterates over a set of items (like the entries in a table or array), name the loop counter with an index-style suffix and find the symbol used in conjunction with it to retrieve an item (as an array index or pointer addition) – this is the collection that is being iterated. Oftentimes the decompiler will incorrectly produce a for, while or do loop in place of one of the other loop types. Look for counters initialized immediately before a while or do block – they will usually look like this:

v31 = 0i64;
do
{

The counter – v31 here – will then typically be referenced in the loop block (although not always).

Sometimes, you might just need to roll the dice and take a guess. Consider this code in Honkai Impact:

qword_7FFF43D747E8 = il2cpp::utils::Memory::Calloc(*(qword_7FFF43D74A38 + 0x30), 8i64);
s_TypeInfoDefinitionTable = il2cpp::utils::Memory::Calloc(
                              s_GlobalMetadataHeader->typeDefinitionsCount / 0x68ui64,
                              8i64);
s_MethodInfoDefinitionTable = il2cpp::utils::Memory::Calloc(s_GlobalMetadataHeader->methodsCount >> 6, 8i64);
qword_7FFF43D74808 = il2cpp::utils::Memory::Calloc(*(qword_7FFF43D74A38 + 64), 8i64);

We have deduced some of the symbols here but not all of them. The corresponding code in the test project is:

s_TypeInfoTable = il2cpp::utils::Memory::Calloc(s_Il2CppMetadataRegistration->typesCount, 8ui64);
s_TypeInfoDefinitionTable = il2cpp::utils::Memory::Calloc(
                              s_GlobalMetadataHeader->typeDefinitionsCount / 0x68ui64,
                              8ui64);
s_MethodInfoDefinitionTable = il2cpp::utils::Memory::Calloc(s_GlobalMetadataHeader->methodsCount / 0x38ui64, 8ui64);
s_GenericMethodTable = il2cpp::utils::Memory::Calloc(s_Il2CppMetadataRegistration->methodSpecsCount, 8ui64);

The memory allocations in the first and last lines of Honkai Impact don’t make any sense to us, but since the order of the known allocations is equivalent, we might be able to guess that the two unlabeled qwords are s_TypeInfoTable and s_GenericMethodTable and name them, even thought we can’t name anything in Il2CppGlobalMetadataHeader here and can’t be entirely sure. This is a good place to put some maybe-prefixed variable names.

When we are presented with code such as this:

v12 = qword_7FFF43D74F88(v8, *v9);
local_ImagesTable[imageIndex] = v12;

where a function in another DLL or via a function pointer is being called (you can tell this because it calls into a qword value), but the resulting assignment is obvious, we just rename the function without worrying about what it does, like this:

pImage = pGetImage(v8, *v9);
local_ImagesTable[imageIndex] = pImage;

If we need to, we can come back to it later.

It is very unlikely you’ll be able to derive every symbol in the function, nor should you try. Pick off what’s easy and move on: there is plenty of code left in the application to analyze! Eventually you’ll come unstuck and won’t be able to rename any more fields. At this point, we have to start delving into other parts of the application – which means we need to find them. How?