Engine: rebuild CClientState::ProcessCreateStringTable()

Rebuild CClientState::ProcessCreateStringTable() and add notes for multiple 'potential' exploit vectors that are currently in place. Also add wrappers for compressing/decompressing net packets.
This commit is contained in:
Kawe Mazidjatari 2024-02-19 20:43:22 +01:00
parent ee64d07e5b
commit f18a4c17a4
8 changed files with 318 additions and 15 deletions

View File

@ -185,6 +185,24 @@ public:
///////////////////////////////////////////////////////////////////////////////////////
// server messages:
///////////////////////////////////////////////////////////////////////////////////////
class SVC_CreateStringTable : public CNetMessage
{
public:
const char* m_szTableName;
int m_nMaxEntries;
int m_nNumEntries;
char m_bUserDataFixedSize;
char _padding0[3];
int m_nUserDataSize;
int m_nUserDataSizeBits;
int m_nDictFlags;
int m_nLength;
bf_read m_DataIn;
bf_write m_DataOut;
char m_bDataCompressed;
char m_szTableNameBuffer[260];
};
class SVC_Print : public CNetMessage
{
public:

View File

@ -12,6 +12,7 @@
#include "tier0/frametask.h"
#include "engine/common.h"
#include "engine/host.h"
#include "engine/host_cmd.h"
#ifndef CLIENT_DLL
#include "engine/server/server.h"
#endif // !CLIENT_DLL
@ -173,6 +174,12 @@ bool CClientState::VProcessServerTick(CClientState* thisptr, SVC_ServerTick* msg
}
}
//------------------------------------------------------------------------------
// Purpose: processes string commands sent from server
// Input : *thisptr -
// *msg -
// Output : true on success, false otherwise
//------------------------------------------------------------------------------
bool CClientState::_ProcessStringCmd(CClientState* thisptr, NET_StringCmd* msg)
{
CClientState* const thisptr_ADJ = thisptr->GetShiftedBasePointer();
@ -207,6 +214,99 @@ bool CClientState::_ProcessStringCmd(CClientState* thisptr, NET_StringCmd* msg)
return true;
}
//------------------------------------------------------------------------------
// Purpose: create's string tables from string table data sent from server
// Input : *thisptr -
// *msg -
// Output : true on success, false otherwise
//------------------------------------------------------------------------------
bool CClientState::_ProcessCreateStringTable(CClientState* thisptr, SVC_CreateStringTable* msg)
{
CClientState* const cl = thisptr->GetShiftedBasePointer();
if (!cl->IsConnected())
return false;
CNetworkStringTableContainer* const container = cl->m_StringTableContainer;
// Must have a string table container at this point!
if (!container)
{
Assert(0);
COM_ExplainDisconnection(true, "String table container missing.\n");
v_Host_Disconnect(true);
return false;
}
container->AllowCreation(true);
const ssize_t startbit = msg->m_DataIn.GetNumBitsRead();
CNetworkStringTable* const table = (CNetworkStringTable*)container->CreateStringTable(false, msg->m_szTableName,
msg->m_nMaxEntries, msg->m_nUserDataSize, msg->m_nUserDataSizeBits, msg->m_nDictFlags);
table->SetTick(cl->GetServerTickCount());
CClientState__HookClientStringTable(cl, msg->m_szTableName);
if (msg->m_bDataCompressed)
{
// TODO[ AMOS ]: check sizes before proceeding to decode
// the string tables
unsigned int msgUncompressedSize = msg->m_DataIn.ReadLong();
unsigned int msgCompressedSize = msg->m_DataIn.ReadLong();
size_t uncompressedSize = msgUncompressedSize;
size_t compressedSize = msgCompressedSize;
bool bSuccess = false;
// TODO[ AMOS ]: this could do better. The engine does UINT_MAX-3
// which doesn't look very great. Clamp to more reasonable values
// than UINT_MAX-3 or UINT_MAX/2? The largest string tables sent
// are settings layout string tables which are roughly 256KiB
// compressed with LZSS. perhaps clamp this to something like 16MiB?
if (msg->m_DataIn.TotalBytesAvailable() > 0 &&
msgCompressedSize <= (unsigned int)msg->m_DataIn.TotalBytesAvailable() &&
msgCompressedSize < UINT_MAX / 2 && msgUncompressedSize < UINT_MAX / 2)
{
// allocate buffer for uncompressed data, align to 4 bytes boundary
uint8_t* const uncompressedBuffer = new uint8_t[PAD_NUMBER(msgUncompressedSize, 4)];
uint8_t* const compressedBuffer = new uint8_t[PAD_NUMBER(msgCompressedSize, 4)];
msg->m_DataIn.ReadBytes(compressedBuffer, msgCompressedSize);
// uncompress data
bSuccess = NET_BufferToBufferDecompress(compressedBuffer, compressedSize, uncompressedBuffer, uncompressedSize);
bSuccess &= (uncompressedSize == msgUncompressedSize);
if (bSuccess)
{
bf_read data(uncompressedBuffer, (int)uncompressedSize);
table->ParseUpdate(data, msg->m_nNumEntries);
}
delete[] uncompressedBuffer;
delete[] compressedBuffer;
}
if (!bSuccess)
{
Assert(false);
DevWarning(eDLL_T::CLIENT, "%s: Received malformed string table message!\n", __FUNCTION__);
}
}
else
{
table->ParseUpdate(msg->m_DataIn, msg->m_nNumEntries);
}
container->AllowCreation(false);
const ssize_t endbit = msg->m_DataIn.GetNumBitsRead();
return (endbit - startbit) == msg->m_nLength;
}
//------------------------------------------------------------------------------
// Purpose: get authentication token for current connection context
// Input : *connectParams -
@ -295,6 +395,7 @@ void VClientState::Detour(const bool bAttach) const
DetourSetup(&CClientState__ConnectionClosing, &CClientState::VConnectionClosing, bAttach);
DetourSetup(&CClientState__ProcessStringCmd, &CClientState::_ProcessStringCmd, bAttach);
DetourSetup(&CClientState__ProcessServerTick, &CClientState::VProcessServerTick, bAttach);
DetourSetup(&CClientState__ProcessCreateStringTable, &CClientState::_ProcessCreateStringTable, bAttach);
DetourSetup(&CClientState__Connect, &CClientState::VConnect, bAttach);
}

