AWP Injection Detection, Hook detection, Infinite Yield detection, Dex Explorer detection

shoutout aidan9382

Hello, dropping some stuff to help keep exploiters away from your game. 99% sure these have both been reported to the developers of AWP, however none have been patched.

These should not be taken as is, and proper testing and implementation should be done to ensure this works in your case. This should not be used as an alternative to any other counter-exploit measures you have.

If you plan on just copy and pasting the code below, expecting it to be a complete prevention of exploiters, please reconsider and take time to understand what this code is doing. As mentioned, this should not be used in place of any other anti-exploit measures, and normal preventions are very much still required.

Most of this post is directed towards AWP users, with some bonus Infinite Yield and Dex Explorer detections. :smiley:
The Infinite Yield and Dex detection targets all executors, not just AWP.

AWP injection detection:

-- 9382
-- on-inject detection targetted at the awp executor (targets getfenv timing flaws)
-- >when I'm so undetectable a LocalScript can detect me :muscle:

if game:GetService("RunService"):IsStudio() then
    return
end
 
-- ratio stats:
-- avg 12.5, avg range 11.5-13.5, 98% ~17.5
-- awp averages about 31, so the difference is more than enough

-- we use a ratio so that slow pcs arent punished for being slow,
-- instead comparing execution time between a base test and getfenv

-- GETIMPORT gets slower after running getfenv, so this matters a lot
local getfenv = getfenv
local tick = tick

local violations = 0
while task.wait(0.2) do
    local t1 = tick()
    for i = 1, 1e4 do
        getfenv()
    end
    local t2 = tick()
    for i = 1, 1e4 do
        -- do nothing lol
    end
    local t3 = tick()
    local ratio = (t2-t1)/(t3-t2)
    if ratio > 25 then
        -- dont punish lag spikes, require 8 fails in a row
        violations = violations + 1
        if violations >= 8 then
            -- insert funny code here (while true do end, etc. etc.)
            warn("awp detected with decent confidence (r8: "..ratio.."), killing detection loop")
            break
        end
    else
        violations = 0
    end
end

Hook detection: (Not sure how many this works on, works on AWP tho, DO NOT USE THIS TO ABUSE THE EXPLOIT ENVIRONMENT! THIS SHOULD PURELY BE USED FOR DETECTION PURPOSES)

-- this can also be done using BindableEvents, and other stuff
-- Edit: moved this over to BindableEvent to prevent the exhaust console spam.
-- __tostring fires on non-string index so when ran through 
-- the function used in the hook, it will run the code through their hook
local Check = true
local env = {}
local Remote = Instance.new("BindableEvent")
local Proxy = newproxy(true)

getmetatable(Proxy).__tostring = function()
	for Level = 1, 20 do
		-- loop through 20 levels to check if any contain getgenv (executor only function)
		local s, fenv = pcall(getfenv, Level)
		
		-- getgenv is present only in an executors environment
		-- you could also use:
		-- if not s or fenv ~= ogEnv then
		-- and define ogEnv earlier in the script, as local ogEnv = getfenv()
		-- credit to robloxelmejor111 in the comments for suggesting this.
		if s and fenv and fenv.getgenv then
			print(string.format("Found an exploit environment at level %s", tostring(Level)))
		end
	end
	
	return ""
end

while Check do
	-- __tostring fired as proxy is userdata and not a string, so our metatable where
	-- we change __tostring will fire, allowing us to get their environment
	Remote:Fire({[Proxy] = {}})
	task.wait()
end

Infinite Yield detection:

-- 9382
-- gc detection for the generic infinite yield script
-- NOTE: This won't work on any executors with a functioning cloneref
-- (if they're running a script version with cloneref that is)
-- WARNING: This CAN false-flag if any of your client scripts use and store NetworkClient
-- They probably don't, but just be aware of that, and test properly

if not game:IsLoaded() then
    game.Loaded:Wait()
end

task.wait(3)

local t = setmetatable({}, {__mode="v"})
while task.wait() do
    t[1] = {}
    t[2] = game:GetService("NetworkClient")
    while t[1] ~= nil do
        -- encourage the gc to move faster by adding a 4kb string
        t[3] = string.rep("ab", 1024*2)
        t[3] = nil
        task.wait()
    end
    if t[2] ~= nil then
        -- insert funny code here
        warn("inf yield detected - invalid gc behaviour")
        break
    end
end

Dex Explorer detection:

-- 9382
-- gc detection for the generic dex explorer script
-- NOTE: This won't work on any executors with a functioning cloneref
-- (if they're running a script version with cloneref that is)
-- WARNING: This CAN false-flag if any of your client scripts for some reason:
--  A) Gets and stores all of game:GetDescendants()
--  B) Gets and stores random objects under game.Chat
-- They probably don't, but just be aware of that, and test properly

if not game:IsLoaded() then
    game.Loaded:Wait()
end

task.wait(3)
local name = tostring(math.random())
local Chat = game:GetService("Chat")
Instance.new("BoolValue", Chat).Name = name -- make it and forget about it

local t = setmetatable({}, {__mode="v"})
while task.wait() do
    t[1] = {}
    t[2] = Chat:FindFirstChild(name)
    while t[1] ~= nil do
        -- encourage the gc to move faster by adding a 4kb string
        t[3] = string.rep("ab", 1024*2)
        t[3] = nil
        task.wait()
    end
    if t[2] ~= nil then
        -- insert funny code here
        warn("dex detected - invalid gc behaviour")
        break
    end
end

Hopefully more detections coming soon. Infinite Yield and Dex detection may be buggy - use these at your own risk as they could possibly false flag (read the comments)

Enjoy!
image

Honourable mentions:



image


image

38 Likes

Very good, thank you. Some of the code is useful for learning.

