Tired of remote-spoofers? Lets fix it!

Overview

Hello fellow developers, Today we will be explaining what remote spoofing/hooking is and how to prevent it.
This tutorial will include a basic example of preventing remote spoofing/hooking the example includes a Sprint script.

If you find my tutorial helpful, please consider upvoting it.
Thanks! - Pingu

What’s remote spoofing?

Okay, so think of Roblox games as a big network of computers, where information needs to be sent back and forth between your computer (the client) and the game server. This information is usually handled using something called “remotes.” Remotes are like messengers that carry information between your computer and the game server, helping to keep everything synchronized.

Now, remote spoofing and hooking are when someone tries to mess with these messengers. It’s like intercepting and altering the messages they’re carrying. Why would exploiters do this, you ask? Well, sadly some exploiters might want to cheat in the game, gain an unfair advantage, or even crash the game for everyone else.

Remote spoofing involves pretending to be someone you’re not. Imagine if I sent a message to the game server pretending to be the game creator or an admin. I could potentially trick the server into doing things it shouldn’t, like giving me special privileges or items.

Remote hooking, on the other hand, is a bit sneakier. It’s like attaching something to the messenger (the remote) without anyone noticing. This can allow someone to spy on the messages being sent or even change them on the fly.

So, how do you prevent remote spoofing and hooking? Well, today we will be explaining that!

What’s the point of anti-remote-spoof?

Anti-remote spoofing in Roblox is like putting a lock on your front door to keep unwanted guests out of your house. In Roblox, we use something called RemoteEvents and Remotes to send messages between our client and the Roblox servers. These messages help make our games fun and interactive. But sometimes, exploiters can try to mess with these messages in a bad way, like using them to cheat or spoofing/hooking them for an unfair advantage.

That’s where anti-remote spoofing comes in. It’s like a security system that makes sure only the right messages get through. Imagine if you had a secret handshake with your friends, and only they knew it. If someone else tried to join your group, they wouldn’t know the handshake and wouldn’t be allowed in.

Anti-remote spoofing works kind of like that. It checks if the messages coming in are from the right sources and if they’re saying the right things. If they’re not, it blocks them, just like your secret handshake would keep out strangers. This helps keep our games fair and fun for everyone.

But why is this important? Well, think about it like playing a sport. If someone cheats and doesn’t follow the rules, it’s not fair or fun for the other players. Anti-remote spoofing helps make sure everyone plays by the same rules, and that’s what keeps Roblox games enjoyable for everyone. So, it’s like our way of being good sports and keeping the game fair and square.

Note:

The code shared in this discussion is purely for educational purposes and should not be applied in actual game production. It’s essential to understand that the code may require further enhancements, such as setting both the script and the script environment to ‘nil’ to deter potential exploits from rendering it ineffective.

Please be aware that the possibility of disabling or deleting the script or remote is already acknowledged and addressed, so there’s no need to comment on that aspect.

Example

Before Anti-Remote Spoof/Hook.

After Anti-Remote Spoof/Hook

Tutorial

A quick side note: indeed, it’s possible to delete the script or remote. It’s highly recommended to implement additional layers of protection, but it’s important to note that this tutorial primarily concentrates on anti-remote spoofing techniques.

Client

Let’s set things up step by step. First, create a LocalScript under "StarterPlayer > StarterCharacterScripts". After that, we’ll get our variables and services in order.

You’ll also want to make sure you have a RemoteEvent named “Packet” placed in "ReplicatedStorage". Don’t worry; you can always change this later if needed.

-- Client.lua | StarterPlayer > StarterCharacterScripts

-- \\ Services // --
local UserInputService			 =			game:GetService("UserInputService")
local ReplicatedStorage 	 	 =			game:GetService("ReplicatedStorage")
local HttpService 				 = 			game:GetService("HttpService")
local Players					 = 			game:GetService("Players")


-- \\ Variables // -- 
local S2C_C2S_Packet 			 = 			ReplicatedStorage:WaitForChild("Packet")
local LocalArgumentCopys		 = 			{}

A brief explanation: "LocalArgumentCopys" stores copies of all the arguments sent to the server in a table, and it’s associated with the "S2C_C2S_Packet" RemoteEvent.


