From 1bf269852c07a781d0a9e179dc7013604e087a77 Mon Sep 17 00:00:00 2001 From: hubdom <26039831+hubdom@users.noreply.github.com> Date: Thu, 17 Sep 2020 15:10:25 +0200 Subject: [PATCH] SprayExploitFixer: initial commit --- .../gamedata/spray_exploit_fixer.txt | 148 +++++++ .../scripting/spray_exploit_fixer.sp | 403 ++++++++++++++++++ 2 files changed, 551 insertions(+) create mode 100644 spray_exploit_fixer/gamedata/spray_exploit_fixer.txt create mode 100644 spray_exploit_fixer/scripting/spray_exploit_fixer.sp diff --git a/spray_exploit_fixer/gamedata/spray_exploit_fixer.txt b/spray_exploit_fixer/gamedata/spray_exploit_fixer.txt new file mode 100644 index 00000000..c3511918 --- /dev/null +++ b/spray_exploit_fixer/gamedata/spray_exploit_fixer.txt @@ -0,0 +1,148 @@ +"Games" +{ + "#default" + { + "Functions" + { + "CGameClient::FileReceived" + { + "signature" "CGameClient::FileReceived" + "callconv" "thiscall" + "return" "int" + "this" "address" + "arguments" + { + "a1" + { + "type" "charptr" + } + "a2" + { + "type" "int" + } + } + } + } + } + + "#default" + { + "#supported" + { + "engine" "css" + "engine" "hl2dm" + "engine" "tf2" + "engine" "zps" + } + + "Offsets" + { + "clients" + { + "windows" "12" + "linux" "12" + } + } + + "Signatures" + { + /* Search: "CGameClient::FileReceived" */ + /* CGameClient::FileReceived(char const*, unsigned int, bool) */ + "CGameClient::FileReceived" + { + "library" "engine" + "linux" "@_ZN11CGameClient12FileReceivedEPKcj" + "windows" "\x55\x8B\x2A\x56\x8B\x2A\x2A\x33\x2A\x57\x8B\x2A\x8D\x2A\x2A\x2A\x2A\x2A\x39" + /* 55 8B ? 56 8B ? ? 33 ? 57 8B ? 8D ? ? ? ? ? 39 */ + } + } + } + + "tf2" + { + /* TF2 function is void. */ + "Functions" + { + "CGameClient::FileReceived" + { + "return" "void" + } + } + } + + "left4dead" + { + /* L4D has an extra argument. */ + "Functions" + { + "CGameClient::FileReceived" + { + "arguments" + { + "a3" + { + "type" "int" + } + } + } + } + + "Offsets" + { + "clients" + { + "windows" "48" + "linux" "48" + } + } + + "Signatures" + { + "CGameClient::FileReceived" + { + "library" "engine" + "linux" "@_ZN11CGameClient12FileReceivedEPKcjb" + "windows" "\x56\x8B\x2A\x2A\x2A\x33\x2A\x8D\x2A\x2A\x2A\x2A\x2A\x8D" + /* 56 8B ? ? ? 33 ? 8D ? ? ? ? ? 8D */ + } + } + } + + "left4dead2" + { + /* L4D2 has an extra argument. */ + "Functions" + { + "CGameClient::FileReceived" + { + "arguments" + { + "a3" + { + "type" "int" + } + } + } + } + + "Offsets" + { + "clients" + { + "windows" "48" + "linux" "48" + } + } + + "Signatures" + { + "CGameClient::FileReceived" + { + "library" "engine" + "linux" "@_ZN11CGameClient12FileReceivedEPKcjb" + "windows" "\x55\x8B\x2A\x56\x8B\x2A\x2A\x33\x2A\x8D\x2A\x2A\x2A\x2A\x2A\x90" + /* 55 8B ? 56 8B ? ? 33 ? 8D ? ? ? ? ? 90 */ + } + } + } +} \ No newline at end of file diff --git a/spray_exploit_fixer/scripting/spray_exploit_fixer.sp b/spray_exploit_fixer/scripting/spray_exploit_fixer.sp new file mode 100644 index 00000000..d61092ec --- /dev/null +++ b/spray_exploit_fixer/scripting/spray_exploit_fixer.sp @@ -0,0 +1,403 @@ +#define PLUGIN_VERSION "1377" + +/*======================================================================================= + Plugin Info: + +* Name : [ANY] Spray Exploit Fixer +* Author : SilverShot +* Descrp : Deletes bad sprays and prevents them from crashing clients. +* Link : https://forums.alliedmods.net/showthread.php?t=323447 +* Plugins : https://sourcemod.net/plugins.php?exact=exact&sortby=title&search=1&author=Silvers + +======================================================================================== + Change Log: + +1.5 (14-May-2020) + - Added better error log message when gamedata file is missing. + - Fixed gamedata for HL2:DM. Thanks to "CliptonHeist" for reporting and "asherkin" for explaining engine != game. + - (Info: the gamedata "engine" key for HL2:DM uses "hl2dm" (the engine name) while the "game" part uses "hl2mp" (game name) e.g. for offsets). + +1.4 (10-May-2020) + - Added support for "Zombie Panic! Source" game. Requires gamedata update. + - Fixed "sm_spray_test" timing out when checking many sprays. Thanks to "Sreaper" for reporting and testing. + - Now checks 50 files and waits 0.1 seconds before checking the next batch. + - TF2 updated to fix clients crashing, but this plugin is still recommended to delete the other randomly uploaded user files. + +1.3 (26-Apr-2020) + - Changed cvar "spray_exploit_fixer_log" to log all files or only invalid sprays. + - Logging now saves to "sourcemod/logs/spray_downloads.log" file. + +1.2 (23-Apr-2020) + - Added better checks to detect more bad sprays. + - Added better checks for TF2 and other games to avoid false positives. + - Prevented banning people in TF2 since many random invalid files are sent, not just sprays. + +1.1 (21-Apr-2020) + - Added better checks to prevent false positives. + - Added ability to detect the users uploading sprays or other files. + - Added cvar "spray_exploit_fixer_ban" to ban players with invalid sprays. + - Added cvar "spray_exploit_fixer_log" to log players and files they uploaded. + - Changed "sm_spray_test" to allow recursive searching the downloads directory. + - Fixed plugin crashing TF2. + - Updated GameData required. + +1.0 (20-Apr-2020) + - Initial release. + +======================================================================================*/ + +#pragma semicolon 1 +#pragma newdecls required + +#include +#include +#include + +#define GAMEDATA "spray_exploit_fixer" +#define MAX_READ 50 + +int g_iVal[] = {86,84,70,0,7,0,0,0,42,0,0,0,42,0,0,0,42,42,42,42,42,42,42,42,42,42,42,0}; +char g_sFilename[PLATFORM_MAX_PATH]; +EngineVersion g_iEngine; + +public Plugin myinfo = +{ + name = "[ANY] Spray Exploit Fixer", + author = "SilverShot + Neon", + description = "Deletes bad sprays and prevents them from crashing clients.", + version = PLUGIN_VERSION, + url = "https://forums.alliedmods.net/showthread.php?t=323447" +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + g_iEngine = GetEngineVersion(); +} + +public void OnPluginStart() +{ + char sPath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sPath, sizeof(sPath), "gamedata/%s.txt", GAMEDATA); + if( FileExists(sPath) == false ) SetFailState("\n==========\nMissing required file: \"%s\".\nRead installation instructions again.\n==========", sPath); + + Handle hGameData = LoadGameConfigFile(GAMEDATA); + if( hGameData == null ) SetFailState("Failed to load \"%s.txt\" gamedata.", GAMEDATA); + + Handle hDetour = DHookCreateFromConf(hGameData, "CGameClient::FileReceived"); + + if( !hDetour ) + SetFailState("Failed to find \"CGameClient::FileReceived\" signature."); + if( !DHookEnableDetour(hDetour, false, FileReceived) ) + SetFailState("Failed to detour \"CGameClient::FileReceived\"."); + if( !DHookEnableDetour(hDetour, true, FileReceivedPost) ) + SetFailState("Failed to detour \"CGameClient::FileReceived\" post."); + + delete hDetour; + delete hGameData; + + RegAdminCmd("sm_spray_test", CmdSprays, ADMFLAG_RCON, "Tests all sprays in the games downloads folder, listing bad ones."); + + CreateConVar( "spray_exploit_fixer", PLUGIN_VERSION, "Spray Exploit Fixer plugin version.", FCVAR_DONTRECORD); +} + +float g_fTime; +public Action CmdSprays(int client, int a) +{ + bool recurse = g_iEngine != Engine_Left4Dead && g_iEngine != Engine_Left4Dead2; + int count, counts; + + g_fTime = GetEngineTime(); + RecursiveSearchDirs(client, recurse, recurse ? "download" : "downloads", count, counts, 0, null); + + return Plugin_Handled; +} + +public Action TimerNext(Handle timer, DataPack dPack) +{ + DirectoryListing hDir; + char sDir[PLATFORM_MAX_PATH]; + int client, count, counts, level; + bool recurse; + + dPack.Reset(); + client = dPack.ReadCell(); + recurse = dPack.ReadCell(); + dPack.ReadString(sDir, sizeof(sDir)); + count = dPack.ReadCell(); + counts = dPack.ReadCell(); + level = dPack.ReadCell(); + hDir = dPack.ReadCell(); + + RecursiveSearchDirs(client, recurse, sDir, count, counts, level, hDir); +} + +void RecursiveSearchDirs(int client, bool recurse, const char[] sDir, int &count, int &counts, int level, DirectoryListing hDir) +{ + char sPath[PLATFORM_MAX_PATH]; + FileType type; + File hFile; + int iRead[sizeof(g_iVal)]; + int total; + level++; + + if( hDir == null ) + hDir = OpenDirectory(sDir, true); + + while( hDir.GetNext(sPath, sizeof(sPath), type) ) + { + if( strcmp(sPath, ".") && strcmp(sPath, "..") ) + { + if( type == FileType_Directory && recurse ) + { + Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath); + RecursiveSearchDirs(client, recurse, sPath, count, counts, level, null); + } + else if( type == FileType_File ) + { + int len = strlen(sPath); + if( len > 4 && strcmp(sPath[len - 4], ".dat") == 0 ) + { + counts++; + Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath); + + hFile = OpenFile(sPath, "rb"); + hFile.Read(iRead, sizeof(iRead), 1); + delete hFile; + + int i = ValFile(iRead); + if( i != -1 ) + { + count++; + PrintToServer("Invalid file: %s: %02d (%02X <> %02X)", sPath, i, iRead[i], g_iVal[i]); + } + } + } + + if( total++ > MAX_READ ) + { + level--; + DataPack dPack; + CreateDataTimer(0.1, TimerNext, dPack); + dPack.WriteCell(client); + dPack.WriteCell(recurse); + dPack.WriteString(sDir); + dPack.WriteCell(count); + dPack.WriteCell(counts); + dPack.WriteCell(level); + dPack.WriteCell(hDir); + return; + } + } + } + + delete hDir; + level--; + + if( level == 0 ) + ReplyToCommand(client, "Sprays checked. Found %d of %d invalid. Took %0.1f seconds.", count, counts, GetEngineTime() - g_fTime); +} + +public MRESReturn FileReceived(int pThis, Handle hReturn, Handle hParams) +{ + char sTemp[PLATFORM_MAX_PATH - 10]; + DHookGetParamString(hParams, 1, sTemp, sizeof(sTemp)); + Format(g_sFilename, sizeof(g_sFilename), "download/%s", sTemp); + + if( FileExists(g_sFilename) ) + { + int len = strlen(g_sFilename); + if( len > 4 && strcmp(g_sFilename[len - 4], ".dat") == 0 ) + { + int iRead[sizeof(g_iVal)]; + File hFile = OpenFile(g_sFilename, "rb", false); + hFile.Read(iRead, sizeof(iRead), 1); + delete hFile; + + int i = ValFile(iRead); + if( i != -1 ) + { + LogCustom("Deleted invalid spray: %s", g_sFilename); + PrintToServer("Invalid file: %s: %02d (%02X <> %02X)", g_sFilename, i, iRead[i], g_iVal[i]); + + DeleteFile(g_sFilename); + + for (int j = 1; j <= MaxClients; j++) + { + if (IsValidClient(j)) + { + char sTemp2[PLATFORM_MAX_PATH - 4]; + GetPlayerDecalFile(j, sTemp2, sizeof(sTemp2)); + Format(sTemp2, sizeof(sTemp2), "%s.dat", sTemp2); + if (StrEqual(sTemp2, g_sFilename[24], false)) + { + char auth[20]; + GetClientAuthId(j, AuthId_Steam2, auth, sizeof(auth)); + KickClient(j, "Please change your spray"); + LogAction(j, -1, "\"%L\" is possibly using a bad spray. Client got kicked and spray got deleted.", j); + LogCustom("Deleted invalid spray: %s from (%N) [%s]", sTemp2, j, auth); + } + } + } + + + DHookSetReturn(hReturn, 0); + return MRES_Supercede; + } + } + } + + return MRES_Ignored; +} + +public MRESReturn FileReceivedPost(int pThis, Handle hReturn, Handle hParams) +{ + /*int client; + + if( FileExists(g_sFilename) ) + { + int len = strlen(g_sFilename); + if( len > 4 && strcmp(g_sFilename[len - 4], ".dat") == 0 ) + { + int iRead[sizeof(g_iVal)]; + File hFile = OpenFile(g_sFilename, "rb", false); + hFile.Read(iRead, sizeof(iRead), 1); + delete hFile; + + int i = ValFile(iRead); + if( i != -1 ) + { + if( !client && g_iOff != -1 ) + { + client = LoadFromAddress(view_as
(pThis + g_iOff), NumberType_Int8); + if( client < 1 || client > MaxClients || !IsClientInGame(client) ) client = 0; + } + + if( client ) + { + char auth[20]; + GetClientAuthId(client, AuthId_Steam2, auth, sizeof(auth)); + LogCustom("Deleted invalid spray: %s from (%N) [%s]", g_sFilename, client, auth); + PrintToServer("Invalid file: %s: %02d (%02X <> %02X) from (%N) [%s]", g_sFilename, i, iRead[i], g_iVal[i], client, auth); + } else { + LogCustom("Deleted invalid spray: %s", g_sFilename); + PrintToServer("Invalid file: %s: %02d (%02X <> %02X)", g_sFilename, i, iRead[i], g_iVal[i]); + } + + DeleteFile(g_sFilename); + + for (int j = 1; j <= MaxClients; j++) + { + if (IsValidClient(j)) + { + char sTemp[PLATFORM_MAX_PATH - 4]; + GetPlayerDecalFile(j, sTemp, sizeof(sTemp)); + Format(sTemp, sizeof(sTemp), "%s.dat", sTemp); + if (StrEqual(sTemp, g_sFilename[24], false)) + { + char auth[20]; + GetClientAuthId(j, AuthId_Steam2, auth, sizeof(auth)); + KickClient(j, "Please change your spray"); + LogAction(j, -1, "\"%L\" is possibly using a bad spray. Client got kicked and spray got deleted.", j); + LogCustom("Deleted invalid spray: %s from (%N) [%s]", sTemp, j, auth); + } + } + } + + + DHookSetReturn(hReturn, 0); + return MRES_Supercede; + } + } + } + + return MRES_Ignored;*/ +} + +int ValFile(int iRead[sizeof(g_iVal)]) +{ + char bytes[10]; + bool read = true; + int n; + + for( int i = 0; i < sizeof(g_iVal); i++ ) + { + if( i == 0 && g_iEngine == Engine_TF2 && iRead[i] == 82 && iRead[i+1] == 73 && iRead[i+2] == 70 && iRead[i+3] == 70 && iRead[i+8] == 87 && iRead[i+9] == 65 && iRead[i+10] == 86 && iRead[i+11] == 69 && iRead[i+12] == 102 && iRead[i+13] == 109 && iRead[i+14] == 116 ) + { + break; + } + else if( g_iVal[i] == 42 ) + { + switch( i ) + { + case 8: read = iRead[i] <= 5; + case 16, 18: + { + Format(bytes, sizeof(bytes), "%02X%02X", iRead[i+1], iRead[i]); + n = HtD(bytes); + if( n < 0 || n > 8192 ) read = false; + } + case 20: + { + Format(bytes, sizeof(bytes), "%02X%02X%02X%02X", iRead[i+3], iRead[i+2], iRead[i+1], iRead[i]); + n = HtD(bytes); + if( n & (0x8000|0x10000|0x800000) ) read = false; + } + } + } else { + read = iRead[i] == g_iVal[i]; + } + + if( !read ) return i; + } + + return -1; +} + +int HtD(char[] bytes) +{ + int len = strlen(bytes); + int base = 1; + int value = 0; + + for( int i = len - 1; i >= 0; i-- ) + { + if( bytes[i] >= '0' && bytes[i] <= '9' ) + { + value += (bytes[i] - 48) * base; + base = base * 16; + } + + else if( bytes[i] >= 'A' && bytes[i] <= 'F' ) + { + value += (bytes[i] - 55) * base; + base = base * 16; + } + } + + return value; +} + +void LogCustom(const char[] format, any ...) +{ + char buffer[512]; + VFormat(buffer, sizeof(buffer), format, 2); + + char FileName[PLATFORM_MAX_PATH], sTime[32]; + BuildPath(Path_SM, FileName, sizeof(FileName), "logs/spray_detector.log"); + File file = OpenFile(FileName, "a+"); + FormatTime(sTime, sizeof(sTime), "%d-%b-%Y - %H:%M:%S"); + file.WriteLine("%s: %s", sTime, buffer); + FlushFile(file); + delete file; +} + +//---------------------------------------------------------------------------------------------------- +// Purpose: +//---------------------------------------------------------------------------------------------------- +stock int IsValidClient(int client, bool nobots = true) +{ + if (client <= 0 || client > MaxClients || !IsClientConnected(client) || (nobots && IsFakeClient(client))) + return false; + + return IsClientInGame(client); +} \ No newline at end of file