UserCmd: limit command backlog to prevent exploitation

Implement UserCmd command backlog limiting (the new convar 'sv_maxUserCmdProcessTicks' dictates how many ticks can be processed per second). Defaulted to 10, which is (default tick interval (0.05) * default cvar val (10) = 0.5ms window), which is equal to the default of cvar 'sv_maxunlag'.

Before this patch, you could stuff several seconds worth of usercmd's in one second and achieve speed hacking.
This commit is contained in:
Kawe Mazidjatari 2024-06-01 11:29:29 +02:00
parent 824b5098b3
commit 42e02b4569
12 changed files with 249 additions and 5 deletions

View File

@ -85,7 +85,13 @@ ConVar* sv_voiceEcho = nullptr;
ConVar* sv_voiceenable = nullptr;
ConVar* sv_alltalk = nullptr;
ConVar* sv_clampPlayerFrameTime = nullptr;
ConVar* playerframetimekick_margin = nullptr;
ConVar* playerframetimekick_decayrate = nullptr;
ConVar* player_userCmdsQueueWarning = nullptr;
ConVar* player_disallow_negative_frametime = nullptr;
#endif // !CLIENT_DLL
ConVar* sv_cheats = nullptr;
@ -208,7 +214,14 @@ void ConVar_InitShipped(void)
sv_voiceenable = g_pCVar->FindVar("sv_voiceenable");
sv_voiceEcho = g_pCVar->FindVar("sv_voiceEcho");
sv_alltalk = g_pCVar->FindVar("sv_alltalk");
sv_clampPlayerFrameTime = g_pCVar->FindVar("sv_clampPlayerFrameTime");
playerframetimekick_margin = g_pCVar->FindVar("playerframetimekick_margin");
playerframetimekick_decayrate = g_pCVar->FindVar("playerframetimekick_decayrate");
player_userCmdsQueueWarning = g_pCVar->FindVar("player_userCmdsQueueWarning");
player_disallow_negative_frametime = g_pCVar->FindVar("player_disallow_negative_frametime");
sv_updaterate_sp->RemoveFlags(FCVAR_DEVELOPMENTONLY);
sv_updaterate_mp->RemoveFlags(FCVAR_DEVELOPMENTONLY);

View File

@ -72,7 +72,13 @@ extern ConVar* sv_voiceEcho;
extern ConVar* sv_voiceenable;
extern ConVar* sv_alltalk;
extern ConVar* sv_clampPlayerFrameTime;
extern ConVar* playerframetimekick_margin;
extern ConVar* playerframetimekick_decayrate;
extern ConVar* player_userCmdsQueueWarning;
extern ConVar* player_disallow_negative_frametime;
#endif // CLIENT_DLL
extern ConVar* sv_cheats;

View File

@ -135,6 +135,8 @@
#include "game/server/detour_impl.h"
#include "game/server/gameinterface.h"
#include "game/server/movehelper_server.h"
#include "game/server/player.h"
#include "game/server/player_command.h"
#include "game/server/physics_main.h"
#include "game/server/vscript_server.h"
#endif // !CLIENT_DLL
@ -663,6 +665,7 @@ void DetourRegister() // Register detour classes to be searched and hooked.
REGISTER(VBaseEntity);
REGISTER(VBaseAnimating);
REGISTER(VPlayer);
REGISTER(VPlayerMove);
#endif // !CLIENT_DLL

View File

