Reverse engineering Star Citizen's serialization system to have it reverse engineer the rest of the game for you

Star Citizen runs of a heavily modified version of Amazon Lumberyard (which is a heavily modified version of CryEngine). The game uses a class called DataCore to serialize game prefabs to disk. How could we leverage this system to our benefit?

Star Citizen runs on a heavily modified version of Lumberyard, which itself is just a heavily modified version of CryEngine. For games made in engines like Unreal and Unity, making cheats is relatively straightforward, as the engine internals are well known, and there are plenty of public tools that utilize the engine's reflection systems to greatly simplify the reverse engineering process. Interestingly enough, there is no public information (that I was able to find) about creating a similar tool for CryEngine / Lumberyard.

After painstakingly reverse engineering Star Engine for around a year, some strings in the game executable started to interest me…

DataCore

DataCore is StarEngine's serialization system. It is a class stored in gEnv (basically CryEngine's equivalent to Unreal's UEngine class) and is entirely unique to StarEngine, although it is similar to CryXMLB.

As the game's serialization system, it is responsible for saving and loading entity prefabs to disk. For example, each weapon in the game is stored in an xml format (this data is viewable through tools modding tools like StarFab), these xml files define everything including, damage, recoil, rate of fire.

Example of XML data viewed through the modding tool StarFab

<weaponStats __type="SWeaponStats" ammoCost="0" ammoCostMultiplier="1.0" burstShots="0" chargeTimeMultiplier="1.0" damageMultiplier="1.0" damageOverTimeMultiplier="1.0" fireRate="0" fireRateMultiplier="1.0" heatGenerationMultiplier="1.0" pellets="0" projectileSpeedMultiplier="1.0" soundRadiusMultiplier="1.0" useAlternateProjectileVisuals="0" useAugmentedRealityProjectiles="0">

So that means DataCore must know the offsets of all these class members if it is loading them into the game. If we reverse it, we can create a struct dumper... right?

Yes! It's actually really easy!

Thankfully for us, Star Citizen's executable is littered with helpful strings, the main function we'll be using is

CDataCore::GetStructDataFields

unsigned __int64 __fastcall CDataCore::GetStructDataFields(void* self, const char* structName, DataField** outFields, char flags)

struct DataField
{
    const char* fieldName;
    uint64_t	fieldOffset;
    uint64_t	fieldSize;
    uint64_t	fieldType;
}

fieldName is the name of the variable, if we call the function with structName "Vector3", the first fieldName would be X
fieldOffset is the offset of the variable in the struct, we can access the variable as usual with (ptr + fieldOffset)
fieldSize is the size of the field, this is useful to us for reversing fieldType, and also for creating padding when generating structs.
fieldType is an enum that tells you whether the variable is a float, bool, etc. here is a list of the ones I was able to find out.

enum FieldType
{
    TYPE_BOOL = 0x1,
    TYPE_UINT32 = 0x4,
    TYPE_FLOAT = 0xB,
    TYPE_CONSTCHAR_PTR = 0xD,
    TYPE_ENUM = 0xF,
    TYPE_STRUCT = 0x10,
    TYPE_STRUCTPTR_AND_TAG = 0x110,
}

Okay, even with just this we are ready to start dumping offsets. Below I have provided some example code (structNameBuffer is just a char array used by an ImGUI text input).

void GetStructOffsets() 
{
    auto fieldNum = globals::CSystem->GetDataCore()->GetStructDataFields(selfPtr, structNameBuffer, outFields, 1);
    //I thought that was the number of fields but i'm really not sure anymore...

    if (fieldNum > 0)
    {
        printf("Fetching struct %s\n", structNameBuffer);
        bool fetched = false;

        // doing 'fieldNum' amount of iterations here proved to cause issues
        for (int i = 0; i < 100; ++i)
        {
            if (outFields[i] != nullptr)
            {
                fetched = true;
                auto dataField = outFields[i];
                printf("%s | Offset 0x%X | Size: 0x%X | Type: 0x%X\n", dataField->fieldName, dataField->fieldOffset, dataField->fieldSize, dataField->fieldType);
            }
            else break;
        }

        if (fetched) printf("Dumped struct %s\n", structNameBuffer);
        else printf("Failed to dump struct\n");

        for (int i = 0; i < 100; ++i)
        {
            if (outFields[i] != nullptr)
            {
                outFields[i] = nullptr;
            }
            else break;
        }

    }
    else printf("Failed to find struct %s\n", structNameBuffer);
    printf("================================\n");
}

You can see that we simply call the function with a struct's name, then loop over outFields, and print out all the data.

You may be wondering now though... how do I actually find the name of structs to dump?

Looking around in IDA you might have noticed a lot of strings in the format "S*Params", all these struct names indicate that they are structs serialized with DataCore.
If we call our function with "SSpreadParams", we will get this.

