From cf9743e9b874b76e56a57d56e60e03aaa8a52ad6 Mon Sep 17 00:00:00 2001 From: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com> Date: Thu, 25 Aug 2022 00:30:30 +0200 Subject: Change file name case sensitivity to lower case (#249) To mirror how it is in refactor PR --- NorthstarDLL/ExploitFixes.cpp | 553 ------------------------------ NorthstarDLL/ExploitFixes.h | 12 - NorthstarDLL/ExploitFixes_UTF8Parser.h | 175 ---------- NorthstarDLL/NSMem.h | 193 ----------- NorthstarDLL/NorthstarDLL.vcxproj | 8 +- NorthstarDLL/NorthstarDLL.vcxproj.filters | 8 +- NorthstarDLL/buildainfile.cpp | 2 +- NorthstarDLL/dedicated.cpp | 2 +- NorthstarDLL/dedicatedmaterialsystem.cpp | 2 +- NorthstarDLL/dllmain.cpp | 2 +- NorthstarDLL/exploitfixes.cpp | 553 ++++++++++++++++++++++++++++++ NorthstarDLL/exploitfixes.h | 12 + NorthstarDLL/exploitfixes_utf8parser.h | 175 ++++++++++ NorthstarDLL/maxplayers.cpp | 2 +- NorthstarDLL/miscserverfixes.cpp | 2 +- NorthstarDLL/nsmem.h | 193 +++++++++++ NorthstarDLL/playlist.cpp | 2 +- NorthstarDLL/serverauthentication.cpp | 2 +- 18 files changed, 949 insertions(+), 949 deletions(-) delete mode 100644 NorthstarDLL/ExploitFixes.cpp delete mode 100644 NorthstarDLL/ExploitFixes.h delete mode 100644 NorthstarDLL/ExploitFixes_UTF8Parser.h delete mode 100644 NorthstarDLL/NSMem.h create mode 100644 NorthstarDLL/exploitfixes.cpp create mode 100644 NorthstarDLL/exploitfixes.h create mode 100644 NorthstarDLL/exploitfixes_utf8parser.h create mode 100644 NorthstarDLL/nsmem.h diff --git a/NorthstarDLL/ExploitFixes.cpp b/NorthstarDLL/ExploitFixes.cpp deleted file mode 100644 index 31fa349a..00000000 --- a/NorthstarDLL/ExploitFixes.cpp +++ /dev/null @@ -1,553 +0,0 @@ -#include "pch.h" - -#include "ExploitFixes.h" -#include "ExploitFixes_UTF8Parser.h" -#include "NSMem.h" -#include "cvar.h" -#include "gameutils.h" - -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; \ - }()) - -struct Float3 -{ - 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; \ - } - -// 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 alignas(4) SV_CUserCmd - { - DWORD command_number; - DWORD tick_count; - float command_time; - Float3 worldViewAngles; - BYTE gap18[4]; - Float3 localViewAngles; - Float3 attackangles; - Float3 move; - DWORD buttons; - BYTE impulse; - short weaponselect; - DWORD meleetarget; - BYTE gap4C[24]; - char headoffset; - BYTE gap65[11]; - Float3 cameraPos; - Float3 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->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; -} - -// 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)) -{ - - 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); -} - -// 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); -} - -// 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)) -{ - static constexpr int LZSS_LOOKSHIFT = 4; - - uint32_t totalBytes = 0; - int getCmdByte = 0, cmdByte = 0; - - struct lzss_header_t - { - uint32_t id, actualSize; - }; - - lzss_header_t header = *(lzss_header_t*)pInput; - - if (pInput == NULL || header.id != 'SSZL' || header.actualSize == 0 || 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 totalBytes; - } - else - { - return 0; - } -} - -////////////////////////////////////////////////// - -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); - } - } - } -} - -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)) -{ - - 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() - }; - - if (command->ArgC() > 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; - } - } - } - - return oSpecialClientCommand(player, command); -} - -void SetupKHook(KHook* hook) -{ - if (hook->Setup()) - { - spdlog::debug("KHook::Setup(): Hooked at {}", hook->targetFuncAddr); - } - else - { - spdlog::critical("\tFAILED to initialize all exploit patches."); - - // Force exit - MessageBoxA(0, "FAILED to initialize all exploit patches.", "Northstar", MB_ICONERROR); - exit(0); - } -} - -void ExploitFixes::LoadCallback_MultiModule(HMODULE baseAddress) -{ - - spdlog::info("ExploitFixes::LoadCallback_MultiModule({}) ...", (void*)baseAddress); - - int hooksEnabled = 0; - for (auto itr = KHook::_allHooks.begin(); itr != KHook::_allHooks.end(); itr++) - { - auto curHook = *itr; - if (GetModuleHandleA(curHook->targetFunc.moduleName) == baseAddress) - { - SetupKHook(curHook); - itr = KHook::_allHooks.erase(itr); // Prevent repeated initialization - - hooksEnabled++; - - if (itr == KHook::_allHooks.end()) - break; - } - } - - spdlog::info("\tEnabled {} hooks.", hooksEnabled); -} - -void ExploitFixes::LoadCallback_Full(HMODULE baseAddress) -{ - spdlog::info("ExploitFixes::LoadCallback_Full ..."); - - spdlog::info("\tByte patching..."); - DoBytePatches(); - - for (KHook* hook : KHook::_allHooks) - SetupKHook(hook); - - spdlog::info("\tInitialized " + std::to_string(KHook::_allHooks.size()) + " late exploit-patch hooks."); - KHook::_allHooks.clear(); - - ns_exploitfixes_log = - new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever ExploitFixes.cpp blocks/corrects something"); - - HookEnabler hook; - ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x2a8a50, &GetEntByIndexHook, reinterpret_cast(&GetEntByIndex)); -} diff --git a/NorthstarDLL/ExploitFixes.h b/NorthstarDLL/ExploitFixes.h deleted file mode 100644 index d0754d72..00000000 --- a/NorthstarDLL/ExploitFixes.h +++ /dev/null @@ -1,12 +0,0 @@ -// KittenPopo's exploit fix hooks, feel free to add more here - -#pragma once -#include "pch.h" - -namespace ExploitFixes -{ - // This callback will be ran muliple times on multiple module loads - void LoadCallback_MultiModule(HMODULE baseAddress); - - void LoadCallback_Full(HMODULE unused); -} // namespace ExploitFixes diff --git a/NorthstarDLL/ExploitFixes_UTF8Parser.h b/NorthstarDLL/ExploitFixes_UTF8Parser.h deleted file mode 100644 index 24545ea3..00000000 --- a/NorthstarDLL/ExploitFixes_UTF8Parser.h +++ /dev/null @@ -1,175 +0,0 @@ -// Reimplementation of a exploitable UTF decoding function in titanfall - -#include "NSMem.h" - -#pragma once - -namespace ExploitFixes_UTF8Parser -{ - bool __fastcall CheckValid(INT64* a1, DWORD* a2, char* strData) - { - static auto sub_F1320 = (INT64(__fastcall*)(DWORD a1, char* a2))NSMem::PatternScan("engine.dll", "83 F9 7F 77 08 88 0A"); - - 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 (!NSMem::IsMemoryReadable(v4, 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; - } -} // namespace ExploitFixes_UTF8Parser diff --git a/NorthstarDLL/NSMem.h b/NorthstarDLL/NSMem.h deleted file mode 100644 index 9b1f9103..00000000 --- a/NorthstarDLL/NSMem.h +++ /dev/null @@ -1,193 +0,0 @@ -#pragma once -#include "pch.h" - -// KittenPopo's memory stuff, made for northstar (because I really can't handle working with northstar's original memory stuff tbh) - -#pragma region Pattern Scanning -namespace NSMem -{ - inline std::vector HexBytesToString(const char* str) - { - std::vector patternNums; - int size = strlen(str); - for (int i = 0; i < size; i++) - { - char c = str[i]; - - // If this is a space character, ignore it - if (c == ' ' || c == '\t') - continue; - - if (c == '?') - { - // Add a wildcard (-1) - patternNums.push_back(-1); - } - else if (i < size - 1) - { - BYTE result = 0; - for (int j = 0; j < 2; j++) - { - int val = 0; - char c = *(str + i + j); - if (c >= 'a') - { - val = c - 'a' + 0xA; - } - else if (c >= 'A') - { - val = c - 'A' + 0xA; - } - else if (isdigit(c)) - { - val = c - '0'; - } - else - { - assert(false); - val = -1; - } - - result += (j == 0) ? val * 16 : val; - } - patternNums.push_back(result); - } - - i++; - } - - return patternNums; - } - - inline void* PatternScan(void* module, const int* pattern, int patternSize, int offset) - { - if (!module) - return NULL; - - auto dosHeader = (PIMAGE_DOS_HEADER)module; - auto ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)module + dosHeader->e_lfanew); - - auto sizeOfImage = ntHeaders->OptionalHeader.SizeOfImage; - - auto scanBytes = (BYTE*)module; - - for (auto i = 0; i < sizeOfImage - patternSize; ++i) - { - bool found = true; - for (auto j = 0; j < patternSize; ++j) - { - if (scanBytes[i + j] != pattern[j] && pattern[j] != -1) - { - found = false; - break; - } - } - - if (found) - { - uintptr_t addressInt = (uintptr_t)(&scanBytes[i]) + offset; - return (uint8_t*)addressInt; - } - } - - return nullptr; - } - - inline void* PatternScan(const char* moduleName, const char* pattern, int offset = 0) - { - std::vector patternNums = HexBytesToString(pattern); - - return PatternScan(GetModuleHandleA(moduleName), &patternNums[0], patternNums.size(), offset); - } - - inline void BytePatch(uintptr_t address, const BYTE* vals, int size) - { - WriteProcessMemory(GetCurrentProcess(), (LPVOID)address, vals, size, NULL); - } - - inline void BytePatch(uintptr_t address, std::initializer_list vals) - { - std::vector bytes = vals; - if (!bytes.empty()) - BytePatch(address, &bytes[0], bytes.size()); - } - - inline void BytePatch(uintptr_t address, const char* bytesStr) - { - std::vector byteInts = HexBytesToString(bytesStr); - std::vector bytes; - for (int v : byteInts) - bytes.push_back(v); - - if (!bytes.empty()) - BytePatch(address, &bytes[0], bytes.size()); - } - - inline void NOP(uintptr_t address, int size) - { - BYTE* buf = (BYTE*)malloc(size); - memset(buf, 0x90, size); - BytePatch(address, buf, size); - free(buf); - } - - inline bool IsMemoryReadable(void* ptr, size_t size) - { - static SYSTEM_INFO sysInfo; - if (!sysInfo.dwPageSize) - GetSystemInfo(&sysInfo); // This should always be 4096 unless ur playing on NES or some shit but whatever - - MEMORY_BASIC_INFORMATION memInfo; - - if (!VirtualQuery(ptr, &memInfo, sizeof(memInfo))) - return false; - - if (memInfo.RegionSize < size) - return false; - - return (memInfo.State & MEM_COMMIT) && !(memInfo.Protect & PAGE_NOACCESS); - } -} // namespace NSMem - -#pragma region KHOOK -struct KHookPatternInfo -{ - const char *moduleName, *pattern; - int offset = 0; - - KHookPatternInfo(const char* moduleName, const char* pattern, int offset = 0) : moduleName(moduleName), pattern(pattern), offset(offset) - { - } -}; - -struct KHook -{ - KHookPatternInfo targetFunc; - void* targetFuncAddr; - void* hookFunc; - void** original; - - static inline std::vector _allHooks; - - KHook(KHookPatternInfo targetFunc, void* hookFunc, void** original) : targetFunc(targetFunc) - { - this->hookFunc = hookFunc; - this->original = original; - _allHooks.push_back(this); - } - - bool Setup() - { - targetFuncAddr = NSMem::PatternScan(targetFunc.moduleName, targetFunc.pattern, targetFunc.offset); - if (!targetFuncAddr) - return false; - - return (MH_CreateHook(targetFuncAddr, hookFunc, original) == MH_OK) && (MH_EnableHook(targetFuncAddr) == MH_OK); - } -}; -#define KHOOK(name, funcPatternInfo, returnType, convention, args) \ - returnType convention hk##name args; \ - auto o##name = (returnType(convention*) args)0; \ - KHook k##name = KHook(KHookPatternInfo funcPatternInfo, reinterpret_cast(&hk##name), (void**)&o##name); \ - returnType convention hk##name args -#pragma endregion diff --git a/NorthstarDLL/NorthstarDLL.vcxproj b/NorthstarDLL/NorthstarDLL.vcxproj index 9d28d258..76560b42 100644 --- a/NorthstarDLL/NorthstarDLL.vcxproj +++ b/NorthstarDLL/NorthstarDLL.vcxproj @@ -564,9 +564,9 @@ - - - + + + @@ -628,7 +628,7 @@ - + diff --git a/NorthstarDLL/NorthstarDLL.vcxproj.filters b/NorthstarDLL/NorthstarDLL.vcxproj.filters index 6496c796..b0da9874 100644 --- a/NorthstarDLL/NorthstarDLL.vcxproj.filters +++ b/NorthstarDLL/NorthstarDLL.vcxproj.filters @@ -1497,7 +1497,7 @@ Header Files\Client - + Source Files\Shared\Exploit Fixes\UTF8Parser @@ -1509,10 +1509,10 @@ Header Files - + Header Files\Shared\ExploitFixes - + Header Files\Shared\ExploitFixes @@ -1679,7 +1679,7 @@ Source Files\Client - + Source Files\Shared\Exploit Fixes diff --git a/NorthstarDLL/buildainfile.cpp b/NorthstarDLL/buildainfile.cpp index 0054c4a1..24a16f74 100644 --- a/NorthstarDLL/buildainfile.cpp +++ b/NorthstarDLL/buildainfile.cpp @@ -4,7 +4,7 @@ #include "hookutils.h" #include #include -#include "NSMem.h" +#include "nsmem.h" namespace fs = std::filesystem; diff --git a/NorthstarDLL/dedicated.cpp b/NorthstarDLL/dedicated.cpp index 0546d5b8..f9e315d3 100644 --- a/NorthstarDLL/dedicated.cpp +++ b/NorthstarDLL/dedicated.cpp @@ -127,7 +127,7 @@ DWORD WINAPI ConsoleInputThread(PVOID pThreadParameter) return 0; } -#include "NSMem.h" +#include "nsmem.h" void InitialiseDedicated(HMODULE engineAddress) { spdlog::info("InitialiseDedicated"); diff --git a/NorthstarDLL/dedicatedmaterialsystem.cpp b/NorthstarDLL/dedicatedmaterialsystem.cpp index 01197407..47440b7a 100644 --- a/NorthstarDLL/dedicatedmaterialsystem.cpp +++ b/NorthstarDLL/dedicatedmaterialsystem.cpp @@ -3,7 +3,7 @@ #include "dedicatedmaterialsystem.h" #include "hookutils.h" #include "gameutils.h" -#include "NSMem.h" +#include "nsmem.h" typedef HRESULT (*__stdcall D3D11CreateDeviceType)( void* pAdapter, diff --git a/NorthstarDLL/dllmain.cpp b/NorthstarDLL/dllmain.cpp index 744abb5a..273bf9c7 100644 --- a/NorthstarDLL/dllmain.cpp +++ b/NorthstarDLL/dllmain.cpp @@ -51,7 +51,7 @@ #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" #include "rapidjson/error/en.h" -#include "ExploitFixes.h" +#include "exploitfixes.h" #include "scriptjson.h" typedef void (*initPluginFuncPtr)(void* (*getPluginObject)(PluginObject)); diff --git a/NorthstarDLL/exploitfixes.cpp b/NorthstarDLL/exploitfixes.cpp new file mode 100644 index 00000000..aa5adae3 --- /dev/null +++ b/NorthstarDLL/exploitfixes.cpp @@ -0,0 +1,553 @@ +#include "pch.h" + +#include "exploitfixes.h" +#include "exploitfixes_utf8parser.h" +#include "nsmem.h" +#include "cvar.h" +#include "gameutils.h" + +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; \ + }()) + +struct Float3 +{ + 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; \ + } + +// 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 alignas(4) SV_CUserCmd + { + DWORD command_number; + DWORD tick_count; + float command_time; + Float3 worldViewAngles; + BYTE gap18[4]; + Float3 localViewAngles; + Float3 attackangles; + Float3 move; + DWORD buttons; + BYTE impulse; + short weaponselect; + DWORD meleetarget; + BYTE gap4C[24]; + char headoffset; + BYTE gap65[11]; + Float3 cameraPos; + Float3 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->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; +} + +// 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)) +{ + + 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); +} + +// 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); +} + +// 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)) +{ + static constexpr int LZSS_LOOKSHIFT = 4; + + uint32_t totalBytes = 0; + int getCmdByte = 0, cmdByte = 0; + + struct lzss_header_t + { + uint32_t id, actualSize; + }; + + lzss_header_t header = *(lzss_header_t*)pInput; + + if (pInput == NULL || header.id != 'SSZL' || header.actualSize == 0 || 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 totalBytes; + } + else + { + return 0; + } +} + +////////////////////////////////////////////////// + +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); + } + } + } +} + +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)) +{ + + 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() + }; + + if (command->ArgC() > 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; + } + } + } + + return oSpecialClientCommand(player, command); +} + +void SetupKHook(KHook* hook) +{ + if (hook->Setup()) + { + spdlog::debug("KHook::Setup(): Hooked at {}", hook->targetFuncAddr); + } + else + { + spdlog::critical("\tFAILED to initialize all exploit patches."); + + // Force exit + MessageBoxA(0, "FAILED to initialize all exploit patches.", "Northstar", MB_ICONERROR); + exit(0); + } +} + +void ExploitFixes::LoadCallback_MultiModule(HMODULE baseAddress) +{ + + spdlog::info("ExploitFixes::LoadCallback_MultiModule({}) ...", (void*)baseAddress); + + int hooksEnabled = 0; + for (auto itr = KHook::_allHooks.begin(); itr != KHook::_allHooks.end(); itr++) + { + auto curHook = *itr; + if (GetModuleHandleA(curHook->targetFunc.moduleName) == baseAddress) + { + SetupKHook(curHook); + itr = KHook::_allHooks.erase(itr); // Prevent repeated initialization + + hooksEnabled++; + + if (itr == KHook::_allHooks.end()) + break; + } + } + + spdlog::info("\tEnabled {} hooks.", hooksEnabled); +} + +void ExploitFixes::LoadCallback_Full(HMODULE baseAddress) +{ + spdlog::info("ExploitFixes::LoadCallback_Full ..."); + + spdlog::info("\tByte patching..."); + DoBytePatches(); + + for (KHook* hook : KHook::_allHooks) + SetupKHook(hook); + + spdlog::info("\tInitialized " + std::to_string(KHook::_allHooks.size()) + " late exploit-patch hooks."); + KHook::_allHooks.clear(); + + ns_exploitfixes_log = + new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever exploitfixes.cpp blocks/corrects something"); + + HookEnabler hook; + ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x2a8a50, &GetEntByIndexHook, reinterpret_cast(&GetEntByIndex)); +} diff --git a/NorthstarDLL/exploitfixes.h b/NorthstarDLL/exploitfixes.h new file mode 100644 index 00000000..d0754d72 --- /dev/null +++ b/NorthstarDLL/exploitfixes.h @@ -0,0 +1,12 @@ +// KittenPopo's exploit fix hooks, feel free to add more here + +#pragma once +#include "pch.h" + +namespace ExploitFixes +{ + // This callback will be ran muliple times on multiple module loads + void LoadCallback_MultiModule(HMODULE baseAddress); + + void LoadCallback_Full(HMODULE unused); +} // namespace ExploitFixes diff --git a/NorthstarDLL/exploitfixes_utf8parser.h b/NorthstarDLL/exploitfixes_utf8parser.h new file mode 100644 index 00000000..24545ea3 --- /dev/null +++ b/NorthstarDLL/exploitfixes_utf8parser.h @@ -0,0 +1,175 @@ +// Reimplementation of a exploitable UTF decoding function in titanfall + +#include "NSMem.h" + +#pragma once + +namespace ExploitFixes_UTF8Parser +{ + bool __fastcall CheckValid(INT64* a1, DWORD* a2, char* strData) + { + static auto sub_F1320 = (INT64(__fastcall*)(DWORD a1, char* a2))NSMem::PatternScan("engine.dll", "83 F9 7F 77 08 88 0A"); + + 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 (!NSMem::IsMemoryReadable(v4, 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; + } +} // namespace ExploitFixes_UTF8Parser diff --git a/NorthstarDLL/maxplayers.cpp b/NorthstarDLL/maxplayers.cpp index 556819f1..2d513c71 100644 --- a/NorthstarDLL/maxplayers.cpp +++ b/NorthstarDLL/maxplayers.cpp @@ -45,7 +45,7 @@ constexpr int Team_PlayerArray_AddedLength = NEW_MAX_PLAYERS - 32; constexpr int Team_PlayerArray_AddedSize = PAD_NUMBER(Team_PlayerArray_AddedLength * 8, 4); constexpr int Team_AddedSize = Team_PlayerArray_AddedSize; -#include "NSMem.h" +#include "nsmem.h" template void ChangeOffset(void* addr, unsigned int offset) { NSMem::BytePatch((uintptr_t)addr, (BYTE*)&offset, sizeof(T)); diff --git a/NorthstarDLL/miscserverfixes.cpp b/NorthstarDLL/miscserverfixes.cpp index e6bd4629..728df737 100644 --- a/NorthstarDLL/miscserverfixes.cpp +++ b/NorthstarDLL/miscserverfixes.cpp @@ -2,7 +2,7 @@ #include "miscserverfixes.h" #include "hookutils.h" -#include "NSMem.h" +#include "nsmem.h" void InitialiseMiscServerFixes(HMODULE baseAddress) { diff --git a/NorthstarDLL/nsmem.h b/NorthstarDLL/nsmem.h new file mode 100644 index 00000000..9b1f9103 --- /dev/null +++ b/NorthstarDLL/nsmem.h @@ -0,0 +1,193 @@ +#pragma once +#include "pch.h" + +// KittenPopo's memory stuff, made for northstar (because I really can't handle working with northstar's original memory stuff tbh) + +#pragma region Pattern Scanning +namespace NSMem +{ + inline std::vector HexBytesToString(const char* str) + { + std::vector patternNums; + int size = strlen(str); + for (int i = 0; i < size; i++) + { + char c = str[i]; + + // If this is a space character, ignore it + if (c == ' ' || c == '\t') + continue; + + if (c == '?') + { + // Add a wildcard (-1) + patternNums.push_back(-1); + } + else if (i < size - 1) + { + BYTE result = 0; + for (int j = 0; j < 2; j++) + { + int val = 0; + char c = *(str + i + j); + if (c >= 'a') + { + val = c - 'a' + 0xA; + } + else if (c >= 'A') + { + val = c - 'A' + 0xA; + } + else if (isdigit(c)) + { + val = c - '0'; + } + else + { + assert(false); + val = -1; + } + + result += (j == 0) ? val * 16 : val; + } + patternNums.push_back(result); + } + + i++; + } + + return patternNums; + } + + inline void* PatternScan(void* module, const int* pattern, int patternSize, int offset) + { + if (!module) + return NULL; + + auto dosHeader = (PIMAGE_DOS_HEADER)module; + auto ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)module + dosHeader->e_lfanew); + + auto sizeOfImage = ntHeaders->OptionalHeader.SizeOfImage; + + auto scanBytes = (BYTE*)module; + + for (auto i = 0; i < sizeOfImage - patternSize; ++i) + { + bool found = true; + for (auto j = 0; j < patternSize; ++j) + { + if (scanBytes[i + j] != pattern[j] && pattern[j] != -1) + { + found = false; + break; + } + } + + if (found) + { + uintptr_t addressInt = (uintptr_t)(&scanBytes[i]) + offset; + return (uint8_t*)addressInt; + } + } + + return nullptr; + } + + inline void* PatternScan(const char* moduleName, const char* pattern, int offset = 0) + { + std::vector patternNums = HexBytesToString(pattern); + + return PatternScan(GetModuleHandleA(moduleName), &patternNums[0], patternNums.size(), offset); + } + + inline void BytePatch(uintptr_t address, const BYTE* vals, int size) + { + WriteProcessMemory(GetCurrentProcess(), (LPVOID)address, vals, size, NULL); + } + + inline void BytePatch(uintptr_t address, std::initializer_list vals) + { + std::vector bytes = vals; + if (!bytes.empty()) + BytePatch(address, &bytes[0], bytes.size()); + } + + inline void BytePatch(uintptr_t address, const char* bytesStr) + { + std::vector byteInts = HexBytesToString(bytesStr); + std::vector bytes; + for (int v : byteInts) + bytes.push_back(v); + + if (!bytes.empty()) + BytePatch(address, &bytes[0], bytes.size()); + } + + inline void NOP(uintptr_t address, int size) + { + BYTE* buf = (BYTE*)malloc(size); + memset(buf, 0x90, size); + BytePatch(address, buf, size); + free(buf); + } + + inline bool IsMemoryReadable(void* ptr, size_t size) + { + static SYSTEM_INFO sysInfo; + if (!sysInfo.dwPageSize) + GetSystemInfo(&sysInfo); // This should always be 4096 unless ur playing on NES or some shit but whatever + + MEMORY_BASIC_INFORMATION memInfo; + + if (!VirtualQuery(ptr, &memInfo, sizeof(memInfo))) + return false; + + if (memInfo.RegionSize < size) + return false; + + return (memInfo.State & MEM_COMMIT) && !(memInfo.Protect & PAGE_NOACCESS); + } +} // namespace NSMem + +#pragma region KHOOK +struct KHookPatternInfo +{ + const char *moduleName, *pattern; + int offset = 0; + + KHookPatternInfo(const char* moduleName, const char* pattern, int offset = 0) : moduleName(moduleName), pattern(pattern), offset(offset) + { + } +}; + +struct KHook +{ + KHookPatternInfo targetFunc; + void* targetFuncAddr; + void* hookFunc; + void** original; + + static inline std::vector _allHooks; + + KHook(KHookPatternInfo targetFunc, void* hookFunc, void** original) : targetFunc(targetFunc) + { + this->hookFunc = hookFunc; + this->original = original; + _allHooks.push_back(this); + } + + bool Setup() + { + targetFuncAddr = NSMem::PatternScan(targetFunc.moduleName, targetFunc.pattern, targetFunc.offset); + if (!targetFuncAddr) + return false; + + return (MH_CreateHook(targetFuncAddr, hookFunc, original) == MH_OK) && (MH_EnableHook(targetFuncAddr) == MH_OK); + } +}; +#define KHOOK(name, funcPatternInfo, returnType, convention, args) \ + returnType convention hk##name args; \ + auto o##name = (returnType(convention*) args)0; \ + KHook k##name = KHook(KHookPatternInfo funcPatternInfo, reinterpret_cast(&hk##name), (void**)&o##name); \ + returnType convention hk##name args +#pragma endregion diff --git a/NorthstarDLL/playlist.cpp b/NorthstarDLL/playlist.cpp index 6f744c6e..e55710c3 100644 --- a/NorthstarDLL/playlist.cpp +++ b/NorthstarDLL/playlist.cpp @@ -71,7 +71,7 @@ int GetCurrentGamemodeMaxPlayersHook() return maxPlayers; } -#include "NSMem.h" +#include "nsmem.h" void InitialisePlaylistHooks(HMODULE baseAddress) { RegisterConCommand("setplaylist", SetPlaylistCommand, "Sets the current playlist", FCVAR_NONE); diff --git a/NorthstarDLL/serverauthentication.cpp b/NorthstarDLL/serverauthentication.cpp index ac1985cf..e66fac3c 100644 --- a/NorthstarDLL/serverauthentication.cpp +++ b/NorthstarDLL/serverauthentication.cpp @@ -14,7 +14,7 @@ #include #include #include "configurables.h" -#include "NSMem.h" +#include "nsmem.h" const char* AUTHSERVER_VERIFY_STRING = "I am a northstar server!"; -- cgit v1.2.3