Anticheat measures found in the binaries of Counter-Strike 2. The analysis is based on the 6 June 2023 update.
Request proto | Response proto | What is does |
---|---|---|
CUserMessageRequestDllStatus | CUserMessage_DllStatus | Trusted Mode |
CUserMessageRequestUtilAction | CUserMessage_UtilMsg_Response | Checks ConVars for unathorized modifications |
CUserMessageRequestInventory | CUserMessage_Inventory_Response | Checks VMT pointers of global interfaces, checks if read-only sections of game DLLs were modified, checks VMT pointers of game entities |
CUserMessageRequestDiagnostic | CUserMessage_Diagnostic_Response | Debugger detection |
// E8 ? ? ? ? 48 8D 8C 24 ? ? ? ? E8 ? ? ? ? F6 43 20 02 (relative jump) @ client.dll
void collectInterfacesData(CUserMessage_Inventory_Response& protobuf);
This function iterates over g_pInterfaceGlobals
array (name from Source 1 engine) and for every interface fills InventoryDetail
protobuf:
message InventoryDetail {
optional int32 index = 1; // index in the g_pInterfaceGlobals array
optional int64 primary = 2; // *g_pInterfaceGlobals[index] (address of the interface)
optional int64 offset = 3; // **g_pInterfaceGlobals[index] (address of virtual method table of the interface)
optional int64 first = 4; // address of the first function in the VMT
optional int64 base = 5;
optional string name = 6;
optional string base_name = 7;
optional int32 base_detail = 8;
optional int32 base_time = 9;
optional int32 base_hash = 10; // interface name hash
}
(fields without a comment are unused)
This check is present in client.dll
under the name "ComputeInventory2".
Game DLLs register themselves by calling Plat_RegisterModule(moduleHandle)
function from tier0.dll
. The list of registered modules can be retrieved by calling Plat_GetRegisteredModules()
.
When game server sends CUserMessageRequestInventory protobuf message to the client, the client responds with CUserMessage_Inventory_Response message containing info about registered modules.
For every registered dll:
struct DllSectionsResult {
void* baseOfDll;
IMAGE_NT_HEADERS* ntHeaders;
void* buffer; // allocated with VirtualAlloc in processDllSections()
DWORD timestamp; // from IMAGE_FILE_HEADER::TimeDateStamp
DWORD pad; // padding
CSHA1 sha1; // sizeof(CSHA1) == 192, seems not to be computed currently
CRC32 readOnlySectionsHash;
};
// E8 ? ? ? ? 8B 8C 24 ? ? ? ? 0F B6 D8 (relative jump) @ client.dll
bool processDllSections(DllSectionsResult& output, HMODULE dll);
processDllSections() allocates the buffer of size IMAGE_OPTIONAL_HEADER::SizeOfImage
and does the following:
IMAGE_OPTIONAL_HEADER::SizeOfHeaders
bytes at dll base)(IMAGE_SECTION_HEADER::Characteristics & IMAGE_SCN_MEM_WRITE) == 0
) to the bufferIMAGE_DIRECTORY_ENTRY_BASERELOC
) and undoes relocations in the buffer (only IMAGE_REL_BASED_DIR64
relocations)IMAGE_DIRECTORY_ENTRY_IAT
) and export directory (IMAGE_DIRECTORY_ENTRY_EXPORT
) in the bufferprocessDllSections() is called by another function:
// E8 ? ? ? ? 0F B6 C0 85 C0 74 7B (relative jump) @ client.dll
bool processDll(
HMODULE dll,
CRC32& readOnlySectionsHash,
char* pdbPath,
DWORD& sizeOfImage,
DWORD& timestamp,
void** imageBase
);
processDll() does the following:
pdbPath
output bufferLater PDB file name is extracted from path and hashed.
InventoryDetail protobuf is filled with the gathered info.
message InventoryDetail {
optional int32 index = 1; // index in the array of registered modules
optional int64 primary = 2; // image base from IMAGE_OPTIONAL_HEADER
optional int64 offset = 3; // size of image
optional int64 first = 4;
optional int64 base = 5; // dll handle
optional string name = 6;
optional string base_name = 7;
optional int32 base_detail = 8; // crc32 of read-only sections
optional int32 base_time = 9; // timestamp from IMAGE_FILE_HEADER
optional int32 base_hash = 10; // PDB filename hash
}
(fields without a comment are unused)
At first Plat_GetDebugMonitor()
from tier0.dll
is called and the response is filled with data from DebugMonitor.
Then a thread of id provided through a parameter is opened (OpenThread(THREAD_ALL_ACCESS, 0, threadId)
) and suspended.
Thread context is then queried and the thread is resumed.
Following values are copied from thread context into response: