diff --git a/gamedata/sbp.games.txt b/gamedata/sbp.games.txt new file mode 100644 index 00000000..7c2d16c4 --- /dev/null +++ b/gamedata/sbp.games.txt @@ -0,0 +1,107 @@ +"Games" +{ + "#default" + { + "Keys" + { + "EngineInterface" "VEngineServer021" + } + "Signatures" + { + "CreateInterface" + { + "library" "engine" + "windows" "@CreateInterface" + "linux" "@CreateInterface" + } + } + "Offsets" + { + "ClientPrintf" + { + "windows" "45" + "linux" "45" + } + } + } + + "cstrike" + { + "Keys" + { + "EngineInterface" "VEngineServer023" + } + "Offsets" + { + "ClientPrintf" + { + "windows" "45" + "linux" "45" + } + } + } + + "tf" + { + "Keys" + { + "EngineInterface" "VEngineServer023" + } + "Offsets" + { + "ClientPrintf" + { + "windows" "45" + "linux" "45" + } + } + } + + "csgo" + { + "Keys" + { + "EngineInterface" "VEngineServer023" + } + "Offsets" + { + "ClientPrintf" + { + "windows" "47" + "linux" "47" + } + } + } + + "left4dead2" + { + "Keys" + { + "EngineInterface" "VEngineServer022" + } + "Offsets" + { + "ClientPrintf" + { + "windows" "46" + "linux" "46" + } + } + } + + "nmrih" + { + "Keys" + { + "EngineInterface" "VEngineServer023" + } + "Offsets" + { + "ClientPrintf" + { + "windows" "45" + "linux" "45" + } + } + } +} diff --git a/scripting/adminhelpstac.sp b/scripting/adminhelpstac.sp new file mode 100644 index 00000000..6cc75b92 --- /dev/null +++ b/scripting/adminhelpstac.sp @@ -0,0 +1,190 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * SourceMod Admin Help Plugin + * Displays and searches SourceMod commands and descriptions. + * + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + * + * Version: $Id$ + */ + +#pragma semicolon 1 + +#include + +#pragma newdecls required + +#define COMMANDS_PER_PAGE 10 + +public Plugin myinfo = +{ + name = "Admin Help", + author = "AlliedModders LLC", + description = "Display command information", + version = SOURCEMOD_VERSION, + url = "http://www.sourcemod.net/" +}; + +public void OnPluginStart() +{ + LoadTranslations("common.phrases"); + LoadTranslations("adminhelp.phrases"); + RegConsoleCmd("sm_help", HelpCmd, "Displays SourceMod commands and descriptions"); + RegConsoleCmd("sm_searchcmd", HelpCmd, "Searches SourceMod commands"); +} + +public Action HelpCmd(int client, int args) +{ + if (client && !IsClientInGame(client)) + { + return Plugin_Handled; + } + + char arg[64], cmdName[20]; + int pageNum = 1; + bool doSearch; + + GetCmdArg(0, cmdName, sizeof(cmdName)); + + if (args >= 1) + { + GetCmdArg(1, arg, sizeof(arg)); + StringToIntEx(arg, pageNum); + pageNum = (pageNum <= 0) ? 1 : pageNum; + } + + doSearch = (strcmp("sm_help", cmdName) == 0) ? false : true; + + if (GetCmdReplySource() == SM_REPLY_TO_CHAT) + { + ReplyToCommand(client, "[SM] %t", "See console for output"); + } + + char name[64]; + char desc[255]; + char noDesc[128]; + CommandIterator cmdIter = new CommandIterator(); + + FormatEx(noDesc, sizeof(noDesc), "%T", "No description available", client); + + if (doSearch) + { + int i = 1; + while (cmdIter.Next()) + { + cmdIter.GetName(name, sizeof(name)); + cmdIter.GetDescription(desc, sizeof(desc)); + + if (CheckCommandAccess(client, "sm_admin", ADMFLAG_ROOT, true)) + { + if ((StrContains(name, arg, false) != -1) && CheckCommandAccess(client, name, cmdIter.Flags)) + { + PrintToConsole(client, "[%03d] %s - %s", i++, name, (desc[0] == '\0') ? noDesc : desc); + } + } + else + { + if ((StrContains(name, arg, false) != -1) && (StrContains(name, "stac", false) == -1) && CheckCommandAccess(client, name, cmdIter.Flags)) + { + PrintToConsole(client, "[%03d] %s - %s", i++, name, (desc[0] == '\0') ? noDesc : desc); + } + } + } + + if (i == 1) + { + PrintToConsole(client, "%t", "No matching results found"); + } + } else { + PrintToConsole(client, "%t", "SM help commands"); + + /* Skip the first N commands if we need to */ + if (pageNum > 1) + { + int i; + int endCmd = (pageNum-1) * COMMANDS_PER_PAGE - 1; + for (i=0; cmdIter.Next() && i +#include +#include +#include +#include +#undef REQUIRE_PLUGIN +#tryinclude + +#pragma newdecls required + +public Plugin myinfo = +{ + name = "[DHooks] Block SM Plugins (Ricochet's Fork)", + description = "", + author = "Bara, Ricochet", + version = "shfjgsh625063", + url = "https://github.com/Bara" +}; + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + RegPluginLibrary("sbpstac"); + + return APLRes_Success; +} + +Handle g_hClientPrintf = null; + +char g_sLogs[PLATFORM_MAX_PATH + 1]; +char stacVersion[32]; // The size of this may be able to be reduced. +bool DISCORD; +bool adminsNotified[TFMAXPLAYERS+1] = {false, ...}; +char hostname[64]; +char realSMVer[32]; +Regex smVerRegex; + +public void OnPluginStart() +{ + // Fake the admin commands for the plugins we lie about... + RegAdminCmd("sm_sql_addadmin", Command_DoNothing, ADMFLAG_ROOT, "Adds an admin to the SQL database"); + RegAdminCmd("sm_sql_deladmin", Command_DoNothing, ADMFLAG_ROOT, "Removes an admin from the SQL database"); + RegAdminCmd("sm_sql_addgroup", Command_DoNothing, ADMFLAG_ROOT, "Adds a group to the SQL database"); + RegAdminCmd("sm_sql_delgroup", Command_DoNothing, ADMFLAG_ROOT, "Removes a group from the SQL database"); + RegAdminCmd("sm_sql_setadmingroups", Command_DoNothing, ADMFLAG_ROOT, "Sets an admin's groups in the SQL database"); + + Handle gameconf = LoadGameConfigFile("sbp.games"); + if (gameconf == null) + { + SetFailState("Failed to find sbp.games.txt gamedata"); + delete gameconf; + } + + int offset = GameConfGetOffset(gameconf, "ClientPrintf"); + if (offset == -1) + { + SetFailState("Failed to find offset for ClientPrintf"); + delete gameconf; + } + + StartPrepSDKCall(SDKCall_Static); + + if (!PrepSDKCall_SetFromConf(gameconf, SDKConf_Signature, "CreateInterface")) + { + SetFailState("Failed to get CreateInterface"); + delete gameconf; + } + + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + char identifier[64]; + if (!GameConfGetKeyValue(gameconf, "EngineInterface", identifier, sizeof(identifier))) + { + SetFailState("Failed to get engine identifier name"); + delete gameconf; + } + + Handle temp = EndPrepSDKCall(); + Address addr = SDKCall(temp, identifier, 0); + + delete gameconf; + delete temp; + + if (!addr) + { + SetFailState("Failed to get engine ptr"); + } + + g_hClientPrintf = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, Hook_ClientPrintf); + DHookAddParam(g_hClientPrintf, HookParamType_Edict); + DHookAddParam(g_hClientPrintf, HookParamType_CharPtr); + DHookRaw(g_hClientPrintf, false, addr); + + char sDate[18]; + FormatTime(sDate, sizeof(sDate), "%y-%m-%d"); + BuildPath(Path_SM, g_sLogs, sizeof(g_sLogs), "logs/sbp-%s.log", sDate); +} + +public void OnMapStart() +{ + smVerRegex = CompileRegex("([1-9]\\d*|0)(\\.(([1-9]\\d*)|0)){0,3}"); // Might break someday lol + char smVerOut[2048]; + ServerCommandEx(smVerOut, sizeof(smVerOut), "sm version"); + GetConVarString(FindConVar("hostname"), hostname, sizeof(hostname)); + + if (MatchRegex(smVerRegex, smVerOut) > 0) + { + GetRegexSubString(smVerRegex, 0, realSMVer, sizeof(realSMVer)); + } + else + { + realSMVer = SOURCEMOD_VERSION; + } + CreateTimer(0.1, checkDiscord); +} + +public void OnClientDisconnect(int client) +{ + adminsNotified[client] = false; +} + +public MRESReturn Hook_ClientPrintf(Handle hParams) +{ + char sBuffer[1024]; + int client = DHookGetParam(hParams, 1); + + if (client == 0) + { + return MRES_Ignored; + } + + if (IsValidAdmin(client)) + { + return MRES_Ignored; + } + + DHookGetParamString(hParams, 2, sBuffer, sizeof(sBuffer)); + // Ideally, I wouldn't need to fake what plugin is loaded and would just remove ours from the list, but that seemingly isn't possible with this approach. + // DiscordAPI + char fakePreSQL[64] = " \"SQL Admins (Prefetch)\" ("; + StrCat(fakePreSQL, sizeof(fakePreSQL), realSMVer); + StrCat(fakePreSQL, sizeof(fakePreSQL), ") by AlliedModders LLC\n"); + //char fakePreSQL[64]; + //Format(fakePreSQL, sizeof(fakePreSQL), " \"SQL Admins (Prefetch)\" (" ... realSMVer ... ") by AlliedModders LLC\n"); + char discordName[] = " \"Discord API\" (1.0) by .#Zipcore, Credits: Shavit, bara, ImACow and Phire\n"; + if (StrEqual(sBuffer, discordName)) + { + notifyAdmins(client); + DHookSetParamString(hParams, 2, fakePreSQL); + return MRES_ChangedHandled; + } + + // StAC + // Create fake Admin Manager string + char fakeAdmMan[64] = " \"SQL Admin Manager\" ("; + StrCat(fakeAdmMan, sizeof(fakeAdmMan), realSMVer); + StrCat(fakeAdmMan, sizeof(fakeAdmMan), ") by AlliedModders LLC\n"); + // Create real StAC string for strcmp + char stacName[128] = " \"Steph's AntiCheat [StAC]\" ("; + StrCat(stacName, sizeof(stacName), stacVersion); + StrCat(stacName, sizeof(stacName), ") by https://sappho.io\n"); // May not be escaped properly. I haven't tried compiling StAC's version yet. + if (StrEqual(sBuffer, stacName)) + { + notifyAdmins(client); + DHookSetParamString(hParams, 2, fakeAdmMan); + return MRES_ChangedHandled; + } + // SBP itself + // Create fake SQL Admins string + char fakeThrSQL[64] = " \"SQL Admins (Threaded)\" ("; + StrCat(fakeThrSQL, sizeof(fakeThrSQL), realSMVer); + StrCat(fakeThrSQL, sizeof(fakeThrSQL), ") by AlliedModders LLC\n"); + // Create SBP string for strcmp + char sbpName[] = " \"[DHooks] Block SM Plugins (Ricochet's Fork)\" (shfjgsh625063) by Bara, Ricochet\n"; + if (StrEqual(sBuffer, sbpName)) + { + notifyAdmins(client); + DHookSetParamString(hParams, 2, fakeThrSQL); + return MRES_ChangedHandled; + } + // Make sure Admin Help looks as if it's bundled (version number) + if (!StrEqual(SOURCEMOD_VERSION, realSMVer)) // No need to do any of this if the compiler version number and real version number are the same + { + // Create fake Admin Help string + char fakeAdmHlp[64] = " \"Admin Help\" ("; + StrCat(fakeAdmHlp, sizeof(fakeAdmHlp), realSMVer); + StrCat(fakeAdmHlp, sizeof(fakeAdmHlp), ") by AlliedModders LLC\n"); + // Create real Admin Help string for strcmp + char admHlpName[64] = " \"Admin Help\" ("; + StrCat(admHlpName, sizeof(admHlpName), SOURCEMOD_VERSION); + StrCat(admHlpName, sizeof(admHlpName), ") by AlliedModders LLC\n"); + if (StrEqual(sBuffer, admHlpName)) + { + notifyAdmins(client); + DHookSetParamString(hParams, 2, fakeAdmHlp); + return MRES_ChangedHandled; + } + } + return MRES_Ignored; +} + +public Action Command_DoNothing(int client, int args) +{ + return Plugin_Handled; +} + +public void OnAllPluginsLoaded() +{ + GetConVarString(FindConVar("stac_version"), stacVersion, sizeof(stacVersion)); + HookConVarChange(FindConVar("stac_version"), getStACVersion); +} + +public void getStACVersion(ConVar convar, const char[] oldValue, const char[] newValue) +{ + if (StrEqual(stacVersion, newValue)) + { + return; + } + GetConVarString(FindConVar("stac_version"), stacVersion, sizeof(stacVersion)); +} + +// print colored chat to all server/sourcemod admins +void PrintToImportant(const char[] format, any ...) +{ + char buffer[254]; + + // print translations in the servers lang first + SetGlobalTransTarget(LANG_SERVER); + // format it properly + VFormat(buffer, sizeof(buffer), format, 2); + buffer[0] = '\0'; + + for (int i = 1; i <= MaxClients; i++) + { + if (IsValidAdmin(i)) + { + SetGlobalTransTarget(i); + VFormat(buffer, sizeof(buffer), format, 2); + MC_PrintToChat(i, "%s", buffer); + } + } +} + +public Action checkDiscord(Handle timer) +{ + // discord functionality + if (GetFeatureStatus(FeatureType_Native, "Discord_SendMessage") == FeatureStatus_Available) + { + DISCORD = true; + } + return Plugin_Handled; +} + +void notifyAdmins(int client) +{ + if (adminsNotified[client]) + { + return; + } + adminsNotified[client] = true; + PrintToImportant("{hotpink}[StAC]{white} %N accessed sm plugins", client); + SendMessageToDiscord(client, "Client accessed sm plugins"); +} + +void SendMessageToDiscord(int client, const char[] format, any ...) +{ + + if (!DISCORD) + { + return; + } + + static char generalTemplate[2048] = \ + "{ \"embeds\": \ + [{ \"title\": \"StAC Notification!\", \"color\": 14177041, \"fields\":\ + [\ + { \"name\": \"Player\", \"value\": \"%N\" } ,\ + { \"name\": \"Message\", \"value\": \"%s\" } ,\ + { \"name\": \"Hostname\", \"value\": \"%s\" } ,\ + { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \ + ]\ + }],\ + \"avatar_url\": \"https://i.imgur.com/RKRaLPl.png\"\ + }"; + + char msg[1024]; + + char message[256]; + VFormat(message, sizeof(message), format, 3); + + char ClName[64]; + GetClientName(client, ClName, sizeof(ClName)); + Discord_EscapeString(ClName, sizeof(ClName)); + + Format + ( + msg, + sizeof(msg), + generalTemplate, + client, + message, + hostname, + GetTime() + ); + + char webhook[8] = "stac"; + Discord_SendMessage(webhook, msg); +} + +bool IsValidClient(int client) +{ + if + ( + (0 < client <= MaxClients) + && IsClientInGame(client) + && !IsClientInKickQueue(client) + && !IsFakeClient(client) + ) + { + return true; + } + return false; +} + +bool IsValidAdmin(int Cl) +{ + if (IsValidClient(Cl)) + { + if + ( + CheckCommandAccess(Cl, "sm_ban", ADMFLAG_GENERIC) + ) + { + return true; + } + } + return false; +} diff --git a/scripting/stac.sp b/scripting/stac.sp index 0806b96c..f96ec259 100755 --- a/scripting/stac.sp +++ b/scripting/stac.sp @@ -15,6 +15,7 @@ #include #include #include +#include #undef REQUIRE_PLUGIN #tryinclude #tryinclude @@ -27,7 +28,7 @@ #pragma semicolon 1 #pragma newdecls required -#define PLUGIN_VERSION "5.4.3" +#define PLUGIN_VERSION "5.4.4" #define UPDATE_URL "https://raw.githubusercontent.com/sapphonie/StAC-tf2/master/updatefile.txt" @@ -70,6 +71,7 @@ public Plugin myinfo = public void OnPluginStart() { StacLog("\n\n----> StAC version [%s] loaded\n", PLUGIN_VERSION); + CreateConVar("stac_version", PLUGIN_VERSION, "StAC version", (FCVAR_DONTRECORD)); // check if tf2, unload if not if (GetEngineVersion() != Engine_TF2) { @@ -93,12 +95,12 @@ public void OnPluginStart() } // reg admin commands - // TODO: make these invisible for non admins RegConsoleCmd("sm_stac_checkall", checkAdmin, "Force check all client convars (ALL CLIENTS) for anticheat stuff"); RegConsoleCmd("sm_stac_detections", checkAdmin, "Show all current detections on all connected clients"); + RegConsoleCmd("sm_stac_version", checkAdmin, "Prints the current active version of StAC."); RegConsoleCmd("sm_stac_getauth", checkAdmin, "Print StAC's cached auth for a client"); RegConsoleCmd("sm_stac_livefeed", checkAdmin, "Show live feed (debug info etc) for a client. This gets printed to SourceTV if available."); - + RegConsoleCmd("sm_stac_printos", checkAdmin, "Shows operating system of each connected client"); // setup regex - "Recording to ".*"" demonameRegex = CompileRegex("Recording to \".*\""); @@ -161,6 +163,8 @@ public void OnPluginStart() // jaypatch OnPluginStart_jaypatch(); + AddCommandListener(joinTeam, "jointeam"); + AddCommandListener(joinClass, "joinclass"); } public void OnPluginEnd() diff --git a/scripting/stac/stac_client.sp b/scripting/stac/stac_client.sp index f1c3520b..6e900c66 100644 --- a/scripting/stac/stac_client.sp +++ b/scripting/stac/stac_client.sp @@ -6,6 +6,8 @@ public void OnClientPutInServer(int Cl) { int userid = GetClientUserId(Cl); + + timeSinceJoined[Cl] = GetEngineTime(); // Store time since joined for auto-class/team join checks if (IsValidClientOrBot(Cl)) { @@ -289,6 +291,8 @@ void ClearClBasedVars(int userid) fakeAngDetects [Cl] = 0; aimsnapDetects [Cl] = -1; // ignore first detect, it's prolly bunk pSilentDetects [Cl] = -1; // ignore first detect, it's prolly bunk + invalidWishVelDetects [Cl] = -1; // ignore first detect, it's prolly bunk + unsyncMoveDetects [Cl] = 0; bhopDetects [Cl] = -1; // set to -1 to ignore single jumps cmdnumSpikeDetects [Cl] = 0; tbotDetects [Cl] = -1; // ignore first detect, it's prolly bunk @@ -309,8 +313,16 @@ void ClearClBasedVars(int userid) playerTaunting [Cl] = false; playerInBadCond [Cl] = 0; userBanQueued [Cl] = false; + clientOS [Cl] = 2; + teamChecked [Cl] = false; + classChecked [Cl] = false; + printedOnce [Cl] = false; // STORED SENS PER CLIENT sensFor [Cl] = 0.0; + // STORED JOYSTICK SHIT + joystick [Cl] = false; + joy_xcon [Cl] = false; + waitTillNextQuery [Cl] = true; // don't bother clearing arrays LiveFeedOn [Cl] = false; @@ -329,6 +341,8 @@ void ClearClBasedVars(int userid) // has client has waited 60 seconds for their first cvar check hasWaitedForCvarCheck [Cl] = false; + joystickQueried [Cl] = false; + joy_xconQueried [Cl] = false; } /********** TIMER FOR NETINFO **********/ diff --git a/scripting/stac/stac_commands.sp b/scripting/stac/stac_commands.sp index 49beb819..18e16382 100755 --- a/scripting/stac/stac_commands.sp +++ b/scripting/stac/stac_commands.sp @@ -11,20 +11,21 @@ Action checkAdmin(int callingCl, int args) if (callingCl != 0) { - bool isAdmin; - AdminId clAdmin = GetUserAdmin(callingCl); - if (GetAdminFlag(clAdmin, Admin_Ban)) + if (IsValidAdmin(callingCl)) { - isAdmin = true; + if (GetClientCount(true) >= 1 && !DEBUG) + { + ReplyToCommand(callingCl, "[StAC] Only one player is on. Most checks are logging only and cvar checking doesn't occur."); + } } - if (!isAdmin) + else { PrintToImportant("{hotpink}[StAC]{white} Client %N attempted to use %s, blocked access." , callingCl, arg0); StacLogSteam(GetClientUserId(callingCl)); StacGeneralPlayerNotify(GetClientUserId(callingCl), "Client %N attempted to use %s, blocked access!", callingCl, arg0); - return Plugin_Handled; + return Plugin_Continue; // Return this instead. This causes non-admins to get an "Unknown Command" message, further disguising the anticheat. } - StacGeneralPlayerNotify(GetClientUserId(callingCl), "Admin %N used %s", callingCl, arg0); + //OracxGeneralPlayerNotify(GetClientUserId(callingCl), "Admin %N used %s", callingCl, arg0); // Why should we notify for this? } if (StrEqual(arg0, "sm_stac_checkall")) @@ -38,6 +39,12 @@ Action checkAdmin(int callingCl, int args) ShowAllDetections(callingCl); return Plugin_Handled; } + + if (StrEqual(arg0, "sm_stac_version")) + { + ShowVersion(callingCl); + return Plugin_Handled; + } if (StrEqual(arg0, "sm_stac_getauth")) { @@ -59,6 +66,11 @@ Action checkAdmin(int callingCl, int args) StacTargetCommand(callingCl, arg0, arg1); return Plugin_Handled; } + if (StrEqual(arg0, "sm_stac_printos")) + { + ShowAllOS(callingCl); + return Plugin_Handled; + } return Plugin_Handled; } @@ -92,6 +104,8 @@ void ShowAllDetections(int callingCl) || cmdnumSpikeDetects [Cl] > 0 || tbotDetects [Cl] > 0 || userinfoSpamDetects [Cl] > 0 + || invalidWishVelDetects [Cl] > 0 + || unsyncMoveDetects [Cl] > 0 ) { PrintToConsole @@ -105,6 +119,8 @@ void ShowAllDetections(int callingCl) \n pSilent %i\ \n Cmdnum spikes %i\ \n Triggerbots %i\ + \n Invalid wish velocity %i\ + \n Unsynchronized movement %i\ \n", Cl, turnTimes [Cl], @@ -112,7 +128,9 @@ void ShowAllDetections(int callingCl) aimsnapDetects [Cl], pSilentDetects [Cl], cmdnumSpikeDetects [Cl], - tbotDetects [Cl] + tbotDetects [Cl], + invalidWishVelDetects [Cl], + unsyncMoveDetects [Cl] ); } } @@ -122,6 +140,47 @@ void ShowAllDetections(int callingCl) return; } +// sm_stac_version +void ShowVersion(int callingCl) +{ + ReplyToCommand(callingCl, "StAC version [%s]", PLUGIN_VERSION); + return; +} + +// sm_stac_printos +void ShowAllOS(int callingCl) +{ + if (callingCl != 0) + { + ReplyToCommand(callingCl, "[StAC] Check your console!"); + } + PrintToConsole(callingCl, "[StAC] === Printing client operating systems ==="); + for (int Cl = 1; Cl <= MaxClients; Cl++) + { + if (IsValidClient(Cl)) + { + switch(clientOS[Cl]) + { + case 0: + { + PrintToConsole(callingCl, "\n%L: Windows/Wine", Cl); + } + case 1: + { + PrintToConsole(callingCl, "\n%L: Linux/MacOS", Cl); + } + default: + { + PrintToConsole(callingCl, "\n%L: Unknown, probably hasn't been queried yet.", Cl); + } + } + } + } + PrintToConsole(callingCl, "[StAC] === Done ==="); + + return; +} + // sm_stac_getauth // sm_stac_livefeed void StacTargetCommand(int callingCl, const char[] arg0, const char[] arg1) diff --git a/scripting/stac/stac_cvar_checks.sp b/scripting/stac/stac_cvar_checks.sp index 705c3a93..3c594d19 100644 --- a/scripting/stac/stac_cvar_checks.sp +++ b/scripting/stac/stac_cvar_checks.sp @@ -30,6 +30,13 @@ char miscVars[][] = "r_portalsopenall", // must be == 1.0 "host_timescale", + // Exists on Linux only + "dxa_nullrefresh_capslock", + // Refresh this every once in a while + // (Supposedly) using the controller + "joystick", + // Controller plugged in + "joy_xcontroller_found" // sv_force_transmit_ents ? // sv_suppress_viewpunch ? // tf_showspeed ? @@ -37,8 +44,9 @@ char miscVars[][] = }; // DEFINITE cheat vars get appended to this array. -// Every cheat except cathook is smart enough to not have queryable cvars. +// Every cheat except fedoraware (dead) is smart enough to not have queryable cvars. // For now. +// Cathook fixed their cvars being queryable but I'll keep the query in for now. char cheatVars[][] = { // lith @@ -51,6 +59,7 @@ char cheatVars[][] = // ncc doesn't have any that i can find lol // cathook "cat_load", + "crash" // ...melancholy? maybe? lol // "caramelldansen", // "SetCursor", @@ -267,6 +276,35 @@ public void ConVarCheck(QueryCookie cookie, int Cl, ConVarQueryResult result, co } } + // dxa_nullrefresh_capslock + else if (StrEqual(cvarName, "dxa_nullrefresh_capslock")) + { + if (result == ConVarQuery_NotFound) + { + clientOS[Cl] = 0; + } + else + { + clientOS[Cl] = 1; + } + } + + // joystick + else if (StrEqual(cvarName, "joystick")) + { + bool joy = (0.0 <= StringToFloat(cvarValue) < 1.0)?false:true; + joystick[Cl] = joy; + joystickQueried[Cl] = true; + } + // joy_xcontroller_found + else if (StrEqual(cvarName, "joy_xcontroller_found")) + { + bool joy = (0.0 <= StringToFloat(cvarValue) < 1.0)?false:true; + joy_xcon[Cl] = joy; + joy_xconQueried[Cl] = true; + return; + } + /* cheat program only cvars */ @@ -279,7 +317,7 @@ public void ConVarCheck(QueryCookie cookie, int Cl, ConVarQueryResult result, co } } // log something about cvar errors - else if (result != ConVarQuery_Okay && !IsCheatOnlyVar(cvarName)) + else if (result != ConVarQuery_Okay && !IsCheatOnlyVar(cvarName) && !StrEqual(cvarName, "dxa_nullrefresh_capslock")) { PrintToImportant("{hotpink}[StAC]{white} Could not query cvar %s on Player %N", cvarName, Cl); StacLog("Could not query cvar %s on player %L", cvarName, Cl); @@ -441,6 +479,12 @@ Action Timer_CheckClientConVars(Handle timer, int userid) // query all cvars and netprops for userid void QueryCvarsEtc(int userid, int i) { + // No point in running this if only one player is on. + if (GetClientCount(true) == 1 && !DEBUG) + { + return; + } + // get client index of userid int Cl = GetClientOfUserId(userid); // don't go no further if client isn't valid! diff --git a/scripting/stac/stac_cvars.sp b/scripting/stac/stac_cvars.sp index 9dc40a90..b22fa527 100755 --- a/scripting/stac/stac_cvars.sp +++ b/scripting/stac/stac_cvars.sp @@ -64,7 +64,41 @@ void initCvars() false, _ ); - HookConVarChange(stac_verbose_info, setStacVars); + HookConVarChange(stac_ban_duration, setStacVars); + + // min time before ban queue fires + FloatToString(minTimeBeforeBan, buffer, sizeof(buffer)); + stac_min_time_before_ban = + AutoExecConfig_CreateConVar + ( + "stac_min_time_before_ban", + buffer, + "[StAC] delay ban AT LEAST this long in seconds after a detection\n\ + (recommended 15)", + FCVAR_NONE, + true, + 1.0, + false, + _ + ); + HookConVarChange(stac_min_time_before_ban, setStacVars); + + // max time before ban queue fires + FloatToString(maxTimeBeforeBan, buffer, sizeof(buffer)); + stac_max_time_before_ban = + AutoExecConfig_CreateConVar + ( + "stac_max_time_before_ban", + buffer, + "[StAC] delay ban AT MOST this long in seconds after a detection\n\ + (recommended 60)", + FCVAR_NONE, + true, + 2.0, + false, + _ + ); + HookConVarChange(stac_max_time_before_ban, setStacVars); // turn seconds FloatToString(maxAllowedTurnSecs, buffer, sizeof(buffer)); @@ -168,6 +202,42 @@ void initCvars() ); HookConVarChange(stac_max_psilent_detections, setStacVars); + // invalid wish velocity detections + IntToString(maxInvalidWishVelDetections, buffer, sizeof(buffer)); + stac_max_invalid_wish_vel_detections = + AutoExecConfig_CreateConVar + ( + "stac_max_invalid_wish_vel_detections", + buffer, + "[StAC] maximum invalid wish velocity detections on a client before they get banned.\n\ + -1 to disable checking for invalid wish velocity (saves cpu), 0 to print to admins/stv but never ban\n\ + (recommended 10 or higher)", + FCVAR_NONE, + true, + -1.0, + false, + _ + ); + HookConVarChange(stac_max_invalid_wish_vel_detections, setStacVars); + + // unsynchronized move detections + IntToString(maxUnsyncMoveDetections, buffer, sizeof(buffer)); + stac_max_unsync_move_detections = + AutoExecConfig_CreateConVar + ( + "stac_max_unsync_move_detections", + buffer, + "[StAC] maximum unsynchronized movement detections on a client before they get banned.\n\ + -1 to disable checking for unsynchronized movement (saves cpu), 0 to print to admins/stv but never ban\n\ + (recommended 10)", + FCVAR_NONE, + true, + -1.0, + false, + _ + ); + HookConVarChange(stac_max_unsync_move_detections, setStacVars); + // bhop detections IntToString(maxBhopDetections, buffer, sizeof(buffer)); stac_max_bhop_detections = @@ -497,87 +567,95 @@ void setStacVars(ConVar convar, const char[] oldValue, const char[] newValue) SetFailState("[StAC] stac_enabled is set to 0 - aborting!"); } - // ban duration var - banDuration = GetConVarInt(stac_ban_duration); + // ban vars + banDuration = GetConVarInt(stac_ban_duration); + minTimeBeforeBan = GetConVarFloat(stac_min_time_before_ban); + maxTimeBeforeBan = GetConVarFloat(stac_max_time_before_ban); // verbose info var - DEBUG = GetConVarBool(stac_verbose_info); + DEBUG = GetConVarBool(stac_verbose_info); // turn seconds var - maxAllowedTurnSecs = GetConVarFloat(stac_max_allowed_turn_secs); + maxAllowedTurnSecs = GetConVarFloat(stac_max_allowed_turn_secs); if (maxAllowedTurnSecs < 0.0 && maxAllowedTurnSecs != -1.0) { maxAllowedTurnSecs = 0.0; } // misccheats - banForMiscCheats = GetConVarBool(stac_ban_for_misccheats); + banForMiscCheats = GetConVarBool(stac_ban_for_misccheats); // optimizecvars - optimizeCvars = GetConVarBool(stac_optimize_cvars); + optimizeCvars = GetConVarBool(stac_optimize_cvars); if (optimizeCvars) { RunOptimizeCvars(); } // aimsnap var - maxAimsnapDetections = GetConVarInt(stac_max_aimsnap_detections); + maxAimsnapDetections = GetConVarInt(stac_max_aimsnap_detections); // psilent var - maxPsilentDetections = GetConVarInt(stac_max_psilent_detections); + maxPsilentDetections = GetConVarInt(stac_max_psilent_detections); + + // invalid wish vel var + maxInvalidWishVelDetections = GetConVarInt(stac_max_invalid_wish_vel_detections); + + // unsync move var + maxUnsyncMoveDetections = GetConVarInt(stac_max_unsync_move_detections); // bhop var - maxBhopDetections = GetConVarInt(stac_max_bhop_detections); + maxBhopDetections = GetConVarInt(stac_max_bhop_detections); // fakeang var - maxFakeAngDetections = GetConVarInt(stac_max_fakeang_detections); + maxFakeAngDetections = GetConVarInt(stac_max_fakeang_detections); // cmdnum spikes var - maxCmdnumDetections = GetConVarInt(stac_max_cmdnum_detections); + maxCmdnumDetections = GetConVarInt(stac_max_cmdnum_detections); // tbot var - maxTbotDetections = GetConVarInt(stac_max_tbot_detections); + maxTbotDetections = GetConVarInt(stac_max_tbot_detections); // max ping reduce detections - clamp to -1 if 0 - maxuserinfoSpamDetections = GetConVarInt(stac_max_cmdrate_spam_detections); + maxuserinfoSpamDetections = GetConVarInt(stac_max_cmdrate_spam_detections); // minterp var - clamp to -1 if 0 - min_interp_ms = GetConVarInt(stac_min_interp_ms); + min_interp_ms = GetConVarInt(stac_min_interp_ms); if (min_interp_ms == 0) { min_interp_ms = -1; } // maxterp var - clamp to -1 if 0 - max_interp_ms = GetConVarInt(stac_max_interp_ms); + max_interp_ms = GetConVarInt(stac_max_interp_ms); if (max_interp_ms == 0) { max_interp_ms = -1; } // min check sec var - minRandCheckVal = GetConVarFloat(stac_min_randomcheck_secs); + minRandCheckVal = GetConVarFloat(stac_min_randomcheck_secs); // max check sec var - maxRandCheckVal = GetConVarFloat(stac_max_randomcheck_secs); + maxRandCheckVal = GetConVarFloat(stac_max_randomcheck_secs); // log to file - logtofile = GetConVarBool(stac_log_to_file); + logtofile = GetConVarBool(stac_log_to_file); // properly fix pingmasking - fixpingmasking = GetConVarBool(stac_fixpingmasking_enabled); + fixpingmasking = GetConVarBool(stac_fixpingmasking_enabled); // kick unauthed clients - kickUnauth = GetConVarBool(stac_kick_unauthed_clients); + kickUnauth = GetConVarBool(stac_kick_unauthed_clients); // silent mode - silent = GetConVarInt(stac_silent); + silent = 0; // This should always be 0. Setting this to anything else is BAD. Comment and I can change it if you disagree, otherwise, do it properly. // max conns from same ip - maxip = GetConVarInt(stac_max_connections_from_ip); + maxip = GetConVarInt(stac_max_connections_from_ip); // should stac work with sv_cheats or not - ignore_sv_cheats = GetConVarBool(stac_work_with_sv_cheats); + ignore_sv_cheats = GetConVarBool(stac_work_with_sv_cheats); } public void GenericCvarChanged(ConVar convar, const char[] oldValue, const char[] newValue) diff --git a/scripting/stac/stac_globals.sp b/scripting/stac/stac_globals.sp index 12695700..55ed5e2f 100644 --- a/scripting/stac/stac_globals.sp +++ b/scripting/stac/stac_globals.sp @@ -9,12 +9,16 @@ /***** Cvar Handles *****/ ConVar stac_enabled; ConVar stac_ban_duration; +ConVar stac_min_time_before_ban; +ConVar stac_max_time_before_ban; ConVar stac_verbose_info; ConVar stac_max_allowed_turn_secs; ConVar stac_ban_for_misccheats; ConVar stac_optimize_cvars; ConVar stac_max_aimsnap_detections; ConVar stac_max_psilent_detections; +ConVar stac_max_invalid_wish_vel_detections; +ConVar stac_max_unsync_move_detections; ConVar stac_max_bhop_detections; ConVar stac_max_fakeang_detections; ConVar stac_max_cmdnum_detections; @@ -33,8 +37,10 @@ ConVar stac_max_connections_from_ip; ConVar stac_work_with_sv_cheats; /***** Misc cheat defaults *****/ -// ban duration +// ban duration & banqueue times int banDuration = 0; +float minTimeBeforeBan = 15.0; +float maxTimeBeforeBan = 60.0; // verbose mode bool DEBUG = false; // interp @@ -61,6 +67,8 @@ bool ignore_sv_cheats = false; int maxAimsnapDetections = 20; int maxPsilentDetections = 10; int maxFakeAngDetections = 5; +int maxInvalidWishVelDetections = 10; // Not sure what these values should be by default since they have never been widely rolled out. +int maxUnsyncMoveDetections = 10; // ^ int maxBhopDetections = 10; int maxCmdnumDetections = 20; int maxTbotDetections = 0; @@ -118,6 +126,8 @@ int turnTimes [TFMAXPLAYERS+1]; int fakeAngDetects [TFMAXPLAYERS+1]; int aimsnapDetects [TFMAXPLAYERS+1] = {-1, ...}; // set to -1 to ignore first detections, as theyre most likely junk int pSilentDetects [TFMAXPLAYERS+1] = {-1, ...}; // ^ +int invalidWishVelDetects [TFMAXPLAYERS+1] = {-1, ...}; // ^ +int unsyncMoveDetects [TFMAXPLAYERS+1]; int bhopDetects [TFMAXPLAYERS+1] = {-1, ...}; // set to -1 to ignore single jumps int cmdnumSpikeDetects [TFMAXPLAYERS+1]; int tbotDetects [TFMAXPLAYERS+1] = {-1, ...}; @@ -147,20 +157,33 @@ int clmouse [TFMAXPLAYERS+1] [2]; float engineTime [TFMAXPLAYERS+1][3]; float fuzzyClangles [TFMAXPLAYERS+1][5][2]; float clpos [TFMAXPLAYERS+1][2][3]; +bool joystickQueried [TFMAXPLAYERS+1] = {false, ...}; // Not sure whether or not it's necessary to set any of these. +bool joy_xconQueried [TFMAXPLAYERS+1] = {false, ...}; +bool joystick [TFMAXPLAYERS+1] = {false, ...}; +bool joy_xcon [TFMAXPLAYERS+1] = {false, ...}; +bool printedOnce [TFMAXPLAYERS+1]; +bool waitTillNextQuery [TFMAXPLAYERS+1] = {true, ...}; // Misc stuff per client [ client index ][char size] char SteamAuthFor [TFMAXPLAYERS+1][64]; -bool highGrav [TFMAXPLAYERS+1]; -bool playerTaunting [TFMAXPLAYERS+1]; -int playerInBadCond [TFMAXPLAYERS+1]; -bool userBanQueued [TFMAXPLAYERS+1]; -float sensFor [TFMAXPLAYERS+1]; +bool highGrav [TFMAXPLAYERS+1]; +bool playerTaunting [TFMAXPLAYERS+1]; +int playerInBadCond [TFMAXPLAYERS+1]; +int clientOS [TFMAXPLAYERS+1] = {2, ...}; +bool teamChecked [TFMAXPLAYERS+1]; +bool classChecked [TFMAXPLAYERS+1]; +bool userBanQueued [TFMAXPLAYERS+1]; +float timeSinceJoined [TFMAXPLAYERS+1]; +float timeSinceJointeam [TFMAXPLAYERS+1]; +float timeSinceJoinclass [TFMAXPLAYERS+1]; +bool userBanQueued [TFMAXPLAYERS+1]; +float sensFor [TFMAXPLAYERS+1]; // weapon name, gets passed to aimsnap check -char hurtWeapon [TFMAXPLAYERS+1][256]; -char lastCommandFor [TFMAXPLAYERS+1][256]; -bool LiveFeedOn [TFMAXPLAYERS+1]; -bool hasBadName [TFMAXPLAYERS+1]; +char hurtWeapon [TFMAXPLAYERS+1][256]; +char lastCommandFor [TFMAXPLAYERS+1][256]; +bool LiveFeedOn [TFMAXPLAYERS+1]; +bool hasBadName [TFMAXPLAYERS+1]; // network info float lossFor [TFMAXPLAYERS+1]; @@ -189,6 +212,7 @@ Handle HudSyncNetwork; // Timer handles Handle QueryTimer [TFMAXPLAYERS+1]; +Handle BanTimer [TFMAXPLAYERS+1]; Handle TriggerTimedStuffTimer; /* diff --git a/scripting/stac/stac_mapchange.sp b/scripting/stac/stac_mapchange.sp index 26ce5ced..bfd6339a 100644 --- a/scripting/stac/stac_mapchange.sp +++ b/scripting/stac/stac_mapchange.sp @@ -10,6 +10,7 @@ public void OnConfigsExecuted() public void OnMapStart() { + checkForBadPlugins(); // Not sure if this is the best place for this. OpenStacLog(); ActuallySetRandomSeed(); DoTPSMath(); @@ -25,6 +26,11 @@ public void OnMapStart() GetConVarString(FindConVar("hostname"), hostname, sizeof(hostname)); } +public void OnAllPluginsLoaded() +{ + checkForBadPlugins(); // Also not sure on this. +} + public Action eRoundStart(Handle event, char[] name, bool dontBroadcast) { DoTPSMath(); @@ -229,3 +235,23 @@ void DoTPSMath() StacLog("tickinterv %f, tps %f", tickinterv, tps); } } + +void checkForBadPlugins() +{ + // Make sure we're not using the original adminhelp. + if (FindPluginByFile("adminhelp.smx") != INVALID_HANDLE) + { + char message[] = "StAC failed to load because a possibly unmodified \"adminhelp\" was found."; + PrintToImportant(message); + SendMessageToDiscord(message); + SetFailState("Unmodified(?) adminhelp copy is still present (Unexpected adminhelp.smx). Delete it from the folder, use \"sm plugins unload adminhelp\", and reload StAC."); + } + // Make sure we're not using the original SBP. + if (FindPluginByFile("sbp.smx") != INVALID_HANDLE) + { + char message[] = "StAC failed to load because a possibly unmodified \"sbp\" was found."; + PrintToImportant(message); + SendMessageToDiscord(message); + SetFailState("Unmodified(?) sbp copy is still present (Unexpected sbp.smx). Delete it from the folder, use \"sm plugins unload sbp\", and reload StAC."); + } +} diff --git a/scripting/stac/stac_misc_checks.sp b/scripting/stac/stac_misc_checks.sp index 664b5633..d6eaac87 100644 --- a/scripting/stac/stac_misc_checks.sp +++ b/scripting/stac/stac_misc_checks.sp @@ -37,6 +37,44 @@ public Action OnClientSayCommand(int Cl, const char[] command, const char[] sArg return Plugin_Continue; } +Action joinTeam(int Cl, const char[] command, int argc) +{ + if (!teamChecked[Cl]) + { + char ClName[64]; + int userid = GetClientUserId(Cl); + GetClientName(Cl, ClName, sizeof(ClName)); + timeSinceJointeam[Cl] = GetEngineTime(); + if (timeSinceJointeam[Cl] - timeSinceJoined[Cl] < 2.5) + { + PrintToImportant("Suspicious: %.2f seconds between %s fully joined and chose team", timeSinceJointeam[Cl] - timeSinceJoined[Cl], ClName); + StacGeneralPlayerNotify(userid, "Suspicious: %.2f seconds between %s fully joined and chose team", timeSinceJointeam[Cl] - timeSinceJoined[Cl], ClName); + } + teamChecked[Cl] = true; + return Plugin_Continue; + } + return Plugin_Continue; +} + +Action joinClass(int Cl, const char[] command, int argc) +{ + if (!classChecked[Cl]) + { + char ClName[64]; + int userid = GetClientUserId(Cl); + GetClientName(Cl, ClName, sizeof(ClName)); + timeSinceJoinclass[Cl] = GetEngineTime(); + if (timeSinceJoinclass[Cl] - timeSinceJoined[Cl] < 2.5) // This value may need to be tweaked. + { + PrintToImportant("Suspicious: %.2f seconds between %s fully joined and chose class", timeSinceJoinclass[Cl] - timeSinceJoined[Cl], ClName); + StacGeneralPlayerNotify(userid, "Suspicious: %.2f seconds between %s fully joined and chose class", timeSinceJoinclass[Cl] - timeSinceJoined[Cl], ClName); + } + classChecked[Cl] = true; + return Plugin_Continue; + } + return Plugin_Continue; +} + void NameCheck(int userid) { int Cl = GetClientOfUserId(userid); diff --git a/scripting/stac/stac_onplayerruncmd.sp b/scripting/stac/stac_onplayerruncmd.sp index 3e00058a..7b5457cb 100755 --- a/scripting/stac/stac_onplayerruncmd.sp +++ b/scripting/stac/stac_onplayerruncmd.sp @@ -9,6 +9,9 @@ - AIM SNAPS - FAKE ANGLES - TURN BINDS + - BHOP CHEATS + - INVALID WISH VELOCITY (can and will false positive on +strafe (left alt key by default)) + - UNSYNCHRONIZED MOVEMENT (will false positive if cl_yawspeed = 0 and client uses turnbinds (+right, +left) and +strafe.) */ public void OnPlayerRunCmdPre @@ -47,10 +50,13 @@ public Action OnPlayerRunCmd OnPlayerRunCmd_jaypatch(Cl, buttons, impulse, vel, angles, weapon, subtype, cmdnum, tickcount, seed, mouse); // sanity check, don't let banned clients do anything! + // This lets them know something is up immediately, removing the point of the banqueue. + /* if (userBanQueued[Cl]) { return Plugin_Handled; } + */ return Plugin_Continue; } @@ -133,7 +139,7 @@ stock void PlayerRunCmd } clcmdnum[Cl][0] = cmdnum; - // grab tickccount + // grab tickcount for (int i = 5; i > 0; --i) { cltickcount[Cl][i] = cltickcount[Cl][i-1]; @@ -226,10 +232,10 @@ stock void PlayerRunCmd if ( // make sure client doesn't have invalid angles. "invalid" in this case means "any angle is 0.000000", usually caused by plugin / trigger based teleportation - !HasValidAngles(Cl) + !HasValidAngles(Cl) // This is actively abused to bypass, according to untrustworthy sources! But, I do see it as very possible. // make sure client doesn't have OUTRAGEOUS ping // most cheater fakeping goes up to 800 so tack on 50 just in case - || pingFor[Cl] > 850.0 + || pingFor[Cl] > 850.0 // I really don't think this is necessary. The other lag checks should prevent any fucky stuff from happening, but what do I know. ) { return; @@ -244,8 +250,8 @@ stock void PlayerRunCmd if ( // make sure client isnt using a spin bind - buttons & IN_LEFT - || buttons & IN_RIGHT + buttons & IN_LEFT // Almost certainly an easy bypass. + || buttons & IN_RIGHT // ^ // make sure we're not lagging and that cmdnum is saneish || IsUserLagging(userid, true, false) ) @@ -256,12 +262,186 @@ stock void PlayerRunCmd aimsnapCheck(userid); triggerbotCheck(userid); psilentCheck(userid); + invalidWishVelCheck(userid, vel[0], vel[1], buttons); // In theory, this doesn't need to be down here. I'm only worried about halloween conditions causing issues for this. + unsynchronizedMoveCheck(userid, buttons, vel[0], vel[1]); return; } +/* + INVALID WISH VELOCITY CHECK + Thanks to Oryx and the devs behind it for this check. + Actually implemented it into the anticheat properly this time! + * Copyright (C) 2018 Nolan O. + * Copyright (C) 2018 shavit. +*/ +void invalidWishVelCheck(int userid, float forwardmove, float sidemove, int buttons) +{ + int Cl = GetClientOfUserId(userid); + + int attack = 0; + if ((clbuttons[Cl][0] & IN_ATTACK)) + { + attack = 1; + } + else if ((clbuttons[Cl][0] & IN_ATTACK2)) + { + attack = 2; + } + + if + ( + ( + // The absolute value of forwardmove and sidemove should NEVER be greater than 450 (or 450.00003 for some reason...) + ( + FloatAbs(forwardmove) > 450.00003 + || + FloatAbs(sidemove) > 450.00003 + /* + Reasoning for the sidemove/forwardmove value of 450.00003: + Essentially, some legit clients report a value of 450.0000305175 (even happened to my client), which IS greater than 450.0. Why does this happen? No clue. + I just wanted to add a bit of leniency. Honestly, this isn't enough to cause any issues. + This could also be caused by +strafe, but who knows. + */ + ) + ) + && + ( + clangles[Cl][1][1] != clangles[Cl][0][1] // (somewhat) Workaround false positives occuring while holding +strafe. Technically may open up room for a bypass, but I have found no other solution. + ) + ) + { + invalidWishVelDetects[Cl]++; + if (invalidWishVelDetects[Cl] > 0) // First detect is ignored. + { + PrintToImportant + ( + "{hotpink}[StAC]{white} Player %N {mediumpurple}has invalid wish velocity{white}!\ + \nThis player is *probably* cheating, especially if they trigger this more than once.\ + \nDetections so far: {palegreen}%i", + Cl, + invalidWishVelDetects[Cl] + ); + PrintToImportant + ( + "{white}Forwardmove: {palegreen}%.10f, {white}Sidemove: {palegreen}%.10f", + forwardmove, + sidemove + ); + if (attack > 0) + { + PrintToImportant + ( + "{hotpink}[StAC]{red} This player was ATTACKING (ATTACK%i), \ + you should ban them manually with the reason \"Banned from server\"", + attack + ); + } + StacLogSteam(userid); + if (invalidWishVelDetects[Cl] == 1 || invalidWishVelDetects[Cl] % 5 == 0) + { + char invalidWishVelInfo[128]; + if (attack > 0) + { + Format + ( + invalidWishVelInfo, + sizeof(invalidWishVelInfo), + "invalid wish velocity WHILE ATTACKING (attack%i, forwardmove: %.10f, sidemove %.10f)", + attack, + forwardmove, + sidemove + ); + } + else + { + Format + ( + invalidWishVelInfo, + sizeof(invalidWishVelInfo), + "invalid wish velocity (forwardmove: %.10f, sidemove %.10f)", + forwardmove, + sidemove + ); + } + StacDetectionNotify(userid, invalidWishVelInfo, invalidWishVelDetects[Cl]); + } + if (invalidWishVelDetects[Cl] >= maxInvalidWishVelDetections && maxInvalidWishVelDetections > 0) + { + char reason[128]; + Format(reason, sizeof(reason), "%t", "invalidWishVelBanMsg", invalidWishVelDetects[Cl]); + char pubreason[256]; + Format(pubreason, sizeof(pubreason), "%t", "invalidWishVelBanAllChat", Cl, invalidWishVelDetects[Cl]); + BanUser(userid, reason, pubreason); + } + } + } +} + +/* + UNSYNCHRONIZED MOVEMENT CHECK -- Catches autostrafers + Thanks to Oryx and the devs behind it for this check. + * Copyright (C) 2018 Nolan O. + * Copyright (C) 2018 shavit. +*/ +// If it was my choice, I would ban controller users and cheaters all the same since they all trigger this. Plus, controller players are annoying. +void unsynchronizedMoveCheck(int userid, int buttons, float forwardmove, float sidemove) +{ + int Cl = GetClientOfUserId(userid); + if (playerUsingController(userid)) + { + return; + } + + if + ( + // don't bother checking if unsync'd move detection is off + maxUnsyncMoveDetections != -1 + && + // Unsynchronized usercmd->forwardmove or usercmd->sidemove. + // cl_forwardspeed and cl_sidespeed are the fully-pressed move values. + // The game will never apply them unless the buttons are added into the usercmd too. (exceptions: controllers) + // https://mxr.alliedmods.net/hl2sdk-css/source/game/client/in_main.cpp#557 + // https://mxr.alliedmods.net/hl2sdk-css/source/game/client/in_main.cpp#842 + ( + (forwardmove == 450.0 && (buttons & IN_FORWARD) == 0) || // Check for unsynchronized forwards movement + (sidemove == -450.0 && (buttons & IN_MOVELEFT) == 0) || // Check for unsynchronized sideways (left) movement + (forwardmove == -450.0 && (buttons & IN_BACK) == 0) || // Check for unsynchronized backwards movement + (sidemove == 450.0 && (buttons & IN_MOVERIGHT) == 0) // Check for unsynchronized sideways (right) movement + ) + ) + { + unsyncMoveDetects[Cl]++; + PrintToImportant + ( + "{hotpink}[StAC]{white} Player %N {mediumpurple}had unsynchronized movement{white}!\ + \nConsecutive detections so far: {palegreen}%i", + Cl, + unsyncMoveDetects[Cl] + ); + PrintToImportant("{hotpink}[StAC]{red} Hey, since this doesn't ban automatically yet (testing mode), you should ban them manually with the reason \"Banned from server\". This goes for any detection, but especially this one."); + StacLogSteam(userid); + + if (unsyncMoveDetects[Cl] == 1 || unsyncMoveDetects[Cl] % 5 == 0) + { + char unsyncMovInfo[128]; + Format(unsyncMovInfo, sizeof(unsyncMovInfo), "unsynchronized movement"); + StacDetectionNotify(userid, unsyncMovInfo, unsyncMoveDetects[Cl]); + } + if (unsyncMoveDetects[Cl] >= maxUnsyncMoveDetections && maxUnsyncMoveDetections > 0) + { + char reason[128]; + Format(reason, sizeof(reason), "%t", "unsynchronizedMoveBanMsg", unsyncMoveDetects[Cl]); + char pubreason[256]; + Format(pubreason, sizeof(pubreason), "%t", "unsynchronizedMoveBanAllChat", Cl, unsyncMoveDetects[Cl]); + BanUser(userid, reason, pubreason); + } + } +} + /* BHOP DETECTION - using lilac and ssac as reference, this one's better tho + There is a better way to do this without notifying the client that they have been bhop checked. (Can be abused to detect StAC running on the server) Look at Oryx/Bash2. */ void bhopCheck(int userid) { @@ -337,7 +517,7 @@ void bhopCheck(int userid) Format(reason, sizeof(reason), "%t", "bhopBanMsg", bhopDetects[Cl]); char pubreason[256]; Format(pubreason, sizeof(pubreason), "%t", "bhopBanAllChat", Cl, bhopDetects[Cl]); - BanUser(userid, reason, pubreason); + BanUserOriginal(userid, reason, pubreason); // We use the original version of this function because you can tell when you've triggered it, therefore leading to discovery of a ban queue system. return; } @@ -621,7 +801,8 @@ void psilentCheck(int userid) // doing this might make it harder to detect legitcheaters but like. legitcheating in a 12 yr old dead game OMEGALUL who fucking cares if ( - aDiffReal >= 1.0 && fuzzy >= 0 + //aDiffReal >= 0.5 && fuzzy >= 0 // I care about legitcheating in a 12 yr old "dead game". I want a fair game. If a client triggers this consistently, especially at a consistent angle value, they are almost certainly cheating. The anticheat handles false positives already. + fuzzy >= 0 ) { pSilentDetects[Cl]++; @@ -651,6 +832,8 @@ void psilentCheck(int userid) } if (pSilentDetects[Cl] % 5 == 0) { + char pSilentInfo[128]; + Format(pSilentInfo, sizeof(pSilentInfo), "psilent (anglediff: %.2f°, fuzzy: %s, norecoil: %s)", aDiffReal, fuzzy == 1 ? "yes" : "no", aDiffReal <= 3.0 ? "yes" : "no"); StacDetectionNotify(userid, "psilent", pSilentDetects[Cl]); } // BAN USER if they trigger too many detections @@ -1049,6 +1232,37 @@ bool isTickcountRepeated(int userid) return false; } +// I would like to check if the player has controller-like movements someday, because if this goes open source, people will force these cvars to avoid detection. +bool playerUsingController(int userid) +{ + int Cl = GetClientOfUserId(userid); + // If we haven't queried these cvars from this client, we don't know if the player is using a controller or not yet. + if (!joystickQueried[Cl] || !joy_xconQueried[Cl]) + { + return true; + } + // If the player has joystick set to 1 (enables controller) and has joy_xcon set to 1 (controller is plugged in), they are using a controller. + if (joystick[Cl] && joy_xcon[Cl]) + { + if (!printedOnce[Cl]) + { + PrintToImportant("{hotpink}[StAC]{white} Player %N {powderblue}appears to be using a controller{white}!", Cl); + StacGeneralPlayerNotify(userid, "Client appears to be using a controller"); + printedOnce[Cl] = true; + } + return true; + } + // Make sure we don't detect on people who plug their controller in mid-game. This will make any checks calling this take longer to kick in, but prevent false positives. + if (waitTillNextQuery[Cl]) + { + waitTillNextQuery[Cl] = false; + joystickQueried[Cl] = false; + joy_xconQueried[Cl] = false; + return true; + } + return false; +} + /********** DETECTION FORGIVENESS TIMERS **********/ Action Timer_decr_aimsnaps(Handle timer, any userid) diff --git a/scripting/stac/stac_stocks.sp b/scripting/stac/stac_stocks.sp index 52270658..a63fd4c8 100644 --- a/scripting/stac/stac_stocks.sp +++ b/scripting/stac/stac_stocks.sp @@ -381,18 +381,84 @@ bool IsValidSrcTV(int client) /********** MISC FUNCS **********/ -void BanUser(int userid, char[] reason, char[] pubreason) +// Thanks to Mitchell on AlliedModders for this. +stock int FindClientBySteamID(char[] SteamID) +{ + char steamid[64]; + for (int Cl = 1; Cl <= MaxClients; Cl++) + { + if (IsClientInGame(Cl)) + { + GetClientAuthId(Cl, AuthId_Steam2, steamid, sizeof(steamid)); + if (StrEqual(steamid, SteamID, false)) + { + return Cl; + } + } + } + return -1; +} + +Action Timer_BanAfterTime(Handle timer, DataPack pack) +{ + int userid; + int Cl; + char reason[128]; + char pubreason[256]; + char bannedID[TFMAXPLAYERS+1][64]; + char steamid[64]; + // We probably want to set this to something so that if Cl == 0, we don't error out on the strcmp because there is nothing stored in the string. + steamid = "STEAM_1:1:00000000"; + + pack.Reset(); + userid = pack.ReadCell(); + Cl = GetClientOfUserId(userid); + pack.ReadString(reason, sizeof(reason)); + pack.ReadString(pubreason, sizeof(pubreason)); + pack.ReadString(bannedID[Cl], sizeof(bannedID)); + + if (Cl != 0) + { + GetClientAuthId(Cl, AuthId_Steam2, steamid, sizeof(steamid)); // Get the client's SteamID + } + + if (strcmp(steamid, bannedID[Cl]) == 0) // Make sure we're not banning the wrong client. + { + BanUserOriginal(userid, reason, pubreason); // Why wasn't I doing this before? Lol. + } + else + { + int bannedClient = FindClientBySteamID(bannedID[Cl]); + if (bannedClient == -1) + { + BanIdentity(bannedID[Cl], 0, BANFLAG_AUTHID, "Banned from server"); // User isn't connected to the server. Add to banlist. + } + else + { + int newuserid = GetClientUserId(bannedClient); + BanUserOriginal(newuserid, reason, pubreason); // Client is connected to the server. Ban and kick them. + } + } + return Plugin_Continue; +} + +/* + We need this still. +*/ +void BanUserOriginal(int userid, char[] reason, char[] pubreason) { int Cl = GetClientOfUserId(userid); // prevent double bans + /* if (userBanQueued[Cl]) { - KickClient(Cl, "Banned by StAC"); + KickClient(Cl, "Banned from server"); return; - } + } + */ - StacGeneralPlayerNotify(userid, reason); + // OracxGeneralPlayerNotify(userid, reason); // make sure we dont detect on already banned players userBanQueued[Cl] = true; @@ -418,22 +484,22 @@ void BanUser(int userid, char[] reason, char[] pubreason) { if (SOURCEBANS) { - SBPP_BanPlayer(0, Cl, banDuration, reason); + SBPP_BanPlayer(0, Cl, banDuration, "Banned from server"); // there's no return value for that native, so we have to just assume it worked lol return; } - if (MATERIALADMIN && MABanPlayer(0, Cl, MA_BAN_STEAM, banDuration, reason)) + if (MATERIALADMIN && MABanPlayer(0, Cl, MA_BAN_STEAM, banDuration, "Banned from server")) { return; } if (GBANS) { - ServerCommand("gb_ban %i, %i, %s", userid, banDuration, reason); + ServerCommand("gb_ban %i, %i, %s", userid, banDuration, "Banned from server"); // there's no return value nor a native for gbans bans (YET), so we have to just assume it worked lol return; } // stock tf2, no ext ban system. if we somehow fail here, keep going. - if (BanClient(Cl, banDuration, BANFLAG_AUTO, reason, reason, _, _)) + if (BanClient(Cl, banDuration, BANFLAG_AUTO, reason, "Banned from server", _, _)) { return; } @@ -443,8 +509,8 @@ void BanUser(int userid, char[] reason, char[] pubreason) // if this returns true, we can still ban the client with their steamid in a roundabout and annoying way. if (!IsActuallyNullString(SteamAuthFor[Cl])) { - ServerCommand("sm_addban %i \"%s\" %s", banDuration, SteamAuthFor[Cl], reason); - KickClient(Cl, "%s", reason); + ServerCommand("sm_addban %i \"%s\" %s", banDuration, SteamAuthFor[Cl], "Banned from server"); + KickClient(Cl, "%s", "Banned from server"); } // if the above returns false, we can only do ip :/ else @@ -453,12 +519,13 @@ void BanUser(int userid, char[] reason, char[] pubreason) GetClientIP(Cl, ip, sizeof(ip)); StacLog("No cached SteamID for %N! Banning with IP %s...", Cl, ip); - ServerCommand("sm_banip %s %i %s", ip, banDuration, reason); + ServerCommand("sm_banip %s %i %s", ip, banDuration, "Banned from server"); // this kick client might not be needed - you get kicked by "being added to ban list" // KickClient(Cl, "%s", reason); } - MC_PrintToChatAll("%s", pubreason); + // MC_PrintToChatAll("%s", pubreason); This still isn't happening. Why should we notify all clients WHY they were banned? Bad idea. + PrintToImportant("%s", pubreason); // This may be redundant. Testing needed. StacLog("%s", pubreason); } @@ -707,16 +774,17 @@ void StacGeneralPlayerNotify(int userid, const char[] format, any ...) static char generalTemplate[2048] = \ "{ \"embeds\": \ - [{ \"title\": \"StAC Detection!\", \"color\": 16738740, \"fields\":\ + [{ \"title\": \"StAC Notification!\", \"color\": 16738740, \"fields\":\ [\ - { \"name\": \"Player\", \"value\": \"%N\" } ,\ - { \"name\": \"SteamID\", \"value\": \"%s\" } ,\ - { \"name\": \"Message\", \"value\": \"%s\" } ,\ - { \"name\": \"Hostname\", \"value\": \"%s\" } ,\ - { \"name\": \"Server IP\", \"value\": \"%s\" } ,\ - { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\ - { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\ - { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \ + { \"name\": \"Player\", \"value\": \"%N\" } ,\ + { \"name\": \"SteamID\", \"value\": \"%s\" } ,\ + { \"name\": \"Operating System\", \"value\": \"%s\" } ,\ + { \"name\": \"Message\", \"value\": \"%s\" } ,\ + { \"name\": \"Hostname\", \"value\": \"%s\" } ,\ + { \"name\": \"Server IP\", \"value\": \"%s\" } ,\ + { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\ + { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\ + { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \ ]\ }],\ \"avatar_url\": \"https://i.imgur.com/RKRaLPl.png\"\ @@ -745,6 +813,24 @@ void StacGeneralPlayerNotify(int userid, const char[] format, any ...) { steamid = "N/A"; } + + char OS[36]; // 34 bytes (largest outcome) + 1 (string read end byte) + 1 (to make it an even number lol) Note: I don't know anything about how to handle arrays! + switch(clientOS[Cl]) + { + case 0: + { + Format(OS, sizeof(OS), "Windows/Wine"); + } + case 1: + { + Format(OS, sizeof(OS), "Linux/MacOS (Most likely Cathook)"); + } + default: + { + Format(OS, sizeof(OS), "Not queried yet or blocked by user"); + } + } + Format ( msg, @@ -752,6 +838,7 @@ void StacGeneralPlayerNotify(int userid, const char[] format, any ...) generalTemplate, Cl, steamid, + OS, message, hostname, hostipandport, @@ -776,15 +863,16 @@ void StacDetectionNotify(int userid, char[] type, int detections) "{ \"embeds\": \ [{ \"title\": \"StAC Detection!\", \"color\": 16738740, \"fields\":\ [\ - { \"name\": \"Player\", \"value\": \"%N\" } ,\ - { \"name\": \"SteamID\", \"value\": \"%s\" } ,\ - { \"name\": \"Detection type\", \"value\": \"%s\" } ,\ - { \"name\": \"Detection\", \"value\": \"%i\" } ,\ - { \"name\": \"Hostname\", \"value\": \"%s\" } ,\ - { \"name\": \"Server IP\", \"value\": \"%s\" } ,\ - { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\ - { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\ - { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \ + { \"name\": \"Player\", \"value\": \"%N\" } ,\ + { \"name\": \"SteamID\", \"value\": \"%s\" } ,\ + { \"name\": \"Operating System\", \"value\": \"%s\" } ,\ + { \"name\": \"Detection type\", \"value\": \"%s\" } ,\ + { \"name\": \"Detection\", \"value\": \"%i\" } ,\ + { \"name\": \"Hostname\", \"value\": \"%s\" } ,\ + { \"name\": \"Server IP\", \"value\": \"%s\" } ,\ + { \"name\": \"Current Demo\", \"value\": \"%s\" } ,\ + { \"name\": \"Demo Tick\", \"value\": \"%i\" } ,\ + { \"name\": \"Unix timestamp\", \"value\": \"%i\" } \ ]\ }],\ \"avatar_url\": \"https://i.imgur.com/RKRaLPl.png\"\ @@ -810,6 +898,23 @@ void StacDetectionNotify(int userid, char[] type, int detections) { steamid = "N/A"; } + + char OS[36]; // 34 bytes (largest outcome) + 1 (string read end byte) + 1 (to make it an even number lol) Note: I don't know anything about how to handle arrays! + switch(clientOS[Cl]) + { + case 0: + { + Format(OS, sizeof(OS), "Windows/Wine"); + } + case 1: + { + Format(OS, sizeof(OS), "Linux/MacOS (Most likely Cathook)"); + } + default: + { + Format(OS, sizeof(OS), "Not queried yet or blocked by user"); + } + } Format ( @@ -818,6 +923,7 @@ void StacDetectionNotify(int userid, char[] type, int detections) detectionTemplate, Cl, steamid, + OS, type, detections, hostname, diff --git a/translations/stac.phrases.txt b/translations/stac.phrases.txt index 9e8e80db..7c90c8db 100755 --- a/translations/stac.phrases.txt +++ b/translations/stac.phrases.txt @@ -366,6 +366,26 @@ "es" "[StAC] Baneado por valores de logros falsos" "de" "[StAC] Gebannt für falsche Achievements" } + "invalidWishVelBanAllChat" + { + "#format" "{1:N},{2:i}" + "en" "{hotpink}[StAC]{white} Player {1} had {mediumpurple}invalid wish velocity{white} using a {mediumpurple}cheat program{white}. Total invalid wish velocities: {mediumpurple}{2}{white}. {palegreen}Queued for ban!" + } + "invalidWishVelBanMsg" + { + "#format" "{1:i}" + "en" "[StAC] Banned for invalid wish velocity after {1} detections" + } + "unsynchronizedMoveBanAllChat" + { + "#format" "{1:N},{2:i}" + "en" "{hotpink}[StAC]{white} Player {1} had {mediumpurple}unsynchronized movement{white}. Total unsynchronized movements: {mediumpurple}{2}{white}. {palegreen}Queued for ban!" + } + "unsynchronizedMoveBanMsg" + { + "#format" "{1:i}" + "en" "[StAC] Banned for unsynchronized movement after {1} detections" + } // newlines in chat "newlineBanAllChat" {