Now, let’s go ahead and configure our sprint handlers using UserInputService, along with our function for creating our ID to ensure more precise argument matching.

-- Client.lua | StarterPlayer > StarterCharacterScripts

local function CreateID() -- ... CreateID Function | This function creates a LocalArgumentCopys ID and checks it before using it.
	local ID
	
	repeat ID = HttpService:GenerateGUID(false) until not LocalArgumentCopys[ID]
	
	return ID	
end


UserInputService.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) -- ... InputBegan Connection | This connection wait's for the user to hold LeftShift.
	if input.KeyCode == Enum.KeyCode.LeftShift and not gameProcessedEvent then
		local ID = CreateID() -- ... We use an ID system to not mitch match arguments.
		LocalArgumentCopys[ID] = {
			[1] = ID, -- ... We use an ID system to not mitch match arguments.
			[2] = "SPRINT_ADD", -- ... This is the PacketType, you can change how this works on the server.
			[3] = 32, -- ... This is the PacketValie, this value is just there walkspeed.
		}
		S2C_C2S_Packet:FireServer(LocalArgumentCopys[ID])
	end
end)


UserInputService.InputEnded:Connect(function(input: InputObject, gameProcessedEvent: boolean) -- ... InputEnded Connection | This connection wait's for the user to stop holding LeftShift.
	if input.KeyCode == Enum.KeyCode.LeftShift and not gameProcessedEvent then
		local ID = CreateID() -- ... We use an ID system to not mitch match arguments.
		LocalArgumentCopys[ID] = {
			[1] = ID, -- ... We use an ID system to not mitch match arguments.
			[2] = "SPRINT_SUBTRACT", -- ... This is the PacketType, you can change how this works on the server.
			[3] = 16, -- ... This is the PacketValie, this value is just there walkspeed.
		}
		S2C_C2S_Packet:FireServer(LocalArgumentCopys[ID])
	end
end)

A brief explanation: When you hold down the left shift key, the system generates an identification number (ID) and makes a copy of the information being sent to the server. Then, it sends this data to the server. Later, when the server responds, it checks if the response matches what was sent.


The final step involves configuring the "OnClientEvent" listener to await the data sent from the server to the client.

-- Client.lua | StarterPlayer > StarterCharacterScripts

S2C_C2S_Packet.OnClientEvent:Connect(function(...: any) -- ... OnClientEvent Connection | This connection wait's for the Packet to be sent back for verification.
	local Arguments = unpack({...}) -- ... Arguments | Argument[1] (ID: String) | Argument[2] (PacketType: String) | Argument[3] (PacketValue: Integer)
	
	
	if not Arguments then -- ... Arguments Check | Check if arguments even exists.
		Players.LocalPlayer:Kick("Arguments[None] | Spoofed!") 
	elseif #Arguments > 3 then -- ... Arguments Check | Check if #Arguments is greater then 3.
		Players.LocalPlayer:Kick("Arguments[>...] | Spoofed!")
	elseif #Arguments < 3 then -- ... Arguments Check | Check if #Arguments is less then 3.
		Players.LocalPlayer:Kick("Arguments[<...] | Spoofed!") 
	elseif LocalArgumentCopys[Arguments[1]][1] ~= Arguments[1] then -- ... Arguments[3] Check | Check's if arguments 1 was spoofed, comparing localcopy to the server copy.
		Players.LocalPlayer:Kick("Arguments[1] | Spoofed!")
	elseif LocalArgumentCopys[Arguments[1]][2] ~= Arguments[2] then -- ... Arguments[3] Check | Check's if arguments 2 was spoofed, comparing localcopy to the server copy.
		Players.LocalPlayer:Kick("Arguments[2] | Spoofed!")
	elseif LocalArgumentCopys[Arguments[1]][3] ~= Arguments[3] then -- ... Arguments[3] Check | Check's if arguments 3 was spoofed, comparing localcopy to the server copy.
		Players.LocalPlayer:Kick("Arguments[3] | Spoofed!")
	end
	
	LocalArgumentCopys[Arguments[1]] = nil -- ... LocalArgumentCopys | This will clear LocalArgumentCopys with the checked arguments.
end)

