Potassium detection on injection

Hello,

I found a pretty simple way of instantly and reliably detecting Potassium on injection.

The detection takes advantage of Potassium’s console handling. Under the right conditions, you can make it hide an error that Roblox would normally add to the console, then check whether that error actually appeared.

As a side effect, the same behavior can also be used to send messages into Potassium’s console.

Showcase

Basic example

Here’s a small example showing the general idea:

-- LocalScript

local LogService = game:GetService("LogService")
local Players = game:GetService("Players")

local oldLogHistory = LogService:GetLogHistory()

-- Trigger an error, `error()` would work too, but LogService:Error
-- is nicer because it doesn't add traceback lines, therefore it
-- slightly avoids spamming the console with unnecessary traceback lines.
pcall(LogService.Error, LogService, "''")

task.wait() -- Wait for Roblox to update the console history

local newLogHistory = LogService:GetLogHistory()

-- If the error didn't appear in the new console history, it means Potassium likely intercepted it.
if #oldLogHistory == #newLogHistory then
  -- Send a large error message to the Potassium console to hide the detection.
  pcall(LogService.Error, LogService, "''" .. string.rep("detected\n", 4000))

  Players.LocalPlayer:Kick("Potassium detected.")
end

Normally, the probe appears in LogService:GetLogHistory(). With Potassium injected, its output handling intercepts the error instead, so it never appears in the history. This works on Potassium’s default configuration.

Don’t use the example above as-is in a real anticheat; Roblox only keeps a limited number of console messages, so simply comparing the number of entries becomes unreliable once the history is full.

If you actually want to use the detection in your game, feel free to use a more reliable version below.

Reliable version
-- LocalScript
-- ReplicatedFirst / StarterPlayerScripts

const LogService = game:GetService("LogService")
const Players = game:GetService("Players")

const LocalPlayer = Players.LocalPlayer

const CheckInterval = 1

while task.wait(CheckInterval) do
  -- Send a unique probe error to the console to check if Potassium intercepts it.
  local currentProbeMessage = "''" .. math.random()
  pcall(LogService.Error, LogService, currentProbeMessage)
  task.wait()

  local newLogHistory = LogService:GetLogHistory()
  local found = false
  for i = #newLogHistory, 1, -1 do
    if newLogHistory[i].message == currentProbeMessage then
      found = true
      break
    end
  end

  if not found then
    -- Do not use the code below as-is in a real anticheat, exploiters
    -- can intercept the `:Kick` function and bypass it. Replace the following
    -- code with your own code, preferably a remote event that triggers player ban.
    pcall(LocalPlayer.Kick, LocalPlayer, "Potassium user")
  end
end

In my testing, these probe errors also haven’t shown up in the game’s Analytics error logs.

The implementation can obviously be taken further, but this should show the general idea.

Why does this happen?

Potassium filters errors caused by scripts executed through it. Those scripts have an empty name, so their tracebacks contain something like Script '', Line 1.

The problem is that its output handling looks for that empty script name when deciding whether an error belongs to Potassium. If we put the same '' into our own error, it gets mistaken for one of Potassium’s errors and filtered out before it reaches Roblox’s normal console history.

This is super easy for them to fix, which is the main reason I’m posting it instead of keeping it private or putting it into one of my anticheats. I still thought it was worth sharing because of how simple it is, and because it can detect Potassium without waiting for the user to execute anything.

If anyone manages to get a false positive with this, or finds another edge case I missed, let me know.


I will be releasing more stuff soon, so stay tuned.


I do Luau programming and Roblox anticheat work, including custom detections and general anticheat development. If you’re working on a game and need a gameplay programmer/engineer or an anticheat developer, feel free to contact me.

22 Likes

Insane find lol, not every day you see something like this posted on the devforum

7 Likes

huh, never knew there were detections so simple

3 Likes

this is genius. how did you find this?

3 Likes

took yall long enough to find :broken_heart:

3 Likes

Wow howd you do this, super cool for ac

1 Like

fairly certain this applies(ed) to volt and wave aswell

Wow nice find. This is massive. The goat???

no, volt doesnt redirect this type of error

if the setting called “Redirect Error” is turned off then it will be bypassed

auto-attach:

local pendingProbe = nil

local oldError = hookfunction(LogService.Error, newcclosure(function(self, message)
    if type(message) == "string" and message:sub(1, 2) == "''" then
        pendingProbe = message
    end
    return oldError(self, message)
end))

local oldGetLogHistory = hookfunction(LogService.GetLogHistory, newcclosure(function(self)
    local history = oldGetLogHistory(self)
    if pendingProbe then
        table.insert(history, {
            message = pendingProbe,
            messageType = Enum.MessageType.MessageError,
            timestamp = os.time()
        })
        pendingProbe = nil
    end
    return history
end))
1 Like