@ -524,6 +524,48 @@ bool CClient::VProcessSetConVar(CClient* pClient, NET_SetConVar* pMsg)
return true;
}
//---------------------------------------------------------------------------------
// Purpose: set UserCmd time buffer
// Input : numUserCmdProcessTicksMax -
// tickInterval -
//---------------------------------------------------------------------------------
void CClientExtended::InitializeMovementTimeForUserCmdProcessing(const int numUserCmdProcessTicksMax, const float tickInterval)
{
// Grant the client some time buffer to execute user commands
m_flMovementTimeForUserCmdProcessingRemaining += tickInterval;
// but never accumulate more than N ticks
if (m_flMovementTimeForUserCmdProcessingRemaining > numUserCmdProcessTicksMax * tickInterval)
m_flMovementTimeForUserCmdProcessingRemaining = numUserCmdProcessTicksMax * tickInterval;
}
//---------------------------------------------------------------------------------
// Purpose: consume UserCmd time buffer
// Input : flTimeNeeded -
// Output : max time allowed for processing
//---------------------------------------------------------------------------------
float CClientExtended::ConsumeMovementTimeForUserCmdProcessing(const float flTimeNeeded)
{
if (m_flMovementTimeForUserCmdProcessingRemaining <= 0.0f)
return 0.0f;
else if (flTimeNeeded > m_flMovementTimeForUserCmdProcessingRemaining + FLT_EPSILON)
{
const float flResult = m_flMovementTimeForUserCmdProcessingRemaining;
m_flMovementTimeForUserCmdProcessingRemaining = 0.0f;
return flResult;
}
else
{
m_flMovementTimeForUserCmdProcessingRemaining -= flTimeNeeded;
if (m_flMovementTimeForUserCmdProcessingRemaining < 0.0f)
m_flMovementTimeForUserCmdProcessingRemaining = 0.0f;
return flTimeNeeded;
}
}
void VClient::Detour(const bool bAttach) const
{
#ifndef CLIENT_DLL

View File

@ -230,6 +230,7 @@ public:
m_flNetProcessTimeBase = 0.0;
m_flStringCommandQuotaTimeStart = 0.0;
m_nStringCommandQuotaCount = NULL;
m_flMovementTimeForUserCmdProcessingRemaining = 0.0f;
m_bInitialConVarsSet = false;
}
@ -247,6 +248,12 @@ public: // Inlines:
inline void SetStringCommandQuotaCount(const int iCount) { m_nStringCommandQuotaCount = iCount; }
inline int GetStringCommandQuotaCount(void) const { return m_nStringCommandQuotaCount; }
inline void SetRemainingMovementTimeForUserCmdProcessing(const float flValue) { m_flMovementTimeForUserCmdProcessingRemaining = flValue; }
inline float GetRemainingMovementTimeForUserCmdProcessing() const { return m_flMovementTimeForUserCmdProcessingRemaining; }
void InitializeMovementTimeForUserCmdProcessing(const int numUserCmdProcessTicksMax, const float tickInterval);
float ConsumeMovementTimeForUserCmdProcessing(const float flTimeNeeded);
private:
// Measure how long this client's packets took to process.
double m_flNetProcessingTimeMsecs;
@ -256,6 +263,9 @@ private:
double m_flStringCommandQuotaTimeStart;
int m_nStringCommandQuotaCount;
// How much of a movement time buffer can we process from this user?
float m_flMovementTimeForUserCmdProcessingRemaining;
bool m_bInitialConVarsSet; // Whether or not the initial ConVar KV's are set
};

View File

