I guess I didn’t know
This ball of string takes a couple of days to unravel even when working fairly efficiently, especially when you’re
blogging it at the same time
We dart back and forth all over the application, decompiling a vast swathe of functions and comparing constantly to the test project
decompilation and the IL2CPP source code for clues. What I have described above is basically a snippet of the entire process,
but this text encapsulates most of the techniques at hand and should be all the information needed for the enterprising
analyst to reproduce the results.
After at least two days of blood, swearing and tears, our masterpiece is ready:
struct Il2CppGlobalMetadataHeader
{
int32_t unknown00;
int32_t unknown04;
int32_t unknown08;
int32_t unknown0C;
int32_t unknown10;
int32_t unknown14;
int32_t unknown18;
int32_t unknown1C;
int32_t unknown20;
int32_t unknown24;
int32_t genericContainersOffset; // Il2CppGenericContainer
int32_t genericContainersCount;
int32_t nestedTypesOffset; // TypeDefinitionIndex
int32_t nestedTypesCount;
int32_t interfacesOffset; // TypeIndex
int32_t interfacesCount;
int32_t vtableMethodsOffset; // EncodedMethodIndex
int32_t vtableMethodsCount;
int32_t interfaceOffsetsOffset; // Il2CppInterfaceOffsetPair
int32_t interfaceOffsetsCount;
int32_t typeDefinitionsOffset; // Il2CppTypeDefinition
int32_t typeDefinitionsCount;
int32_t rgctxEntriesOffset; // Il2CppRGCTXDefinition
int32_t rgctxEntriesCount;
int32_t unknown60;
int32_t unknown64;
int32_t unknown68;
int32_t unknown6C;
int32_t imagesOffset; // Il2CppImageDefinition
int32_t imagesCount;
int32_t assembliesOffset; // Il2CppAssemblyDefinition
int32_t assembliesCount;
int32_t fieldsOffset; // Il2CppFieldDefinition
int32_t fieldsCount;
int32_t genericParametersOffset; // Il2CppGenericParameter
int32_t genericParametersCount;
int32_t fieldAndParameterDefaultValueDataOffset; // uint8_t
int32_t fieldAndParameterDefaultValueDataCount;
int32_t fieldMarshaledSizesOffset; // Il2CppFieldMarshaledSize
int32_t fieldMarshaledSizesCount;
int32_t referencedAssembliesOffset; // int32_t
int32_t referencedAssembliesCount;
int32_t attributesInfoOffset; // Il2CppCustomAttributeTypeRange
int32_t attributesInfoCount;
int32_t attributeTypesOffset; // TypeIndex
int32_t attributeTypesCount;
int32_t unresolvedVirtualCallParameterTypesOffset; // TypeIndex
int32_t unresolvedVirtualCallParameterTypesCount;
int32_t unresolvedVirtualCallParameterRangesOffset; // Il2CppRange
int32_t unresolvedVirtualCallParameterRangesCount;
int32_t windowsRuntimeTypeNamesOffset; // Il2CppWindowsRuntimeTypeNamePair
int32_t windowsRuntimeTypeNamesSize;
int32_t exportedTypeDefinitionsOffset; // TypeDefinitionIndex
int32_t exportedTypeDefinitionsCount;
int32_t unknownD8;
int32_t unknownDC;
int32_t parametersOffset; // Il2CppParameterDefinition
int32_t parametersCount;
int32_t genericParameterConstraintsOffset; // TypeIndex
int32_t genericParameterConstraintsCount;
int32_t unknownF0;
int32_t unknownF4;
int32_t metadataUsagePairsOffset; // Il2CppMetadataUsagePair
int32_t metadataUsagePairsCount;
int32_t unknown100;
int32_t unknown104;
int32_t unknown108;
int32_t unknown10C;
int32_t fieldRefsOffset; // Il2CppFieldRef
int32_t fieldRefsCount;
int32_t eventsOffset; // Il2CppEventDefinition
int32_t eventsCount;
int32_t propertiesOffset; // Il2CppPropertyDefinition
int32_t propertiesCount;
int32_t methodsOffset; // Il2CppMethodDefinition
int32_t methodsCount;
int32_t parameterDefaultValuesOffset; // Il2CppParameterDefaultValue
int32_t parameterDefaultValuesCount;
int32_t fieldDefaultValuesOffset; // Il2CppFieldDefaultValue
int32_t fieldDefaultValuesCount;
int32_t unknown140;
int32_t unknown144;
int32_t unknown148;
int32_t unknown14C;
int32_t metadataUsageListsOffset; // Il2CppMetadataUsageList
int32_t metadataUsageListsCount;
} Il2CppGlobalMetadataHeader;
We note that the string literal, string literal index and .NET symbol tables are conspicuously absent, accesses to them having been replaced by external function calls. Most everything else seems to be present and correct, although substantially re-arranged in blocks: it looks like something a human has done rather than true randomization, because several groups of fields have been clumped and moved together while retaining their order within the group. This is indicative of some feisty Ctrl+C Ctrl+V action.
We are getting close to something we can drop into an Il2CppInspector plugin now, but there’s a problem. During our caffeine-fueled header-deobfuscating rampage, we noticed some disturbing patterns in a few of the functions. By this point we’ve named many functions; let’s have a look at a snippet of il2cpp::vm::SetupFieldsLocked
. First the test project:
fieldDef = il2cpp::vm::MetadataCache::GetFieldDefinitionFromIndex(v10);
*(v11 - 8) = il2cpp::vm::MetadataCache::GetIl2CppTypeFromIndex(fieldDef->typeIndex);
*(v11 - 16) = il2cpp::vm::MetadataCache::GetStringFromIndex(fieldDef->nameIndex);
*v11 = v3;
v13 = il2cpp::vm::MetadataCache::GetIndexForTypeDefinition(v3);
*(v11 + 8) = il2cpp::vm::MetadataCache::GetFieldOffsetFromIndexLocked(v13, v10 - v9, (v11 - 16), v2);
v11 += 40i64;
++v10;
*(v11 - 28) = fieldDef->customAttributeIndex;
*(v11 - 24) = fieldDef->token;
Now Honkai Impact:
fieldDef = il2cpp::vm::MetadataCache::GetFieldDefinitionFromIndex(v12);
*(v13 - 8) = il2cpp::vm::MetadataCache::GetIl2CppTypeFromIndex(fieldDef->typeIndex);
*(v13 - 16) = il2cpp::vm::MetadataCache::GetStringFromIndex(fieldDef->customAttributeIndex);
*v13 = v3;
v15 = il2cpp::vm::MetadataCache::GetIndexForTypeDefinition(v3);
*(v13 + 8) = il2cpp::vm::MetadataCache::GetFieldOffsetFromIndexLocked(v15, (v12 - v10));
v13 += 40i64;
++v12;
*(v13 - 28) = fieldDef->nameIndex;
*(v13 - 24) = fieldDef->token;
Take a few moments to look over it carefully. Can you spot the difference?
nameIndex
and customAttributeIndex
have been switched. Shenanigans! We create a new Il2CppFieldDefinition
struct and swap the two fields around to match.
This, of course, means that not just the header has been modified. If Il2CppFieldDefinition
has also been reordered, it’s not beyond the realm of possibility that others have, too. There is good news and bad news here. The bad news is that – upon making this discovery – any metadata table in the entire file is now fair game as a candidate for having been obfuscated. The good news is that once we have the completed header in place, we have several options for deobfuscation:
- We can just run Il2CppInspector in a debugger, and every time it crashes, find which struct contains the problem value (generally a crash will occur when an index in one table entry that points to another table is out of bounds, so then you know the wrong field is being selected for the index and can assume the entire table layout is obfuscated)
- We can run Il2CppInspector in a debugger, setting a breakpoint at the end of the metadata loader then use the Autos debugger window to look at every field in each table to see if they make sense. If unsure, we can spin up a second instance with the same breakpoint and load the test project into it to perform a side-by-side comparison. We can also create a debugger plugin that dumps the first few items of each table in a human-readable format (implement the
PostProcessMetadata
hook if you want to do this)
- We know the offsets of every table now, so we can compare the tables with the ones in the test project’s
global-metadata.dat
and check if any items are out of place
- We can retrace our steps in IDA and check every table access. This is the most time-consuming but also the most accurate method, however at this point we will have located and named many functions so finding them again will be easy
For what it’s worth, I used a combination of options 1-3. Ultimately, the modified tables were
Il2CppTypeDefinition
, Il2CppMethodDefinition
, Il2CppFieldDefinition
and Il2CppPropertyDefinition
. The changes were not extremely drastic – essentially some fields
were reordered – but this is enough to defeat automated tools.