407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			SourcePawn
		
	
	
	
	
	
			
		
		
	
	
			407 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			SourcePawn
		
	
	
	
	
	
| #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 <sourcemod>
 | |
| #include <sdktools>
 | |
| #include <dhooks>
 | |
| 
 | |
| #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();
 | |
|     return APLRes_Success;
 | |
| }
 | |
| 
 | |
| 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);
 | |
|     return Plugin_Handled;
 | |
| }
 | |
| 
 | |
| 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]);
 | |
| 				//LogCustom("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"); //if you have no spray you get flagged too by mistake. so better to remove kicking.
 | |
| 							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<Address>(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);
 | |
| }
 |