@ -73,6 +73,8 @@ add_sources( SOURCE_GROUP "Network"
add_sources( SOURCE_GROUP "Player"
"server/player.cpp"
"server/player.h"
"server/player_command.cpp"
"server/player_command.h"
"server/playerlocaldata.h"
)

View File

@ -22,10 +22,10 @@
//-----------------------------------------------------------------------------
// This is called when a new game is started. (restart, map)
//-----------------------------------------------------------------------------
void CServerGameDLL::GameInit(void)
bool CServerGameDLL::GameInit(void)
{
const static int index = 1;
CallVFunc<void>(index, this);
return CallVFunc<bool>(index, this);
}
//-----------------------------------------------------------------------------

View File

@ -19,7 +19,7 @@ class ServerClass;
class CServerGameDLL
{
public:
void GameInit(void);
bool GameInit(void);
void PrecompileScriptsJob(void);
void LevelShutdown(void);
void GameShutdown(void);
@ -48,11 +48,15 @@ class CServerGameEnts : public IServerGameEnts
};
inline void(*CServerGameDLL__OnReceivedSayTextMessage)(void* thisptr, int senderId, const char* text, bool isTeamChat);
inline bool(*CServerGameDLL__GameInit)(void);
inline void(*CServerGameClients__ProcessUserCmds)(CServerGameClients* thisp, edict_t edict, bf_read* buf,
int numCmds, int totalCmds, int droppedPackets, bool ignore, bool paused);
inline void(*v_RunFrameServer)(double flFrameTime, bool bRunOverlays, bool bUniformUpdate);
inline float* g_pflServerFrameTimeBase = nullptr;
extern CServerGameDLL* g_pServerGameDLL;
extern CServerGameClients* g_pServerGameClients;
extern CServerGameEnts* g_pServerGameEntities;
@ -65,8 +69,10 @@ class VServerGameDLL : public IDetour
virtual void GetAdr(void) const
{
LogFunAdr("CServerGameDLL::OnReceivedSayTextMessage", CServerGameDLL__OnReceivedSayTextMessage);
LogFunAdr("CServerGameDLL::GameInit", CServerGameDLL__GameInit);
LogFunAdr("CServerGameClients::ProcessUserCmds", CServerGameClients__ProcessUserCmds);
LogFunAdr("RunFrameServer", v_RunFrameServer);
LogVarAdr("g_flServerFrameTimeBase", g_pflServerFrameTimeBase);
LogVarAdr("g_pServerGameDLL", g_pServerGameDLL);
LogVarAdr("g_pServerGameClients", g_pServerGameClients);
LogVarAdr("g_pServerGameEntities", g_pServerGameEntities);
@ -75,12 +81,14 @@ class VServerGameDLL : public IDetour
virtual void GetFun(void) const
{
g_GameDll.FindPatternSIMD("85 D2 0F 8E ?? ?? ?? ?? 4C 8B DC").GetPtr(CServerGameDLL__OnReceivedSayTextMessage);
g_GameDll.FindPatternSIMD("48 83 EC 28 48 8B 0D ?? ?? ?? ?? 48 8D 15 ?? ?? ?? ?? 48 8B 01 FF 90 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? 48 8B 01").GetPtr(CServerGameDLL__GameInit);
g_GameDll.FindPatternSIMD("48 89 5C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 55 41 55 41 57").GetPtr(CServerGameClients__ProcessUserCmds);
g_GameDll.FindPatternSIMD("48 89 5C 24 ?? 57 48 83 EC 30 0F 29 74 24 ?? 48 8D 0D ?? ?? ?? ??").GetPtr(v_RunFrameServer);
}
virtual void GetVar(void) const
{
g_pGlobals = g_GameDll.FindPatternSIMD("4C 8B 0D ?? ?? ?? ?? 48 8B D1").ResolveRelativeAddressSelf(0x3, 0x7).RCast<CGlobalVars**>();
g_pflServerFrameTimeBase = CMemory(CServerGameDLL__GameInit).FindPatternSelf("F3 0F 11 0D").ResolveRelativeAddressSelf(0x4, 0x8).RCast<float*>();
}
virtual void GetCon(void) const { }
virtual void Detour(const bool bAttach) const;

View File

@ -13,6 +13,9 @@
#include "engine/server/server.h"
// NOTE[ AMOS ]: default tick interval (0.05) * default cvar value (10) = total time buffer of 0.5, which is the default of cvar 'sv_maxunlag'.
static ConVar sv_maxUserCmdProcessTicks("sv_maxUserCmdProcessTicks", "10", FCVAR_NONE, "Maximum number of client-issued UserCmd ticks that can be replayed in packet loss conditions, 0 to allow no restrictions.");
//------------------------------------------------------------------------------
// Purpose: executes a null command for this player
//------------------------------------------------------------------------------
@ -64,7 +67,7 @@ inline void CPlayer::SetTimeBase(float flTimeBase)
SetLastUCmdSimulationRemainderTime(flTime);
float flSimulationTime = flTimeBase - m_lastUCmdSimulationRemainderTime * (*g_pGlobals)->m_flTickInterval;
float flSimulationTime = flTimeBase - m_lastUCmdSimulationRemainderTime * TICK_INTERVAL;
if (flSimulationTime >= 0.0f)
{
flTime = flSimulationTime;
@ -241,6 +244,25 @@ void CPlayer::SetLastUserCommand(CUserCmd* pUserCmd)
m_LastCmd.Copy(pUserCmd);
}
//------------------------------------------------------------------------------
// Purpose: run physics simulation for player
// Input : *player (this) -
// numPerIteration -
// adjustTimeBase -
//------------------------------------------------------------------------------
bool Player_PhysicsSimulate(CPlayer* player, int numPerIteration, bool adjustTimeBase)
{
CClientExtended* const cle = g_pServer->GetClientExtended(player->GetEdict() - 1);
const int numUserCmdProcessTicksMax = sv_maxUserCmdProcessTicks.GetInt();
if (numUserCmdProcessTicksMax && (*g_pGlobals)->m_nGameMode != GameMode_t::SP_MODE) // don't apply this filter in SP games
cle->InitializeMovementTimeForUserCmdProcessing(numUserCmdProcessTicksMax, TICK_INTERVAL);
else // Otherwise we don't care to track time
cle->SetRemainingMovementTimeForUserCmdProcessing(FLT_MAX);
return CPlayer__PhysicsSimulate(player, numPerIteration, adjustTimeBase);
}
/*
=====================
CC_CreateFakePlayer_f
@ -286,3 +308,8 @@ static void CC_CreateFakePlayer_f(const CCommand& args)
}
static ConCommand sv_addbot("sv_addbot", CC_CreateFakePlayer_f, "Creates a bot on the server", FCVAR_RELEASE);
void VPlayer::Detour(const bool bAttach) const
{
DetourSetup(&CPlayer__PhysicsSimulate, &Player_PhysicsSimulate, bAttach);
}

View File

@ -241,6 +241,7 @@ struct SpeedChangeHistoryEntry
class CPlayer : public CBaseCombatCharacter
{
friend class CPlayerMove;
public:
void RunNullCommand(void);
QAngle* EyeAngles(QAngle* pAngles);
@ -260,6 +261,8 @@ public:
inline bool IsConnected() const { return m_iConnected != PlayerDisconnected; }
inline bool IsDisconnecting() const { return m_iConnected == PlayerDisconnecting; }
inline bool IsBot() const { return (GetFlags() & FL_FAKECLIENT) != 0; }
private:
int m_StuckLast;
char gap_5a8c[4];
@ -796,6 +799,7 @@ static_assert(sizeof(CPlayer) == 0x7EF0); // !TODO: backwards compatibility.
inline QAngle*(*CPlayer__EyeAngles)(CPlayer* pPlayer, QAngle* pAngles);
inline void(*CPlayer__PlayerRunCommand)(CPlayer* pPlayer, CUserCmd* pUserCmd, IMoveHelper* pMover);
inline bool(*CPlayer__PhysicsSimulate)(CPlayer* pPlayer, int numPerIteration, bool adjustTimeBase);
///////////////////////////////////////////////////////////////////////////////
class VPlayer : public IDetour
@ -804,15 +808,17 @@ class VPlayer : public IDetour
{
LogFunAdr("CPlayer::EyeAngles", CPlayer__EyeAngles);
LogFunAdr("CPlayer::PlayerRunCommand", CPlayer__PlayerRunCommand);
LogFunAdr("CPlayer::PhysicsSimulate", CPlayer__PhysicsSimulate);
}
virtual void GetFun(void) const
{
g_GameDll.FindPatternSIMD("40 53 48 83 EC 30 F2 0F 10 05 ?? ?? ?? ??").GetPtr(CPlayer__EyeAngles);
g_GameDll.FindPatternSIMD("E8 ?? ?? ?? ?? 8B 03 49 81 C6 ?? ?? ?? ??").FollowNearCallSelf().GetPtr(CPlayer__PlayerRunCommand);
g_GameDll.FindPatternSIMD("E8 ?? ?? ?? ?? 48 8B 15 ?? ?? ?? ?? 84 C0 74 06").FollowNearCallSelf().GetPtr(CPlayer__PhysicsSimulate);
}
virtual void GetVar(void) const { }
virtual void GetCon(void) const { }
virtual void Detour(const bool bAttach) const { }
virtual void Detour(const bool bAttach) const;
};
///////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,60 @@
//====== Copyright © 1996-2005, Valve Corporation, All rights reserved. =======//
//
// Purpose:
//
// $NoKeywords: $
//=============================================================================//
#include "engine/server/server.h"
#include "engine/client/client.h"
#include "player_command.h"
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CPlayerMove::CPlayerMove(void)
{
}
//-----------------------------------------------------------------------------
// Purpose: Runs movement commands for the player
// Input : *player -
// *ucmd -
// *moveHelper -
//-----------------------------------------------------------------------------
void CPlayerMove::StaticRunCommand(CPlayerMove* thisp, CPlayer* player, CUserCmd* ucmd, IMoveHelper* moveHelper)
{
CClientExtended* const cle = g_pServer->GetClientExtended(player->GetEdict() - 1);
const float playerCurTime = (player->m_lastUCmdSimulationRemainderTime * TICK_INTERVAL) + player->m_totalExtraClientCmdTimeAttempted;
float playerFrameTime;
// Always default to clamped UserCmd frame time if this cvar is set
if (player_disallow_negative_frametime->GetBool())
playerFrameTime = fmaxf(ucmd->frametime, 0.0f);
else
{
if (player->m_bGamePaused)
playerFrameTime = 0.0f;
else
playerFrameTime = TICK_INTERVAL;
if (ucmd->frametime)
playerFrameTime = ucmd->frametime;
}
if (sv_clampPlayerFrameTime->GetBool() && player->m_joinFrameTime > ((*g_pflServerFrameTimeBase) + playerframetimekick_margin->GetFloat()))
playerFrameTime = 0.0f;
const float timeAllowedForProcessing = cle->ConsumeMovementTimeForUserCmdProcessing(playerFrameTime);
if (!player->IsBot() && (timeAllowedForProcessing < playerFrameTime))
return; // Don't process this command
CPlayerMove__RunCommand(thisp, player, ucmd, moveHelper);
}
void VPlayerMove::Detour(const bool bAttach) const
{
DetourSetup(&CPlayerMove__RunCommand, &CPlayerMove::StaticRunCommand, bAttach);
}

View File

@ -0,0 +1,67 @@
//====== Copyright © 1996-2005, Valve Corporation, All rights reserved. =======//
//
// Purpose:
//
// $NoKeywords: $
//=============================================================================//
#ifndef PLAYER_COMMAND_H
#define PLAYER_COMMAND_H
#include "edict.h"
#include "game/shared/usercmd.h"
#include "game/server/player.h"
class IMoveHelper;
class CMoveData;
class CBasePlayer;
//-----------------------------------------------------------------------------
// Purpose: Server side player movement
//-----------------------------------------------------------------------------
class CPlayerMove
{
public:
//DECLARE_CLASS_NOBASE(CPlayerMove);
// Construction/destruction
CPlayerMove(void);
virtual ~CPlayerMove(void) {}
// Hook statics:
static void StaticRunCommand(CPlayerMove* thisp, CPlayer* player, CUserCmd* ucmd, IMoveHelper* moveHelper);
// Public interfaces:
// Run a movement command from the player
virtual void RunCommand(CPlayer* player, CUserCmd* ucmd, IMoveHelper* moveHelper) = 0;
protected:
// Prepare for running movement
virtual void SetupMove(CPlayer* player, CUserCmd* ucmd, CMoveData* move) = 0;
// Finish movement
virtual void FinishMove(CPlayer* player, CUserCmd* ucmd, CMoveData* move) = 0;
// Called before and after any movement processing
virtual void StartCommand(CPlayer* player, IMoveHelper* pHelper, CUserCmd* cmd) = 0;
};
inline void (*CPlayerMove__RunCommand)(CPlayerMove* thisp, CPlayer* player, CUserCmd* ucmd, IMoveHelper* moveHelper);
///////////////////////////////////////////////////////////////////////////////
class VPlayerMove : public IDetour
{
virtual void GetAdr(void) const
{
LogFunAdr("CPlayerMove::RunCommand", CPlayerMove__RunCommand);
}
virtual void GetFun(void) const
{
g_GameDll.FindPatternSIMD("48 8B C4 55 53 56 57 41 57 48 8D A8 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 44 0F 29 50 ??").GetPtr(CPlayerMove__RunCommand);
}
virtual void GetVar(void) const { }
virtual void GetCon(void) const { }
virtual void Detour(const bool bAttach) const;
};
///////////////////////////////////////////////////////////////////////////////
#endif // PLAYER_COMMAND_H