Cast aside
When an IL2CPP application loads, it takes the majority of the definitions in global-metadata.dat
and turns them into runtime objects. These objects provide runtime type information to the application, and
are also used to track things like whether a class’s static constructor has been called as well as many other properties.
Let’s take a look at a snippet of the first decompilation of il2cpp::vm::MetadataCache::Initialize()
from the test project again:
s_TypeInfoDefinitionTable = (Il2CppClass **)il2cpp::utils::Memory::Calloc(
s_GlobalMetadataHeader->typeDefinitionsCount / 0x68ui64,
8ui64);
s_MethodInfoDefinitionTable = (MethodInfo **)il2cpp::utils::Memory::Calloc(
s_GlobalMetadataHeader->methodsCount / 0x38ui64,
8ui64);
Notice that for each Il2CppTypeDefinition
in the metadata, one pointer to Il2CppClass
is allocated. For each Il2CppMethodDefinition
, one pointer to MethodInfo
is allocated, and so on. We can also import these types from libil2cpp/il2cpp-class-internals.h
, however it is quite a bit of hassle because of all the macros, method definitions, #include
s and #define
s, all of which IDA hates.
Note: Once you have imported types via C headers, you must synchronize them to the idb database before they will be recognized by the IDA decompiler. Right-click the desired types in the Local Types window (you can select more than one at a time; Ctrl+A selects every type) and select Synchronize to idb to do this.
Tip: We have a handy trick for you in Il2CppInspector to help with IL2CPP header imports: in
Il2CppInspector/Il2CppInspector.Common/Cpp/UnityHeaders
, you can find IDA and
Ghidra-compatible header files for every version of Unity, not just the two files we have mentioned so far but also numerous others. We
use a script to auto-generate this (source code in the link).
Technically, we don’t need these additional headers, but there is an excellent reason to include them: it prevents us from making really dumb mistakes. Remember earlier on when I said we’d name this function because its meaning was obvious:
pImage = pGetImage(v8, *v9);
local_ImagesTable[imageIndex] = pImage;
After applying the runtime object types, this decompiles as:
pImage = pGetImage(v12, *v13);
*(&local_ImagesTable->name + imageIndex_1) = pImage;
(the last line is equivalent to local_imagesTable[imageIndex_1].name = pImage)
Why would we be storing an Il2CppImage*
in a name field? Let’s re-enable casting (which is actually the default):
pImage = (char *)pGetImage(v12, *v13);
*(const char **)((char *)&local_ImagesTable->name + imageIndex_1) = pImage;
Well this is alarming. EventInfo::name
is a char *
(as you might expect from a string). There are two possibilities: either our assumption about the purpose of pGetImage
is incorrect, or there is some data obfuscation going on. Given that pGetImage
is an external call, and that we previously detected custom decryption code that the game assembly called into UnityPlayer.dll
to access, we may have stumbled across more shenanigans. What does the corresponding test project decompilation look like?
v9 = &v2[local_GlobalMetadataHeader->stringOffset + *v3];
s_ImagesTable[imageindex_1].name = v9;
(v3
in the test project corresponds to v13
in the real application)
Sneaky! The string table has been cunningly replaced with a function call. We’ll come back to this later, but note that we only realize this at this point because we imported the IL2CPP runtime object definitions and saw that an assignment didn’t make sense. The moral of the story is to use as much information as possible to aid in your analysis, even if it doesn’t seem particularly directly relevant.
Tip: Here are some useful IDA shortcuts used during this exercise:
Go to symbol in disassembly: G, type symbol name, Enter
Rename symbol: N, variable name, Enter
Change symbol type or function signature: Y, type signature, Enter
Convert variable to struct *
:
right-click variable, select Convert to struct *…
List cross references to symbol or function:
place cursor on symbol or function signature, X
Navigate to static data from decompiler: double-click data symbol
Toggle casts: \ (backslash key)
Open local types window: Shift+F1
Create struct from C type definition:
Shift+F1, Ins, type in definition, Ctrl+Enter
Parse C header file: Ctrl+F9
Add all imported types to project database: Shift+F1, Ctrl+A, right-click, select Synchronize to idb
Search for text (in decompiler or disassembly): Alt+T
Find next text match: Ctrl+T