Disassembler

Artificial intelligence is no match for natural stupidity.
05leden2018

Meltdown a Spectre - nahlédnutí do útrob procesoru


Pravděpodobně jste již zaregistrovali zveřejnění informací o poměrně závažných lokálních zranitelnostech v procesorech Intel, z nichž ta nejméně závažná postihuje i procesory AMD a ARM. Zranitelnosti jsou vedeny pod čísly CVE-2017-5753, CVE-2017-5715 a CVE-2017-5754 a protože Speculative execution side-channel attack nezní vůbec sexy, přezdívá se jim Spectre a Meltdown.

Přízraky a strašidla


Zranitelnosti byly objeveny a popsány někdy během druhé poloviny loňského roku, ale do nedávna byly drženy pod pokličkou, aby se na jejich odhalení výrobci hardware a operačních systémů mohli patřičně připravit. Datum zveřejnění mělo být až 9. ledna, ale někteří se už nemohli dočkat. Google Project Zero team, což je tým bezpečnostních analytiků pod taktovkou Googlu, šťourající se ve všech možných produktech a odhalujících více či méně závažné problémy, rozlišuje tři varianty popsané zranitelnosti. Všechny tři pramení ze stejného nedostatku a rozlišují se podle rozsahu škod, které mohou napáchat.

Varianty 1 a 2 byly shrnuty pod pseudonym Spectre. Jedná se o ty méně závažné. Varianta 1 umožňuje přečtení dat ze spekulativní větve pouze v rámci stejného procesu, bez překračování jakýchkoliv bezpečnostních mezí. Byla demonstrována na procesorech Intel, AMD i ARM. Potvrzena je na následujících modelových řadách:

Varianta 2 už je pak trochu zajímavější, protože umožňuje „natrénovat“ procesor, resp. branch predictor a postranním kanálem pak krást nacachovaná data privilegovaného procesu. Project Zero team uvádí, že se jim tímto způsobem dařilo z KVM virtuálky leakovat paměť hypervizoru rychlostí cca 1500 bytů za sekundu. Tímto problémem už by procesory ARM a AMD trpět neměly.

No a pak tu máme třetí variantu. A ta je prosím pěkně průser jak mraky, takže si vysloužila vlastní označení „Meltdown“. Tato varianta umožňuje neprivilegovanému procesu ve spekulativní větvi číst data z privilegované části paměti (typicky z paměťového prostoru kernelu) prakticky jakkoliv se mu zachce. S jistotou postihuje všechny Intely uvedené v prvním bodě výše uvedeného výčtu. ARM a AMD je opět z obliga.

Vidím město veliké...


Fajn. Takže už víme, že je něco špatně, protože speculative execution umožňuje udělat něco, co by se rozhodně dát udělat nemělo. Co se v procesoru vlastně děje, že je taková věc možná? Vysvětlení zahájím vousatým vtipem.

Proč je Pentium rychlejší než 486? Protože 486 výsledky počítá, kdežto Pentium je odhaduje!

Tento vtip původně vznikl v reakci na známý FDIV bug, ale pokud jste nerozkousali polštářek trapnosti, tak vám prozradím, že to vlastně vůbec není vtip, ale prosté konstatování toho, jak moderní out-of-order execution procesory fungují. Aby se z procesoru dalo vymačkat maximum výkonu, musí počítat a provádět instrukce i když je zrovna v danou chvíli program nepotřebuje. Když procesor provádí úkon, který z jeho pohledu trvá dlouho – například načtení dat z RAM – tak nečeká, až se data přesypou, aby mohl pokračovat s další instrukcí. Místo toho si instrukce předpřipraví a hezky si je seřadí tak, jak se budou vykovávat, až se data milostivě překlopí do cache. Takovýto řádek v záhonku instrukcí se nazývá pipeline a obsahuje ono „co by, kdyby...“ čemuž říkáme spekulativní spuštění. V případě, že se paměť z jakéhokoliv důvodu nepodaří načíst, vznikne chyba (fault) a tu procesor ohlásí operačnímu systému. Operační systém pak obvykle proces shodí a u toho vyrobí nějaký dump pro následnou analýzu. Pak prostor okupovaný spadenou aplikací vyčistí. Chybná instrukce tím pádem neshodí celý systém, ale jen tu část, která kvůli chybě nemůže pokračovat. Windows 9x v tomhle třeba moc zručné nebyly, takže špatně napsané aplikace uživateli rády dopřávaly pohled na obrazovku plnou uklidňující modré barvy.

