aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/exploitfixes.cpp
diff options
context:
space:
mode:
authorBobTheBob <32057864+BobTheBob9@users.noreply.github.com>2022-10-17 23:26:07 +0100
committerGitHub <noreply@github.com>2022-10-17 23:26:07 +0100
commit841881af9ea6ec73b1d505d5a8f7c1f766273724 (patch)
tree91feb40fe810984b59d2d2da440e289370b0a137 /NorthstarDLL/exploitfixes.cpp
parentdc0934d29caacc8da1e7df8b775d24b4e99c381c (diff)
downloadNorthstarLauncher-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.cpp543
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");
}