#include "pch.h" #include "ExploitFixes.h" #include "ExploitFixes_UTF8Parser.h" #include "NSMem.h" #include "cvar.h" typedef char(__fastcall* function_containing_emit_t)(uint64_t a1, uint64_t a2); function_containing_emit_t function_containing_emit; ConVar* 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) \ { \ std::stringstream stream; \ stream << "ExploitFixes.cpp: " << BLOCK_PREFIX << s; \ spdlog::error(stream.str()); \ } \ return false; \ }()) // Make sure 3 or less floats are valid bool ValidateFloats(float a, float b = 0, float c = 0) { return !isnan(a) && !isnan(b) && !isnan(c); } struct Vector { float x, y, z; Vector(float x = 0, float y = 0, float z = 0) : x(x), y(y), z(z) {} bool IsValid() { return ValidateFloats(x, y, z); } }; struct Angle { float pitch, yaw, roll; Angle(float pitch = 0, float yaw = 0, float roll = 0) : pitch(pitch), yaw(yaw), roll(roll) {} bool IsInvalid() { if (!ValidateFloats(pitch, yaw, roll)) return false; return (pitch > 90 || pitch < -90) || (yaw > 180 || yaw < -180) || (roll > 180 || roll < -180); } }; #define BLOCK_NETMSG_FUNC(name, pattern) \ KHOOK(name, ("engine.dll", pattern), bool, __fastcall, (void* thisptr, void* buffer)) \ { \ 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"); // 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"); KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48 FF 60 10"), bool, __fastcall, (void* pMsg)) { 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 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: "; if (areWeServer) { 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 (NSMem::IsMemoryReadable(entry, 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"); auto realVar = g_pCVar->FindVar(entry->name); if (realVar) 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 } if (!isValidFlags) { if (!realVar) { return BLOCKED_INFO("Invalid flags on nonexistant cvar (how tho???)"); } else { return BLOCKED_INFO( "Invalid flags (" << std::hex << "0x" << realVar->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 oCClient_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)) { 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 << ")"); } 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); } KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall, (void* buf, void* pCmd_move, void* pCmd_from)) { // Let normal usercmd read happen first, it's safe oReadUsercmd(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 __declspec(align(4)) SV_CUserCmd { DWORD command_number; DWORD tick_count; float command_time; Angle worldViewAngles; BYTE gap18[4]; Angle localViewAngles; Angle attackangles; Vector move; DWORD buttons; BYTE impulse; short weaponselect; DWORD meleetarget; BYTE gap4C[24]; char headoffset; BYTE gap65[11]; Vector cameraPos; Angle 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) + "): "; if (cmd->worldViewAngles.IsInvalid()) { BLOCKED_INFO("CMD has invalid worldViewAngles"); goto INVALID_CMD; } if (cmd->attackangles.IsInvalid()) { BLOCKED_INFO("CMD has invalid attackangles"); goto INVALID_CMD; } if (cmd->localViewAngles.IsInvalid()) { BLOCKED_INFO("CMD has invalid localViewAngles"); goto INVALID_CMD; } if (cmd->cameraAngles.IsInvalid()) { BLOCKED_INFO("CMD has invalid cameraAngles"); goto INVALID_CMD; } 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 } if (!cmd->move.IsValid()) { BLOCKED_INFO("Invalid move vector"); goto INVALID_CMD; } if (!cmd->cameraPos.IsValid()) { BLOCKED_INFO("Invalid cameraPos"); // IIRC this can crash spectating clients or anyone watching replays goto INVALID_CMD; } 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 = Angle(0, 0, 0); cmd->tick_count = cmd->frameTime = 0; cmd->move = cmd->cameraPos = Vector(0, 0, 0); cmd->buttons = 0; 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 #include "gameutils.h" KHOOK(IsValveMod, ("engine.dll", "48 83 EC 28 48 8B 0D ? ? ? ? 48 8D 15 ? ? ? ? E8 ? ? ? ? 85 C0 74 63"), bool, __fastcall, ()) { return !CommandLine()->CheckParm("-norestrictservercommands"); } // 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)) { static void* targetRetAddr = NSMem::PatternScan("engine.dll", "84 C0 75 2C 49 8B 16"); if (_ReturnAddress() == targetRetAddr) { 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); } // 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); } ////////////////////////////////////////////////// void DoBytePatches() { uintptr_t engineBase = (uintptr_t)GetModuleHandleA("engine.dll"); uintptr_t serverBase = (uintptr_t)GetModuleHandleA("server.dll"); // 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"); // disconnect concommand { uintptr_t addr = engineBase + 0x5ADA2D; int val = *(int*)addr | FCVAR_SERVER_CAN_EXECUTE; NSMem::BytePatch(addr, (BYTE*)&val, sizeof(int)); } { // 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); } } } } char function_containing_emit_hook(uint64_t unknown_value, uint64_t command_ptr) { char* command_string = *(char**)(command_ptr + 1040); // From decompile if (!sv_cheats->m_Value.m_nValue && !_strnicmp(command_string, "emit", 5)) { spdlog::info("Blocking command \"emit\" because sv_cheats was 0"); return 1; } return function_containing_emit(unknown_value, command_ptr); } void ExploitFixes::LoadCallback(HMODULE baseAddress) { spdlog::info("ExploitFixes::LoadCallback ..."); spdlog::info("\tByte patching..."); DoBytePatches(); if (KHook::InitAllHooks()) { spdlog::info("\tInitialized " + std::to_string(KHook::_allHooks.size()) + " exploit-patch hooks."); } else { spdlog::critical("\tFAILED to initialize all exploit patches."); // Force exit? MessageBoxA(0, "FAILED to initialize all exploit patches.", "Northstar", MB_ICONERROR); exit(0); } ns_exploitfixes_log = new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever ExploitFixes.cpp blocks/corrects something"); HookEnabler hook; sv_cheats = g_pCVar->FindVar("sv_cheats"); ENABLER_CREATEHOOK( hook, (char*)baseAddress + 0x5889A0, &function_containing_emit_hook, reinterpret_cast<LPVOID*>(&function_containing_emit)); ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x2a8a50, &GetEntByIndexHook, reinterpret_cast<LPVOID*>(&GetEntByIndex)); }