View File

@ -5,6 +5,7 @@
#include "public/inetmsghandler.h"
#include "public/isnapshotmgr.h"
#include "engine/net_chan.h"
#include "engine/networkstringtable.h"
#include "engine/debugoverlay.h"
#include "engine/clockdriftmgr.h"
#include "engine/framesnapshot.h"
@ -38,8 +39,10 @@ public: // Hook statics.
static void VConnectionClosing(CClientState* thisptr, const char* szReason);
static bool _ProcessStringCmd(CClientState* thisptr, NET_StringCmd* msg);
static bool VProcessServerTick(CClientState* thisptr, SVC_ServerTick* msg);
static bool _ProcessCreateStringTable(CClientState* thisptr, SVC_CreateStringTable* msg);
static void VConnect(CClientState* thisptr, connectparams_t* connectParams);
public:
bool IsPaused() const;
bool IsActive(void) const;
@ -63,7 +66,12 @@ public:
protected:
FORCEINLINE CClientState* GetShiftedBasePointer(void)
{
char* const pShifted = reinterpret_cast<char*>(this) - 0x10; // Shifted due to compiler optimizations.
// NOTE: you must check in the disassembler if the CClientState method
// you detour is shifting the 'this' pointer with 16 bytes forward, if
// so, you need to call this and use this pointer instead! The shifting
// happens as part of a compiler optimization that truncated the vtable
// pointers off so the 'this' pointer points directly to the class data
char* const pShifted = reinterpret_cast<char*>(this) - 0x10;
return reinterpret_cast<CClientState*>(pShifted);
}
@ -123,7 +131,7 @@ public:
_QWORD m_pServerClasses;
int m_nServerClasses;
int m_nServerClassBits;
__int64 m_StringTableContainer;
CNetworkStringTableContainer* m_StringTableContainer;
char m_PersistenceData[98304];
_BYTE m_bPersistenceBaselineRecvd;
int m_nPersistenceBaselineEntries;
@ -215,8 +223,11 @@ inline void(*CClientState__RunFrame)(CClientState* thisptr);
inline void(*CClientState__Connect)(CClientState* thisptr, connectparams_t* connectParams);
inline void(*CClientState__Disconnect)(CClientState* thisptr, bool bSendTrackingContext);
inline void(*CClientState__ConnectionClosing)(CClientState* thisptr, const char* szReason);
inline bool(*CClientState__HookClientStringTable)(CClientState* thisptr, const char* tableName);
inline bool(*CClientState__ProcessStringCmd)(CClientState* thisptr, NET_StringCmd* msg);
inline bool(*CClientState__ProcessServerTick)(CClientState* thisptr, SVC_ServerTick* msg);
inline bool(*CClientState__ProcessCreateStringTable)(CClientState* thisptr, SVC_CreateStringTable* msg);
///////////////////////////////////////////////////////////////////////////////
class VClientState : public IDetour
@ -227,8 +238,10 @@ class VClientState : public IDetour
LogFunAdr("CClientState::Connect", CClientState__Connect);
LogFunAdr("CClientState::Disconnect", CClientState__Disconnect);
LogFunAdr("CClientState::ConnectionClosing", CClientState__ConnectionClosing);
LogFunAdr("CClientState::HookClientStringTable", CClientState__HookClientStringTable);
LogFunAdr("CClientState::ProcessStringCmd", CClientState__ProcessStringCmd);
LogFunAdr("CClientState::ProcessServerTick", CClientState__ProcessServerTick);
LogFunAdr("CClientState::ProcessCreateStringTable", CClientState__ProcessCreateStringTable);
LogVarAdr("g_ClientState", g_pClientState);
LogVarAdr("g_ClientState_Shifted", g_pClientState_Shifted);
}
@ -238,8 +251,10 @@ class VClientState : public IDetour
g_GameDll.FindPatternSIMD("48 89 5C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 81 EC ?? ?? ?? ?? 48 8B 32").GetPtr(CClientState__Connect);
g_GameDll.FindPatternSIMD("40 56 57 41 54 41 55 41 57 48 83 EC 30 44 0F B6 FA").GetPtr(CClientState__Disconnect);
g_GameDll.FindPatternSIMD("40 53 48 83 EC 20 83 B9 ?? ?? ?? ?? ?? 48 8B DA 0F 8E ?? ?? ?? ??").GetPtr(CClientState__ConnectionClosing);
g_GameDll.FindPatternSIMD("48 89 5C 24 ?? 57 48 83 EC 20 48 8B D9 48 8B FA 48 8B 89 ?? ?? ?? ?? 48 85 C9 0F 84 ?? ?? ?? ??").GetPtr(CClientState__HookClientStringTable);
g_GameDll.FindPatternSIMD("40 53 48 81 EC ?? ?? ?? ?? 80 B9 ?? ?? ?? ?? ?? 48 8B DA").GetPtr(CClientState__ProcessStringCmd);
g_GameDll.FindPatternSIMD("40 57 48 83 EC 20 83 B9 ?? ?? ?? ?? ?? 48 8B F9 7C 66").GetPtr(CClientState__ProcessServerTick);
g_GameDll.FindPatternSIMD("48 89 4C 24 ?? 53 56 48 81 EC ?? ?? ?? ?? 83 B9 ?? ?? ?? ?? ??").GetPtr(CClientState__ProcessCreateStringTable);
}
virtual void GetVar(void) const
{

View File

@ -57,6 +57,54 @@ int NET_SendDatagram(SOCKET s, void* pPayload, int iLenght, netadr_t* pAdr, bool
return result;
}
//-----------------------------------------------------------------------------
// Purpose: compresses the input buffer into the output buffer
// Input : *dest -
// *destLen -
// *source -
// sourceLen -
// Output : true on success, false otherwise
//-----------------------------------------------------------------------------
bool NET_BufferToBufferCompress(uint8_t* const dest, size_t* const destLen, uint8_t* const source, const size_t sourceLen)
{
CLZSS lzss;
uint32_t compLen = (uint32_t)sourceLen;
if (!lzss.CompressNoAlloc(source, (uint32_t)sourceLen, dest, &compLen))
{
memcpy(dest, source, sourceLen);
*destLen = sourceLen;
return false;
}
*destLen = compLen;
return true;
}
//-----------------------------------------------------------------------------
// Purpose: decompresses the input buffer into the output buffer
// Input : *source -
// &sourceLen -
// *dest -
// destLen -
// Output : true on success, false otherwise
//-----------------------------------------------------------------------------
unsigned int NET_BufferToBufferDecompress(uint8_t* const source, size_t& sourceLen, uint8_t* const dest, const size_t destLen)
{
Assert(source);
Assert(sourceLen);
CLZSS lzss;
if (lzss.IsCompressed(source))
{
return lzss.SafeUncompress(source, dest, (unsigned int)destLen);
}
return 0;
}
//-----------------------------------------------------------------------------
// Purpose: safely decompresses the input buffer into the output buffer
// Input : *lzss -
@ -65,7 +113,7 @@ int NET_SendDatagram(SOCKET s, void* pPayload, int iLenght, netadr_t* pAdr, bool
// unBufSize -
// Output : total decompressed bytes
//-----------------------------------------------------------------------------
unsigned int NET_Decompress(CLZSS* lzss, unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize)
unsigned int NET_BufferToBufferDecompress_LZSS(CLZSS* lzss, unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize)
{
return lzss->SafeUncompress(pInput, pOutput, unBufSize);
}
@ -301,7 +349,9 @@ void VNet::Detour(const bool bAttach) const
DetourSetup(&v_NET_Config, &NET_Config, bAttach);
DetourSetup(&v_NET_ReceiveDatagram, &NET_ReceiveDatagram, bAttach);
DetourSetup(&v_NET_SendDatagram, &NET_SendDatagram, bAttach);
DetourSetup(&v_NET_Decompress, &NET_Decompress, bAttach);
DetourSetup(&v_NET_BufferToBufferCompress, &NET_BufferToBufferCompress, bAttach);
DetourSetup(&v_NET_BufferToBufferDecompress_LZSS, &NET_BufferToBufferDecompress_LZSS, bAttach);
DetourSetup(&v_NET_PrintFunc, &NET_PrintFunc, bAttach);
}

View File

@ -24,7 +24,8 @@ inline void(*v_NET_SetKey)(netkey_t* pKey, const char* szHash);
inline void(*v_NET_Config)(void);
inline bool(*v_NET_ReceiveDatagram)(int iSocket, netpacket_s* pInpacket, bool bRaw);
inline int(*v_NET_SendDatagram)(SOCKET s, void* pPayload, int iLenght, netadr_t* pAdr, bool bEncrypted);
inline unsigned int(*v_NET_Decompress)(CLZSS* lzss, unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize);
inline bool(*v_NET_BufferToBufferCompress)(uint8_t* const dest, size_t* const destLen, uint8_t* const source, const size_t sourceLen);
inline unsigned int(*v_NET_BufferToBufferDecompress_LZSS)(CLZSS* lzss, unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize);
inline void(*v_NET_PrintFunc)(const char* fmt, ...);
///////////////////////////////////////////////////////////////////////////////
@ -35,6 +36,11 @@ void NET_GenerateKey();
void NET_PrintFunc(const char* fmt, ...);
void NET_RemoveChannel(CClient* pClient, int nIndex, const char* szReason, uint8_t bBadRep, bool bRemoveNow);
bool NET_BufferToBufferCompress(uint8_t* const dest, size_t* const destLen, uint8_t* const source, const size_t sourceLen);
unsigned int NET_BufferToBufferDecompress(uint8_t* pInput, size_t& coBufsize, uint8_t* pOutput, const size_t unBufSize);
unsigned int NET_BufferToBufferDecompress_LZSS(CLZSS* lzss, unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize);
bool NET_ReadMessageType(int* outType, bf_read* buffer);
///////////////////////////////////////////////////////////////////////////////
@ -53,7 +59,8 @@ class VNet : public IDetour
LogFunAdr("NET_SetKey", v_NET_SetKey);
LogFunAdr("NET_ReceiveDatagram", v_NET_ReceiveDatagram);
LogFunAdr("NET_SendDatagram", v_NET_SendDatagram);
LogFunAdr("NET_Decompress", v_NET_Decompress);
LogFunAdr("NET_BufferToBufferCompress", v_NET_BufferToBufferCompress);
LogFunAdr("NET_BufferToBufferDecompress_LZSS", v_NET_BufferToBufferDecompress_LZSS);
LogFunAdr("NET_PrintFunc", v_NET_PrintFunc);
LogVarAdr("g_NetAdr", g_pNetAdr);
LogVarAdr("g_NetKey", g_pNetKey);
@ -66,7 +73,10 @@ class VNet : public IDetour
g_GameDll.FindPatternSIMD("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 20 48 8B F9 41 B8").GetPtr(v_NET_SetKey);
g_GameDll.FindPatternSIMD("48 89 74 24 18 48 89 7C 24 20 55 41 54 41 55 41 56 41 57 48 8D AC 24 50 EB").GetPtr(v_NET_ReceiveDatagram);
g_GameDll.FindPatternSIMD("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 56 41 57 48 81 EC ?? 05 ?? ??").GetPtr(v_NET_SendDatagram);
g_GameDll.FindPatternSIMD("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 41 56 45 33 F6").GetPtr(v_NET_Decompress);
g_GameDll.FindPatternSIMD("48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC 50 48 8B 05 ?? ?? ?? ?? 49 8B E9").GetPtr(v_NET_BufferToBufferCompress);
g_GameDll.FindPatternSIMD("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 41 56 45 33 F6").GetPtr(v_NET_BufferToBufferDecompress_LZSS);
g_GameDll.FindPatternSIMD("48 89 54 24 10 4C 89 44 24 18 4C 89 4C 24 20 C3 48").GetPtr(v_NET_PrintFunc);
}
virtual void GetVar(void) const

