CClient: add rate limit logic for 'ProcessStringCmd'

Client's can run string commands on the server with no rate limit. This means when you run 50k+ commands that are unknown, or spam 30k 'status' commands, you will be able to hang the server for 800/1200ms (15k/30kms if script printing to console is enabled!). Although the netchan processing budget system will kick you, the damage has already been done at this point. This change effectively breaks the ability to DOS the server from the client using networked string commands.
In easier words; binding 'status' to your mousewheel will get you kicked from the server, without hitching the server.
This commit is contained in:
Kawe Mazidjatari 2022-09-20 22:48:55 +02:00
parent 793c2e8e50
commit 7804241376
8 changed files with 83 additions and 3 deletions

View File

@ -87,6 +87,13 @@ public:
bf_write m_DataOut;
};
struct NET_StringCmd : CNetMessage, INetMessageHandler
{
const char* cmd;
char buffer[1024];
};
//-------------------------------------------------------------------------
// MM_HEARTBEAT
//-------------------------------------------------------------------------

View File

@ -9,6 +9,7 @@
//
///////////////////////////////////////////////////////////////////////////////////
#include "core/stdafx.h"
#include "tier1/cvar.h"
#include "engine/server/server.h"
#include "engine/client/client.h"
@ -279,16 +280,64 @@ void CClient::Disconnect(const Reputation_t nRepLvl, const char* szReason, ...)
}
}
//---------------------------------------------------------------------------------
// Purpose: process string commands (kicking anyone attempting to DOS)
// Input : *pClient - (ADJ)
// *pMsg -
// Output : false if cmd should be passed to CServerGameClients
//---------------------------------------------------------------------------------
bool CClient::VProcessStringCmd(CClient* pClient, NET_StringCmd* pMsg)
{
#ifndef CLIENT_DLL
#if defined (GAMEDLL_S0) || defined (GAMEDLL_S1)
CClient* pClient_Adj = pClient;
#elif defined (GAMEDLL_S2) || defined (GAMEDLL_S3)
/* Original function called method "CClient::ExecuteStringCommand" with an optimization
* that shifted the 'this' pointer with 8 bytes.
* Since this has been inlined with "CClient::ProcessStringCmd" as of S2, the shifting
* happens directly to anything calling this function. */
char* pShifted = reinterpret_cast<char*>(pClient) - 8;
CClient* pClient_Adj = reinterpret_cast<CClient*>(pShifted);
#endif // !GAMEDLL_S0 || !GAMEDLL_S1
ServerPlayer_t* pSlot = &g_ServerPlayer[pClient_Adj->GetUserID()];
double flStartTime = Plat_FloatTime();
int nCmdQuota = sv_quota_stringCmdsPerSecond->GetInt();
if (!nCmdQuota)
return true;
if (flStartTime - pSlot->m_flStringCommandQuotaTimeStart >= 1.0)
{
pSlot->m_flStringCommandQuotaTimeStart = flStartTime;
pSlot->m_nStringCommandQuotaCount = 0;
}
++pSlot->m_nStringCommandQuotaCount;
if (pSlot->m_nStringCommandQuotaCount > nCmdQuota)
{
Warning(eDLL_T::SERVER, "Removing client '%s' from slot '%i' ('%llu' exceeded string command quota!)\n",
pClient_Adj->GetNetChan()->GetAddress(), pClient_Adj->GetUserID(), pClient->GetNucleusID());
pClient_Adj->Disconnect(Reputation_t::REP_MARK_BAD, "#DISCONNECT_STRINGCMD_OVERFLOW");
return true;
}
#endif // !CLIENT_DLL
return v_CClient_ProcessStringCmd(pClient, pMsg);
}
///////////////////////////////////////////////////////////////////////////////////
void CBaseClient_Attach()
{
DetourAttach((LPVOID*)&v_CClient_Clear, &CClient::VClear);
DetourAttach((LPVOID*)&v_CClient_Connect, &CClient::VConnect);
DetourAttach((LPVOID*)&v_CClient_ProcessStringCmd, &CClient::VProcessStringCmd);
}
void CBaseClient_Detach()
{
DetourDetach((LPVOID*)&v_CClient_Clear, &CClient::VClear);
DetourDetach((LPVOID*)&v_CClient_Connect, &CClient::VConnect);
DetourDetach((LPVOID*)&v_CClient_ProcessStringCmd, &CClient::VProcessStringCmd);
}
///////////////////////////////////////////////////////////////////////////////

