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
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
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.
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(){autofieldNum = 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 issuesfor(int i = 0; i < 100; ++i){if(outFields[i] != nullptr){fetched = true;
autodataField = outFields[i];
printf("%s | Offset 0x%X | Size: 0x%X | Type: 0x%X\n",dataField->fieldName,dataField->fieldOffset,dataField->fieldSize,dataField->fieldType);
}elsebreak;
}if(fetched)printf("Dumped struct %s\n",structNameBuffer);
elseprintf("Failed to dump struct\n");
for(int i = 0; i < 100; ++i){if(outFields[i] != nullptr){outFields[i] = nullptr;
}elsebreak;
}}elseprintf("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.
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.
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.
staticconstexpr 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(){autofieldNum = 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;
autodataField = outFields[i];
//Do we need to make paddingif(currentOffset != dataField->fieldOffset){autopadAmount = dataField->fieldOffset - currentOffset;
printf(" char pad_%i[%i];\n",currentOffset,padAmount);
currentOffset += padAmount;
}//Create fieldif(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 whateverif(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;}elsebreak;
}printf("};\n");
if(fetched)printf("Dumped struct %s\n",dumpStructNameBuffer);
elseprintf("Failed to dump struct\n");
for(int i = 0; i < 100; ++i){if(outFields[i] != nullptr){outFields[i] = nullptr;
}elsebreak;
}}elseprintf("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