View File

@ -9,19 +9,32 @@
#define NETWORKSTRINGTABLE_H
#include "tier0/fasttimer.h"
#include "tier1/utlvector.h"
#include "tier1/utlhashtable.h"
#include "tier1/bitbuf.h"
#include "public/networkstringtabledefs.h"
#include "client/client.h"
typedef int TABLEID;
class INetworkStringTable
{
INetworkStringTable* m_pVTable;
};
class CNetworkStringTable : public INetworkStringTable
{
public:
// Updating/Writing
virtual bool WriteStringTable(bf_write& buf) = 0;
virtual bool ReadStringTable(bf_read& buf) = 0;
virtual void ParseUpdate(bf_read& buf, int numStrings) = 0;
virtual pfnStringChanged GetCallback(void) = 0;
virtual int WriteUpdate(CClient* const client, bf_write& buf, int tickAck) = 0;
virtual bool WriteBaselines(SVC_CreateStringTable& msg, char* msgBuffer, int msgBufferSize) = 0;
virtual void PurgeAllClientSide(void) = 0;
virtual void EnableRollback(bool bState) = 0;
// TODO[ AMOS ]: there are a few more entries below in the vftable that
// need to be mapped out, most of them set/get bit fields;
// see [ r5apex_ds + 0x1329888 ]
TABLEID GetTableId(void) const;
int GetMaxStrings(void) const;
const char* GetTableName(void) const;
@ -41,16 +54,20 @@ private:
// !TODO
};
class CNetworkStringTableContainer : public INetworkStringTable
class CNetworkStringTableContainer : public INetworkStringTableContainer
{
public:
static void WriteUpdateMessage(CNetworkStringTableContainer* thisp, CClient* client, unsigned int tick_ack, bf_write* msg);
// Guards so game .dll can't create tables at the wrong time
inline void AllowCreation(bool state) { m_bAllowCreation = state; }
private:
bool m_bAllowCreation; // create guard
int m_nTickCount; // current tick
bool m_bLocked; // currently locked?
bool m_bEnableRollback; // enables rollback feature
CUtlVector < CNetworkStringTable* > m_Tables; // the string tables
};