A brief explanation: This part of the code waits for the server to send back the arguments and then employs the "LocalArgumentCopys" to compare the local and server arguments. If a discrepancy is detected between them, it’s an indication that the remote might have been spoofed.


That’s it for the client check out the Server section to get the server-code!

Server

Before we dive into this, let’s start by setting things up on the server side. Begin by creating a server script within the "ServerScriptService" folder. Once that’s done, we’ll make sure our variables and servers are all in order.

-- Server.lua | ServerScriptService

-- \\ Services // --
local UserInputService			 =			game:GetService("UserInputService")
local ReplicatedStorage 	 	 =			game:GetService("ReplicatedStorage")
local HttpService 				 = 			game:GetService("HttpService")
local Players					 = 			game:GetService("Players")


-- \\ Variables // -- 
local S2C_C2S_Packet 			 = 			ReplicatedStorage:WaitForChild("Packet")

A brief explanation: A brief explanation: "S2C_C2S_Packet" RemoteEvent.


This is the final step, where we set the player’s speed to the value provided as the third argument. Afterward, we send these arguments back to the client for comparison between local and server values, serving as a precaution against potential remote spoofing or hooking.

-- Server.lua | ServerScriptService

-- \\ RunTime // --
S2C_C2S_Packet.OnServerEvent:Connect(function(Player: Player, ...: any) -- ... OnServerEvent Connection | This connection waits for the Packet to send info to it.
	local Arguments = unpack({...}) -- ... Arguments | Argument[1] (ID: String) | Argument[2] (PacketType: String) | Argument[3] (PacketValue: Integer)
	S2C_C2S_Packet:FireClient(Player, Arguments) -- ... FireClient | This allows the client to compare the ServerArguments and ClientArguments.
	
	if Arguments[2] == "SPRINT_ADD" then -- ... PacketType | SPRINT_ADD, Adds sprint speed to the user using the arguments.
		Player.Character.Humanoid.WalkSpeed = Arguments[3]
	elseif Arguments[2] == "SPRINT_SUBTRACT" then -- .. PacketType | SPRINT_SUBTRACT, Removes sprint speed to the user using the arguments.
		Player.Character.Humanoid.WalkSpeed = Arguments[3]
	end
end)

A brief explanation: This connection adjusts the player’s walk speed according to the third argument while also using the second argument to determine what the server needs to do. it also sends the server’s response back to the client for comparison with the local arguments.


Code

A quick side note: indeed, it’s possible to delete the script or remote. It’s highly recommended to implement additional layers of protection, but it’s important to note that this tutorial primarily concentrates on anti-remote spoofing techniques.

Downloads

Roblox model of code…
AntiRemoteSpoof - Pingu - Roblox

Full code…
FullCode.rbxm (4.8 KB)

  • Yes I use ChatGPT to improve my English.
  • Contact pingulovesu on Discord if you need any assistance or leave a comment on this topic.
18 Likes

This is just security through obscurity. Not only will this not fix your problem but it also hinges on the client not modifying the memory of the LocalArguments table as well which you don’t have that guarantee.

The amount of security you really need to apply on your remotes is going to hinge a lot on the context of the remote in question. In most cases, argument validation is the most you’ll need, while in other cases you will want something a bit more complex.

Some assumptions this code makes:

  • The client is being truthful about what it’s giving the server. In exploiters’ cases, they aren’t.
  • The client does not reroute the functions. They can simply cut out the “packet” checking by using a new function that doesn’t include it.
  • The client does not modify any of the contents of LocalArgumentCopies. They can.
  • The client does not modify the kick function. They can.
  • The client does not disconnect the receiver for the remote. They can.
  • The client doesn’t do anything at all to invalidate the entire receiver function. They can.

Also, just a note, but putting a significant chunk of your thread in bold text makes it incredibly difficult to read.

39 Likes

This is basically sanity checks done on the client

8 Likes

What in the heck did I just read
I think I just lost half of my remaining brain cells…

Also let’s talk about the fact that the “punishment” code was made on the client (sanity checks SHOULD be made on the SERVER for OBVIOUS REASONS)

