SableAC - Version 1.0.0-alpha

SableAC

Hey Developers!
Today I present something I’ve been working on recently, this is SableAC or better yet: Sable AntiCheat

SableAC is my first ever project I have released to the public so I apologise if this is not the best format for me to present it!

SableAC - https://create.roblox.com/store/asset/17565484699/SableAC

SableAC is a free open-source AntiCheat module capable of the following:
Noclip Detection,
Fly Detection,
Miniumum Join Requirements,
Admin Bypasses,
Remote Security Checks

Click below for Setup instructions

How to Setup SableAC

First off you’re going to want to aquire the asset from the creator store which you can then add it to your toolbox.

From there, you want to make a SERVER script to require the module (which should be located in ServerScriptService.
Inside the server script you want the following:

local SableAC = require(game:GetService("ServerScriptService".SableAC)
SableAC:Init()

That’s all the code you need to startup the AntiCheat!

If you don’t use a server script the script will be automatically terminated (See below)

if not RunService:IsServer() then
	warn("SableAC is not running on the server. This script will be terminated shortly.")
	for i = 3, 1, -1 do
		warn(i)
		wait(1)
	end
	warn("Terminated.")
	script:Destroy()
else
	print("\nAuthor: FBIagent9903\nVersion: 1.0.0\nBuildID: 1.0.0.202405211852\nSableAC")
end

The Settings!

Full Settings
local Settings = {
	ENFORCE_SECURE_EVENTS = true, -- whether or not to check if someone is firing remote events properly
	FLY_DETECTION = true,
	NOCLIP_DETECTION = true,
	SPEED_DETECTION = true, -- NEXT RELEASE
	JUMP_DETECTION = true, -- NEXT RELEASE

	ENFORCE_JOIN_REQUIREMENTS = true, -- if people need to pass the listed requirements to join the game

	USER_BYPASS = {
		{289394644, false} :: UserIdBypass
	},
	LIGHT_REQUIREMENT_CHECK = { -- bypass people requirements IF they are given the "true" value as the second index
		Account_Age = 100
	},
	FULL_REQUIREMENT_CHECK = { -- if ENFORCE_JOIN_REQUIREMENTS is true then these will be the requirements that are checked
		Account_Age = 100,
		Friend_Count = 15,
		Group_Count = 5
	},


	FLY_SETTINGS = {
		RefreshStrikes = 6, -- how many times player can exceed max height until they are refreshed
		KickStrikes = 5, -- how many times a player gets refreshed before they are kicked from anticheat
		MaxHeight = Vector3.new(0, -15, 0), -- how high can the player go before being refreshed :: -15 - (-20) recommended
		HBTime = 15, -- how many HeartBeats until the game checks all players
		PlatformStandingStrikes = true
	},
	NO_CLIP_SETTINGS = {
		KickStrikes = 2, -- How many times the game can flag an individual for noclipping before kicking them.
		HBTime = 15
	}
	
}

The main settings you want to change are these:

FLY_DETECTION = true,
	NOCLIP_DETECTION = true,
	SPEED_DETECTION = true, -- NEXT RELEASE
	JUMP_DETECTION = true, -- NEXT RELEASE

	ENFORCE_JOIN_REQUIREMENTS = true, -- if people need to pass the listed requirements to join the game

These toggle whether certain parts of the AntiCheat are actually used. So if you disable the NOCLIP_DETECTION value the noclip detection will not work and will be :Disconnect()'ed.

Another setting you might want to change is…

FULL_REQUIREMENT_CHECK = { -- if ENFORCE_JOIN_REQUIREMENTS is true then these will be the requirements that are checked
		Account_Age = 100,
		Friend_Count = 15,
		Group_Count = 5
	},

Basically changes what the requirements are for people to join the game if ENFORCE_JOIN_REQUIREMENTS is equal to true.

Module Specific Settings:

FLY_SETTINGS = {
		RefreshStrikes = 6, -- how many times player can exceed max height until they are refreshed
		KickStrikes = 5, -- how many times a player gets refreshed before they are kicked from anticheat
		MaxHeight = Vector3.new(0, -15, 0), -- how high can the player go before being refreshed :: -15 - (-20) recommended
		HBTime = 15, -- how many HeartBeats until the game checks all players
		PlatformStandingStrikes = true
	},

Here everything is already prelabeled, and is pretty clear. For MaxHeight only change the Y Axis otherwise it will flag people for flying even though they’re on the Z or X axis. Finally, ensure that the Y Axis value always remains as a negative otherwise it will flag someone as flying even if they are not as it tracks the players distance from the floor which is a negative increase as they are in the air!

Anything below 15 could possibly cause the game to think they are flying when they are just doing a basic jump!

Thanks For Reading This Post and Trying Out SableAC.

As I said earlier, this is my first ever open-source script and it is in alpha version 1.0.0-alpha.

Full Script
--------------------------------
---------- CREDITS -------------
--------------------------------

--[[
Author: FBIagent9903
Version: 1.0.0
BuildID: 1.0.0.202405211852
Sable Anti Cheat

DO NOT CHANGE ANY OF THE CODE OUTSIDE OF SETTINGS UNLESS YOU UNDERSTAND WHAT YOU ARE DOING!!!
---------------------------------------------------------------------------------------------
To use SableAC, you must make a SERVER script in ServerScriptService and do require(path.to.SableAC) and then call the :Init() method.
Example:
require(game:GetService("ServerScriptService").SableAC):Init()

Outside Credit:
	AntiFly Script: Ph4ntomize :: https://devforum.roblox.com/t/how-to-make-a-basic-anti-fly-script/1389277/5
]]

--------------------------------
---------- SERVICES ------------
--------------------------------

local GroupService = game:GetService('GroupService')
local PlayersService = game:GetService('Players')
local RunService = game:GetService('RunService')


--------------------------------
-------- METAMETHODS -----------
--------------------------------

local SableAC = {}
SableAC.__index = SableAC
SableAC.__tostring = function(self)
	return ("\nAuthor: FBIagent9903\nVersion: 1.0.0\nBuildID: 1.0.0.202405211852\nSableAC")
end

--------------------------------
-------- DEPENDECIES -----------
--------------------------------

local random = Random.new()

--------------------------------
-------- CUSTOM TYPES ----------
--------------------------------

export type UserNameBypass = {
	Name: string,
	doCheck: boolean
}

export type UserIdBypass = {
	Id: number,
	doCheck: boolean
}
--------------------------------
------- USER CONTAINER ---------
--------------------------------

SableAC.UserContainer = {}

--------------------------------
----------- SETTINGS -----------
--------------------------------

local Settings = {
	ENFORCE_SECURE_EVENTS = true, -- whether or not to check if someone is firing remote events properly
	FLY_DETECTION = true,
	NOCLIP_DETECTION = true,
	SPEED_DETECTION = true, -- NEXT RELEASE
	JUMP_DETECTION = true, -- NEXT RELEASE

	ENFORCE_JOIN_REQUIREMENTS = true, -- if people need to pass the listed requirements to join the game

	USER_BYPASS = {
		{289394644, false} :: UserIdBypass
	},
	LIGHT_REQUIREMENT_CHECK = { -- bypass people requirements IF they are given the "true" value as the second index
		Account_Age = 100
	},
	FULL_REQUIREMENT_CHECK = { -- if ENFORCE_JOIN_REQUIREMENTS is true then these will be the requirements that are checked
		Account_Age = 100,
		Friend_Count = 15,
		Group_Count = 5
	},


	FLY_SETTINGS = {
		RefreshStrikes = 6, -- how many times player can exceed max height until they are refreshed
		KickStrikes = 5, -- how many times a player gets refreshed before they are kicked from anticheat
		MaxHeight = Vector3.new(0, -15, 0), -- how high can the player go before being refreshed :: -15 - (-20) recommended
		HBTime = 15, -- how many HeartBeats until the game checks all players
		PlatformStandingStrikes = true
	},
	NO_CLIP_SETTINGS = {
		KickStrikes = 2, -- How many times the game can flag an individual for noclipping before kicking them.
		HBTime = 15
	}
	
}

local Strikes = {
	KickStrikes = {},
	GlobalStrikes = {},
	NoClipStrikes = {}
}
--------------------------------
------ PRIVATE FUNCTIONS -------
--------------------------------

local function typeEqualsUserIdBypass(entry: any): boolean
	return typeof(entry) == "table" and entry[1] ~= nil and entry[2] ~= nil and typeof(entry[1]) == "number" and typeof(entry[2]) == "boolean"
end

local function typeEqualsUserNameBypass(entry: any): boolean
	return typeof(entry) == "table" and entry[1] ~= nil and entry[2] ~= nil and typeof(entry[1]) == "string" and typeof(entry[2]) == "boolean"
end


function doCheck(player: Player): boolean
	return player.AccountAge >= Settings.LIGHT_REQUIREMENT_CHECK.Account_Age 
end

function bypass(player: Player): boolean
	for _, entry in pairs(Settings.USER_BYPASS) do
		print(entry, type(entry))
		if typeEqualsUserIdBypass(entry) then
			if entry[2] == true then
				return doCheck(player)
			else
				if player.UserId == entry[1] then
					return true
				else
					continue
				end
			end
		elseif typeEqualsUserNameBypass(entry) then
			if entry.doCheck == true then
				return doCheck(player)
			else
				if player.Name == entry[1] then
					return true
				else
					continue
				end
			end
		end
	end
	return false
end

function userCanJoinGame(user: Player): boolean
	local requirements = Settings.FULL_REQUIREMENT_CHECK

	local attempts = 0

	local function group()
		return #GroupService:GetGroupsAsync(user.UserId) >= requirements.Group_Count
	end
	local function friends()
		local s, r = pcall(function()
			return PlayersService:GetFriendsAsync(user.UserId)
		end)
		if s then
			local userFriends = 0
			while true do
				wait(0.1)
				userFriends += #r:GetCurrentPage()
				if r.IsFinished then
					break
				else
					r:AdvanceToNextPageAsync()
				end
			end
			return userFriends >= requirements.Friend_Count
		else
			attempts += 1
			warn(r)
			if attempts < 5 then
				return friends()
			else
				user:Kick("AntiCheat ran into an error evaluating player! Please attempt rejoining")
				return
			end
		end
	end
	local function age()
		return user.AccountAge >= requirements.Account_Age
	end
	return group() and friends() and age()
end

local function ResfreshPlayerPos(player: Player, Pos: Vector3)
	player:LoadCharacter()
	player.Character:SetPrimaryPartCFrame(CFrame.new(Pos))
	if player.Character:FindFirstChildWhichIsA("ForceField") then
		player.Character:FindFirstChildWhichIsA("ForceField"):Destroy()
	end
	warn("Refreshed",player.Name)
end

local function newCode()
	local lettersIndex = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'}
	local code = ""
	
	for i = 1, 20 do
		local rl = lettersIndex[random:NextInteger(1, #lettersIndex)]
		if random:NextNumber() > .5 then
			rl = string.upper(rl)
		end
		code = code .. rl
	end
end

local function listenForInvoke(event: RemoteFunction)
	event.OnServerInvoke = function()
		return SableAC.securityCode
	end
end

--------------------------------
------ PUBLIC FUNCTIONS --------
--------------------------------

function SableAC:Init()
	local int = 0
	local nint = 0 
	game.Players.PlayerAdded:Connect(function(player)
		if not userCanJoinGame(player) and not bypass(player) then
			player:Kick("You do not meet the minimum requirements for this experience!")
		else
			print("Game requirements met")
			Strikes.KickStrikes[player.Name] = 0
			Strikes.GlobalStrikes[player.Name] = 0
			Strikes.NoClipStrikes[player.Name] = 0
		end
	end)
	PlayersService.PlayerRemoving:Connect(function(plr)
		Strikes.KickStrikes[plr.Name] = nil
		Strikes.GlobalStrikes[plr.Name] = nil
		Strikes.NoClipStrikes[plr.Name] = nil
	end)
	
	--------------------------------
	------ FLIGHT DETECTION --------
	--------------------------------
	
	local FlightDetection = RunService.Heartbeat:Connect(function(dt)
		--print(dt)
		if int >= Settings.FLY_SETTINGS.HBTime then
			int = 0
			local Characters = {}
			for _, player in pairs(PlayersService:GetPlayers()) do
				if player.Character then
					table.insert(Characters, player.Character)
				end
			end
			local Params = RaycastParams.new()
			Params.FilterType = Enum.RaycastFilterType.Blacklist
			Params.FilterDescendantsInstances = Characters

			for _, player in pairs(PlayersService:GetPlayers()) do
				if player.Character then
					local root = player.Character:FindFirstChild("HumanoidRootPart")
					local hum = player.Character:FindFirstChildWhichIsA("Humanoid")
					local head = player.Character:FindFirstChild("Head")
					if hum and hum.Health > 0 then
						if not root then
							ResfreshPlayerPos(player, head.Position)
							continue
						end
						
						local ground = workspace:Raycast(root.Position, Settings.FLY_SETTINGS.MaxHeight, Params)
						if not ground then
							if Settings.FLY_SETTINGS.PlatformStandingStrikes then
								if hum:GetState() == Enum.HumanoidStateType.PlatformStanding then
									Strikes.GlobalStrikes[player.Name] += 2
								else
									Strikes.GlobalStrikes[player.Name] += 1
								end
							end
							if Strikes.GlobalStrikes[player.Name] >= Settings.FLY_SETTINGS.RefreshStrikes then
								if Strikes.KickStrikes[player.Name] >= Settings.FLY_SETTINGS.KickStrikes then
									player:Kick("AntiCheat has suspected you for cheating.")
									continue
								end
								local groundPos = root.Position
								local g =  workspace:Raycast(root.Position, Vector3.new(0, -300, 0), Params) 
								if g then
									groundPos = g.Position + Vector3.new(0, 5, 0)
								end
								Strikes.GlobalStrikes[player.Name] = 0
								ResfreshPlayerPos(player, groundPos)
								Strikes.KickStrikes[player.Name] += 1
							end
						end
					end
				end
			end
		end
		int += 1
	end)
	
	--------------------------------
	------ NOCLIP DETECTION --------
	--------------------------------
	
	local noClipDetection = RunService.Heartbeat:Connect(function(dt)
		if nint >= Settings.NO_CLIP_SETTINGS.HBTime then
			nint = 0
			for _, player in pairs(PlayersService:GetPlayers()) do
				if not player.Character then
					continue
				else
					for _, v in pairs(player.Character:GetChildren()) do
						if v:IsA("BasePart") then
							if v.CanCollide == false then
								v.CanCollide = true
								Strikes.NoClipStrikes[player.Name] += 1
								if Strikes.NoClipStrikes[player.Name] == Settings.NO_CLIP_SETTINGS.KickStrikes then
									player:Kick("AntiCheat has suspected you for cheating.")
									continue
								end
							end
						end
					end
				end
			end
		end
	end)
	if not Settings.FLY_DETECTION then
		FlightDetection:Disconnect()
	elseif not Settings.NOCLIP_DETECTION then
		noClipDetection:Disconnect()
	end
end



function SableAC.onStartup()
	local SableRemoteFunction = Instance.new("RemoteFunction"); SableRemoteFunction.Parent = game.ReplicatedStorage
	SableRemoteFunction.Name = "SableACRemote"
	
	SableAC.securityCode = newCode()
	listenForInvoke(SableRemoteFunction)
	
	if Settings.ENFORCE_SECURE_EVENTS then
		for _, remote in pairs(game:GetDescendants()) do
			if remote:IsA("RemoteEvent") or remote:IsA("RemoteFunction") then
				local s,r = pcall(function()
					remote.OnServerEvent:Connect(function(player, securityCode, ...)
						if securityCode ~= SableAC.securityCode then
							player:Kick("AntiCheat has suspected you for cheating.")
						end
					end)
				end)
				if not s then
					local s2, r2 = pcall(function()
						remote.OnServerInvoke = function(player, code, ...)
							if code ~= SableAC.securityCode then
								player:Kick("AntiCheat has suspected you for cheating.")
							end
						end
					end)
					if not s2 then
						warn(string.format("SableAC | Encountered an error evaluating remote %s with error\n%s", remote.Name, r2 or r))
					end
				end
			end
		end
	end
end

if not RunService:IsServer() or RunService:IsStudio() then
	warn("SableAC is not running on the server. This script will be terminated shortly.")
	for i = 3, 1, -1 do
		warn(i)
		wait(1)
	end
	warn("Terminated.")
	script:Destroy()
else
	print("\nAuthor: FBIagent9903\nVersion: 1.0.0\nBuildID: 1.0.0.202405211852\nSableAC")
end
SableAC.onStartup()

return SableAC

[I’m adding a small poll to see if people like it]

  • FBIagent9903
  • SableAC is Good!
  • SableAC is Meh
  • SableAC is Bad

0 voters

6 Likes

Cool thing, but not sure because exploiters will just find a random remote event that kills everyone or something.

1 Like

A few issues.

  1. Default settings for the fly detection are too sensitive for even the default movement settings. Was getting kicked for simply jumping on a flat buildplate. (Not major as can be configured easily enough)

  2. Noclip detection won’t work as CanCollide set on the client will not replicate to the server.

  3. The remote protection is not very secure for 3 reasons.
    3a. The code never changes after server startup so a remote spy can find the code and attach it to any sent remotes.
    3b. If the exploiter decided to look into the remotes sent on connection, they will notice the remotefunction that gives the client the copy of the code and be able to retrieve it themselves whenever they please by firing this remotefunction.
    3c. The function for the remote will still be performed as the original function for it is never hooked. This detection should be called in the original function and not a seperate one as to allow blocking the rest of the code from running. In most cases this isn’t major. But if a game has a bug like dropping negative money, it would still be a problem.

3 is a bit more major as it can give a false sense of security about remotes to developers who don’t know this sort of stuff.

Edit: The remote detection also is not checking for newly created remotes once the script is finished searching. So games that store remotes on things like tools will have this ‘protection’ on some remotes but not others.

Makes it pretty confusing which to add it to and potentially creates a race condition if another script sometimes creates a remote before the AC has finished this or not.

2 Likes

So you protect your remote events…

2 Likes

Noclip detection won’t work because the server will not get the CanCollide update, and Noclip doesn’t work with CanCollide either way.

1 Like

They need the code which is part of this. There is a setting to make it so they need a security key to fire remote events iirc.

1 Like

Hello and thank you for your feedback!

I have read through each of your reports and am working actively on it currently.

This here was set in place as I forgot to set it to default when I was testing it earlier. Thank you for this oversight; I will ensure I have it preset to an optimal input on my next release.

This here I am working actively on a client side replicator which I should be able to get released within 48 hours.

This here I will see if I can find a work around

1 Like

instead of adding a clientside replicator ( which is a bad idea ) you should add a whitelist for parts

1 Like

Replicating client information would be bad for a number of reasons:

  • Unperformant, having an event constantly asking clients “U good vro?” is expensive, leaving aside the networking side of things
  • Unsafe by design, if so, I could go and just tell it that all my workspace is non collide for example.

Besides that, you should opt to use a network library like BridgeNet2, Red, etc which would allow you more flexibility with remotes in general.

If you want to do for example a wall check, raycast it, don’t work in some weird system which would be Unperformant and unsafe

1 Like

I have begun working on this with raycasts, this will take a while for me to complete alongside the other changes I am accomodating for; this is the current new version of NOCLIP:

--------------------------------
	------ NOCLIP DETECTION --------
	--------------------------------
	local number = 0
	
	local noClipDetection = RunService.Heartbeat:Connect(function(dt)
		if nint >= Settings.NO_CLIP_SETTINGS.HBTime then
			nint = 0
			for _, player in pairs(PlayersService:GetPlayers()) do
				if not player.Character then
					continue
				else
					local root = player.Character:FindFirstChild("HumanoidRootPart")
					if not root then 
						print("SableAC | Player has no RootPart.")
						continue
					else
						local rayDir = {
							Vector3.new(0,0,-1),
							Vector3.new(0, 0, 1),
							Vector3.new(-1, 0, 0),
							Vector3.new(1, 0, 0),
							Vector3.new(0, 1, 0),
							Vector3.new(0, -1, 0),
						}
					
						
						for _, v3 in pairs(rayDir) do
							local origin = root.Position
							local direction = v3 * Settings.NO_CLIP_SETTINGS.rayLength
							local p = RaycastParams.new()
							p.FilterDescendantsInstances = {player.Character}
							p.FilterType = Enum.RaycastFilterType.Blacklist
							
							local raycast = workspace:Raycast(origin, direction, p)
							if raycast and raycast.Instance and raycast.Instance.CanCollide then
								if not raycast.Instance:IsDescendantOf(player.Character) and not CollectionService:HasTag(raycast.Instance, "NoclipBypass") then
									number += 1
									print(number)
									if number >= Settings.NO_CLIP_SETTINGS.requiredHits then
										root.Position = root.Position - v3 * 2
										warn(`{player.Name} has been flagged for noclipping.`)
										number = 0
									end
								end
							end
						end
					end
				end
			end
		end
		nint += 1

This version right now is a bit sensitive and is detecting players whenever they hug a wall rather than clip into it right now. I am working on a fix for this.

1 Like

You should store their last position in a variable and raycast to that

1 Like