Fajn, tohle spekulativní spuštění asi může fungovat, ale jen do doby než se dostaneme k nějakým podmínkám, flow control nebo jinému programovému větvení. A co potom? No, potom začne být Pentium rychlejší než 486, protože začne výsledky odhadovat. Moderní procesory v sobě obsahují šikovný obvod zvaný branch predictor. Branch predictor, podobně jako kněžna Libuše, hledí do budoucnosti a věští, co se bude dít. Není to jen nějaké náhodné tipování, protože branch predictor se zároveň jako Jirásek dívá i do minulosti (a nejspíše i jako Koniáš do díry) a na základě toho, jak byly předchozí větve vyhodnoceny, si skládá heuristickou analýzu toho, jak by asi mohla budoucí větvení dopadnout. Procesor tedy nečeká, až se milá podmínka spočte a vyhodnotí, ale odhadne, že tahle podmínka se asi vyhodnotí tak či onak a narve navazující instrukce do pipeline. Teprve až se flow fakticky dostane k vyhodnocení dané podmínky, procesor koukne, jak to dopadlo. Buď se branch predictor trefil a procesor okamžitě použije ke zpracování nasyslenou pipeline anebo se netrefil a tak musí pipeline uvolnit (unwind) a začít zpracovávat správnou větev a znova načítat patřičná data z pomalé RAM nebo jiných ještě pomalejších koutů počítače. Pokud byste někdy řešili optimalizaci na takto brutálně nízké úrovni, dá se vlastností branch predictoru využít. Program se dá přepsat tak, aby byl jeho běh celkově předvídatelnější a díky tomu rychlejší i za cenu toho, že se zvýší celkový počet řádků a instrukcí. Takové optimalizace při běžném programování pro osobní počítače nebo servery ale rozhodně není potřeba řešit. Také se občas dá branch predictor usvědčit z nějaké hezké a zajímavé anomálie.

Aplikovaný Alzheimer


Vezměme si následující příklad

1. $A = nejaka_funkce()
2. if ($A == 0) {
3.     $B = nacti_pamet(nejaka_adresa)
4.     $C = nacti_pamet($B)
5. }

Procesor, který neimplementuje out-of-order execution a jede hezky pomalu lineárně, nejprve zavolá funkci nejaka_funcke(), její výsledek uloží do proměnné A, tu porovná a pokud je její hodnota rovna nule, začne provádět tělo podmínky. V té na řádku tři načte hodnotu v paměti na adrese nejaka_adresa. Pokud se načtení zadaří, použije tuto hodnotu jako parametr pro další načtení na řádku čtyři. Pokud se ale načtení nepodaří, čtvrtý řádek se nikdy nevykoná a procesor místo toho zahlásí chybu a nechá operační systém, ať vyšle úklidovou četu. Náš moderní procesor s věšteckou jednou takto ale nefunguje. Ten zavolá funkci na prvním řádku, ale nečeká na to, až se mu vrátí návratová hodnota a rovnou pokračuje dál. Odhadne, že podmínka by se mohla vyhodnotit pravdivě a hned začne do proměnné B ládovat data z paměti. Za to hned zařadí řádek čtyři, který ale okamžitě vyhodnotit nemůže, protože je závislý na vrácené hodnotě z předchozího řádku. Pokud je ale nejaka_adresa neplatná, proces místo toho zařadí do pipeline chybu. Znovu připomínám, že v tuhle chvíli ještě vůbec neví, zda se tělo funkce vůbec bude vykovávat, protože předběhl flow a zpracovává instrukce do zásoby. Pokud se náhodou podmínka vyhodnotí nepravdivě, procesor zahodí pipeline a naváže zpracování správným směrem za poslední provedenou instrukcí a k chybě nikdy nedojde.

K chybě přístupu k paměti může dojít ze dvou důvodů. Buď je daná adresa opravdu neplatná a neexistuje, resp. nic na ní není. Anebo je platná, ale aplikace k ní nemá přístup, protože se jedná o privilegovanou paměť vyhrazenou nějakému nadřazenému procesu. Například byste neměli být schopní z běžící aplikace přistoupit k adrese patřící jádru operačního systému nebo byste neměli být schopní z virtuálního stroje přistoupit k adrese patřící hypervizoru. Pokud ale procesor Intel trefí v pipeline na adresu, která sice existuje, ale nemá k ní oprávnění, chybu do pipeline korektně zařadí, ale i přesto pokračuje dál ve zpracování spekulativní větve. To by zřejmě nemělo vadit, protože buď flow k chybě doběhne, skončí a za čtení z privilegované adresy se nikdy pokračovat nebude. Anebo se branch predictor s předpovědí netrefil a daná pipeline se uvolní, aniž by se vůbec kdy prováděla. Nemělo by to vadit, ale vadí to.

