Obranu, kterou vedeš jako hardening, může držet správnost
NX bit se učí jako bezpečnostní opatření. Označíš paměť jako nespustitelnou a útočník z ní nespustí kód, který tam podstrčil. Hezký, uzavřený příběh, a proto se NX v hlavě uloží do kategorie ochrana před útokem.
Autorovi hypervisoru na ARM64 tuhla mašina. Příčina neměla s útočníkem nic společného. Procesor spekulativně načítal instrukce z regionu uzamčeného bootromu na adrese 0x0, což je paměť typu Device. Spekulativní přístup do device paměti architektura zakazuje, takže se stroj zasekl. Chyběl NX bit — a nechyběl jako obrana, chyběl jako podmínka správného běhu.
Dva různé spekulativní kanály
Rozdíl, na kterém to celé stojí, je jemný a v dokumentaci snadno přehlédnutelný. Spekulativní přístup k datům a spekulativní načtení instrukce jsou dvě oddělené cesty s odlišnými pravidly.
Datovou cestu umíš zavřít označením paměti jako Device. Procesor pak ví, že tam nesmí sahat dopředu, protože čtení z MMIO má vedlejší efekty a nedá se vzít zpět.
Instrukční cesta tenhle atribut ignoruje. Prefetcher se dívá jen na to, jestli je paměť spustitelná, a spustitelnou paměť považuje za Normal, ať už je Device nebo ne. Jediný příznak, kterým mu to zakážeš, je právě non-executable.
Spoušť byla dynamický skok. Statické bl má cíl v instrukci, takže predikce nikam nezabloudí. Dynamické blr bere cíl z registru, predikce se splete a fetcher vyrazí na adresu, kterou by architektonicky nikdy nenavštívil.
Kategorie je motivace, ne mechanismus
Tady je ta přenositelná část. NX bit je zařazený podle toho, proč vznikl, ne podle toho, co dělá. Dělá jednu jedinou věc: zabrání procesoru sahat po instrukcích do daného rozsahu. Že to zablokuje shellcode, je aplikace toho mechanismu. Že to zabrání zatuhnutí na MMIO, je jiná aplikace téhož mechanismu.
Zaškatulkování podle motivace má měřitelnou cenu. Opatření zapsané jako bezpečnostní se při optimalizaci škrtá první, protože se zdá, že platíš za scénář, který u tebe nenastane. Vývojář firmwaru, který ví, že mu do adresního prostoru nikdo cizí nesahá, nemá důvod NX nastavovat. A pak mu stroj tuhne způsobem, který se ladí týdny, protože příčina je v kategorii, kterou vyloučil na začátku.
Co si z toho odnést
Než odstraníš jakoukoli ochranu s odůvodněním, že daný útok u tebe nehrozí, popiš si ji mechanismem. Ne k čemu je, ale co konkrétně brání procesoru, kernelu nebo runtimu udělat. Ten popis skoro vždycky pokrývá víc situací než původní záměr.
Druhá věc je předpoklad, který v hlavě nosí skoro každý: že se nevykonaná větev nevykoná. Spekulace ho ruší. Cokoli, na co se dá spočítat adresa, se může načíst, a to včetně cest, které jsou architektonicky nedosažitelné. U běžné paměti to nic nestojí, u MMIO je to porušení specifikace.
Nastavuj proto NX na všechno, co spustitelné být nemá, i tam, kde žádný útočník není. Není to obrana do hloubky, je to popis toho, jak se paměť smí používat, a procesor ho čte doslova.