diff options
author | BobTheBob <32057864+BobTheBob9@users.noreply.github.com> | 2022-10-17 23:26:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-17 23:26:07 +0100 |
commit | 841881af9ea6ec73b1d505d5a8f7c1f766273724 (patch) | |
tree | 91feb40fe810984b59d2d2da440e289370b0a137 /NorthstarDLL/exploitfixes.cpp | |
parent | dc0934d29caacc8da1e7df8b775d24b4e99c381c (diff) | |
download | NorthstarLauncher-841881af9ea6ec73b1d505d5a8f7c1f766273724.tar.gz NorthstarLauncher-841881af9ea6ec73b1d505d5a8f7c1f766273724.zip |
big refactor (#171)v1.10.0-rc1
* use in-file macros rather than global funcs for registering dll load callbacks
* move more things to macros
* fix debug crashes
* move sqvm funcs to sq managers
* get rid of context file
* refactor some squirrel stuff and ingame compilation error message
* move tier0 and playlist funcs to namespaces
* uiscript_reset concommand: don't loop forever if compilation fails
* improve showing console for ui script compile errors
* standardise concommand func naming in c++
* use lambdas for dll load callbacks so intellisense shits itself less
* use cvar change callbacks for unescaping ns_server_name and ns_server_desc
* add proper helpstrings to masterserver cvars
* add cvar help and find
* allow parsing of convar flags from string
* normalise mod fs paths to be lowercase
* move hoststate to its own file and add host_init hooks
* better IsFlagSet def
* replace files in ReadFromCache
* rename g_ModManager to g_pModManager
* formatting changes
* make cvar print work on dedi, move demo fix stuff, add findflags
* add proper map autocompletes and maps command
* formatting changes
* separate gameutils into multiple r2 headers
* Update keyvalues.cpp
* move sqvm funcs into wrappers in the manager class
* remove unnecessary header files
* lots of cleanup and starting moving to new hooking macros
* update more stuff to new hook macros
* rename project folder (:tf: commit log)
* fix up postbuild commands to use relative dir
* almost fully replaced hooking lib
* completely remove old hooking
* add nsprefix because i forgot to include it
* move exploit prevention and limits code out of serverauthentication, and have actual defs for CBasePlayer
* use modular ServerPresence system for registering servers
* add new memory lib
* accidentally pushed broke code oops
* lots of stuff idk
* implement some more prs
* improve rpakfilesystem
* fix line endings on vcxproj
* Revert "fix line endings on vcxproj"
This reverts commit 4ff7d022d2602c2dba37beba8b8df735cf5cd7d9.
* add more prs
* i swear i committed these how are they not there
* Add ability to load Datatables from files (#238)
* first version of kinda working custom datatables
* Fix copy error
* Finish custom datatables
* Fix Merge
* Fix line endings
* Add fallback to rpak when ns_prefere_datatable_from_disk is true
* fix typo
* Bug fixess
* Fix Function Registration hook
* Set convar value
* Fix Client and Ui VM
* enable server auth with ms agian
* Add Filters
* FIx unused import
* Merge remote-tracking branch 'upsteam/bobs-big-refactor-pr' into datatables
Co-authored-by: RoyalBlue1 <realEmail@veryRealURL.com>
* Add some changes from main to refactor (#243)
* Add PR template
* Update CI folder location
* Delete startup args txt files
* Fix line endings (hopefully) (#244)
* Fix line endings (hopefully)
* Fix more line endings
* Update refactor (#250)
* Add PR template
* Update CI folder location
* Delete startup args txt files
* Add editorconfig file (#246)
* Add editorconfig file
It's a cross-editor compatible config file that defines certain editor
behaviour (e.g. adding/removing newline at end of file)
It is supported by major editors like Visual Studio (Code) and by
version control providers like GitHub.
Should end the constant adding/removing of final newline in PRs
* More settings
- unicode by default
- trim newlines
- use tabs for indentation (ugh)
* Ignore folder rename (#245)
* Hot reload banlist on player join (#233)
* added banlist hotreload
* fix formatting
* didnt append, cleared whole file oopsie
* unfuckedunban not rewriting file
* fixed not checking for new line
Co-authored-by: ScureX <47725553+ScureX@users.noreply.github.com>
* Refactor cleanup (#256)
* Fix indentation
* Fix path in clang-format command in readme
* Refactor cleanup (some formatting fixes) (#257)
* Fix some formatting
* More formatting fixes
* add scriptdatatable.cpp rewrite
* Some formatting fixes (#260)
* More formatting stuff (#261)
* various formatting changes and fixes
* Fix changed icon (#264)
* clang format, fix issues with server registration and rpak loading
* fix more formatting
* update postbuild step
* set launcher directory and error on fail creating log files
* change some stuff in exploitfixes
* only unrestrict dev commands when commandline flag is present
* fix issues with cvar flag commit
* fixup command flags better and reformat
* bring up to date with main
* fixup formatting
* improve cvar flag fixup and remove temp thing from findflags
* set serverfilter better
* avoid ptr decay when setting auth token
* add more entity functions
* Fix the MS server registration issues. (#285)
* Port ms presence reporter to std::async
* Fix crash due to std::optional being assigned nullptr.
* Fix formatting.
* Wait 20 seconds if MS returns DUPLICATE_SERVER.
* Change PERSISTENCE_MAX_SIZE to fix player authentication (#287)
The size check added in the refactor was incorrect:
- 56306: expected pdata size based on the pdef
- 512: allowance for trailing junk (r2 adds 137 bytes of trailing junk)
- 100: for some wiggle room
Co-Authored-By: pg9182 <96569817+pg9182@users.noreply.github.com>
* change miscserverscript to use actual entity arguments rather than
player index jank
* Fix token clearing hook (#290)
A certain someone forgot to put an `0x` in front of their hex number, meaning the offset is wrong.
This would cause token to be leaked again
Co-authored-by: Maya <malte.hoermeyer@web.de>
Co-authored-by: RoyalBlue1 <realEmail@veryRealURL.com>
Co-authored-by: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com>
Co-authored-by: ScureX <47725553+ScureX@users.noreply.github.com>
Co-authored-by: Erlite <ys.aameziane@gmail.com>
Co-authored-by: Emma Miler <emma.pi@protonmail.com>
Co-authored-by: pg9182 <96569817+pg9182@users.noreply.github.com>
Diffstat (limited to 'NorthstarDLL/exploitfixes.cpp')
-rw-r--r-- | NorthstarDLL/exploitfixes.cpp | 543 |
1 files changed, 223 insertions, 320 deletions
diff --git a/NorthstarDLL/exploitfixes.cpp b/NorthstarDLL/exploitfixes.cpp index 0aa0a3bf..240c352c 100644 --- a/NorthstarDLL/exploitfixes.cpp +++ b/NorthstarDLL/exploitfixes.cpp @@ -1,52 +1,63 @@ #include "pch.h" - -#include "exploitfixes.h" -#include "exploitfixes_utf8parser.h" -#include "nsmem.h" #include "cvar.h" -#include "gameutils.h" +#include "limits.h" +#include "dedicated.h" +#include "tier0.h" +#include "r2engine.h" +#include "r2client.h" +#include "vector.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_ns_exploitfixes_log; +ConVar* Cvar_ns_should_log_all_clientcommands; + +ConVar* Cvar_sv_cheats; -ConVar* ns_exploitfixes_log; -#define SHOULD_LOG (ns_exploitfixes_log->m_Value.m_nValue > 0) #define BLOCKED_INFO(s) \ ( \ [=]() -> bool \ { \ - if (SHOULD_LOG) \ + if (Cvar_ns_exploitfixes_log->GetBool()) \ { \ std::stringstream stream; \ - stream << "exploitfixes.cpp: " << BLOCK_PREFIX << s; \ + stream << "ExploitFixes.cpp: " << BLOCK_PREFIX << s; \ spdlog::error(stream.str()); \ } \ return false; \ }()) -struct Float3 +// 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 { - float vals[3]; - - void MakeValid() - { - for (auto& val : vals) - if (isnan(val)) - val = 0; - } -}; - -#define BLOCK_NETMSG_FUNC(name, pattern) \ - KHOOK(name, ("engine.dll", pattern), bool, __fastcall, (void* thisptr, void* buffer)) \ - { \ - return false; \ - } + return false; +} -// Servers can literally request a screenshot from any client, yeah no -BLOCK_NETMSG_FUNC(CLC_Screenshot_WriteToBuffer, "48 89 5C 24 ? 57 48 83 EC 20 8B 42 10"); -BLOCK_NETMSG_FUNC(CLC_Screenshot_ReadFromBuffer, "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 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 +{ + return false; +} -// This is unused ingame and a big exploit vector -BLOCK_NETMSG_FUNC(Base_CmdKeyValues_ReadFromBuffer, "40 55 48 81 EC ? ? ? ? 48 8D 6C 24 ? 48 89 5D 70"); +// 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; +} -KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48 FF 60 10"), bool, __fastcall, (void* pMsg)) +// 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; @@ -69,25 +80,12 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48 }; auto msg = (NET_SetConVar*)pMsg; + bool bIsServerFrame = Tier0::ThreadInServerFrameThread(); - bool areWeServer; - - { - // Figure out of we are the client or the server - // To do this, we utilize the msg's m_pMessageHandler pointer - // m_pMessageHandler points to a virtual class that handles all net messages - // The first virtual table function of our m_pMessageHandler will differ if it is IServerMessageHandler or IClientMessageHandler - void* msgHandlerVTableFirstFunc = **(void****)(msg->m_pMessageHandler); - static auto engineBaseAddress = (uintptr_t)GetModuleHandleA("engine.dll"); - auto offset = uintptr_t(msgHandlerVTableFirstFunc) - engineBaseAddress; - - constexpr uintptr_t CLIENTSTATE_FIRST_VFUNC_OFFSET = 0x8A15C; - areWeServer = offset != CLIENTSTATE_FIRST_VFUNC_OFFSET; - } - - std::string BLOCK_PREFIX = std::string {"NET_SetConVar ("} + (areWeServer ? "server" : "client") + "): Blocked dangerous/invalid msg: "; + std::string BLOCK_PREFIX = + std::string {"NET_SetConVar ("} + (bIsServerFrame ? "server" : "client") + "): Blocked dangerous/invalid msg: "; - if (areWeServer) + if (bIsServerFrame) { constexpr int SETCONVAR_SANITY_AMOUNT_LIMIT = 69; if (msg->m_ConVars_count < 1 || msg->m_ConVars_count > SETCONVAR_SANITY_AMOUNT_LIMIT) @@ -101,9 +99,8 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48 auto entry = msg->m_ConVars + i; // Safety check for memory access - if (NSMem::IsMemoryReadable(entry, sizeof(*entry))) + if (MemoryAddress(entry).IsMemoryReadable(sizeof(*entry))) { - // Find null terminators bool nameValid = false, valValid = false; for (int i = 0; i < ENTRY_STR_LEN; i++) @@ -117,36 +114,19 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48 if (!nameValid || !valValid) return BLOCKED_INFO("Missing null terminators"); - auto realVar = g_pCVar->FindVar(entry->name); + ConVar* pVar = R2::g_pCVar->FindVar(entry->name); - if (realVar) + if (pVar) + { memcpy( entry->name, - realVar->m_ConCommandBase.m_pszName, - strlen(realVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case - - bool isValidFlags = true; - if (areWeServer) - { - if (realVar) - isValidFlags = ConVar::IsFlagSet(realVar, FCVAR_USERINFO); // ConVar MUST be userinfo var - } - else - { - // TODO: Should probably have some sanity checks, but can't find any that are consistent - } + pVar->m_ConCommandBase.m_pszName, + strlen(pVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case - if (!isValidFlags) - { - if (!realVar) - { - return BLOCKED_INFO("Invalid flags on nonexistant cvar (how tho???)"); - } - else - { + int iFlags = bIsServerFrame ? FCVAR_USERINFO : FCVAR_REPLICATED; + if (!pVar->IsFlagSet(iFlags)) return BLOCKED_INFO( - "Invalid flags (" << std::hex << "0x" << realVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name); - } + "Invalid flags (" << std::hex << "0x" << pVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name); } } else @@ -155,11 +135,14 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48 } } - return oCClient_ProcessSetConVar(msg); + return CClient_ProcessSetConVar(msg); } -// Purpose: prevent invalid user CMDs -KHOOK(CClient_ProcessUsercmds, ("engine.dll", "40 55 56 48 83 EC 58"), bool, __fastcall, (void* thisptr, void* pMsg)) +// 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 { @@ -186,22 +169,19 @@ KHOOK(CClient_ProcessUsercmds, ("engine.dll", "40 55 56 48 83 EC 58"), bool, __f return BLOCKED_INFO("Invalid m_nNewCommands (" << msg->m_nNewCommands << ")"); } - constexpr int NUMCMD_SANITY_LIMIT = 16; - if ((msg->m_nNewCommands + msg->m_nBackupCommands) > NUMCMD_SANITY_LIMIT) - { - return BLOCKED_INFO("Command count is too high (new: " << msg->m_nNewCommands << ", backup: " << msg->m_nBackupCommands << ")"); - } - if (msg->m_nLength <= 0) return BLOCKED_INFO("Invalid message length (" << msg->m_nLength << ")"); - return oCClient_ProcessUsercmds(thisptr, pMsg); + return CClient_ProcessUsercmds(thisptr, pMsg); } -KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall, (void* buf, void* pCmd_move, void* pCmd_from)) +// 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 - oReadUsercmd(buf, pCmd_move, pCmd_from); + 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 @@ -209,11 +189,11 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall DWORD command_number; DWORD tick_count; float command_time; - Float3 worldViewAngles; + Vector3 worldViewAngles; BYTE gap18[4]; - Float3 localViewAngles; - Float3 attackangles; - Float3 move; + Vector3 localViewAngles; + Vector3 attackangles; + Vector3 move; DWORD buttons; BYTE impulse; short weaponselect; @@ -221,8 +201,8 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall BYTE gap4C[24]; char headoffset; BYTE gap65[11]; - Float3 cameraPos; - Float3 cameraAngles; + Vector3 cameraPos; + Vector3 cameraAngles; BYTE gap88[4]; int tickSomething; DWORD dword90; @@ -237,7 +217,7 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall std::string BLOCK_PREFIX = "ReadUsercmd (command_number delta: " + std::to_string(cmd->command_number - fromCmd->command_number) + "): "; - // Fix invalid player angles + // fix invalid player angles cmd->worldViewAngles.MakeValid(); cmd->attackangles.MakeValid(); cmd->localViewAngles.MakeValid(); @@ -249,7 +229,7 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall // Fix invaid movement vector cmd->move.MakeValid(); - if (cmd->tick_count == 0 || cmd->command_time <= 0) + 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 @@ -260,6 +240,7 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall 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}; @@ -269,287 +250,209 @@ INVALID_CMD: cmd->meleetarget = 0; } -// basically: by default r2 isn't set as a valve mod, meaning that m_bRestrictServerCommands is false -// this is HORRIBLE for security, because it means servers can run arbitrary concommands on clients -// especially since we have script commands this could theoretically be awful -KHOOK(IsValveMod, ("engine.dll", "48 83 EC 28 48 8B 0D ? ? ? ? 48 8D 15 ? ? ? ? E8 ? ? ? ? 85 C0 74 63"), bool, __fastcall, ()) -{ - bool result = !CommandLine()->CheckParm("-norestrictservercommands"); - spdlog::info("ExploitFixes: Overriding IsValveMod to {}...", result); - return result; -} - -// Fix respawn's crappy UTF8 parser so it doesn't crash -_- -// This also means you can launch multiplayer with "communities_enabled 1" and not crash, you're welcome -KHOOK( - CrashFunc_ParseUTF8, - ("engine.dll", "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"), - bool, - __fastcall, - (INT64 * a1, DWORD* a2, char* strData)) +// 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); + R2::g_pModName = new char[iSize + 1]; + strcpy(R2::g_pModName, pModName); - static void* targetRetAddr = NSMem::PatternScan("engine.dll", "84 C0 75 2C 49 8B 16"); - -#ifdef _MSC_VER - if (_ReturnAddress() == targetRetAddr) -#else - if (__builtin_return_address(0) == targetRetAddr) -#endif - { - if (!ExploitFixes_UTF8Parser::CheckValid(a1, a2, strData)) - { - const char* BLOCK_PREFIX = "ParseUTF8 Hook: "; - BLOCKED_INFO("Ignoring potentially-crashing utf8 string"); - return false; - } - } - - return oCrashFunc_ParseUTF8(a1, a2, strData); + return (!strcmp("r2", pModName) || !strcmp("r1", pModName)) && !Tier0::CommandLine()->CheckParm("-norestrictservercommands"); } -// GetEntByIndex (called by ScriptGetEntByIndex) doesn't check for the index being out of bounds when it's -// above the max entity count. This allows it to be used to crash servers. -typedef void*(__fastcall* GetEntByIndexType)(int idx); -GetEntByIndexType GetEntByIndex; - -static void* GetEntByIndexHook(int idx) -{ - if (idx >= 0x4000) - { - spdlog::info("GetEntByIndex {} is out of bounds", idx); - return nullptr; - } - return GetEntByIndex(idx); -} +// ratelimit stringcmds, and prevent remote clients from calling commands that they shouldn't +bool (*CCommand__Tokenize)(CCommand& self, const char* pCommandString, R2::cmd_source_t commandSource); -// RELOCATED FROM https://github.com/R2Northstar/NorthstarLauncher/commit/25dbf729cfc75107a0fcf0186924b58ecc05214b -// 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. -KHOOK( - LZSS_SafeUncompress, - ("engine.dll", "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 33 ED 41 8B F9"), - uint32_t, - __fastcall, - (void* self, const unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize)) +// clang-format off +AUTOHOOK(CGameClient__ExecuteStringCommand, engine.dll + 0x1022E0, +bool, __fastcall, (R2::CBaseClient* self, uint32_t unknown, const char* pCommandString)) +// clang-format on { - static constexpr int LZSS_LOOKSHIFT = 4; + if (Cvar_ns_should_log_all_clientcommands->GetBool()) + spdlog::info("player {} (UID: {}) sent command: \"{}\"", self->m_Name, self->m_UID, pCommandString); - uint32_t totalBytes = 0; - int getCmdByte = 0, cmdByte = 0; - - struct lzss_header_t + if (!g_pServerLimits->CheckStringCommandLimits(self)) { - uint32_t id, actualSize; - }; + R2::CBaseClient__Disconnect(self, 1, "Sent too many stringcmd commands"); + return false; + } - lzss_header_t header = *(lzss_header_t*)pInput; + // 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 (pInput == NULL || header.id != 'SSZL' || header.actualSize == 0 || header.actualSize > unBufSize) - return 0; + if (!CCommand__Tokenize(tempCommand, pCommandString, R2::cmd_source_t::kCommandSrcCode) || !tempCommand.ArgC()) + return false; - pInput += sizeof(lzss_header_t); + ConCommand* command = R2::g_pCVar->FindCommand(tempCommand.Arg(0)); - for (;;) + // 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)) { - if (!getCmdByte) - cmdByte = *pInput++; + // ensure FCVAR_GAMEDLL concommands without FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS can't be executed by remote clients + if (IsDedicatedServer()) + return false; - getCmdByte = (getCmdByte + 1) & 0x07; + if (strcmp(self->m_UID, R2::g_pLocalPlayerUserID)) + return false; + } - if (cmdByte & 0x01) - { - int position = *pInput++ << LZSS_LOOKSHIFT; - position |= (*pInput >> LZSS_LOOKSHIFT); - position += 1; + // 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) - int count = (*pInput++ & 0x0F) + 1; - if (count == 1) - break; + // These both execute a command for every single entity for some reason, nice one valve + "pre_go_to_hub", + "pre_go_to_calibration", - // Ensure reference chunk exists entirely within our buffer - if (position > totalBytes) - return 0; + "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() + }; - totalBytes += count; - if (totalBytes > unBufSize) - return 0; + int iCmdLength = strlen(tempCommand.Arg(0)); - unsigned char* pSource = pOutput - position; - for (int i = 0; i < count; i++) - *pOutput++ = *pSource++; - } - else + bool bIsBadCommand = false; + for (auto& blockedCommand : blockedCommands) { - totalBytes++; - if (totalBytes > unBufSize) - return 0; + 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 - *pOutput++ = *pInput++; + // this is a command we need to block + return false; + NEXT_COMMAND:; } - cmdByte = cmdByte >> 1; } - if (totalBytes == header.actualSize) - { - return totalBytes; - } - else - { - return 0; - } + return CGameClient__ExecuteStringCommand(self, unknown, pCommandString); } -////////////////////////////////////////////////// +// prevent clients from crashing servers through overflowing CNetworkStringTableContainer::WriteBaselines +bool bWasWritingStringTableSuccessful; -void DoBytePatches() +// clang-format off +AUTOHOOK(CBaseClient__SendServerInfo, engine.dll + 0x104FB0, +void, __fastcall, (void* self)) +// clang-format on { - uintptr_t engineBase = (uintptr_t)GetModuleHandleA("engine.dll"); - uintptr_t serverBase = (uintptr_t)GetModuleHandleA("server.dll"); + bWasWritingStringTableSuccessful = true; + CBaseClient__SendServerInfo(self); + if (!bWasWritingStringTableSuccessful) + R2::CBaseClient__Disconnect( + self, 1, "Overflowed CNetworkStringTableContainer::WriteBaselines, try restarting your client and reconnecting"); +} - // patches to make commands run from client/ui script still work - // note: this is likely preventable in a nicer way? test prolly - NSMem::BytePatch(engineBase + 0x4FB65, "EB 11"); - NSMem::BytePatch(engineBase + 0x4FBAC, "EB 16"); +// 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; - // disconnect concommand + if (i >= MAX_ENT_IDX) { - uintptr_t addr = engineBase + 0x5ADA2D; - int val = *(int*)addr | FCVAR_SERVER_CAN_EXECUTE; - NSMem::BytePatch(addr, (BYTE*)&val, sizeof(int)); + spdlog::warn("GetEntByIndex {} is out of bounds (max {})", i, MAX_ENT_IDX); + return nullptr; } - { // Dumb ANTITAMPER patches (they negatively impact performance and security) - - constexpr const char* ANTITAMPER_EXPORTS[] = { - "ANTITAMPER_SPOTCHECK_CODEMARKER", - "ANTITAMPER_TESTVALUE_CODEMARKER", - "ANTITAMPER_TRIGGER_CODEMARKER", - }; - - // Prevent thesefrom actually doing anything - for (auto exportName : ANTITAMPER_EXPORTS) - { - - auto address = (uintptr_t)GetProcAddress(GetModuleHandleA("server.dll"), exportName); - if (!address) - { - spdlog::warn("Failed to find AntiTamper function export \"{}\"", exportName); - } - else - { - // Just return, none of them have any args or are userpurge - NSMem::BytePatch(address, "C3"); - - spdlog::info("Patched AntiTamper function export \"{}\"", exportName); - } - } - } + return GetEntByIndex(i); } - -KHOOK( - SpecialClientCommand, - ("server.dll", "48 89 5C 24 ? 48 89 74 24 ? 55 57 41 56 48 8D 6C 24 ? 48 81 EC ? ? ? ? 83 3A 00"), - bool, - __fastcall, - (void* player, CCommand* command)) +// clang-format off +AUTOHOOK(CL_CopyExistingEntity, engine.dll + 0x6F940, +bool, __fastcall, (void* a1)) +// clang-format on { - - static ConVar* sv_cheats = g_pCVar->FindVar("sv_cheats"); - - if (sv_cheats->GetBool()) - return oSpecialClientCommand(player, command); // Don't block anything if sv_cheats is on - - // These are mostly from Portal 2 (sigh) - 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() + struct CEntityReadInfo + { + BYTE gap[40]; + int nNewEntity; }; - if (command->ArgC() > 0) + CEntityReadInfo* pReadInfo = (CEntityReadInfo*)a1; + if (pReadInfo->nNewEntity >= 0x1000 || pReadInfo->nNewEntity < 0) { - std::string cmdStr = command->Arg(0); - for (char& c : cmdStr) - c = tolower(c); - - for (const char* blockedCommand : blockedCommands) - { - if (cmdStr.find(blockedCommand) != std::string::npos) - { - // Block this command - spdlog::warn("Blocked exploititive client command \"{}\".", cmdStr); - return true; - } - } + // 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 oSpecialClientCommand(player, command); + return CL_CopyExistingEntity(a1); } -void SetupKHook(KHook* hook) +ON_DLL_LOAD("engine.dll", EngineExploitFixes, (CModule module)) { - if (hook->Setup()) - { - spdlog::debug("KHook::Setup(): Hooked at {}", hook->targetFuncAddr); - } - else - { - spdlog::critical("\tFAILED to initialize all exploit patches."); + AUTOHOOK_DISPATCH_MODULE(engine.dll) - // Force exit - MessageBoxA(0, "FAILED to initialize all exploit patches.", "Northstar", MB_ICONERROR); - exit(0); - } -} - -void ExploitFixes::LoadCallback_MultiModule(HMODULE baseAddress) -{ + CCommand__Tokenize = module.Offset(0x418380).As<bool (*)(CCommand&, const char*, R2::cmd_source_t)>(); - spdlog::info("ExploitFixes::LoadCallback_MultiModule({}) ...", (void*)baseAddress); + // allow client/ui to run clientcommands despite restricting servercommands + module.Offset(0x4FB65).Patch("EB 11"); + module.Offset(0x4FBAC).Patch("EB 16"); - int hooksEnabled = 0; - for (auto itr = KHook::_allHooks.begin(); itr != KHook::_allHooks.end(); itr++) + // patch to set bWasWritingStringTableSuccessful in CNetworkStringTableContainer::WriteBaselines if it fails { - auto curHook = *itr; - if (GetModuleHandleA(curHook->targetFunc.moduleName) == baseAddress) - { - SetupKHook(curHook); - itr = KHook::_allHooks.erase(itr); // Prevent repeated initialization + MemoryAddress writeAddress(&bWasWritingStringTableSuccessful - module.Offset(0x234EDC).m_nAddress); - hooksEnabled++; + MemoryAddress addr = module.Offset(0x234ED2); + addr.Patch("C7 05"); + addr.Offset(2).Patch((BYTE*)&writeAddress, sizeof(writeAddress)); - if (itr == KHook::_allHooks.end()) - break; - } - } + addr.Offset(6).Patch("00 00 00 00"); - spdlog::info("\tEnabled {} hooks.", hooksEnabled); + addr.Offset(10).NOP(5); + } } -void ExploitFixes::LoadCallback_Full(HMODULE baseAddress) +ON_DLL_LOAD_RELIESON("server.dll", ServerExploitFixes, ConVar, (CModule module)) { - spdlog::info("ExploitFixes::LoadCallback_Full ..."); + AUTOHOOK_DISPATCH_MODULE(server.dll) - spdlog::info("\tByte patching..."); - DoBytePatches(); + // 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"); - for (KHook* hook : KHook::_allHooks) - SetupKHook(hook); - - spdlog::info("\tInitialized " + std::to_string(KHook::_allHooks.size()) + " late exploit-patch hooks."); - KHook::_allHooks.clear(); + // Dumb ANTITAMPER patches (they negatively impact performance and security) + constexpr const char* ANTITAMPER_EXPORTS[] = { + "ANTITAMPER_SPOTCHECK_CODEMARKER", + "ANTITAMPER_TESTVALUE_CODEMARKER", + "ANTITAMPER_TRIGGER_CODEMARKER", + }; - ns_exploitfixes_log = - new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever exploitfixes.cpp blocks/corrects something"); + // Prevent these from actually doing anything + for (auto exportName : ANTITAMPER_EXPORTS) + { + MemoryAddress 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); + } + } - g_pCVar->FindCommand("migrateme")->m_nFlags &= ~FCVAR_SERVER_CAN_EXECUTE; + 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"); - HookEnabler hook; - ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x2a8a50, &GetEntByIndexHook, reinterpret_cast<LPVOID*>(&GetEntByIndex)); + Cvar_sv_cheats = R2::g_pCVar->FindVar("sv_cheats"); } |