Uvolnění pipeline má efekt jen na instrukce a data, které se v pipeline zpracovávají nebo by se zpracovávaly, kdyby na ně došlo. I přesto, že přístupy k paměti, které následují až potom, co procesor zařadil chybu a nejsou tak přímo viditelné pro kód daného programu, jsou viditelné nepřímo postranním kanálem. Každý přístup k paměti má totiž specifickou charakteristiku, například zpomalí všechny ostatní paralelně probíhající přístupy k paměti. K útoku je nejprve potřeba trochu zahřát branch predictor a naučit jej, že jistá větev ve škodlivém programu se vždy vyhodnotí stejným způsobem, nejlépe v nějakém cyklu. Až je branch predictor dostatečně zblbnutý, podstrčí se mu větev, kterou odhadne špatně. Ta bude navržena tak, že v ní dojde ke čtení privilegované paměti, což by normálně mělo za následek chybu. Ve spekulativní větvi na to ale Intel zvysoka kašle a dál frčí jako rozjetý parní válec. Následně se v této větvi bude procesor pokoušet číst z adres, které získal z obsahu prvního kola čtení paměti na adresách, kam by neměl mít přístup. I přesto, že se tento sled instrukcí provede pouze spekulativně, stačí to k tomu, aby byla data vytažená z privilegované části paměti uložena v cache procesoru. Nakonec samozřejmě procesoru dojde, že tentokrát branch predictor neuhodl, pipeline uvolní a bude pokračovat správnou větví programu jako by se nic nestalo. Ale to už je pozdě, protože přesným měřením doby přístupu do paměti je možno zjistit, která cache-line se během pokusu o druhé čtení z paměti naplnila. Tímto způsobem se pak pomaloučku (tedy alespoň vzhledem k rychlostem L1 cache a procesoru) dá vykrást libovolná paměť, do které by proces nikdy neměl dostat přístup.

Co s tím?


Není důvod k panice. Útoky tohoto typu je možno provést pouze lokálně (což vzhledem k blbosti běžných uživatelů sice není až tak limitující, ale pořád to významně zmenšuje attack surface), ale hlavně není možno tímto způsobem podvrhnout nebo upravit data. Je možno je pouze číst. Sice píšu „pouze“, ale zranitelnost je natolik rozsáhlá, že je teoreticky možno JavaScriptem z prohlížeče číst heslo, které uživatel zadává do své klíčenky v úplně jiném procesu pod úplně jiným uživatelským účtem.

Jádro problému je samozřejmě ve špatném návrhu hardwarové architektury. Ta se, jak je procesor jednou vyrobený, opravuje celkem blbě, takže tvůrci operačních systémů přišli s jiným trikem. Jak jsem již říkal, přístup do paměti může být neplatný buď proto, že v paměti nic není anebo proto, že je paměť privilegovaná. Jelikož nad absencí oprávnění Intel mávne rukou, je potřeba z privilegované paměti udělat zároveň paměť neexistující, aby to i natvrdlý Intel pochopil. Jelikož je procesorový čas jednotlivým procesům přidělován plánovačem operačního systému, má OS zároveň i moc nad tím, co daný proces v paměti uvidí. Trik tedy spočívá v tom, vyčistit veškerou privilegovanou paměť předtím než má běžet program, a až skončí a má pokračovat operační systém, paměť zase nastěhovat zpátky. Procesor tedy nebude schopen z privilegované paměti nic přečíst ani ve spekulativní větvi, protože tam prostě žádná není. Logicky tak nemůže ani nic leaknout. Toto opatření se v Linuxu jmenuje Kernel Page Table Isolation (KPTI), ale mohlo se jmenovat i daleko méně politicky korektně.

To ale také znamená, že operační systém implementující tento způsob ochrany bude kvůli přesýpání paměti o chlup pomalejší, než systém běžící na ekvivalentním procesoru, jehož speculative execution neignoruje oprávnění. Jelikož k převzetí řízení dochází během hardwarových a softwarových přerušení, bude zpomalení nejvíce znát na systémech vykonávajících hromadu I/O operací. To jsou typicky různé databázové systémy, hypervizory a NAS. Na domácích počítadlech a mobilních zařízeních by mělo být zpomalení zanedbatelné.

Technické notičky


Níže přikládám snůšku odkazů na technote jednotlivých výrobců procesorů a operačních systémů. Opravu aplikujete prostým updatem operačního systému, je již součástí klasických aktualizací. Pozor ale na systémech Windows. Oprava nebude automaticky aplikována, pokud Antivirový systém nenastaví příznak kompatibility, jinak může dojít k pádu operačního systému. Antivirové programy totiž často využívají různých technik kontroly paměti, které si s touto opravou nesednou. Průběžně aktualizovanou tabulku kompatibility naleznete zde.

Procesory:

Operační systémy:

Další čtení: