Key Files - trigger-segfault/TriggersTools.CatSystem2 GitHub Wiki
The CatSystem2 engine looks for 3 different key files in the install folder on launch. These keys are used as a basic form of DRM, and as a method to hide debug mode from players.
Each key file is 64 bytes long, and generated with slightly different methods. All key files are generated using the game's V_CODE
value.
Note: The keys, key_com.dat
and cs2_gk.dat
are not used in modern CatSystem2 versions. Last appearance was around 2010.
Filename | Usage | Difference |
---|---|---|
direct.dat |
Launch game without a CD/document/APP/direct is 1
|
(base behavior) |
key.dat |
Launch game from CD install when not direct launch |
Add serial number to TocSeed (for volume of %WINDOWS% path)
|
cs2_debug_key.dat |
Enable debug mode features (always checked) |
Append "@@--cs2-debug-key--@@" to V_CODE
|
key_com.dat (old)
|
Common key used before direct.dat key |
Append "@@--cs2-common-key--@@" to V_CODE
|
cs2_gk.dat (old)
|
Global key with exe hashes to prevent tampering | Encrypted MD5 hashes for variable number of exes (see below) |
Unlike key.dat
with its required volume serial number, both direct.dat
and cs2_debug_key.dat
are a constant for each game (V_CODE
).
Games requiring this key have never been encountered in assembly. However the file has appeared in a select number of trial games, like Djibril 4. This does not follow the typical 64-byte format of other keys. It contains encrypted MD5 hashes for a variable number of files that are considered "protected", in this case, we see the game binary (cs2.exe) and installer (install.exe).
Note: Newer engine versions using 64-character filenames in KIF archives seem to used char[64]
filenames here as well.
Data Type | Value | Description |
---|---|---|
char[32] |
Cs2File | Filename of cs2.exe for this game |
byte[16] |
Cs2Hash | Encrypted MD5 hash of Cs2File (Blowfish encryption using V_CODE seed as key) |
char[32] |
InstallFile | Filename of install.exe for this game |
byte[16] |
InstallHash | Encrypted MD5 hash of InstallFile (Blowfish encryption using V_CODE seed as key) |
... | ... |
These are the outlined steps required in creating a key file, however a Code Example for generating them is below.
-
Create string using V_CODE1:
- Set string to V_CODE1 value
- (cs2_debug_key.dat) Append "@@--cs2-debug-key--@@"
- (key_com.dat) Append "@@--cs2-common-key--@@"
-
Generate Seed for Mersenne Twister:
- GenerateTocSeed with new V_CODE1 string
- (key.dat) Add volume serial number (volume of %WINDOWS% path)
-
Generate Blowfish Key and File data:
- MersenneTwister.SetSeed with new Seed
- MersenneTwister.GenRand 16 values (64 bytes for encryption key)
- MersenneTwister.GenRand 16 more values (64 bytes for key file data)
-
Encrypt Key File data:
- Blowfish.SetKey with new 64-byte key (first 16 MT values)
- Blowfish.Encrypt with 64-byte file data (next 16 MT values)
🚧 This section is a work in progress
-
Create string using V_CODE1:
- Set string to V_CODE1 value
-
Generate Blowfish Key:
- GenerateTocSeed with new V_CODE1 string
-
Generate Blowfish Key and File Hash:
- Blowfish.SetKey with new Seed (treat as 4-byte array)
- Create MD5 hash from target file (16 bytes)
- Blowfish.Encrypt MD5 hash value
-
Write Global Key Entry:
- Write target file name (32 bytes)
- Write encrypted MD5 hash (16 bytes)
- (repeat step 3 for each target file, append each to key data)
- Blowfish Cipher
- Mersenne Twister (except cs2_gk.dat)
- CRC-32, BZIP2 variation (see GenerateTocSeed)
- Game
V_CODE
string -
For key.dat only: Volume serial number (of
%WINDOWS%
path) - For cs2_gk.dat only: MD5 hash algorithm
enum KeyFileType {
Direct, // "direct.dat"
Key, // "key.dat" (requires volume serial number)
Cs2DebugKey, // "cs2_debug_key.dat"
CommonKey, // "key_com.dat"
}
static class Cs2KeyFile {
// Generate the data of a key file, that can then be saved to a game's installdir
public static byte[] GenerateKeyFile(KeyFileType keyType, string vcode1) {
// "open_cs2" encountered with cs2_open.exe Toolset v4.01, not fully understood
// (Likely used in the absence of /document/APP/v_code in startup.xml)
// (V_CODE (1) executable resource is ignored... I think)
if (vcode1 == null)
vcode1 = "open_cs2";
//======== GENERATE MERSENNE TWISTER SEED ========
// Difference for "cs2_debug_key.dat": append "@@--cs2-debug-key--@@"
if (keyType == KeyFileType.Cs2DebugKey)
vcode1 += "@@--cs2-debug-key--@@";
// Difference for "key_com.dat": append "@@--cs2-common-key--@@"
if (keyType == KeyFileType.CommonKey)
vcode1 += "@@--cs2-common-key--@@";
// Use a CRC-32-style checksum (same as in KIF Archives) of vcode for Mersenne Twister seed
uint vcode1Seed = Util.GenerateTocSeed(vcode1);
// Difference for "key.dat": add volume serial number (of %WINDOWS% path)
if (keyType == KeyFileType.Key)
vcode1Seed += Util.GetVolumeSerialNumber();
//======== GENERATE BLOWFISH KEY + FILE DATA ========
// Generate MT PRNG values for the key file's Blowfish key and file data.
// (I've rarely seen Cs2 generate multiple MT values from one seed)
uint[] tmpUIntBuffer = uint[32]; // C# workaround without unsafe
byte[] blowfishKey = new byte[64]; // First 16 MT values
byte[] fileData = new byte[64]; // Next 16 MT values
// Generate 32 MT values: Blowfish key (i=0-15), File data (i=16-31)
MersenneTwister mt = new MersenneTwister();
mt.SetSeed(vcode1Seed);
for (int i = 0; i < 32; i++) {
tmpUIntBuffer[i] = mt.GenRand();
}
// C# workaround uint[] -> byte[]: Blowfish key (i=0-63), File data (i=64-127)
Buffer.BlockCopy(tmpUIntBuffer, 0, blowfishKey, 0, 64);
Buffer.BlockCopy(tmpUIntBuffer, 64, fileData, 0, 64);
//======== ENCRYPT FILE DATA ========
// Encrypt key file data using generated Blowfish key
Blowfish bf = new Blowfish();
blowfish.SetKey(blowfishKey);
blowfish.Encrypt(fileData, 0, 64); // Encrypt to the same buffer
return fileData; // Return the now-encrypted data
}
}
static class Util {
// Same as used in KIFINT archives, (modified CRC-32/BZIP2)
internal static uint GenerateTocSeed(string vcode) {
uint crc = 0xffffffff; // Initial value
for (int i = 0; i < vcode.Length; i++) {
crc ^= ((uint) vcode[i] << 24);
for (int j = 0; j < 8; j++) {
if ((crc & 0x80000000) != 0)
crc = (crc << 1) ^ 0x04c11db7; // Polynomial
else
crc <<= 1;
}
crc = ~crc; // Negate very byte instead of at the end
}
return crc;
}
// Get volume serial number of %Windows% path on the target machine
internal static uint GetVolumeSerialNumber() {
// Includes path root, thus is accepted by the WINAPI function
string rootPathName = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
// Be aware: there is a difference between hard drive and volume serial numbers
bool result = NativeMethods.GetVolumeInformation(
rootPathName,
null, 0, // (unused parameters)
out uint volumeSerialNumber,
out _, out _, null, 0); // (unused parameters)
if (!result)
throw new Win32Exception();
return volumeSerialNumber;
}
}
static class NativeMethods {
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationa
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal extern static bool GetVolumeInformation(
string rootPathName, // only input
StringBuilder volumeNameBuffer,
int volumeNameSize,
out uint volumeSerialNumber,
out uint maximumComponentLength,
out uint fileSystemFlags, // flags that we don’t need
StringBuilder fileSystemNameBuffer,
int nFileSystemNameSize);
}