View File

@ -22,7 +22,7 @@ class CClient;
///////////////////////////////////////////////////////////////////////////////
extern CClient* g_pClient;
class CClient : INetChannelHandler, IClientMessageHandler
class CClient : IClientMessageHandler, INetChannelHandler
{
public:
CClient* GetClient(int nIndex) const;
@ -52,6 +52,7 @@ public:
static bool VConnect(CClient* pClient, const char* szName, void* pNetChannel, bool bFakePlayer, void* a5, char* szMessage, int nMessageSize);
void Clear(void);
static void VClear(CClient* pClient);
static bool VProcessStringCmd(CClient* pClient, NET_StringCmd* pMsg);
private:
uint32_t m_nUserID; //0x0010
@ -106,6 +107,9 @@ inline auto v_CClient_Disconnect = p_CClient_Disconnect.RCast<bool (*)(CClient*
inline CMemory p_CClient_Clear;
inline auto v_CClient_Clear = p_CClient_Clear.RCast<void (*)(CClient* pClient)>();
inline CMemory p_CClient_ProcessStringCmd;
inline auto v_CClient_ProcessStringCmd = p_CClient_ProcessStringCmd.RCast<bool (*)(CClient* pClient, NET_StringCmd* pMsg)>();
///////////////////////////////////////////////////////////////////////////////
void CBaseClient_Attach();
void CBaseClient_Detach();
@ -118,6 +122,7 @@ class VClient : public IDetour
spdlog::debug("| FUN: CClient::Connect : {:#18x} |\n", p_CClient_Connect.GetPtr());
spdlog::debug("| FUN: CClient::Disconnect : {:#18x} |\n", p_CClient_Disconnect.GetPtr());
spdlog::debug("| FUN: CClient::Clear : {:#18x} |\n", p_CClient_Clear.GetPtr());
spdlog::debug("| FUN: CClient::ProcessStringCmd : {:#18x} |\n", p_CClient_ProcessStringCmd.GetPtr());
spdlog::debug("| VAR: g_pClient[128] : {:#18x} |\n", reinterpret_cast<uintptr_t>(g_pClient));
spdlog::debug("+----------------------------------------------------------------+\n");
}
@ -130,10 +135,16 @@ class VClient : public IDetour
p_CClient_Disconnect = g_GameDll.FindPatternSIMD(reinterpret_cast<rsig_t>("\x48\x8B\xC4\x4C\x89\x40\x18\x4C\x89\x48\x20\x53\x56\x57\x48\x81\xEC\x00\x00\x00\x00\x83\xB9\x00\x00\x00\x00\x00\x49\x8B\xF8\x8B\xF2"), "xxxxxxxxxxxxxxxxx????xx?????xxxxx");
#endif
p_CClient_Clear = g_GameDll.FindPatternSIMD(reinterpret_cast<rsig_t>("\x40\x53\x41\x56\x41\x57\x48\x83\xEC\x20\x48\x8B\xD9\x48\x89\x74"), "xxxxxxxxxxxxxxxx");
#if defined (GAMEDLL_S0) || defined (GAMEDLL_S1)
p_CClient_ProcessStringCmd = g_GameDll.FindPatternSIMD(reinterpret_cast<rsig_t>("\x48\x83\xEC\x28\x4C\x8B\x42\x20"), "xxxxxxxx");
#elif defined (GAMEDLL_S2) || defined (GAMEDLL_S3)
p_CClient_ProcessStringCmd = g_GameDll.FindPatternSIMD(reinterpret_cast<rsig_t>("\x48\x89\x6C\x24\x00\x57\x48\x81\xEC\x00\x00\x00\x00\x48\x8B\x7A\x20"), "xxxx?xxxx????xxxx");
#endif // !GAMEDLL_S0 || !GAMEDLL_S1
v_CClient_Connect = p_CClient_Connect.RCast<bool (*)(CClient*, const char*, void*, bool, void*, char*, int)>(); /*48 89 5C 24 ?? 48 89 6C 24 ?? 56 57 41 56 48 83 EC 20 41 0F B6 E9*/
v_CClient_Disconnect = p_CClient_Disconnect.RCast<bool (*)(CClient*, const Reputation_t, const char*, ...)>(); /*48 8B C4 4C 89 40 18 4C 89 48 20 53 56 57 48 81 EC ?? ?? ?? ?? 83 B9 ?? ?? ?? ?? ?? 49 8B F8 8B F2*/
v_CClient_Clear = p_CClient_Clear.RCast<void (*)(CClient*)>(); /*40 53 41 56 41 57 48 83 EC 20 48 8B D9 48 89 74*/
v_CClient_ProcessStringCmd = p_CClient_ProcessStringCmd.RCast<bool (*)(CClient*, NET_StringCmd*)>(); /*48 89 6C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 7A 20*/
}
virtual void GetVar(void) const
{

View File

@ -1071,6 +1071,7 @@ playlists
"DISCONNECT_SEND_RELIABLEOVERFLOW" "Connection to server overflowed (code:river)."
"DISCONNECT_SEND_OVERFLOW" "Connection to server overflowed (code:dam)."
"DISCONNECT_NETCHAN_OVERFLOW" "Connection to server overflowed (code:vulkan)."
"DISCONNECT_STRINGCMD_OVERFLOW" "Connection to server overflowed (code:mountain)."
}
}
}

