1818
1919
2020
21- #define PLUGIN_VERSION " 2.26 "
21+ #define PLUGIN_VERSION " 2.28 "
2222
2323/* =======================================================================================
2424 Plugin Info:
3232========================================================================================
3333 Change Log:
3434
35+ 2.28 (02-Dec-2025)
36+ - Added file size validation to detect malformed sprays by comparing header size with actual file size.
37+ - Extended g_iVal array to cover offset 62 for better VTF header validation.
38+ - Added VTF format constants and helper functions (GetFormatInfo, CalcSize, MaxValC).
39+ - Added 5% tolerance threshold for file size comparison to minimize false positives.
40+ - Enhanced logging to include size mismatch details (actual size, expected size, difference percentage).
41+ - Forward "OnSprayExploit" now uses special code (-2) for size mismatch errors.
42+ - Thanks to null138 for reporting and testing.
43+
44+ 2.27 (24-Jun-2025)
45+ - Added a check for the latest spray exploit. Thanks to ".Rushaway" for fixing and reporting.
46+
35472.26 (21-May-2025)
3648 - Added native "SprayExploitFixer_LogCustom" to log custom messages. Requested by ".Rushaway".
3749 - Added RegPluginLibrary "spray_exploit_fixer".
195207#define TIMEOUT_LOG 10.0
196208#define PATH_BACKUP " backup_sprays"
197209
198- 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 };
210+ // VTF formats
211+ #define FVTF_RGBA8888 0
212+ #define FVTF_ABGR8888 1
213+ #define FVTF_RGB888 2
214+ #define FVTF_BGR888 3
215+ #define FVTF_RGB565 4
216+ #define FVTF_I8 5
217+ #define FVTF_IA88 6
218+ #define FVTF_DXT1 7
219+ #define FVTF_DXT5 9
220+ #define FVTF_BGRA8888 12
221+ #define FVTF_DXT1_ALT 13
222+ #define FVTF_DXT3 14
223+ #define FVTF_DXT5_ALT 15
224+ #define FVTF_BGRX8888 16
225+
226+ #define SIZE_TOLERANCE 0.05 // 5% tolerance
227+
228+ 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 };
229+ bool g_bIsVTFExploit [MAXPLAYERS +1 ] = {false , ...};
199230char g_sFilename [PLATFORM_MAX_PATH ];
200231char g_sMoveFiles [PLATFORM_MAX_PATH ];
201232char g_sDownloads [PLATFORM_MAX_PATH ];
@@ -355,6 +386,7 @@ public void OnClientPutInServer(int client)
355386
356387public void OnClientConnected (int client )
357388{
389+ g_bIsVTFExploit [client ] = false ;
358390 g_fSprayed [client ] = 0.0 ;
359391 g_sPath1 [client ][0 ] = 0 ;
360392 g_sPath2 [client ][0 ] = 0 ;
@@ -373,6 +405,7 @@ public void OnClientDisconnect(int client)
373405 g_sAuth [client ][0 ] = 0 ;
374406 g_sAuth [client ][6 ] = 0 ;
375407 g_sAuthUnverified [client ][0 ] = 0 ;
408+ g_bIsVTFExploit [client ] = false ;
376409
377410 /*
378411 static char sPath[PLATFORM_MAX_PATH];
@@ -797,6 +830,7 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl
797830 g_fSprayed [client ] = GetGameTime ();
798831 if ( g_hCvarLog .IntValue ) LogCustom (" Blocked invalid spray: %s from (%N ) [%s ]" , g_sFilename , client , auth );
799832 if ( g_hCvarMsg .IntValue ) PrintToServer (" [Spray Exploit] Blocked invalid spray: %s from (%N ) [%s ]" , g_sFilename , client , auth );
833+ g_bIsVTFExploit [client ] = true ;
800834 }
801835
802836 if ( g_hCvarPunish .IntValue == 1 || g_hCvarPunish .IntValue >= 3 )
@@ -811,6 +845,7 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl
811845
812846 if ( g_hCvarLog .IntValue ) LogCustom (" Blocked unchecked spray - missing file: %s from (%N ) [%s ]" , g_sFilename , client , auth );
813847 if ( g_hCvarMsg .IntValue == 1 ) PrintToServer (" [Spray Exploit] Blocked unchecked spray - missing file: %s from (%N ) [%s ]" , g_sFilename , client , auth );
848+ g_bIsVTFExploit [client ] = true ;
814849 }
815850 }
816851
@@ -832,7 +867,7 @@ void ReqTempEnt(DataPack hPack)
832867
833868 int client = hPack .ReadCell ();
834869 client = GetClientOfUserId (client );
835- if ( client )
870+ if ( client && ! g_bIsVTFExploit [ client ] )
836871 {
837872 float vPos [3 ];
838873 vPos [0 ] = hPack .ReadFloat ();
@@ -1003,6 +1038,10 @@ void FileCheck()
10031038 if ( hFile )
10041039 {
10051040 hFile .Read (iRead , sizeof (iRead ), 1 );
1041+
1042+ // Get actual file size
1043+ hFile .Seek (0 , SEEK_END );
1044+ int actualSize = hFile .Position ;
10061045 delete hFile ;
10071046
10081047 int i = ValFile (iRead );
@@ -1026,6 +1065,8 @@ void FileCheck()
10261065 if ( g_hCvarMsg .IntValue ) PrintToServer (" [Spray Exploit] Invalid spray: %s : %02d (%02X <> %02X )" , g_sFilename , i , iRead [i ], g_iVal [i ]);
10271066 }
10281067
1068+ g_bIsVTFExploit [client ] = true ;
1069+
10291070 Call_StartForward (g_hExploit );
10301071 Call_PushCell (client );
10311072 Call_PushCell (i );
@@ -1039,6 +1080,46 @@ void FileCheck()
10391080 return ;
10401081 }
10411082
1083+ // Check file size
1084+ int calculatedSize = CalcSize (iRead );
1085+ if ( calculatedSize > 0 )
1086+ {
1087+ float sizeDiff = FloatAbs (float (actualSize - calculatedSize )) / float (calculatedSize );
1088+ if ( sizeDiff > SIZE_TOLERANCE )
1089+ {
1090+ int client = GetClientFromSpray ();
1091+ if ( ! client ) client = GetClientFromJingle ();
1092+ if ( client )
1093+ {
1094+ static char auth [64 ];
1095+ if ( g_sAuth [client ][6 ] == ' I' )
1096+ FormatEx (auth , sizeof (auth ), " Unverified: %s " , g_sAuthUnverified [client ]);
1097+ else
1098+ FormatEx (auth , sizeof (auth ), " %s " , g_sAuth [client ]);
1099+
1100+ 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 );
1101+ 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 );
1102+ } else {
1103+ if ( g_hCvarLog .IntValue ) LogCustom (" Invalid spray (size mismatch): %s - Actual: %d , Expected: %d , Diff: %.1f%% " , g_sFilename , actualSize , calculatedSize , sizeDiff * 100.0 );
1104+ 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 );
1105+ }
1106+
1107+ g_bIsVTFExploit [client ] = true ;
1108+
1109+ Call_StartForward (g_hExploit );
1110+ Call_PushCell (client );
1111+ Call_PushCell (- 2 ); // Special code for size mismatch
1112+ Call_PushCell (actualSize );
1113+ Call_Finish ();
1114+
1115+ if ( g_hCvarPunish .IntValue >= 2 )
1116+ TestClient (client );
1117+
1118+ g_smChecked .SetValue (g_sFilename , false );
1119+ return ;
1120+ }
1121+ }
1122+
10421123 g_smChecked .SetValue (g_sFilename , true );
10431124 } else {
10441125 if ( g_hCvarLog .IntValue ) LogCustom (" Missing file: %s " , g_sFilename );
@@ -1057,7 +1138,8 @@ int ValFile(int iRead[sizeof(g_iVal)])
10571138 return - 1 ;
10581139 }
10591140
1060- if ( iRead [21 ] == 42 && iRead [24 ] > 1 || iRead [16 ] == 80 && iRead [24 ] > 1 )
1141+ // 66 frames is more than enough?
1142+ if ((iRead [24 ] | (iRead [25 ] << 8 )) > 66 )
10611143 {
10621144 return 24 ;
10631145 }
@@ -1072,7 +1154,7 @@ int ValFile(int iRead[sizeof(g_iVal)])
10721154 {
10731155 switch ( i )
10741156 {
1075- case 8 : read = iRead [i ] <= 5 ;
1157+ case 8 : read = iRead [i ] <= 5 ;
10761158 case 16 , 18 :
10771159 {
10781160 FormatEx (bytes , sizeof (bytes ), " %02X%02X " , iRead [i + 1 ], iRead [i ]);
@@ -1083,7 +1165,7 @@ int ValFile(int iRead[sizeof(g_iVal)])
10831165 {
10841166 FormatEx (bytes , sizeof (bytes ), " %02X%02X%02X%02X " , iRead [i + 3 ], iRead [i + 2 ], iRead [i + 1 ], iRead [i ]);
10851167 n = HexToDec (bytes );
1086- if ( n & (0x8000 | 0x10000 | 0x800000 ) ) read = false ;
1168+ if ( n & (0x8000 | 0x10000 | 0x80000 | 0x800000 ) ) read = false ; // added pre_srgb check
10871169 }
10881170 /*
10891171 case 25:
@@ -1128,6 +1210,129 @@ int HexToDec(char[] bytes)
11281210 return value ;
11291211}
11301212
1213+ stock int MaxValC (int a , int b )
1214+ {
1215+ return (a > b ) ? a : b ;
1216+ }
1217+
1218+ bool GetFormatInfo (int fmt , bool &compressed , int &bpp )
1219+ {
1220+ switch (fmt )
1221+ {
1222+ case FVTF_RGBA8888 , FVTF_ABGR8888 , FVTF_BGRA8888 , FVTF_BGRX8888 :
1223+ {
1224+ bpp = 32 ;
1225+ compressed = false ;
1226+ return true ;
1227+ }
1228+
1229+ case FVTF_RGB888 , FVTF_BGR888 :
1230+ {
1231+ bpp = 24 ;
1232+ compressed = false ;
1233+ return true ;
1234+ }
1235+
1236+ case FVTF_RGB565 , FVTF_IA88 :
1237+ {
1238+ bpp = 16 ;
1239+ compressed = false ;
1240+ return true ;
1241+ }
1242+
1243+ case FVTF_I8 :
1244+ {
1245+ bpp = 8 ;
1246+ compressed = false ;
1247+ return true ;
1248+ }
1249+
1250+ case FVTF_DXT1 , FVTF_DXT1_ALT :
1251+ {
1252+ bpp = 8 ;
1253+ compressed = true ;
1254+ return true ;
1255+ }
1256+
1257+ case FVTF_DXT3 , FVTF_DXT5 , FVTF_DXT5_ALT :
1258+ {
1259+ bpp = 16 ;
1260+ compressed = true ;
1261+ return true ;
1262+ }
1263+
1264+ default :
1265+ {
1266+ bpp = 32 ;
1267+ compressed = false ;
1268+ return true ;
1269+ }
1270+ }
1271+ }
1272+
1273+ int CalcSize (int iRead [sizeof (g_iVal )])
1274+ {
1275+ int versionMajor = iRead [4 ] | (iRead [5 ]<< 8 ) | (iRead [6 ]<< 16 ) | (iRead [7 ]<< 24 );
1276+ int versionMinor = iRead [8 ] | (iRead [9 ]<< 8 ) | (iRead [10 ]<< 16 ) | (iRead [11 ]<< 24 );
1277+
1278+ if (versionMajor != 7 || versionMinor > 6 )
1279+ {
1280+ return - 1 ;
1281+ }
1282+
1283+ int width = iRead [16 ] | (iRead [17 ]<< 8 );
1284+ int height = iRead [18 ] | (iRead [19 ]<< 8 );
1285+ int frames = MaxValC (1 , iRead [24 ] | (iRead [25 ]<< 8 ));
1286+ int numMip = iRead [56 ];
1287+
1288+ int highresFmt = iRead [52 ] | (iRead [53 ]<< 8 ) | (iRead [54 ]<< 16 ) | (iRead [55 ]<< 24 );
1289+ int lowresFmt = iRead [57 ];
1290+
1291+ int lowresW = iRead [61 ];
1292+ int lowresH = iRead [62 ];
1293+
1294+ bool compressed ; int bpp ;
1295+ if (! GetFormatInfo (highresFmt , compressed , bpp ))
1296+ return - 1 ;
1297+
1298+ int highresSize = 0 ;
1299+ for (int mip = 0 ; mip < numMip ; mip ++ )
1300+ {
1301+ int mw = MaxValC (1 , width >> mip );
1302+ int mh = MaxValC (1 , height >> mip );
1303+ if (compressed )
1304+ {
1305+ int bw = (mw + 3 )/ 4 ;
1306+ int bh = (mh + 3 )/ 4 ;
1307+ highresSize += bw * bh * bpp ;
1308+ }
1309+ else
1310+ {
1311+ highresSize += mw * mh * bpp / 8 ;
1312+ }
1313+ }
1314+ highresSize *= frames ;
1315+
1316+ // thumbnail
1317+ if (! GetFormatInfo (lowresFmt , compressed , bpp ))
1318+ return highresSize ; // sometimes this might happen
1319+ int thumbSize = 0 ;
1320+ int mw = lowresW ;
1321+ int mh = lowresH ;
1322+ if (compressed )
1323+ {
1324+ int bw = (mw + 3 )/ 4 ;
1325+ int bh = (mh + 3 )/ 4 ;
1326+ thumbSize = bw * bh * bpp ;
1327+ }
1328+ else
1329+ {
1330+ thumbSize = mw * mh * bpp / 8 ;
1331+ }
1332+
1333+ return highresSize + thumbSize ;
1334+ }
1335+
11311336void LogCustom (const char [] format , any ...)
11321337{
11331338 static char buffer [512 ];
0 commit comments