Skip to content

Commit 0462439

Browse files
authored
feat(core): patch latest sprays exploit (#24)
1 parent 949026c commit 0462439

File tree

1 file changed

+211
-6
lines changed

1 file changed

+211
-6
lines changed

addons/sourcemod/scripting/FixSprayExploit.sp

Lines changed: 211 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919

2020

21-
#define PLUGIN_VERSION "2.26"
21+
#define PLUGIN_VERSION "2.28"
2222

2323
/*=======================================================================================
2424
Plugin Info:
@@ -32,6 +32,18 @@
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+
3547
2.26 (21-May-2025)
3648
- Added native "SprayExploitFixer_LogCustom" to log custom messages. Requested by ".Rushaway".
3749
- Added RegPluginLibrary "spray_exploit_fixer".
@@ -195,7 +207,26 @@
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, ...};
199230
char g_sFilename[PLATFORM_MAX_PATH];
200231
char g_sMoveFiles[PLATFORM_MAX_PATH];
201232
char g_sDownloads[PLATFORM_MAX_PATH];
@@ -355,6 +386,7 @@ public void OnClientPutInServer(int client)
355386

356387
public 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+
11311336
void LogCustom(const char[] format, any ...)
11321337
{
11331338
static char buffer[512];

0 commit comments

Comments
 (0)