From 5971ce7f6760eb227e0c4b06765f0fa902d60216 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Thu, 4 Dec 2025 16:44:01 +0100 Subject: [PATCH] feat(core): Patch latest sprays exploit --- addons/sourcemod/scripting/FixSprayExploit.sp | 256 ++++++++++++++++-- 1 file changed, 239 insertions(+), 17 deletions(-) diff --git a/addons/sourcemod/scripting/FixSprayExploit.sp b/addons/sourcemod/scripting/FixSprayExploit.sp index 20e9063..73955ce 100644 --- a/addons/sourcemod/scripting/FixSprayExploit.sp +++ b/addons/sourcemod/scripting/FixSprayExploit.sp @@ -18,7 +18,7 @@ -#define PLUGIN_VERSION "2.25" +#define PLUGIN_VERSION "2.28" /*======================================================================================= Plugin Info: @@ -32,6 +32,22 @@ ======================================================================================== Change Log: +2.28 (02-Dec-2025) + - Added file size validation to detect malformed sprays by comparing header size with actual file size. + - Extended g_iVal array to cover offset 62 for better VTF header validation. + - Added VTF format constants and helper functions (GetFormatInfo, CalcSize, MaxValC). + - Added 5% tolerance threshold for file size comparison to minimize false positives. + - Enhanced logging to include size mismatch details (actual size, expected size, difference percentage). + - Forward "OnSprayExploit" now uses special code (-2) for size mismatch errors. + - Thanks to null138 for reporting and testing. + +2.27 (24-Jun-2025) + - Added a check for the latest spray exploit. Thanks to ".Rushaway" for fixing and reporting. + +2.26 (21-May-2025) + - Added native "SprayExploitFixer_LogCustom" to log custom messages. Requested by ".Rushaway". + - Added RegPluginLibrary "spray_exploit_fixer". + 2.25 (04-Jan-2025) - Changes to the recent exploit fix. Thanks to "Madness (null138)" for fixing and reporting. @@ -184,13 +200,33 @@ #include #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}; +// VTF formats +#define FVTF_RGBA8888 0 +#define FVTF_ABGR8888 1 +#define FVTF_RGB888 2 +#define FVTF_BGR888 3 +#define FVTF_RGB565 4 +#define FVTF_I8 5 +#define FVTF_IA88 6 +#define FVTF_DXT1 7 +#define FVTF_DXT5 9 +#define FVTF_BGRA8888 12 +#define FVTF_DXT1_ALT 13 +#define FVTF_DXT3 14 +#define FVTF_DXT5_ALT 15 +#define FVTF_BGRX8888 16 + +#define SIZE_TOLERANCE 0.05 // 5% tolerance + +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,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42}; +bool g_bIsVTFExploit[MAXPLAYERS+1] = {false, ...}; char g_sFilename[PLATFORM_MAX_PATH]; char g_sMoveFiles[PLATFORM_MAX_PATH]; char g_sDownloads[PLATFORM_MAX_PATH]; @@ -240,6 +276,10 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max MarkNativeAsOptional("SBPP_BanPlayer"); MarkNativeAsOptional("MABanPlayer"); + RegPluginLibrary("spray_exploit_fixer"); + + CreateNative("SprayExploitFixer_LogCustom", Native_LogCustom); + g_iEngine = GetEngineVersion(); g_bLate = late; @@ -318,7 +358,7 @@ public Action OnPlayerRunCmd(int client, int &buttons, int &impulse, float vel[3 static char sTemp[PLATFORM_MAX_PATH]; GetPlayerJingleFile(client, sTemp, sizeof(sTemp)); - Format(cc, sizeof(cc), "/%c%c/", sTemp[0], sTemp[1]); + FormatEx(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); @@ -346,6 +386,7 @@ public void OnClientPutInServer(int client) public void OnClientConnected(int client) { + g_bIsVTFExploit[client] = false; g_fSprayed[client] = 0.0; g_sPath1[client][0] = 0; g_sPath2[client][0] = 0; @@ -364,6 +405,7 @@ public void OnClientDisconnect(int client) g_sAuth[client][0] = 0; g_sAuth[client][6] = 0; g_sAuthUnverified[client][0] = 0; + g_bIsVTFExploit[client] = false; /* static char sPath[PLATFORM_MAX_PATH]; @@ -607,7 +649,7 @@ void RecursiveSearchDirs(int client, ArrayList aList, int &count, int &counts, b pos = FindCharInString(sPath, '/', true); if( pos != -1 ) sPath[pos] = 0; - Format(sNew, sizeof(sNew), "%s/%s/%s", g_sMoveFiles, PATH_BACKUP, sPath[pos + 1]); + FormatEx(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] = '/'; @@ -761,7 +803,7 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl 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]); + FormatEx(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); @@ -777,9 +819,9 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl { static char auth[64]; if ( g_sAuth[client][6] == 'I' ) - Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); + FormatEx(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); else - Format(auth, sizeof(auth), "%s", g_sAuth[client]); + FormatEx(auth, sizeof(auth), "%s", g_sAuth[client]); if( FileExists(g_sFilename) ) { @@ -788,6 +830,7 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl 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); + g_bIsVTFExploit[client] = true; } if( g_hCvarPunish.IntValue == 1 || g_hCvarPunish.IntValue >= 3) @@ -802,6 +845,7 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl 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); + g_bIsVTFExploit[client] = true; } } @@ -823,7 +867,7 @@ void ReqTempEnt(DataPack hPack) int client = hPack.ReadCell(); client = GetClientOfUserId(client); - if( client ) + if( client && !g_bIsVTFExploit[client] ) { float vPos[3]; vPos[0] = hPack.ReadFloat(); @@ -942,9 +986,9 @@ public Action OnFileReceive(int client, const char[] sFile) { static char auth[64]; if ( g_sAuth[client][6] == 'I' ) - Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); + FormatEx(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); else - Format(auth, sizeof(auth), "%s", g_sAuth[client]); + FormatEx(auth, sizeof(auth), "%s", g_sAuth[client]); LogCustom("File received: %s from (%N) [%s]", sFile, client, auth); } @@ -994,6 +1038,10 @@ void FileCheck() if( hFile ) { hFile.Read(iRead, sizeof(iRead), 1); + + // Get actual file size + hFile.Seek(0, SEEK_END); + int actualSize = hFile.Position; delete hFile; int i = ValFile(iRead); @@ -1006,9 +1054,9 @@ void FileCheck() { static char auth[64]; if ( g_sAuth[client][6] == 'I' ) - Format(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); + FormatEx(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); else - Format(auth, sizeof(auth), "%s", g_sAuth[client]); + FormatEx(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); @@ -1017,6 +1065,8 @@ void FileCheck() if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray: %s: %02d (%02X <> %02X)", g_sFilename, i, iRead[i], g_iVal[i]); } + g_bIsVTFExploit[client] = true; + Call_StartForward(g_hExploit); Call_PushCell(client); Call_PushCell(i); @@ -1030,6 +1080,46 @@ void FileCheck() return; } + // Check file size + int calculatedSize = CalcSize(iRead); + if( calculatedSize > 0 ) + { + float sizeDiff = FloatAbs(float(actualSize - calculatedSize)) / float(calculatedSize); + if( sizeDiff > SIZE_TOLERANCE ) + { + int client = GetClientFromSpray(); + if( !client ) client = GetClientFromJingle(); + if( client ) + { + static char auth[64]; + if ( g_sAuth[client][6] == 'I' ) + FormatEx(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]); + else + FormatEx(auth, sizeof(auth), "%s", g_sAuth[client]); + + if( g_hCvarLog.IntValue ) LogCustom("Invalid spray (size mismatch): %s from (%N) [%s] - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, client, auth, actualSize, calculatedSize, sizeDiff * 100.0); + if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray (size mismatch): %s from (%N) [%s] - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, client, auth, actualSize, calculatedSize, sizeDiff * 100.0); + } else { + if( g_hCvarLog.IntValue ) LogCustom("Invalid spray (size mismatch): %s - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, actualSize, calculatedSize, sizeDiff * 100.0); + if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray (size mismatch): %s - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, actualSize, calculatedSize, sizeDiff * 100.0); + } + + g_bIsVTFExploit[client] = true; + + Call_StartForward(g_hExploit); + Call_PushCell(client); + Call_PushCell(-2); // Special code for size mismatch + Call_PushCell(actualSize); + Call_Finish(); + + 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); @@ -1048,7 +1138,8 @@ int ValFile(int iRead[sizeof(g_iVal)]) return -1; } - if( iRead[16] == 80 && iRead[24] > 1 ) + // 66 frames is more than enough? + if ((iRead[24] | (iRead[25] << 8)) > 66) { return 24; } @@ -1063,18 +1154,18 @@ int ValFile(int iRead[sizeof(g_iVal)]) { switch( i ) { - case 8: read = iRead[i] <= 5; + case 8: read = iRead[i] <= 5; case 16, 18: { - Format(bytes, sizeof(bytes), "%02X%02X", iRead[i+1], iRead[i]); + FormatEx(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]); + FormatEx(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; + if( n & (0x8000|0x10000|0x80000|0x800000) ) read = false; // added pre_srgb check } /* case 25: @@ -1119,6 +1210,129 @@ int HexToDec(char[] bytes) return value; } +stock int MaxValC(int a, int b) +{ + return (a > b) ? a : b; +} + +bool GetFormatInfo(int fmt, bool &compressed, int &bpp) +{ + switch(fmt) + { + case FVTF_RGBA8888, FVTF_ABGR8888, FVTF_BGRA8888, FVTF_BGRX8888: + { + bpp = 32; + compressed = false; + return true; + } + + case FVTF_RGB888, FVTF_BGR888: + { + bpp = 24; + compressed = false; + return true; + } + + case FVTF_RGB565, FVTF_IA88: + { + bpp = 16; + compressed = false; + return true; + } + + case FVTF_I8: + { + bpp = 8; + compressed = false; + return true; + } + + case FVTF_DXT1, FVTF_DXT1_ALT: + { + bpp = 8; + compressed = true; + return true; + } + + case FVTF_DXT3, FVTF_DXT5, FVTF_DXT5_ALT: + { + bpp = 16; + compressed = true; + return true; + } + + default: + { + bpp = 32; + compressed = false; + return true; + } + } +} + +int CalcSize(int iRead[sizeof(g_iVal)]) +{ + int versionMajor = iRead[4] | (iRead[5]<<8) | (iRead[6]<<16) | (iRead[7]<<24); + int versionMinor = iRead[8] | (iRead[9]<<8) | (iRead[10]<<16) | (iRead[11]<<24); + + if(versionMajor != 7 || versionMinor > 6) + { + return -1; + } + + int width = iRead[16] | (iRead[17]<<8); + int height = iRead[18] | (iRead[19]<<8); + int frames = MaxValC(1, iRead[24] | (iRead[25]<<8)); + int numMip = iRead[56]; + + int highresFmt = iRead[52] | (iRead[53]<<8) | (iRead[54]<<16) | (iRead[55]<<24); + int lowresFmt = iRead[57]; + + int lowresW = iRead[61]; + int lowresH = iRead[62]; + + bool compressed; int bpp; + if(!GetFormatInfo(highresFmt, compressed, bpp)) + return -1; + + int highresSize = 0; + for(int mip=0; mip>mip); + int mh = MaxValC(1, height>>mip); + if(compressed) + { + int bw = (mw+3)/4; + int bh = (mh+3)/4; + highresSize += bw*bh*bpp; + } + else + { + highresSize += mw*mh*bpp/8; + } + } + highresSize *= frames; + + // thumbnail + if(!GetFormatInfo(lowresFmt, compressed, bpp)) + return highresSize; // sometimes this might happen + int thumbSize = 0; + int mw = lowresW; + int mh = lowresH; + if(compressed) + { + int bw = (mw+3)/4; + int bh = (mh+3)/4; + thumbSize = bw*bh*bpp; + } + else + { + thumbSize = mw*mh*bpp/8; + } + + return highresSize + thumbSize; +} + void LogCustom(const char[] format, any ...) { static char buffer[512]; @@ -1132,3 +1346,11 @@ void LogCustom(const char[] format, any ...) FlushFile(file); delete file; } + +public int Native_LogCustom(Handle plugin, int numParams) +{ + char sBuffer[2048]; + FormatNativeString(0, 1, 2, sizeof(sBuffer), _, sBuffer); + LogCustom(sBuffer); + return 1; +} \ No newline at end of file