Client kicks, for the 100th time, can be bypassed with the following:

local localPlayer = cloneref(game:GetService("Players")).LocalPlayer
local oldKick = hookfunction(localPlayer.Kick, newcclosure(function(...) --> Undetected Varargs
    if rawequal(select(1, ...), localPlayer) then
        return;
    end

    return oldKick(...)
end))

This will make development so hard for no reason, and all of this will be figured out by exploiters

Theres no reason to structure your code like this anyways.

11 Likes

Once again I made it very clear that punishment’s.
Should not be done on the client, Players.LocalPlayer:Kick() was a fast way of me getting it done and was only used so I would not have to also add a handshake to this tutorial.

2 Likes

Not totally, and if done correctly I don’t see this being a complete problem.

1 Like

A-lot of these points you suggest have nothing to do with the AntiRemote Spoof/Hook it’s self and instead haft to do with the Remote/Script which I’ve clearly said more protection would need to be used to prevent exploiters from using GetReg etc,etc.
This could be done by using a nil script.

2 Likes

You never specified the punishment was to be made on the server, at least from the quote you gave

2 Likes

True but this should already been known as a problem.

1 Like

If you already knew it was a problem, you should kick and do the sanity checks on the SERVER. Period.

ACCIDENTALLY TYPED CLIENT HELP

3 Likes

You as the developer cannot fix either “spoofing” or “hooking”. Exploiters run code with elevated access to the environment and can tamper with things in the engine, send spoofed arguments or “hook” into remotes maliciously. However, I think it’s wrong to say that my post is unrelated, and I think you shouldn’t get too combative about feedback.

It is important to look at the bigger scope though when dealing with security and the method you’re sharing not only doesn’t resolve the issues you’re talking about, but it’s also incredibly insecure by making the client the source of truth here, and that is a mistake in security practice.

There’s no reason to complicate things. Like I said, depending on the context, argument validation and/or sanitisation is enough to make sure that arguments given by the client stay within certain constraints that you set and can just silently reject if not passing.

Placing scripts in nil doesn’t do anything either. This again operates under the assumption that exploiters will be working with the hierarchy to access code which neither the engine nor exploiters need to do. They can just override the memory addresses of relevant vectors they want to attack.

8 Likes

I’m not trying to be combative, when people can’t take time to read a post before making a negative comment it infuriates me, this has happened on almost all security posts I have on this Devfourm account.

Just like some nn going on my Anti-Cheat topic and claiming he hade a full bypass for my Anti-Cheat and showing code that just breaks replication of movement from the Client to the Server, point standing it was not a bypass just someone breaking character replication.

Also you think people will be changing their memory addresses for an Roblox-Game anti-cheat?
This is not CSGO.
And if so this would’ve already happened with client anticheat’s like Combat Warriors, Pls Donate and many more.

Also that’s what Byfron exists for.

1 Like

i think i just lost my last brain cell reading this topic.

what even is this, why are you doing the checks on the client, this isnt effective this just makes changing your game a pain in the ass

1 Like

Yes they absolutely will. It takes one semi-experienced exploiter to make one script that cracks this anti-cheat, and every single script kiddie will have it within a week.

Searching “combat warriors hacks” on Youtube immediately reveals that it has actually been cracked.

1 Like

i just lost my braincells reading this topic

you are doing sanity checks on the client, which is bad as exploiters can bypass it and spoof it

also client kicks can be easily bypassed as-well

2 Likes

You can’t physically lose brain cells from reading, but I appreciate the metaphor and if it’s so easy to bypass then please write a bypass!

if its on the client whats the point of doing this exactly?

1 Like
local connections = getconnections(game.ReplicatedStorage.Packet.OnClientEvent);
for _, connection in next, connections do
    connection:Disconnect();
end

This should prevent the packet from being read by the client entirely. I’m not sure what you expected by having the client validate itself when an exploiter has full control over it.

1 Like

Gladly! I’ll make sure to publish it here too.

this reply contains a bypass to client kicks

@betrelIe even provided an bypass to the sanity checks (it disconnects all connections)

so im not gonna provide one because someone else gave some bypasses