View File

@ -31,17 +31,23 @@ struct ServerPlayer_t
ServerPlayer_t(void)
: m_flCurrentNetProcessTime(0.0)
, m_flLastNetProcessTime(0.0)
, m_flStringCommandQuotaTimeStart(0.0)
, m_nStringCommandQuotaCount(0)
, m_bPersistenceEnabled(false)
{}
inline void Reset(void)
{
m_flCurrentNetProcessTime = 0.0;
m_flLastNetProcessTime = 0.0;
m_flStringCommandQuotaTimeStart = 0.0;
m_nStringCommandQuotaCount = 0;
m_bPersistenceEnabled = false;
}
double m_flCurrentNetProcessTime;
double m_flLastNetProcessTime;
double m_flStringCommandQuotaTimeStart;
int m_nStringCommandQuotaCount;
bool m_bPersistenceEnabled;
};

View File

@ -105,7 +105,8 @@ void ConVar::Init(void) const
sv_banlistRefreshInterval = ConVar::Create("sv_banlistRefreshInterval", "1.0", FCVAR_RELEASE, "Banlist refresh interval (seconds).", true, 1.f, false, 0.f, nullptr, nullptr);
sv_statusRefreshInterval = ConVar::Create("sv_statusRefreshInterval" , "0.5", FCVAR_RELEASE, "Server status bar update interval (seconds).", false, 0.f, false, 0.f, nullptr, nullptr);
sv_autoReloadRate = ConVar::Create("sv_autoReloadRate" , "0", FCVAR_RELEASE, "Time in seconds between each server auto-reload (disabled if null). ", true, 0.f, false, 0.f, nullptr, nullptr);
sv_autoReloadRate = ConVar::Create("sv_autoReloadRate" , "0", FCVAR_RELEASE, "Time in seconds between each server auto-reload (disabled if null). ", true, 0.f, false, 0.f, nullptr, nullptr);
sv_quota_stringCmdsPerSecond = ConVar::Create("sv_quota_stringCmdsPerSecond", "16", FCVAR_RELEASE, "How many string commands per second clients are allowed to submit, 0 to disallow all string commands.", true, 0.f, false, 0.f, nullptr, nullptr);
#ifdef DEDICATED
sv_rcon_debug = ConVar::Create("sv_rcon_debug" , "0" , FCVAR_RELEASE, "Show rcon debug information ( !slower! ).", false, 0.f, false, 0.f, nullptr, nullptr);
sv_rcon_sendlogs = ConVar::Create("sv_rcon_sendlogs" , "0" , FCVAR_RELEASE, "Network console logs to connected and authenticated sockets.", false, 0.f, false, 0.f, nullptr, nullptr);
@ -203,7 +204,7 @@ void ConVar::Init(void) const
net_tracePayload = ConVar::Create("net_tracePayload" , "0", FCVAR_DEVELOPMENTONLY , "Log the payload of the send/recv datagram to a file on the disk.", false, 0.f, false, 0.f, nullptr, nullptr);
net_encryptionEnable = ConVar::Create("net_encryptionEnable" , "1", FCVAR_DEVELOPMENTONLY | FCVAR_REPLICATED , "Use AES encryption on game packets.", false, 0.f, false, 0.f, nullptr, nullptr);
net_useRandomKey = ConVar::Create("net_useRandomKey" , "1" , FCVAR_RELEASE , "Use random AES encryption key for game packets.", false, 0.f, false, 0.f, &NET_UseRandomKeyChanged_f, nullptr);
net_processTimeBudget = ConVar::Create("net_processTimeBudget" ,"200" , FCVAR_RELEASE , "Net message process budget in milliseconds (removing netchannel if exceeded).", true, 0.f, false, 0.f, nullptr, "0 = disabled.");
net_processTimeBudget = ConVar::Create("net_processTimeBudget" ,"150" , FCVAR_RELEASE , "Net message process budget in milliseconds (removing netchannel if exceeded).", true, 0.f, false, 0.f, nullptr, "0 = disabled.");
//-------------------------------------------------------------------------
// NETWORKSYSTEM |
pylon_matchmaking_hostname = ConVar::Create("pylon_matchmaking_hostname", "ms.r5reloaded.com", FCVAR_RELEASE , "Holds the pylon matchmaking hostname.", false, 0.f, false, 0.f, &MP_HostName_Changed_f, nullptr);
@ -263,6 +264,7 @@ void ConVar::InitShipped(void) const
#ifndef CLIENT_DLL
ai_script_nodes_draw->SetValue(-1);
bhit_enable->SetValue(0);
#endif // !CLIENT_DLL
#ifndef DEDICATED
cl_threaded_bone_setup->RemoveFlags(FCVAR_DEVELOPMENTONLY);

