#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); }