diff options
author | Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> | 2023-12-27 00:32:01 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-27 01:32:01 +0100 |
commit | f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287 (patch) | |
tree | 90f2c6a4885dbd181799e2325cf33588697674e1 /primedev/shared/exploit_fixes | |
parent | bb8ed59f6891b1196c5f5bbe7346cd171c8215fa (diff) | |
download | NorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.tar.gz NorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.zip |
Folder restructuring from primedev (#624)v1.21.2-rc3v1.21.2
Copies of over the primedev folder structure for easier cherry-picking of further changes
Co-authored-by: F1F7Y <filip.bartos07@proton.me>
Diffstat (limited to 'primedev/shared/exploit_fixes')
-rw-r--r-- | primedev/shared/exploit_fixes/exploitfixes.cpp | 461 | ||||
-rw-r--r-- | primedev/shared/exploit_fixes/exploitfixes_lzss.cpp | 78 | ||||
-rw-r--r-- | primedev/shared/exploit_fixes/exploitfixes_utf8parser.cpp | 199 | ||||
-rw-r--r-- | primedev/shared/exploit_fixes/ns_limits.cpp | 298 | ||||
-rw-r--r-- | primedev/shared/exploit_fixes/ns_limits.h | 52 |
5 files changed, 1088 insertions, 0 deletions
diff --git a/primedev/shared/exploit_fixes/exploitfixes.cpp b/primedev/shared/exploit_fixes/exploitfixes.cpp new file mode 100644 index 00000000..8064d5ac --- /dev/null +++ b/primedev/shared/exploit_fixes/exploitfixes.cpp @@ -0,0 +1,461 @@ +#include "core/convar/cvar.h" +#include "ns_limits.h" +#include "dedicated/dedicated.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "core/math/vector.h" +#include "core/vanilla.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_ns_exploitfixes_log; +ConVar* Cvar_ns_should_log_all_clientcommands; + +ConVar* Cvar_sv_cheats; + +#define BLOCKED_INFO(s) \ + ( \ + [=]() -> bool \ + { \ + if (Cvar_ns_exploitfixes_log->GetBool()) \ + { \ + std::stringstream stream; \ + stream << "ExploitFixes.cpp: " << BLOCK_PREFIX << s; \ + spdlog::error(stream.str()); \ + } \ + return false; \ + }()) + +// block bad netmessages +// Servers can literally request a screenshot from any client, yeah no +// clang-format off +AUTOHOOK(CLC_Screenshot_WriteToBuffer, engine.dll + 0x22AF20, +bool, __fastcall, (void* thisptr, void* buffer)) // 48 89 5C 24 ? 57 48 83 EC 20 8B 42 10 +// clang-format on +{ + if (g_pVanillaCompatibility->GetVanillaCompatibility()) + return CLC_Screenshot_WriteToBuffer(thisptr, buffer); + return false; +} + +// clang-format off +AUTOHOOK(CLC_Screenshot_ReadFromBuffer, engine.dll + 0x221F00, +bool, __fastcall, (void* thisptr, void* buffer)) // 48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B DA 48 8B 52 38 +// clang-format on +{ + if (g_pVanillaCompatibility->GetVanillaCompatibility()) + return CLC_Screenshot_ReadFromBuffer(thisptr, buffer); + return false; +} + +// This is unused ingame and a big client=>server=>client exploit vector +// clang-format off +AUTOHOOK(Base_CmdKeyValues_ReadFromBuffer, engine.dll + 0x220040, +bool, __fastcall, (void* thisptr, void* buffer)) // 40 55 48 81 EC ? ? ? ? 48 8D 6C 24 ? 48 89 5D 70 +// clang-format on +{ + return false; +} + +// clang-format off +AUTOHOOK(CClient_ProcessSetConVar, engine.dll + 0x75CF0, +bool, __fastcall, (void* pMsg)) // 48 8B D1 48 8B 49 18 48 8B 01 48 FF 60 10 +// clang-format on +{ + + constexpr int ENTRY_STR_LEN = 260; + struct SetConVarEntry + { + char name[ENTRY_STR_LEN]; + char val[ENTRY_STR_LEN]; + }; + + struct NET_SetConVar + { + void* vtable; + void* unk1; + void* unk2; + void* m_pMessageHandler; + SetConVarEntry* m_ConVars; // convar entry array + void* unk5; // these 2 unks are just vector capacity or whatever + void* unk6; + int m_ConVars_count; // amount of cvar entries in array (this will not be out of bounds) + }; + + auto msg = (NET_SetConVar*)pMsg; + bool bIsServerFrame = ThreadInServerFrameThread(); + + std::string BLOCK_PREFIX = + std::string {"NET_SetConVar ("} + (bIsServerFrame ? "server" : "client") + "): Blocked dangerous/invalid msg: "; + + if (bIsServerFrame) + { + constexpr int SETCONVAR_SANITY_AMOUNT_LIMIT = 69; + if (msg->m_ConVars_count < 1 || msg->m_ConVars_count > SETCONVAR_SANITY_AMOUNT_LIMIT) + { + return BLOCKED_INFO("Invalid m_ConVars_count (" << msg->m_ConVars_count << ")"); + } + } + + for (int i = 0; i < msg->m_ConVars_count; i++) + { + auto entry = msg->m_ConVars + i; + + // Safety check for memory access + if (CMemoryAddress(entry).IsMemoryReadable(sizeof(*entry))) + { + // Find null terminators + bool nameValid = false, valValid = false; + for (int i = 0; i < ENTRY_STR_LEN; i++) + { + if (!entry->name[i]) + nameValid = true; + if (!entry->val[i]) + valValid = true; + } + + if (!nameValid || !valValid) + return BLOCKED_INFO("Missing null terminators"); + + ConVar* pVar = g_pCVar->FindVar(entry->name); + + if (pVar) + { + memcpy( + entry->name, + pVar->m_ConCommandBase.m_pszName, + strlen(pVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case + + int iFlags = bIsServerFrame ? FCVAR_USERINFO : FCVAR_REPLICATED; + if (!pVar->IsFlagSet(iFlags)) + return BLOCKED_INFO( + "Invalid flags (" << std::hex << "0x" << pVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name); + } + } + else + { + return BLOCKED_INFO("Unreadable memory at " << (void*)entry); // Not risking that one, they all gotta be readable + } + } + + return CClient_ProcessSetConVar(msg); +} + +// prevent invalid user CMDs +// clang-format off +AUTOHOOK(CClient_ProcessUsercmds, engine.dll + 0x1040F0, +bool, __fastcall, (void* thisptr, void* pMsg)) // 40 55 56 48 83 EC 58 +// clang-format on +{ + struct CLC_Move + { + BYTE gap0[24]; + void* m_pMessageHandler; + int m_nBackupCommands; + int m_nNewCommands; + int m_nLength; + // bf_read m_DataIn; + // bf_write m_DataOut; + }; + + auto msg = (CLC_Move*)pMsg; + + const char* BLOCK_PREFIX = "ProcessUserCmds: "; + + if (msg->m_nBackupCommands < 0) + { + return BLOCKED_INFO("Invalid m_nBackupCommands (" << msg->m_nBackupCommands << ")"); + } + + if (msg->m_nNewCommands < 0) + { + return BLOCKED_INFO("Invalid m_nNewCommands (" << msg->m_nNewCommands << ")"); + } + + if (msg->m_nLength <= 0) + return BLOCKED_INFO("Invalid message length (" << msg->m_nLength << ")"); + + return CClient_ProcessUsercmds(thisptr, pMsg); +} + +// clang-format off +AUTOHOOK(ReadUsercmd, server.dll + 0x2603F0, +void, __fastcall, (void* buf, void* pCmd_move, void* pCmd_from)) // 4C 89 44 24 ? 53 55 56 57 +// clang-format on +{ + // Let normal usercmd read happen first, it's safe + ReadUsercmd(buf, pCmd_move, pCmd_from); + + // Now let's make sure the CMD we read isnt messed up to prevent numerous exploits (including server crashing) + struct alignas(4) SV_CUserCmd + { + DWORD command_number; + DWORD tick_count; + float command_time; + Vector3 worldViewAngles; + BYTE gap18[4]; + Vector3 localViewAngles; + Vector3 attackangles; + Vector3 move; + DWORD buttons; + BYTE impulse; + short weaponselect; + DWORD meleetarget; + BYTE gap4C[24]; + char headoffset; + BYTE gap65[11]; + Vector3 cameraPos; + Vector3 cameraAngles; + BYTE gap88[4]; + int tickSomething; + DWORD dword90; + DWORD predictedServerEventAck; + DWORD dword98; + float frameTime; + }; + + auto cmd = (SV_CUserCmd*)pCmd_move; + auto fromCmd = (SV_CUserCmd*)pCmd_from; + + std::string BLOCK_PREFIX = + "ReadUsercmd (command_number delta: " + std::to_string(cmd->command_number - fromCmd->command_number) + "): "; + + // fix invalid player angles + cmd->worldViewAngles.MakeValid(); + cmd->attackangles.MakeValid(); + cmd->localViewAngles.MakeValid(); + + // Fix invalid camera angles + cmd->cameraPos.MakeValid(); + cmd->cameraAngles.MakeValid(); + + // Fix invaid movement vector + cmd->move.MakeValid(); + + if (cmd->frameTime <= 0 || cmd->tick_count == 0 || cmd->command_time <= 0) + { + BLOCKED_INFO( + "Bogus cmd timing (tick_count: " << cmd->tick_count << ", frameTime: " << cmd->frameTime + << ", commandTime : " << cmd->command_time << ")"); + goto INVALID_CMD; // No simulation of bogus-timed cmds + } + + return; + +INVALID_CMD: + + // Fix any gameplay-affecting cmd properties + // NOTE: Currently tickcount/frametime is set to 0, this ~shouldn't~ cause any problems + cmd->worldViewAngles = cmd->localViewAngles = cmd->attackangles = cmd->cameraAngles = {0, 0, 0}; + cmd->tick_count = cmd->frameTime = 0; + cmd->move = cmd->cameraPos = {0, 0, 0}; + cmd->buttons = 0; + cmd->meleetarget = 0; +} + +// ensure that GetLocalBaseClient().m_bRestrictServerCommands is set correctly, which the return value of this function controls +// this is IsValveMod in source, but we're making it IsRespawnMod now since valve didn't make this one +// clang-format off +AUTOHOOK(IsRespawnMod, engine.dll + 0x1C6360, +bool, __fastcall, (const char* pModName)) // 48 83 EC 28 48 8B 0D ? ? ? ? 48 8D 15 ? ? ? ? E8 ? ? ? ? 85 C0 74 63 +// clang-format on +{ + // somewhat temp, store the modname here, since we don't have a proper ptr in engine to it rn + int iSize = strlen(pModName); + g_pModName = new char[iSize + 1]; + strcpy(g_pModName, pModName); + + if (g_pVanillaCompatibility->GetVanillaCompatibility()) + return false; + + return (!strcmp("r2", pModName) || !strcmp("r1", pModName)) && !CommandLine()->CheckParm("-norestrictservercommands"); +} + +// ratelimit stringcmds, and prevent remote clients from calling commands that they shouldn't +// clang-format off +AUTOHOOK(CGameClient__ExecuteStringCommand, engine.dll + 0x1022E0, +bool, __fastcall, (CBaseClient* self, uint32_t unknown, const char* pCommandString)) +// clang-format on +{ + if (Cvar_ns_should_log_all_clientcommands->GetBool()) + spdlog::info("player {} (UID: {}) sent command: \"{}\"", self->m_Name, self->m_UID, pCommandString); + + if (!g_pServerLimits->CheckStringCommandLimits(self)) + { + CBaseClient__Disconnect(self, 1, "Sent too many stringcmd commands"); + return false; + } + + // verify the command we're trying to execute is FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS, if it's a concommand + char* commandBuf[1040]; // assumedly this is the size of CCommand since we don't have an actual constructor + memset(commandBuf, 0, sizeof(commandBuf)); + CCommand tempCommand = *(CCommand*)&commandBuf; + + if (!CCommand__Tokenize(tempCommand, pCommandString, cmd_source_t::kCommandSrcCode) || !tempCommand.ArgC()) + return false; + + ConCommand* command = g_pCVar->FindCommand(tempCommand.Arg(0)); + + // if the command doesn't exist pass it on to ExecuteStringCommand for script clientcommands and stuff + if (command && !command->IsFlagSet(FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS)) + { + // ensure FCVAR_GAMEDLL concommands without FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS can't be executed by remote clients + if (IsDedicatedServer()) + return false; + + if (strcmp(self->m_UID, g_pLocalPlayerUserID)) + return false; + } + + // check for and block abusable legacy portal 2 commands + // these aren't actually concommands weirdly enough, they seem to just be hardcoded + if (!Cvar_sv_cheats->GetBool()) + { + constexpr const char* blockedCommands[] = { + "emit", // Sound-playing exploit (likely for Portal 2 coop devs testing splitscreen sound or something) + + // These both execute a command for every single entity for some reason, nice one valve + "pre_go_to_hub", + "pre_go_to_calibration", + + "end_movie", // Calls "__MovieFinished" script function, not sure exactly what this does but it certainly isn't needed + "load_recent_checkpoint" // This is the instant-respawn exploit, literally just calls RespawnPlayer() + }; + + int iCmdLength = strlen(tempCommand.Arg(0)); + + bool bIsBadCommand = false; + for (auto& blockedCommand : blockedCommands) + { + if (iCmdLength != strlen(blockedCommand)) + continue; + + for (int i = 0; tempCommand.Arg(0)[i]; i++) + if (tolower(tempCommand.Arg(0)[i]) != blockedCommand[i]) + goto NEXT_COMMAND; // break out of this loop, then go to next command + + // this is a command we need to block + return false; + NEXT_COMMAND:; + } + } + + return CGameClient__ExecuteStringCommand(self, unknown, pCommandString); +} + +// prevent clients from crashing servers through overflowing CNetworkStringTableContainer::WriteBaselines +bool bWasWritingStringTableSuccessful; + +// clang-format off +AUTOHOOK(CBaseClient__SendServerInfo, engine.dll + 0x104FB0, +void, __fastcall, (void* self)) +// clang-format on +{ + bWasWritingStringTableSuccessful = true; + CBaseClient__SendServerInfo(self); + if (!bWasWritingStringTableSuccessful) + CBaseClient__Disconnect( + self, 1, "Overflowed CNetworkStringTableContainer::WriteBaselines, try restarting your client and reconnecting"); +} + +// return null when GetEntByIndex is passed an index >= 0x4000 +// this is called from exactly 1 script clientcommand that can be given an arbitrary index, and going above 0x4000 crashes +// clang-format off +AUTOHOOK(GetEntByIndex, server.dll + 0x2A8A50, +void*, __fastcall, (int i)) +// clang-format on +{ + const int MAX_ENT_IDX = 0x4000; + + if (i >= MAX_ENT_IDX) + { + spdlog::warn("GetEntByIndex {} is out of bounds (max {})", i, MAX_ENT_IDX); + return nullptr; + } + + return GetEntByIndex(i); +} +// clang-format off +AUTOHOOK(CL_CopyExistingEntity, engine.dll + 0x6F940, +bool, __fastcall, (void* a1)) +// clang-format on +{ + struct CEntityReadInfo + { + BYTE gap[40]; + int nNewEntity; + }; + + CEntityReadInfo* pReadInfo = (CEntityReadInfo*)a1; + if (pReadInfo->nNewEntity >= 0x1000 || pReadInfo->nNewEntity < 0) + { + // Value isn't sanitized in release builds for + // every game powered by the Source Engine 1 + // causing read/write outside of array bounds. + // This defect has let to the achievement of a + // full-chain RCE exploit. We hook and perform + // sanity checks for the value of m_nNewEntity + // here to prevent this behavior from happening. + return false; + } + + return CL_CopyExistingEntity(a1); +} + +ON_DLL_LOAD("engine.dll", EngineExploitFixes, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + // allow client/ui to run clientcommands despite restricting servercommands + module.Offset(0x4FB65).Patch("EB 11"); + module.Offset(0x4FBAC).Patch("EB 16"); + + // patch to set bWasWritingStringTableSuccessful in CNetworkStringTableContainer::WriteBaselines if it fails + { + CMemoryAddress writeAddress(&bWasWritingStringTableSuccessful - module.Offset(0x234EDC).m_nAddress); + + CMemoryAddress addr = module.Offset(0x234ED2); + addr.Patch("C7 05"); + addr.Offset(2).Patch((BYTE*)&writeAddress, sizeof(writeAddress)); + + addr.Offset(6).Patch("00 00 00 00"); + + addr.Offset(10).NOP(5); + } +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerExploitFixes, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) + + // ret at the start of CServerGameClients::ClientCommandKeyValues as it has no benefit and is forwarded to client (i.e. security issue) + // this prevents the attack vector of client=>server=>client, however server=>client also has clientside patches + module.Offset(0x153920).Patch("C3"); + + // Dumb ANTITAMPER patches (they negatively impact performance and security) + constexpr const char* ANTITAMPER_EXPORTS[] = { + "ANTITAMPER_SPOTCHECK_CODEMARKER", + "ANTITAMPER_TESTVALUE_CODEMARKER", + "ANTITAMPER_TRIGGER_CODEMARKER", + }; + + // Prevent these from actually doing anything + for (auto exportName : ANTITAMPER_EXPORTS) + { + CMemoryAddress exportAddr = module.GetExport(exportName); + if (exportAddr) + { + // Just return, none of them have any args or are userpurge + exportAddr.Patch("C3"); + spdlog::info("Patched AntiTamper function export \"{}\"", exportName); + } + } + + Cvar_ns_exploitfixes_log = + new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever ExploitFixes.cpp blocks/corrects something"); + Cvar_ns_should_log_all_clientcommands = + new ConVar("ns_should_log_all_clientcommands", "0", FCVAR_NONE, "Whether to log all clientcommands"); + + Cvar_sv_cheats = g_pCVar->FindVar("sv_cheats"); +} diff --git a/primedev/shared/exploit_fixes/exploitfixes_lzss.cpp b/primedev/shared/exploit_fixes/exploitfixes_lzss.cpp new file mode 100644 index 00000000..ccb6ac18 --- /dev/null +++ b/primedev/shared/exploit_fixes/exploitfixes_lzss.cpp @@ -0,0 +1,78 @@ + +AUTOHOOK_INIT() + +static constexpr int LZSS_LOOKSHIFT = 4; + +struct lzss_header_t +{ + unsigned int id; + unsigned int actualSize; +}; + +// Rewrite of CLZSS::SafeUncompress to fix a vulnerability where malicious compressed payloads could cause the decompressor to try to read +// out of the bounds of the output buffer. +// clang-format off +AUTOHOOK(CLZSS__SafeDecompress, engine.dll + 0x432A10, +unsigned int, __fastcall, (void* self, const unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize)) +// clang-format on +{ + unsigned int totalBytes = 0; + int getCmdByte = 0; + int cmdByte = 0; + + lzss_header_t header = *(lzss_header_t*)pInput; + + if (!pInput || !header.actualSize || header.id != 0x53535A4C || header.actualSize > unBufSize) + return 0; + + pInput += sizeof(lzss_header_t); + + for (;;) + { + if (!getCmdByte) + cmdByte = *pInput++; + + getCmdByte = (getCmdByte + 1) & 0x07; + + if (cmdByte & 0x01) + { + int position = *pInput++ << LZSS_LOOKSHIFT; + position |= (*pInput >> LZSS_LOOKSHIFT); + position += 1; + int count = (*pInput++ & 0x0F) + 1; + if (count == 1) + break; + + // Ensure reference chunk exists entirely within our buffer + if (position > totalBytes) + return 0; + + totalBytes += count; + if (totalBytes > unBufSize) + return 0; + + unsigned char* pSource = pOutput - position; + for (int i = 0; i < count; i++) + *pOutput++ = *pSource++; + } + else + { + totalBytes++; + if (totalBytes > unBufSize) + return 0; + + *pOutput++ = *pInput++; + } + cmdByte = cmdByte >> 1; + } + + if (totalBytes != header.actualSize) + return 0; + + return totalBytes; +} + +ON_DLL_LOAD("engine.dll", ExploitFixes_LZSS, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/primedev/shared/exploit_fixes/exploitfixes_utf8parser.cpp b/primedev/shared/exploit_fixes/exploitfixes_utf8parser.cpp new file mode 100644 index 00000000..3d97f750 --- /dev/null +++ b/primedev/shared/exploit_fixes/exploitfixes_utf8parser.cpp @@ -0,0 +1,199 @@ + +AUTOHOOK_INIT() + +INT64(__fastcall* sub_F1320)(DWORD a1, char* a2); + +// Reimplementation of an exploitable UTF decoding function in titanfall +bool __fastcall CheckUTF8Valid(INT64* a1, DWORD* a2, char* strData) +{ + DWORD v3; // eax + char* v4; // rbx + char v5; // si + char* _strData; // rdi + char* v7; // rbp + char v11; // al + DWORD v12; // er9 + DWORD v13; // ecx + DWORD v14; // edx + DWORD v15; // er8 + int v16; // eax + DWORD v17; // er9 + int v18; // eax + DWORD v19; // er9 + DWORD v20; // ecx + int v21; // eax + int v22; // er9 + DWORD v23; // edx + int v24; // eax + int v25; // er9 + DWORD v26; // er9 + DWORD v27; // er10 + DWORD v28; // ecx + DWORD v29; // edx + DWORD v30; // er8 + int v31; // eax + DWORD v32; // er10 + int v33; // eax + DWORD v34; // er10 + DWORD v35; // ecx + int v36; // eax + int v37; // er10 + DWORD v38; // edx + int v39; // eax + int v40; // er10 + DWORD v41; // er10 + INT64 v43; // r8 + INT64 v44; // rdx + INT64 v45; // rcx + INT64 v46; // rax + INT64 v47; // rax + char v48; // al + INT64 v49; // r8 + INT64 v50; // rdx + INT64 v51; // rcx + INT64 v52; // rax + INT64 v53; // rax + + v3 = a2[2]; + v4 = (char*)(a1[1] + *a2); + v5 = 0; + _strData = strData; + v7 = &v4[*((UINT16*)a2 + 2)]; + if (v3 >= 2) + { + ++v4; + --v7; + if (v3 != 2) + { + while (1) + { + if (!CMemoryAddress(v4).IsMemoryReadable(1)) + return false; // INVALID + + v11 = *v4++; // crash potential + if (v11 != 92) + goto LABEL_6; + v11 = *v4++; + if (v11 == 110) + break; + switch (v11) + { + case 't': + v11 = 9; + goto LABEL_6; + case 'r': + v11 = 13; + goto LABEL_6; + case 'b': + v11 = 8; + goto LABEL_6; + case 'f': + v11 = 12; + goto LABEL_6; + } + if (v11 != 117) + goto LABEL_6; + v12 = *v4 | 0x20; + v13 = v4[1] | 0x20; + v14 = v4[2] | 0x20; + v15 = v4[3] | 0x20; + v16 = 87; + if (v12 <= 0x39) + v16 = 48; + v17 = v12 - v16; + v18 = 87; + v19 = v17 << 12; + if (v13 <= 0x39) + v18 = 48; + v20 = v13 - v18; + v21 = 87; + v22 = (v20 << 8) | v19; + if (v14 <= 0x39) + v21 = 48; + v23 = v14 - v21; + v24 = 87; + v25 = (16 * v23) | v22; + if (v15 <= 0x39) + v24 = 48; + v4 += 4; + v26 = (v15 - v24) | v25; + if (v26 - 55296 <= 0x7FF) + { + if (v26 >= 0xDC00) + return true; + if (*v4 != 92 || v4[1] != 117) + return true; + + v27 = v4[2] | 0x20; + v28 = v4[3] | 0x20; + v29 = v4[4] | 0x20; + v30 = v4[5] | 0x20; + v31 = 87; + if (v27 <= 0x39) + v31 = 48; + v32 = v27 - v31; + v33 = 87; + v34 = v32 << 12; + if (v28 <= 0x39) + v33 = 48; + v35 = v28 - v33; + v36 = 87; + v37 = (v35 << 8) | v34; + if (v29 <= 0x39) + v36 = 48; + v38 = v29 - v36; + v39 = 87; + v40 = (16 * v38) | v37; + if (v30 <= 0x39) + v39 = 48; + v4 += 6; + v41 = ((v30 - v39) | v40) - 56320; + if (v41 > 0x3FF) + return true; + v26 = v41 | ((v26 - 55296) << 10); + } + _strData += (DWORD)sub_F1320(v26, _strData); + LABEL_7: + if (v4 == v7) + goto LABEL_48; + } + v11 = 10; + LABEL_6: + v5 |= v11; + *_strData++ = v11; + goto LABEL_7; + } + } +LABEL_48: + return true; +} + +// prevent utf8 parser from crashing when provided bad data, which can be sent through user-controlled openinvites +// clang-format off +AUTOHOOK(Rson_ParseUTF8, engine.dll + 0xEF670, +bool, __fastcall, (INT64* a1, DWORD* a2, char* strData)) // 48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 41 54 41 55 41 56 41 57 48 83 EC 20 8B 1A +// clang-format on +{ + static void* targetRetAddr = CModule("engine.dll").FindPattern("84 C0 75 2C 49 8B 16"); + + // only call if we're parsing utf8 data from the network (i.e. communities), otherwise we get perf issues + void* pReturnAddress = +#ifdef _MSC_VER + _ReturnAddress() +#else + __builtin_return_address(0) +#endif + ; + + if (pReturnAddress == targetRetAddr && !CheckUTF8Valid(a1, a2, strData)) + return false; + + return Rson_ParseUTF8(a1, a2, strData); +} + +ON_DLL_LOAD("engine.dll", EngineExploitFixes_UTF8Parser, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + sub_F1320 = module.FindPattern("83 F9 7F 77 08 88 0A").RCast<INT64(__fastcall*)(DWORD, char*)>(); +} diff --git a/primedev/shared/exploit_fixes/ns_limits.cpp b/primedev/shared/exploit_fixes/ns_limits.cpp new file mode 100644 index 00000000..bd855ee4 --- /dev/null +++ b/primedev/shared/exploit_fixes/ns_limits.cpp @@ -0,0 +1,298 @@ +#include "ns_limits.h" +#include "engine/hoststate.h" +#include "client/r2client.h" +#include "engine/r2engine.h" +#include "server/r2server.h" +#include "core/tier0.h" +#include "core/math/vector.h" +#include "server/auth/serverauthentication.h" + +AUTOHOOK_INIT() + +ServerLimitsManager* g_pServerLimits; + +float (*CEngineServer__GetTimescale)(); + +// todo: make this work on higher timescales, also possibly disable when sv_cheats is set +void ServerLimitsManager::RunFrame(double flCurrentTime, float flFrameTime) +{ + if (Cvar_sv_antispeedhack_enable->GetBool()) + { + // for each player, set their usercmd processing budget for the frame to the last frametime for the server + for (int i = 0; i < g_pGlobals->m_nMaxClients; i++) + { + CBaseClient* player = &g_pClientArray[i]; + + if (m_PlayerLimitData.find(player) != m_PlayerLimitData.end()) + { + PlayerLimitData* pLimitData = &g_pServerLimits->m_PlayerLimitData[player]; + if (pLimitData->flFrameUserCmdBudget < g_pGlobals->m_flTickInterval * Cvar_sv_antispeedhack_maxtickbudget->GetFloat()) + { + pLimitData->flFrameUserCmdBudget += g_pServerLimits->Cvar_sv_antispeedhack_budgetincreasemultiplier->GetFloat() * + fmax(flFrameTime, g_pGlobals->m_flFrameTime * CEngineServer__GetTimescale()); + } + } + } + } +} + +void ServerLimitsManager::AddPlayer(CBaseClient* player) +{ + PlayerLimitData limitData; + limitData.flFrameUserCmdBudget = + g_pGlobals->m_flTickInterval * CEngineServer__GetTimescale() * Cvar_sv_antispeedhack_maxtickbudget->GetFloat(); + + m_PlayerLimitData.insert(std::make_pair(player, limitData)); +} + +void ServerLimitsManager::RemovePlayer(CBaseClient* player) +{ + if (m_PlayerLimitData.find(player) != m_PlayerLimitData.end()) + m_PlayerLimitData.erase(player); +} + +bool ServerLimitsManager::CheckStringCommandLimits(CBaseClient* player) +{ + if (CVar_sv_quota_stringcmdspersecond->GetInt() != -1) + { + // note: this isn't super perfect, legit clients can trigger it in lobby if they try, mostly good enough tho imo + if (Plat_FloatTime() - m_PlayerLimitData[player].lastClientCommandQuotaStart >= 1.0) + { + // reset quota + m_PlayerLimitData[player].lastClientCommandQuotaStart = Plat_FloatTime(); + m_PlayerLimitData[player].numClientCommandsInQuota = 0; + } + + m_PlayerLimitData[player].numClientCommandsInQuota++; + if (m_PlayerLimitData[player].numClientCommandsInQuota > CVar_sv_quota_stringcmdspersecond->GetInt()) + { + // too many stringcmds, dc player + return false; + } + } + + return true; +} + +bool ServerLimitsManager::CheckChatLimits(CBaseClient* player) +{ + if (Plat_FloatTime() - m_PlayerLimitData[player].lastSayTextLimitStart >= 1.0) + { + m_PlayerLimitData[player].lastSayTextLimitStart = Plat_FloatTime(); + m_PlayerLimitData[player].sayTextLimitCount = 0; + } + + if (m_PlayerLimitData[player].sayTextLimitCount >= Cvar_sv_max_chat_messages_per_sec->GetInt()) + return false; + + m_PlayerLimitData[player].sayTextLimitCount++; + return true; +} + +// clang-format off +AUTOHOOK(CNetChan__ProcessMessages, engine.dll + 0x2140A0, +char, __fastcall, (void* self, void* buf)) +// clang-format on +{ + enum eNetChanLimitMode + { + NETCHANLIMIT_WARN, + NETCHANLIMIT_KICK + }; + + double startTime = Plat_FloatTime(); + char ret = CNetChan__ProcessMessages(self, buf); + + // check processing limits, unless we're in a level transition + if (g_pHostState->m_iCurrentState == HostState_t::HS_RUN && ThreadInServerFrameThread()) + { + // player that sent the message + CBaseClient* sender = *(CBaseClient**)((char*)self + 368); + + // if no sender, return + // relatively certain this is fine? + if (!sender || !g_pServerLimits->m_PlayerLimitData.count(sender)) + return ret; + + // reset every second + if (startTime - g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart >= 1.0 || + g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart == -1.0) + { + g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart = startTime; + g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime = 0.0; + } + g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime += (Plat_FloatTime() * 1000) - (startTime * 1000); + + if (g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime >= + g_pServerLimits->Cvar_net_chan_limit_msec_per_sec->GetInt()) + { + spdlog::warn( + "Client {} hit netchan processing limit with {}ms of processing time this second (max is {})", + (char*)sender + 0x16, + g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime, + g_pServerLimits->Cvar_net_chan_limit_msec_per_sec->GetInt()); + + // never kick local player + if (g_pServerLimits->Cvar_net_chan_limit_mode->GetInt() != NETCHANLIMIT_WARN && strcmp(g_pLocalPlayerUserID, sender->m_UID)) + { + CBaseClient__Disconnect(sender, 1, "Exceeded net channel processing limit"); + return false; + } + } + } + + return ret; +} + +bool ServerLimitsManager::CheckConnectionlessPacketLimits(netpacket_t* packet) +{ + static const ConVar* Cvar_net_data_block_enabled = g_pCVar->FindVar("net_data_block_enabled"); + + // don't ratelimit datablock packets as long as datablock is enabled + if (packet->adr.type == NA_IP && + (!(packet->data[4] == 'N' && Cvar_net_data_block_enabled->GetBool()) || !Cvar_net_data_block_enabled->GetBool())) + { + // bad lookup: optimise later tm + UnconnectedPlayerLimitData* sendData = nullptr; + for (UnconnectedPlayerLimitData& foundSendData : g_pServerLimits->m_UnconnectedPlayerLimitData) + { + if (!memcmp(packet->adr.ip, foundSendData.ip, 16)) + { + sendData = &foundSendData; + break; + } + } + + if (!sendData) + { + sendData = &g_pServerLimits->m_UnconnectedPlayerLimitData.emplace_back(); + memcpy(sendData->ip, packet->adr.ip, 16); + } + + if (Plat_FloatTime() < sendData->timeoutEnd) + return false; + + if (Plat_FloatTime() - sendData->lastQuotaStart >= 1.0) + { + sendData->lastQuotaStart = Plat_FloatTime(); + sendData->packetCount = 0; + } + + sendData->packetCount++; + + if (sendData->packetCount >= g_pServerLimits->Cvar_sv_querylimit_per_sec->GetInt()) + { + spdlog::warn( + "Client went over connectionless ratelimit of {} per sec with packet of type {}", + g_pServerLimits->Cvar_sv_querylimit_per_sec->GetInt(), + packet->data[4]); + + // timeout for a minute + sendData->timeoutEnd = Plat_FloatTime() + 60.0; + return false; + } + } + + return true; +} + +// this is weird and i'm not sure if it's correct, so not using for now +/*AUTOHOOK(CBasePlayer__PhysicsSimulate, server.dll + 0x5A6E50, bool, __fastcall, (void* self, int a2, char a3)) +{ + spdlog::info("CBasePlayer::PhysicsSimulate"); + return CBasePlayer__PhysicsSimulate(self, a2, a3); +}*/ + +struct alignas(4) SV_CUserCmd +{ + DWORD command_number; + DWORD tick_count; + float command_time; + Vector3 worldViewAngles; + BYTE gap18[4]; + Vector3 localViewAngles; + Vector3 attackangles; + Vector3 move; + DWORD buttons; + BYTE impulse; + short weaponselect; + DWORD meleetarget; + BYTE gap4C[24]; + char headoffset; + BYTE gap65[11]; + Vector3 cameraPos; + Vector3 cameraAngles; + BYTE gap88[4]; + int tickSomething; + DWORD dword90; + DWORD predictedServerEventAck; + DWORD dword98; + float frameTime; +}; + +// clang-format off +AUTOHOOK(CPlayerMove__RunCommand, server.dll + 0x5B8100, +void, __fastcall, (void* self, CBasePlayer* player, SV_CUserCmd* pUserCmd, uint64_t a4)) +// clang-format on +{ + if (g_pServerLimits->Cvar_sv_antispeedhack_enable->GetBool()) + { + CBaseClient* pClient = &g_pClientArray[player->m_nPlayerIndex - 1]; + + if (g_pServerLimits->m_PlayerLimitData.find(pClient) != g_pServerLimits->m_PlayerLimitData.end()) + { + PlayerLimitData* pLimitData = &g_pServerLimits->m_PlayerLimitData[pClient]; + + pLimitData->flFrameUserCmdBudget = fmax(0.0, pLimitData->flFrameUserCmdBudget - pUserCmd->frameTime); + + if (pLimitData->flFrameUserCmdBudget <= 0.0) + { + spdlog::warn("player {} went over usercmd budget ({})", pClient->m_Name, pLimitData->flFrameUserCmdBudget); + return; + } + } + } + + CPlayerMove__RunCommand(self, player, pUserCmd, a4); +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerLimits, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + g_pServerLimits = new ServerLimitsManager; + + g_pServerLimits->CVar_sv_quota_stringcmdspersecond = new ConVar( + "sv_quota_stringcmdspersecond", + "60", + FCVAR_GAMEDLL, + "How many string commands per second clients are allowed to submit, 0 to disallow all string commands, -1 to disable"); + g_pServerLimits->Cvar_net_chan_limit_mode = + new ConVar("net_chan_limit_mode", "0", FCVAR_GAMEDLL, "The mode for netchan processing limits: 0 = warn, 1 = kick"); + g_pServerLimits->Cvar_net_chan_limit_msec_per_sec = new ConVar( + "net_chan_limit_msec_per_sec", + "100", + FCVAR_GAMEDLL, + "Netchannel processing is limited to so many milliseconds, abort connection if exceeding budget"); + g_pServerLimits->Cvar_sv_querylimit_per_sec = new ConVar("sv_querylimit_per_sec", "15", FCVAR_GAMEDLL, ""); + g_pServerLimits->Cvar_sv_max_chat_messages_per_sec = new ConVar("sv_max_chat_messages_per_sec", "5", FCVAR_GAMEDLL, ""); + g_pServerLimits->Cvar_sv_antispeedhack_enable = + new ConVar("sv_antispeedhack_enable", "0", FCVAR_NONE, "whether to enable antispeedhack protections"); + g_pServerLimits->Cvar_sv_antispeedhack_maxtickbudget = new ConVar( + "sv_antispeedhack_maxtickbudget", + "64", + FCVAR_GAMEDLL, + "Maximum number of client-issued usercmd ticks that can be replayed in packet loss conditions"); + g_pServerLimits->Cvar_sv_antispeedhack_budgetincreasemultiplier = new ConVar( + "sv_antispeedhack_budgetincreasemultiplier", + "1", + FCVAR_GAMEDLL, + "Increase usercmd processing budget by tickinterval * value per tick"); + + CEngineServer__GetTimescale = module.Offset(0x240840).RCast<float (*)()>(); +} + +ON_DLL_LOAD("server.dll", ServerLimitsServer, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) +} diff --git a/primedev/shared/exploit_fixes/ns_limits.h b/primedev/shared/exploit_fixes/ns_limits.h new file mode 100644 index 00000000..546fec6f --- /dev/null +++ b/primedev/shared/exploit_fixes/ns_limits.h @@ -0,0 +1,52 @@ +#pragma once +#include "engine/r2engine.h" +#include "core/convar/convar.h" +#include <unordered_map> + +struct PlayerLimitData +{ + double lastClientCommandQuotaStart = -1.0; + int numClientCommandsInQuota = 0; + + double lastNetChanProcessingLimitStart = -1.0; + double netChanProcessingLimitTime = 0.0; + + double lastSayTextLimitStart = -1.0; + int sayTextLimitCount = 0; + + float flFrameUserCmdBudget = 0.0; +}; + +struct UnconnectedPlayerLimitData +{ + char ip[16]; + double lastQuotaStart = 0.0; + int packetCount = 0; + double timeoutEnd = -1.0; +}; + +class ServerLimitsManager +{ +public: + ConVar* CVar_sv_quota_stringcmdspersecond; + ConVar* Cvar_net_chan_limit_mode; + ConVar* Cvar_net_chan_limit_msec_per_sec; + ConVar* Cvar_sv_querylimit_per_sec; + ConVar* Cvar_sv_max_chat_messages_per_sec; + ConVar* Cvar_sv_antispeedhack_enable; + ConVar* Cvar_sv_antispeedhack_maxtickbudget; + ConVar* Cvar_sv_antispeedhack_budgetincreasemultiplier; + + std::unordered_map<CBaseClient*, PlayerLimitData> m_PlayerLimitData; + std::vector<UnconnectedPlayerLimitData> m_UnconnectedPlayerLimitData; + +public: + void RunFrame(double flCurrentTime, float flFrameTime); + void AddPlayer(CBaseClient* player); + void RemovePlayer(CBaseClient* player); + bool CheckStringCommandLimits(CBaseClient* player); + bool CheckChatLimits(CBaseClient* player); + bool CheckConnectionlessPacketLimits(netpacket_t* packet); +}; + +extern ServerLimitsManager* g_pServerLimits; |