View File

@ -51,6 +51,22 @@ public:
struct CS_INetChannelHandler : INetChannelHandler
{};
enum netsocket_e
{
NS_CLIENT = 0, // client socket
NS_SERVER, // server socket
// unknown as this seems unused in R5, but if the socket equals to this in
// CServer::ConnectionlessPacketHandler() in case C2S_Challenge, the packet
// sent back won't be encrypted
NS_UNK0,
// used for chat room, communities, discord presence, EA/Origin, etc
NS_PRESENCE,
MAX_SOCKETS // 4 in R5
};
typedef struct netpacket_s
{
netadr_t from;

View File

@ -0,0 +1,76 @@
//===== Copyright <20> 1996-2005, Valve Corporation, All rights reserved. ======//
//
// Purpose:
//
// $NoKeywords: $
//===========================================================================//
#ifndef NETWORKSTRINGTABLEDEFS_H
#define NETWORKSTRINGTABLEDEFS_H
typedef int TABLEID;
#define INVALID_STRING_TABLE -1
const unsigned short INVALID_STRING_INDEX = ( unsigned short )-1;
// table index is sent in log2( MAX_TABLES ) bits
#define MAX_TABLES 32 // Table id is 4 bits
//-----------------------------------------------------------------------------
// Forward declarations
//-----------------------------------------------------------------------------
class INetworkStringTable;
typedef void ( *pfnStringChanged )( void* object, INetworkStringTable* stringTable, int stringNumber, char const* newString, void const* newData );
//-----------------------------------------------------------------------------
// Purpose: Game .dll shared string table interfaces
//-----------------------------------------------------------------------------
class INetworkStringTable
{
public:
virtual ~INetworkStringTable( void ) {};
// Table Info
virtual const char *GetTableName( void ) const = 0;
virtual TABLEID GetTableId( void ) const = 0;
virtual int GetNumStrings( void ) const = 0;
virtual int GetMaxStrings( void ) const = 0;
virtual int GetEntryBits( void ) const = 0;
// Networking
virtual void SetTick( int tick ) = 0;
virtual bool ChangedSinceTick( int tick ) const = 0;
// Accessors (length -1 means don't change user data if string already exits)
virtual int AddString( bool bIsServer, const char *value, int length = -1, const void *userdata = 0 ) = 0;
virtual int AddString( const char* value ) = 0;
virtual const char* GetString( int stringNumber ) = 0;
virtual int Unused0( void ) = 0; // returns -1
virtual int Unused1( void ) const = 0; // returns -1
virtual int Unused2( void ) const = 0; // returns 0
virtual void SetStringUserData( int stringNumber, int length, const void *userdata ) = 0;
virtual const void* GetStringUserData( int stringNumber, int* length ) = 0;
};
class INetworkStringTableContainer
{
public:
virtual ~INetworkStringTableContainer( void ) {};
// table creation/destruction
virtual INetworkStringTable *CreateStringTable( bool createTableMemory, const char *tableName, int maxentries, int userdatafixedsize = 0, int userdatanetworkbits = 0, int dictFlags = 0 ) = 0;
virtual void RemoveAllTables( void ) = 0;
// table infos
virtual INetworkStringTable *FindTable( const char *tableName ) const = 0;
virtual INetworkStringTable *GetTable( TABLEID stringTable ) const = 0;
virtual int GetNumTables( void ) const = 0;
virtual void SetAllowClientSideAddString( INetworkStringTable *table, bool bAllowClientSideAddString ) = 0;
};
#endif // NETWORKSTRINGTABLEDEFS_H