2 Likes

Awesome. I think to check if it is not the same is better, they may do __index to the ENV of the exploit, also add a check in case they do a return instead of the tostring :tongue:

-- this can also be done using BindableEvents, and other stuff
-- __tostring fires on non-string index so when ran through 
-- the function used in the hook, it will run the code through their hook
local Check = true
local Remote = Instance.new("RemoteEvent")
local Proxy = newproxy(true)

local OriginalENV = getfenv()
local Expected = 1 -- some exploits "cannot change numbers to bools"

getmetatable(Proxy).__tostring = (function()
	for Level = 1, 20 do
		local StackFunc = debug.info(Level, "f")
		if not StackFunc then break end

		local Success, StackENV = pcall(getfenv, Level)
		if not Success or StackENV ~= OriginalENV then
			print(string.format("Found an exploit environment at level %s", tostring(Level)))
		else
			Expected = true
		end
	end
	
	return ""
end)

while Check do
	Expected = 1
	-- I do not recommend using FireServer, it will say “did you forget to implement OnServerEvent...” in the console but OK.
	Remote:FireServer({[Proxy] = true}) -- It could be any function, it should only run in __namecall

	if Expected ~= true then
		print("Found an tostring redirection")
	end
	task.wait()
end
2 Likes

May your soul be blessed, never thought metatables could be used this way.

3 Likes

very nice work

this op has a brain

6 Likes


It’s amazing, whoever discovered it.

1 Like

Yet you didn’t discover it, Arsenal has been using this method since 2024.

Nice edit, probably would work better.

And yeah, BindableEvents may work better or any other namecall that allows table argument.

Modified to move over to BindableEvent and do the Expected = true check,
Can’t get anyone to check if this works because AWP is down (targeted by recent updates lol)

local Check = true
local env = {}
local Event = Instance.new("BindableEvent")
local Proxy = newproxy(true)

local ogEnv = getfenv()
local Expected = 1

getmetatable(Proxy).__tostring = function()
	for Level = 1, 20 do
		local StackFunc = debug.info(Level, "f")
		if not StackFunc then break end
		
		local s, fEnv = pcall(getfenv, Level)
		
		--if s and fenv and fenv.getgenv then -- previous env grab, changing to another method provided by robloxelmejor111, when i can check if this works i'll edit the post
		if not s or fEnv ~= ogEnv then
			print(string.format("Found an exploit environment at level %s", tostring(Level)))
		else
			Expected = true
		end
	end
	
	return ""
end

while Check do
	Expected = 1
	
	Event:Fire({[Proxy] = {}})
	task.wait()
	
	if Expected ~= true then
		print("tostring redirection")
	end
end

It was not discovered by Arsenal. It was discovered by a user on Discord back in 2023 who sent it to a couple people now it’s somehow everywhere.

1 Like

Yeah just saying this kid did not find it like he’s claiming, been a thing for time. Just saying Arsenal use it.

woudn’t it better to wrap the tostring with newcclosure:

local newcclosure = function(func)
	return coroutine.wrap(function(...)
		while true do
			coroutine.yield(func(...))
		end
	end)
end

getmetatable(Proxy).__tostring = newcclosure(function()
    -- code
end)

that way people can’t do debug.setupvalue?

1 Like

This could be bypassed easier than not using it.
None of these methods are “hard” to bypass at all unfortunately, however it would prevent the average skid from trying to do random things on your game.

debug.setupvalue isn’t required to bypass it either, it’s much simpler.

Not going to be showing these bypasses here for obvious reasons, however trust me when I say it’s a one-line bypass.

If you’re using this in a game regularly using the functions used in the env leak bypass, it may become slightly more difficult to bypass, however still not too hard for someone who knows what they’re doing.

If anyone plans on using these in their own games, they should be cautious of the bypasses and look into things they can do to avoid them, which would require quite a few more checks. But regardless, these methods should not be used in place of regular exploit preventions.

that skid didnt discover it at all lol

1 Like

I originally gave maple hospital the detection in November of 2024 but in December told him to give it to the arsenal developer. I’ve known about this detection for roughly 2 or 3 years now (give or take). I was kinda under the assumption it was at least slightly known but I guess not.
Anyways a quick explanation on why it works is due to how roblox serializes data. In a table roblox serializes keys by tostringing them. The issue with exploits though is they don’t hide their hooking stack (yes you can stack count) nor do they spoof the environment (you can manually do this by doing setfenv(2, getfenv(2)) which functions as a bootleg bypass but it doesnt solve the underlining issue). The only exploit to have done it properly was Synapse V3 (oth.hook) but we all know what happened to them.
The functions I found that have this serialization were Bindables, Remotes, and StarterGui.SetCore

2 Likes

Your newcclosure “is functional for this case”, but I don’t recommend it if you must pass any arguments, here is an example with which it would fail:

local newcclosure = function(func)
	return coroutine.wrap(function(...)
		while true do
			coroutine.yield(func(...))
		end
	end)
end

local Func = newcclosure(function(Extra)
	return Extra
end)

print(Func(1))
print(Func(100))

I also don’t think it will work well because coroutine.wrap will remove all the previous stacks (The first one is without anything and the second one is with your newcclosure):

2 Likes
local newcclosure = function(func)
    return coroutine.wrap(function(...)
        return coroutine.yield(func(...))
    end)
end

local Func = newcclosure(function(Extra)
    return Extra
end)

print(debug.info(Func, "s"))
print(Func(1))
print(Func(100))
1 Like

If you run “Func” once more it will no longer work (“cannot resume dead coroutine”), besides that was not the main problem, coroutine.wrap takes out all the previous stacks making it not detect anything.

2 Likes

this is gonna be patched so fast by AWP developers

1 Like