diff --git a/spray_exploit_fixer/scripting/spray_exploit_fixer_2024.sp b/spray_exploit_fixer/scripting/spray_exploit_fixer_2024.sp new file mode 100644 index 00000000..c4b2bed9 --- /dev/null +++ b/spray_exploit_fixer/scripting/spray_exploit_fixer_2024.sp @@ -0,0 +1,1118 @@ +/* +* Spray Exploit Fixer +* Copyright (C) 2024 Silvers +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + + + +#define PLUGIN_VERSION "2.23" + +/*======================================================================================= + 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: + +2.23 (05-Nov-2024) - Update by ".Rushaway" + - Added cvar "spray_exploit_fixer_punish" to specify which exploits to test for. + - Added cvar "spray_exploit_fixer_bantime" to set the ban length. + - Now less checks of GetClientAuthId by storing them. + - Switch to Steam3 format for AuthID. + - Fixed g_smWaiting not removing data for unverified clients. + - Prevent g_smWaiting not removing data if client was already disconnected. + - LogAction now print infos even if client is not verified. + +2.22 (28-Jan-2024) + - Fixed memory leak caused by clearing StringMap/ArrayList data instead of deleting. + +2.21 (19-Feb-2023) + - Now prevents even more log spamming duplicate entries. Thanks to ".Rushaway" for reporting. + +2.20 (20-Jan-2023) + - Now logs if a Steam ID is unverified. + - Now prevents log spamming duplicate entries. + - Fixed checking bots for sprays. + - Thanks to ".Rushaway" for reporting and help testing. + +2.19 (07-Jan-2023) + - Fixed processing getting stuck. Thanks to "SuperConker" for reporting and help testing. + - Fixed invalid handle errors. Thanks to "nikooo777" for reporting. + +2.18 (24-Dec-2022) + - Changed moving sprays to use an asynchronous method to prevent a script execution timed out error. Thanks to ".Rushaway" for reporting. + +2.17 (08-Oct-2022) + - Fixed command "sm_spray_test" getting stuck processing under certain conditions. + - Re-wrote the recursive directory function to avoid several bugs under several conditions. + - Now only moves sprays (.dat or .dat.ztmp) and not other files to the "backup_sprays" folder. + - Now deletes empty directories on plugin start after moving sprays to the "backup_sprays" folder. + +2.16 (30-Sep-2022) + - Fixed not moving all sprays on disconnect. + - Fixed client not in game errors when renaming sprays. + - Now moves "dat.ztmp" spray files to backup folder. + +2.15 (22-Sep-2022) + - Fixed not deleting the old backup if the names match. + +2.14 (22-Sep-2022) + - Added cvar "spray_exploit_fixer_msg" to control if messages should print to the server console. Requested by ".Rushaway". + - Plugin now moves all sprays to the "download/backup_sprays" folder on plugin start and client disconnect. + - Removed saving checked and blocked sprays to file. All sprays will be checked. + +2.13 (22-May-2022) + - More detailed "LogAction" when kicking or banning clients. + +2.12 (22-May-2022) + - Added some more "LogAction" when kicking or banning clients. + +2.11 (22-May-2022) + - Added cvar "spray_exploit_fixer_kick" to kick clients. Ban cvar overrides this. Requested by ".Rushaway". + - Changes to fix not kicking or banning clients under some conditions. + +2.10 (23-Apr-2022) + - Fixed the plugin blocking sprays on some servers. Thanks to "SuperConker" for reporting and lots of testing. + +2.9 (10-Apr-2022) + - Fixed showing the wrong invalid files count. Thanks to "sappho" for reporting. + +2.8 (20-Mar-2022) + - Added another check and prevention against crash exploits. Thanks to "Sreaper" and "ficool2" for lots of help. + - Fixed some false positives due to recent updates. + +2.7 (08-Mar-2022) + - Added support for banning using the "Material Admin" plugin. Thanks to "lechuga" for adding. + +2.6 (01-Mar-2022) + - Another crash exploit fixed. Thanks to Kenzzer for reporting. + +2.5 (15-Jan-2022) + - Fixed randomly using recursive folder and extension names in spray filenames causing validation failure. Thanks to "A1m" for reporting. + +2.4 (02-Dec-2021) + - Added support for banning using the "SourceBans" plugin. Thanks to "lechuga" for adding. + +2.3 (12-Nov-2021) + - Added a check for missing downloads folder and filename. Thanks to "nebsun" for reporting. + - Changes to fix warnings when compiling on SourceMod 1.11. + +2.2 (30-Jun-2021) + - Fixed another Spray exploit. Thanks to "Madness (null138)" for fixing and reporting. + +2.1 (31-Mar-2021) + - Added a check for "sm_sprays_allowed" in the command admin_overrides.cfg to only allow specific flag groups to use sprays. + +2.0 (09-Aug-2020) + - Now should support all games. + - Added more checks for invalid files. + - Added cvar "spray_exploit_fixer_path" to specify the downloads folder if not correctly detected. + - Removed gamedata and DHooks dependency. + - Removed cvar "spray_exploit_fixer_name". + +1.6 (15-Jul-2020) + - Fixed issue with CSS game. Thanks to "NeonC" for reporting. + - Added cvar "spray_exploit_fixer_name" to choose the method for retrieving the spray owner. + +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 + +#undef REQUIRE_PLUGIN +#tryinclude +#tryinclude +#define REQUIRE_PLUGIN + +#pragma newdecls required + +#include +#include + + +#define MAX_READ 50 +#define TIMEOUT_LOG 10.0 +#define PATH_BACKUP "backup_sprays" + +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,0,0,0,0,0,0,0,0}; +char g_sFilename[PLATFORM_MAX_PATH]; +char g_sMoveFiles[PLATFORM_MAX_PATH]; +char g_sDownloads[PLATFORM_MAX_PATH]; +char g_sPath1[MAXPLAYERS+1][PLATFORM_MAX_PATH]; +char g_sPath2[MAXPLAYERS+1][PLATFORM_MAX_PATH]; +char g_sAuth[MAXPLAYERS+1][64]; +char g_sAuthUnverified[MAXPLAYERS+1][64]; +float g_fSprayed[MAXPLAYERS+1]; +ConVar g_hCvarBan, g_hCvarBanTime, g_hCvarKick, g_hCvarLog, g_hCvarMsg, g_hCvarPath, g_hCvarPunish; +EngineVersion g_iEngine; +StringMap g_smChecked; +StringMap g_smReceive; +StringMap g_smWaiting; +int g_iTotal; +float g_fTime; +bool g_bLate; +bool g_bProc; +bool g_bDecal; +bool g_bSourceBans; +bool g_bMaterialAdmin; + + + +// Added this here so it compiles on the forum without the SourceBans/MaterialAdmin includes. +#if !defined _sourcebanspp_included +native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); +#endif +#if !defined _materialadmin_included +native bool MABanPlayer(int iClient, int iTarget, int iType, int iTime, char[] sReason); +#define MA_BAN_STEAM 1 +#endif + + + +public Plugin myinfo = +{ + name = "[ANY] Spray Exploit Fixer", + author = "SilverShot", + 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) +{ + MarkNativeAsOptional("SBPP_BanPlayer"); + MarkNativeAsOptional("MABanPlayer"); + + g_iEngine = GetEngineVersion(); + g_bLate = late; + + return APLRes_Success; +} + +public void OnLibraryAdded(const char []name) +{ + if( strcmp(name, "sourcebans++") == 0 ) + g_bSourceBans = true; + else if( strcmp(name, "materialadmin") == 0 ) + g_bMaterialAdmin = true; +} + +public void OnLibraryRemoved(const char []name) +{ + if( strcmp(name, "sourcebans++") == 0 ) + g_bSourceBans = false; + else if( strcmp(name, "materialadmin") == 0 ) + g_bMaterialAdmin = false; +} + +public void OnPluginStart() +{ + RegAdminCmd("sm_spray_test", CmdSprays, ADMFLAG_ROOT, "Tests all sprays in the games downloads folder, listing bad ones."); + + switch( g_iEngine ) + { + case Engine_SourceSDK2006, Engine_SourceSDK2007, Engine_Left4Dead, Engine_Left4Dead2: + { + g_sDownloads = "downloads/"; + } + default: + { + g_sDownloads = "download/user_custom/cc/"; + } + } + + CreateConVar( "spray_exploit_fixer", PLUGIN_VERSION, "Spray Exploit Fixer plugin version.", FCVAR_DONTRECORD); + g_hCvarPunish = CreateConVar( "spray_exploit_fixer_punish", "3", "0=Off. 1=PlayerDecal. 2=FileCheck. 3=Both. Which exploits to test for."); + if( g_iEngine != Engine_TF2 ) + { + g_hCvarBan = CreateConVar( "spray_exploit_fixer_ban", "0", "0=Off. 1=Ban users who trigger invalid sprays (may still be some false positives)."); + g_hCvarKick = CreateConVar( "spray_exploit_fixer_kick", "0", "0=Off. 1=Kick users who trigger invalid sprays (may still be some false positives)."); + g_hCvarBanTime = CreateConVar( "spray_exploit_fixer_bantime", "5", "0=Permanent. Ban time (in minutes)."); + } + g_hCvarLog = CreateConVar( "spray_exploit_fixer_log", "1", "Logging saved to sourcemod/logs/spray_downloads.log: 0=Off. 1=Log all user uploads. 2=Log invalid sprays only."); + g_hCvarMsg = CreateConVar( "spray_exploit_fixer_msg", "1", "Print to server console: 0=Off. 1=Missing sprays and invalid sprays. 2=Only invalid sprays."); + g_hCvarPath = CreateConVar( "spray_exploit_fixer_path", g_sDownloads, "Path to the downloads folder of sprays. Add /cc/ if sprays are stored in individual 2 character folders. Must contain trailing / slash."); + AutoExecConfig(true, "spray_exploit_fixer"); + g_hCvarPath.AddChangeHook(ConVarChanged_Cvars); + + g_smChecked = new StringMap(); + g_smReceive = new StringMap(); + g_smWaiting = new StringMap(); + + AddTempEntHook("Player Decal", PlayerDecal); + + char sPath[PLATFORM_MAX_PATH]; + strcopy(sPath, sizeof(sPath), g_sDownloads); + ReplaceString(sPath, sizeof(sPath), "/cc", ""); + StrCat(sPath, sizeof(sPath), PATH_BACKUP); + CreateDirectory(sPath, 511, true); + + if( !g_bLate ) + MoveSprays(); +} + +public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3], float angles[3]) +{ + if( impulse == 0xCA ) + { + static char cc[6]; + static char sTemp[PLATFORM_MAX_PATH]; + GetPlayerJingleFile(client, sTemp, sizeof(sTemp)); + + Format(cc, sizeof(cc), "/%c%c/", sTemp[0], sTemp[1]); + Format(sTemp, sizeof(sTemp), "%s%s.dat", g_sDownloads, sTemp); + ReplaceString(sTemp, sizeof(sTemp), "/cc/", cc); + + bool val; + if( !g_smChecked.GetValue(sTemp, val) || !val ) + { + impulse = 0; + return Plugin_Changed; + } + } + + return Plugin_Continue; +} + +public void OnClientPutInServer(int client) +{ + char sSteamID[64]; + GetClientAuthId(client, AuthId_Steam3, sSteamID, sizeof(sSteamID)); + FormatEx(g_sAuth[client], sizeof(g_sAuth[]), "%s", sSteamID); + + char sSteamIDUnverified[32]; + GetClientAuthId(client, AuthId_Steam3, sSteamIDUnverified, sizeof(sSteamIDUnverified), false); + FormatEx(g_sAuthUnverified[client], sizeof(g_sAuthUnverified[]), "%s", sSteamIDUnverified); +} + +public void OnClientConnected(int client) +{ + g_fSprayed[client] = 0.0; + g_sPath1[client][0] = 0; + g_sPath2[client][0] = 0; +} + +public void OnClientDisconnect(int client) +{ + if( IsFakeClient(client) ) return; + + g_smWaiting.Remove(g_sAuthUnverified[client]); + g_smChecked.Remove(g_sPath1[client]); + g_smReceive.Remove(g_sPath1[client]); + g_smChecked.Remove(g_sPath2[client]); + g_smReceive.Remove(g_sPath2[client]); + + g_sAuth[client][0] = 0; + g_sAuth[client][6] = 0; + g_sAuthUnverified[client][0] = 0; + + /* + static char sPath[PLATFORM_MAX_PATH]; + static char sOld[PLATFORM_MAX_PATH]; + static char sNew[PLATFORM_MAX_PATH]; + + for( int i = 0; i < 2; i++ ) + { + sPath[0] = 0; + + switch( i ) + { + case 0: + { + if( g_sPath1[client][0] ) + strcopy(sPath, sizeof(sPath), g_sPath1[client]); + else if( IsClientInGame(client) ) + GetPlayerDecalFile(client, sPath, sizeof(sPath)); + } + case 1: + { + if( g_sPath2[client][0] ) + strcopy(sPath, sizeof(sPath), g_sPath2[client]); + else if( IsClientInGame(client) ) + GetPlayerJingleFile(client, sPath, sizeof(sPath)); + } + } + + if( sPath[0] ) + { + if( i == 0 ) + { + g_smChecked.Remove(g_sPath1[client]); + g_smReceive.Remove(g_sPath1[client]); + } + else + { + g_smChecked.Remove(g_sPath2[client]); + g_smReceive.Remove(g_sPath2[client]); + } + + Format(sOld, sizeof(sOld), "%s%s.dat", g_sDownloads, sPath); + Format(sNew, sizeof(sNew), "%s%s/%s.dat", g_sDownloads, PATH_BACKUP, sPath); + + if( FileExists(sOld) ) + { + if( FileExists(sNew, true) ) DeleteFile(sNew, true); + RenameFile(sNew, sOld, true); + } + + StrCat(sOld, sizeof(sOld), ".ztmp"); + StrCat(sNew, sizeof(sNew), ".ztmp"); + + if( FileExists(sOld) ) + { + if( FileExists(sNew, true) ) DeleteFile(sNew, true); + RenameFile(sNew, sOld, true); + } + } + } + */ +} + +public void OnMapEnd() +{ + MoveSprays(); + + // .Clear() is creating a memory leak + // g_smReceive.Clear(); + // g_smWaiting.Clear(); + delete g_smReceive; + delete g_smWaiting; + g_smReceive = new StringMap(); + g_smWaiting = new StringMap(); + + for( int i = 1; i <= MaxClients; i++ ) + { + g_fSprayed[i] = 0.0; + } +} + +void ConVarChanged_Cvars(Handle convar, const char[] oldValue, const char[] newValue) +{ + g_hCvarPath.GetString(g_sDownloads, sizeof(g_sDownloads)); +} + +Action CmdSprays(int client, int args) +{ + if( g_bProc ) + { + ReplyToCommand(client, "[Sprays] Already processing."); + return Plugin_Handled; + } + + ReplyToCommand(client, "[Sprays] checking files, please wait..."); + + g_iTotal = 0; + g_bProc = true; + g_fTime = GetEngineTime(); + + ArrayList aList = new ArrayList(ByteCountToCells(PLATFORM_MAX_PATH)); + + int pos = StrContains(g_sDownloads, "/"); + if( pos != -1 ) g_sDownloads[pos] = 0; + + RecursiveFiles(aList, false, g_sDownloads); + + if( pos != -1 ) g_sDownloads[pos] = '/'; + + int count, counts; + RecursiveSearchDirs(client, aList, count, counts, false); + + return Plugin_Handled; +} + +void RecursiveFiles(ArrayList aList, bool move, const char sDir[PLATFORM_MAX_PATH]) +{ + FileType type; + DirectoryListing hDir; + File hFile; + int iRead[4]; + int moving; + + if( DirExists(sDir) ) + { + hDir = OpenDirectory(sDir, true); + + if( hDir ) + { + char sPath[PLATFORM_MAX_PATH]; + + while( ReadDirEntry(hDir, sPath, sizeof(sPath), type) ) + { + if( strcmp(sPath, ".") && strcmp(sPath, "..") ) + { + moving = 0; + + switch( type ) + { + case FileType_Directory: + { + if( !move || strcmp(sPath, PATH_BACKUP) ) + { + Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath); + RecursiveFiles(aList, move, sPath); + } + } + case FileType_File: + { + int len = strlen(sPath); + if( len > 4 ) + { + if( strcmp(sPath[len - 4], ".dat") == 0 ) + moving = 1; + else if( move && strcmp(sPath[len - 5], ".ztmp") == 0) + moving = 2; + + if( moving ) + { + if( moving == 2 ) + { + Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath); + ReplaceString(sPath, sizeof(sPath), ".ztmp", ""); + + if( FileExists(sPath, true) == false ) + { + moving = 0; + } + else + { + StrCat(sPath, sizeof(sPath), ".ztmp"); + } + } + else + { + Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath); + } + + if( moving ) + { + hFile = OpenFile(sPath, "rb", false); + if( hFile ) + { + hFile.Read(iRead, sizeof(iRead), 1); + delete hFile; + if( + (iRead[0] == 86 && iRead[1] == 84 && iRead[2] == 70 && iRead[3] == 0) || + (moving == 2 && iRead[0] == 76 && iRead[1] == 90 && iRead[2] == 83 && iRead[3] == 83) + ) + { + aList.PushString(sPath); + } + } + } + } + } + } + } + } + } + + delete hDir; + } + } +} + +void MoveSprays() +{ + int count, counts; + + ArrayList aList = new ArrayList(ByteCountToCells(PLATFORM_MAX_PATH)); + + strcopy(g_sMoveFiles, sizeof(g_sMoveFiles), g_sDownloads); + + int pos = StrContains(g_sMoveFiles, "/"); + if( pos != -1 ) g_sMoveFiles[pos] = 0; + + RecursiveFiles(aList, true, g_sMoveFiles); + RecursiveSearchDirs(0, aList, count, counts, true); +} + +void RecursiveSearchDirs(int client, ArrayList aList, int &count, int &counts, bool move = false) +{ + static char sNew[PLATFORM_MAX_PATH]; + static char sPath[PLATFORM_MAX_PATH]; + + File hFile; + int iRead[sizeof(g_iVal)]; + int len, pos, i; + + while( aList.Length > 0 ) + { + aList.GetString(0, sPath, sizeof(sPath)); + aList.Erase(0); + + len = strlen(sPath); + if( (len > 4 && strcmp(sPath[len - 4], ".dat") == 0) || (move && len > 4 && strcmp(sPath[len - 5], ".ztmp") == 0) ) + { + if( move ) + { + pos = FindCharInString(sPath, '/', true); + if( pos != -1 ) sPath[pos] = 0; + + Format(sNew, sizeof(sNew), "%s/%s/%s", g_sMoveFiles, PATH_BACKUP, sPath[pos + 1]); + if( FileExists(sNew, true) ) DeleteFile(sNew, true); + + if( pos != -1 ) sPath[pos] = '/'; + + RenameFile(sNew, sPath, true); + } + else + { + counts++; + + hFile = OpenFile(sPath, "rb"); + if( hFile ) + { + hFile.Read(iRead, sizeof(iRead), 1); + + delete hFile; + + i = ValFile(iRead); + if( i != -1 ) + { + count++; + + PrintToConsole(client, "Invalid file: %s: %02d (%02X <> %02X)", sPath, i, iRead[i], g_iVal[i]); + } + } + } + } + + if( g_iTotal++ > MAX_READ ) + { + g_iTotal = 0; + + DataPack dPack; + CreateDataTimer(0.1, TimerNext, dPack); + dPack.WriteCell(client); + dPack.WriteCell(aList); + dPack.WriteCell(count); + dPack.WriteCell(counts); + dPack.WriteCell(move); + return; + } + } + + if( aList.Length == 0 ) + { + if( !move ) + { + g_bProc = false; + + ReplyToCommand(client, "[Sprays] downloads checked. Found %d of %d invalid. Took %0.2f seconds.", count, counts, GetEngineTime() - g_fTime); + + delete aList; + } + else + { + DeleteEmptyDirs(g_sMoveFiles); + + delete aList; + } + } +} + +Action TimerNext(Handle timer, DataPack dPack) +{ + ArrayList aList; + int client, count, counts; + bool move; + + dPack.Reset(); + client = dPack.ReadCell(); + aList = dPack.ReadCell(); + count = dPack.ReadCell(); + counts = dPack.ReadCell(); + move = dPack.ReadCell(); + + RecursiveSearchDirs(client, aList, count, counts, move); + + return Plugin_Continue; +} + +void DeleteEmptyDirs(const char sDir[PLATFORM_MAX_PATH]) +{ + FileType type; + DirectoryListing hDir; + bool del = true; + + if( DirExists(sDir) ) + { + hDir = OpenDirectory(sDir, true); + + if( hDir ) + { + char sPath[PLATFORM_MAX_PATH]; + + while( ReadDirEntry(hDir, sPath, sizeof(sPath), type) ) + { + if( strcmp(sPath, ".") && strcmp(sPath, "..") ) + { + switch( type ) + { + case FileType_Directory: + { + Format(sPath, sizeof(sPath), "%s/%s", sDir, sPath); + DeleteEmptyDirs(sPath); + del = false; + } + case FileType_File: + { + del = false; + } + } + } + } + + delete hDir; + } + + if( del ) + { + RemoveDir(sDir); + } + } +} + +Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, float delay) +{ + if( g_bDecal ) return Plugin_Continue; + + int client = TE_ReadNum("m_nPlayer"); + if( !client || !IsClientInGame(client) || !CheckCommandAccess(client, "sm_sprays_allowed", 0, true) ) + { + return Plugin_Handled; + } + + if( IsFakeClient(client) ) + { + return Plugin_Continue; + } + + g_sFilename[0] = 0; + GetPlayerDecalFile(client, g_sFilename, sizeof(g_sFilename)); + + bool val; + if( g_sFilename[0] ) + { + char cc[6]; + ReplaceString(g_sFilename, sizeof(g_sFilename), g_sDownloads, ""); + ReplaceString(g_sFilename, sizeof(g_sFilename), ".dat", ""); + + Format(cc, sizeof(cc), "/%c%c/", g_sFilename[0], g_sFilename[1]); + Format(g_sFilename, sizeof(g_sFilename), "%s%s.dat", g_sDownloads, g_sFilename); + ReplaceString(g_sFilename, sizeof(g_sFilename), "/cc/", cc); + + if( !g_smChecked.GetValue(g_sFilename, val) ) + { + FileCheck(); + + g_smChecked.GetValue(g_sFilename, val); + } + } + + if( !val ) + { + static char auth[64]; + if ( g_sAuth[client][6] == 'I' ) + Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); + else + Format(auth, sizeof(auth), "%s", g_sAuth[client]); + + if( FileExists(g_sFilename) ) + { + if( GetGameTime() - g_fSprayed[client] > TIMEOUT_LOG ) + { + g_fSprayed[client] = GetGameTime(); + if( g_hCvarLog.IntValue ) LogCustom("Blocked invalid spray: %s from (%N) [%s]", g_sFilename, client, auth); + if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Blocked invalid spray: %s from (%N) [%s]", g_sFilename, client, auth); + } + + if( g_hCvarPunish.IntValue == 1 || g_hCvarPunish.IntValue >= 3) + TestClient(client); + } + else + { + if( GetGameTime() - g_fSprayed[client] > TIMEOUT_LOG && !g_smWaiting.GetValue(g_sAuthUnverified[client], val) ) + { + g_fSprayed[client] = GetGameTime(); + g_smWaiting.SetValue(g_sAuthUnverified[client], true); + + if( g_hCvarLog.IntValue ) LogCustom("Blocked unchecked spray - missing file: %s from (%N) [%s]", g_sFilename, client, auth); + if( g_hCvarMsg.IntValue == 1 ) PrintToServer("[Spray Exploit] Blocked unchecked spray - missing file: %s from (%N) [%s]", g_sFilename, client, auth); + } + } + + float vPos[3]; + TE_ReadVector("m_vecOrigin", vPos); + DataPack hPack = new DataPack(); + hPack.WriteCell(GetClientUserId(client)); + hPack.WriteFloat(vPos[0]); + hPack.WriteFloat(vPos[1]); + hPack.WriteFloat(vPos[2]); + RequestFrame(ReqTempEnt, hPack); + return Plugin_Handled; + } + + return Plugin_Continue; +} + +void ReqTempEnt(DataPack hPack) +{ + hPack.Reset(); + + int client = hPack.ReadCell(); + client = GetClientOfUserId(client); + if( client ) + { + float vPos[3]; + vPos[0] = hPack.ReadFloat(); + vPos[1] = hPack.ReadFloat(); + vPos[2] = hPack.ReadFloat(); + + g_bDecal = true; + TE_Start("Player Decal"); + TE_WriteVector("m_vecOrigin", vPos); + TE_WriteNum("m_nEntity", 0); + TE_WriteNum("m_nPlayer", client); + TE_SendToClient(client); + g_bDecal = false; + } + delete hPack; +} + +int GetClientFromSpray() +{ + char hex[10]; + for( int i = 1; i <= MaxClients; i++ ) + { + if( IsClientInGame(i) ) + { + hex[0] = 0; + GetPlayerDecalFile(i, hex, sizeof(hex)); + if( hex[0] && StrContains(g_sFilename, hex) != -1 ) + return i; + } + } + + return 0; +} + +int GetClientFromJingle() +{ + char hex[10]; + for( int i = 1; i <= MaxClients; i++ ) + { + if( IsClientInGame(i) ) + { + hex[0] = 0; + GetPlayerJingleFile(i, hex, sizeof(hex)); + if( hex[0] && StrContains(g_sFilename, hex) != -1 ) + return i; + } + } + + return 0; +} + +void TestClient(int client) +{ + if( g_iEngine != Engine_TF2 && client ) + { + if( g_hCvarBan.IntValue ) + { + int iDuration = g_hCvarBanTime.IntValue; + + if( g_bSourceBans ) + SBPP_BanPlayer(0, client, iDuration, "Invalid spray"); + else if( g_bMaterialAdmin ) + MABanPlayer(0, client, MA_BAN_STEAM, iDuration, "Invalid spray"); + else + BanClient(client, iDuration, BANFLAG_AUTO, "Invalid spray"); + + LogAction(client, -1, "[Spray Exploit] %N %s was banned %d minutes for invalid Spray", client, g_sAuthUnverified[client], iDuration); + return; + } + else if( g_hCvarKick.IntValue ) + { + KickClient(client, "Invalid spray. Please change it"); + LogAction(client, -1, "[Spray Exploit] %N %s was kicked for invalid Spray.", client, g_sAuthUnverified[client]); + return; + } + } +} + +public Action OnFileReceive(int client, const char[] sFile) +{ + strcopy(g_sFilename, sizeof(g_sFilename), sFile); + + client = GetClientFromSpray(); + if( !client ) client = GetClientFromJingle(); + + bool log; + + if( client ) + { + static char sPath[PLATFORM_MAX_PATH]; + + GetPlayerDecalFile(client, sPath, sizeof(sPath)); + if( strcmp(sPath, g_sPath1[client]) ) + { + ReplaceString(sPath, sizeof(sPath), g_sDownloads, ""); + strcopy(g_sPath1[client], sizeof(g_sPath1[]), sPath); + log = true; + } + + GetPlayerJingleFile(client, sPath, sizeof(sPath)); + if( strcmp(sPath, g_sPath2[client]) ) + { + ReplaceString(sPath, sizeof(sPath), g_sDownloads, ""); + strcopy(g_sPath2[client], sizeof(g_sPath2[]), sPath); + log = true; + } + } + else + { + log = true; + } + + if( log && g_hCvarLog.IntValue == 1 ) + { + if( client ) + { + static char auth[64]; + if ( g_sAuth[client][6] == 'I' ) + Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); + else + Format(auth, sizeof(auth), "%s", g_sAuth[client]); + + LogCustom("File received: %s from (%N) [%s]", sFile, client, auth); + } + else + { + int val; + if( !g_smReceive.GetValue(sFile, val) ) + { + g_smReceive.SetValue(sFile, true); + LogCustom("File received: %s", sFile); + } + } + } + + return Plugin_Continue; +} + +public Action OnFileSend(int client, const char[] sFile) +{ + strcopy(g_sFilename, sizeof(g_sFilename), sFile); + + bool val; + if( g_smChecked.GetValue(sFile, val) ) + { + if( !val ) return Plugin_Handled; + } else { + FileCheck(); + + if( g_smChecked.GetValue(sFile, val) ) + { + if( !val ) return Plugin_Handled; + } + } + + return Plugin_Continue; +} + +void FileCheck() +{ + 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); + if( hFile ) + { + hFile.Read(iRead, sizeof(iRead), 1); + delete hFile; + + int i = ValFile(iRead); + + if( i != -1 ) + { + int client = GetClientFromSpray(); + if( !client ) client = GetClientFromJingle(); + if( client ) + { + static char auth[64]; + if ( g_sAuth[client][6] == 'I' ) + Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); + else + Format(auth, sizeof(auth), "%s", g_sAuth[client]); + + if( g_hCvarLog.IntValue ) LogCustom("Invalid spray: %s from (%N) [%s]", g_sFilename, client, auth); + if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray: %s: %02d (%02X <> %02X) from (%N) [%s]", g_sFilename, i, iRead[i], g_iVal[i], client, auth); + } else { + if( g_hCvarLog.IntValue ) LogCustom("Invalid spray: %s", g_sFilename); + if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray: %s: %02d (%02X <> %02X)", g_sFilename, i, iRead[i], g_iVal[i]); + } + + if( g_hCvarPunish.IntValue >= 2 ) + TestClient(client); + + g_smChecked.SetValue(g_sFilename, false); + return; + } + + g_smChecked.SetValue(g_sFilename, true); + } else { + if( g_hCvarLog.IntValue ) LogCustom("Missing file: %s", g_sFilename); + if( g_hCvarMsg.IntValue == 1 ) PrintToServer("[Spray Exploit] Missing file: %s", g_sFilename); + } + } + } +} + +int ValFile(int iRead[sizeof(g_iVal)]) +{ + //this was suggested by madness to solve new spray exploit december 2024. + //he was a bit vague, just saying header[21] == 42 && header[24] > 1 has to be blocked + if (iRead[21] == 42 && iRead[24] > 1) + { + LogMessage("blocked spray with madness option."); + return -1; + } + if( iRead[0] == 82 && iRead[1] == 73 && iRead[2] == 70 && iRead[3] == 70 && iRead[8] == 87 && iRead[9] == 65 && iRead[10] == 86 && iRead[11] == 69 ) + { + if( iRead[34] + iRead[35] * 256 == 32 ) + return 34; + return -1; + } + + char bytes[10]; + bool read = true; + int n; + + for( int i = 0; i < sizeof(g_iVal); i++ ) + { + 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 = HexToDec(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 = HexToDec(bytes); + if( n & (0x8000|0x10000|0x800000) ) read = false; + } + /* + case 25: + { + if( iRead[i] > 0 ) read = false; + } + // */ + } + } + else if( i < 27 ) + { + read = iRead[i] == g_iVal[i]; + } + + if( !read ) return i; + } + + return -1; +} + +int HexToDec(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 ...) +{ + static char buffer[512]; + VFormat(buffer, sizeof(buffer), format, 2); + + static char sPath[PLATFORM_MAX_PATH], sTime[32]; + BuildPath(Path_SM, sPath, sizeof(sPath), "logs/spray_downloads.log"); + File file = OpenFile(sPath, "a+"); + FormatTime(sTime, sizeof(sTime), "%d-%b-%Y - %H:%M:%S"); + file.WriteLine("%s: %s", sTime, buffer); + FlushFile(file); + delete file; +}