SSpreadParams
min | Offset: 0x8 | Size: 0x4 | Type: 0xb
max | Offset: 0xc | Size: 0x4 | Type: 0xb
firstAttack | Offset: 0x10 | Size: 0x4 | Type: 0xb
attack | Offset: 0x14 | Size: 0x4 | Type: 0xb
decay | Offset: 0x18 | Size: 0x4 | Type: 0xb

But what if we want to dump every struct in the game without knowing the names of all of them beforehand?
By hooking DataCore::RegisterStruct at startup, we can get the names of every struct as they are being registered, and store them in a vector for dumping.

typedef void* (__thiscall*tDataCoreRegisterStruct)(
        void* a1,
        const char *a2,
        void* a3,
        void* a4,
        void* a5,
        void* a6,
        char a7,
        void* a8)

The only important parameter to us is a2, this is the struct's name. Simply hooking this function at game start and pushing a2 into a vector every time it's called will give us a list of every struct in the game.

Dumping offsets is cool and all, but what about generating C++ structs directly? Turns out we can do that as well.

static constexpr int MAX_TYPE = 0x110;
std::string typeNames[MAX_TYPE + 1];

void populateTypeNames() 
{
    typeNames[TYPE_BOOL]                = "bool";
    typeNames[TYPE_UINT32]              = "uint32_t";
    typeNames[TYPE_FLOAT]               = "float";
    typeNames[TYPE_CONSTCHAR_PTR]       = "const char *";
    typeNames[TYPE_STRUCTPTR_AND_TAG]   = "StructPointer_WithTag";
}
 
 
void DumpStruct()
{
    auto fieldNum = globals::CSystem->GetDataCore()->GetStructDataFields(selfPtr, dumpStructNameBuffer, outFields, 1);
    //I thought that was the number of fields but i'm really not sure anymore...

    if (fieldNum > 0)
    {
        printf("Dumping struct %s\n", dumpStructNameBuffer);
        printf("struct %s\n{\n", dumpStructNameBuffer);
        bool fetched = false;

        //StarCitizen::DataField* previousDataField = nullptr;
        uint32_t currentOffset = 0;

        for (int i = 0; i < 100; ++i)
        {
            if (outFields[i] != nullptr)
            {
                fetched = true;
                auto dataField = outFields[i];

                //Do we need to make padding
                if (currentOffset != dataField->fieldOffset) 
                {
                    auto padAmount = dataField->fieldOffset - currentOffset;
                    printf("    char pad_%i[%i];\n", currentOffset, padAmount);
                    currentOffset += padAmount;
                }  

                //Create field
                if (dataField->fieldType > MAX_TYPE) //unknown type
                {
                    printf("    char UNK_%s[%i];", dataField->fieldName, dataField->fieldSize);
                    printf("        //UNKNOWN TYPE OFFSET [0x%X] | SIZE [0x%X] | TYPE [0x%X]\n", dataField->fieldOffset, dataField->fieldSize, dataField->fieldType);
                    currentOffset += dataField->fieldSize;
                } else
                {
                    if (typeNames[dataField->fieldType].empty()) //unknown type
                    {
                        printf("    char UNK_%s[%i];", dataField->fieldName, dataField->fieldSize);
                        printf("        //UNKNOWN TYPE OFFSET [0x%X] | SIZE [0x%X] | TYPE [0x%X]\n", dataField->fieldOffset, dataField->fieldSize, dataField->fieldType);
                        currentOffset += dataField->fieldSize;
                    }
                    else
                    {
                        //i probably shouldve just used a switch for this instead of an array, i thought it'd be cool but whatever
                        if (dataField->fieldType == 0x110)
                        {
                            printf("    void* p_%s;", dataField->fieldName);
                            printf("        //OFFSET [0x%X] | SIZE [0x%X]\n", dataField->fieldOffset, dataField->fieldSize - 8);
                            printf("    int64_t tag_%s;", dataField->fieldName);
                            printf("        //OFFSET [0x%X] | SIZE [0x%X]\n", dataField->fieldOffset  + 8, dataField->fieldSize - 8);
                        } else
                        {
                            printf("    %s %s;", typeNames[dataField->fieldType].c_str(), dataField->fieldName);
                            printf("        //OFFSET [0x%X] | SIZE [0x%X]\n", dataField->fieldOffset, dataField->fieldSize);
                        }
                        currentOffset += dataField->fieldSize;
                    }
                }
                
                //printf("%s | Offset 0x%X | Size: 0x%X | Type: 0x%X\n", dataField->fieldName, dataField->fieldOffset, dataField->fieldSize, dataField->fieldType);
                //previousDataField = dataField;
                
            }
            else break;
        }

        printf("};\n");

        if (fetched) printf("Dumped struct %s\n", dumpStructNameBuffer);
        else printf("Failed to dump struct\n");

        for (int i = 0; i < 100; ++i)
        {
            if (outFields[i] != nullptr)
            {
                outFields[i] = nullptr;
            }
            else break;
        }
    }
    else printf("Failed to find struct %s\n", dumpStructNameBuffer);
    printf("================================\n");
}

Below is an example of the struct generator in action. This tool automates almost all of the reverse engineering process that goes into finding exploits