View File

@ -69,6 +69,7 @@ ConVar* sv_statusRefreshInterval = nullptr;
ConVar* sv_forceChatToTeamOnly = nullptr;
ConVar* sv_autoReloadRate = nullptr;
ConVar* sv_quota_stringCmdsPerSecond = nullptr;
#ifdef DEDICATED
ConVar* sv_rcon_debug = nullptr;
@ -173,6 +174,7 @@ ConVar* net_encryptionEnable = nullptr;
ConVar* net_useRandomKey = nullptr;
ConVar* net_usesocketsforloopback = nullptr;
ConVar* net_processTimeBudget = nullptr;
ConVar* pylon_matchmaking_hostname = nullptr;
ConVar* pylon_host_update_interval = nullptr;
ConVar* pylon_showdebuginfo = nullptr;

View File

@ -65,6 +65,7 @@ extern ConVar* sv_statusRefreshInterval;
extern ConVar* sv_forceChatToTeamOnly;
extern ConVar* sv_autoReloadRate;
extern ConVar* sv_quota_stringCmdsPerSecond;
#ifdef DEDICATED
extern ConVar* sv_rcon_debug;
@ -168,6 +169,7 @@ extern ConVar* net_encryptionEnable;
extern ConVar* net_useRandomKey;
extern ConVar* net_usesocketsforloopback;
extern ConVar* net_processTimeBudget;
extern ConVar* pylon_matchmaking_hostname;
extern ConVar* pylon_host_update_interval;
extern ConVar* pylon_showdebuginfo;