Nestačí, protože Spectre útok umožňuje přečíst cokoliv z paměti procesu. Takže javascript spuštěný v prohlížeči může teoreticky přečíst z paměti prohlížeče heslo, které zadávám na úplně jiné stránce. Obecně je nutné znovu přeložit všechno, co zpracovává nějaký nedůvěrýhodný vstup (stdin, soubor, data ze sítě...). Takže prakticky všechno.
Spectre bug není o sahání do paměti jádra, Spectre umožňuje jednomu procesu číst pamět jiného procesu tak, že attacker proces podstrčí victim procesu vhodný (chybný) vstup, který způsobí spekulativní vykonání kódu ve victim procesu (např. čtení mimo meze pole, to se spekulativně vykoná, i když kontrola mezí pole neprojde) a side kanálem si potom attackter proces přečte paměť victim procesu.
Victim process se musí opatchovat (znovu přeložit s patřičnými přepínači GCC), aby takový spekulativní kód (čtení mimo meze pole) nikdy nevykonával.
Mě teda přijde Spectre že funguje jen v rámci procesu (což pořád neeliminuje javascript běžící v prohlížeči). Nějak jsem nepochopil, jak na útočníkově straně najdu tu stránku, kam oběť spekulativně přistoupila, protože máme izolované adresové prostory.
Strana oběti také musí buď obsahovat kód speciálního patternu, který jsem schopen ovládat, byť obsahuje kontrolu rozsahů. nebo má možnost vykonat můj kód, i přesto, že je zabezpečen kontrolou rozsahů. Vše ale v rámci jednoho procesu. Tedy buď do oběti přesunu i měření přístupu na stránky, nebo zase v procesu oběti najdu kód, který mi přístup do mou vybrané stránky umožní a já přitom dokážu změřit, jak dlouho to trvalo
tl;rd - útočník musí mít v procesu oběti zajištěny dvě věci
1) kód na kterým lze exekuovat exploit
2) kód který pak přistupuje na spekulativně natažené stránky v cache se schopnosti měřit čas přístupu.
Bez zaruky na spravnost mojej odpovede ma napadaju veci ako shared memory alebo pamatovo mapovane subory. Neviem sice aky efekt bude mat volanie flush napr. na stranku pamate v ktorej je ulozeny kod zdielanej kniznice, ale ak bude nenulovy, potom je osa utoku v skutocnosti primitivna:
1) loadnut si kniznicu, ktoru loaduje aj obet, ziskat memory mapping tej kniznice v pamati obete (to moze byt netrivialne, pripustam)
2) flushnut stranku zodpovedajucu nejakej deterministickej adresy v zavislosti od kodu, ktory sposobi side-efekt
3) donutit obet vykonat kod, ktory sposobi side-efekt
4) zistit, ci je stranka v cache, alebo nie
Problemom moze byt, ze ak sa bude flushovat nejaka often used stranka, tento proces bude mat asi znacnu chybovost vystupu, to sa ale da vzdy poladit.
Spectre v principu funguje i mezi oddělenými procesy, stačí k tomu, aby se ve victim procesu nacházel např. nějaký takový kód:
if (x < array1_size)
y = array2[array1[x] * 256];
Victim proces se donutí, aby spekulativně provedl přístup do paměti s x větším než array1_size a přečtená paměť zůstane v cache. Potom stačí uvedený kód zavolat znovu s náhodným x menším než array1_size. Pokud je to rychlé (je to v cache od minula ze spekulativního čtení), tak vím že jsem se trefil a znám hodnotu paměti array1[prvni_x_mimo_meze]. Pokud to trvá dlouho, tak jsem se netrefil, vyprázdním cache a zkouším to znovu. Není potřeba sdílená paměť ani nic dalšího speciálního.
Tohle je jen jeden z mnoha možných vektorů útoku, možností jak zkonstruovat postranní kanál je mnohem víc. Hezky je to popsáno ve Spectre paperu.
Nepotřebuju žádná složitá sychronizační primitiva, stačí atomic proměnná, která sice způsobí cache miss u jiných CPU jader, ale projeví se to jenom na konktétní cache line, ke které victim proces nepřistupuje. Pro tento typ útoku více než dostatečné. Nicméně je to stejně hypotetická debata, možností jak přesně měřit čas je spousta.
Spectre právě sandboxing umí obejít. Ten buffer overflow nastane jen spekulativně, např. v korektním kódu:
char a[10];
if (b < 10) {
c = a[b];
}
Nikdy teoreticky přístup mimo meze pole nemůže nastat. Ale CPU začně výraz c = a[b] spekulatvně provádět ještě předtím, než zná výsledek testu if (b < 10). Takže fyzicky proběhne čtení mimo meze pole. Ten výsledek se zahodí hned potom, co CPU vyhodnotí podmínku (b < 10) jako false, ale to už je pozdě, protože spekulativní vykonávání čtení mimo meze pole už ovlivnilo vnitřní stav procesoru, mohla se např. změnit cache v závislosti na výsledku a[b], což může být možné side kanálem přečíst a vyhodnotit z jiného procesu.
Jenze tady se zadnej kod z pohledu aplikace nevykona, on se vykona jen z pohledu CPU, kterej to proste zkusi, nacez se zjisti, ze to padne/nejsou na to prava/neni splnena podminka... ale vysledek uz je stejne v cache.
Jinak receno, kdyz mas v kodu neco jako
if () a+b else a*b
Tak se bez ohledu na vysledek podminky spocita oboji (proto aby po vyhodnoceni podminky byl uz vysledek hotovej a necekalo se) a oba vysledky lze ziskat, coz je prave ten pruser. A viz lopata, mezi CPU a RAM neni zadnej plot, CPU proste muze cist kdekoli cokoli. Takze ti i nacte casti pameti ktery lezej mimo hranice kam ma pristup aplikace. Ty sice vysledek neziskas primo, ale muzes si ho najit v cache. Jednoduse proto, ze CPU proste ty data nacet, co kdyby.
> proč nestačí to cache invalidovat vždy jen v takovém případě
Protože tím vytvoříš úplně stejný side channel - kvůli omezené asociativitě vyhodíš nějaký jiný (dost konkrétní) řádek a útočníkovi pak stačí zjistit, který řádek se nenacachoval (oproti současnému, kdy zjišťuje, který řádek se nacachoval).
Podle mě to má jediné řešení, i při prefetchingu kontrolovat oprávnění. Netuším, jaký to bude mít dopad na výkon.
Ta výjimka se nevyvolá. Útočník natrénuje optimalizátor CPU, že podmínka:
if (a < 10) b = pole1[pole2[a]];
má očekávat a < 10, a CPU pak bude spekulativně číst stránku, kam ukazuje pole1[pole2[a]]
. Pak útočníkovi stačí předat vyšší a, CPU to spekulativně vykoná (je natrénované, že v naprosté většině případů a < 10), načte stránku, kam by to sáhlo, ale výjimku nevyvolá, protože to se testuje až při commitu (jinak by to házelo výjimky pořád). Při commitu if (a < 10)
se ale zjistí, že se to nemělo vykonávat, a tak se vykonaný spekulativní kód zahodí. Jenže ta stránka s adresou odpovídající hodnotě na pole2[a]
(což může odkazovat na bajt kdekoliv v adresovatelné paměti) už v cache zůstane a přítomnost té stránky lze detekovat.
Spectre nedokáže číst paměť jiného procesu, ale jádra či v rámci jednoho procesu. Procesy mají oddělené mapování, nelze z jednoho procesu testovat adresní rozsah jiného, a to ani spekulativně. Jádro je ale namapované do paměti každého procesu, a je sice chráněné ringy, takže nelze číst přímo, ale lze vyvolat spekulativní spuštění kódu v jádře, což ringy nekontroluje, vhodný kód natáhne do cache stránku s adresou odpovídající bajtu paměti, kterou chceme přečíst, a která stránka byla natažena, lze odvodit, protože čtení takové stránky bude oproti ostatním velmi rychlé (pro konkrétní bajt je již v cache, pro ostatní ne). U sandboxu řešeného kontrolou přístupů k paměti (typicky hromadou if
ů) lze takto vyvolat spekulativní čtení mimo sandbox, které to sice neprovede (ten if
selže a třeba to vyhodí výjimku), ale opět se tím natáhne do cache stránku s adresou odpovídající nějakému bajtu.
V jednom procesu nejsou virtuální adresy mapovány na celou RAM, ale jen na stránky, ke kterým má ten proces přístup, a jádro. Při přepínání procesů se pak mění mapování virtuálních stránek. Mapovat to jinak ani nejde, protože v rámci mapování stránek se stránky oddělují jen přes ringy a všechny procesy běží ve stejném ringu, takže pokud by byla mapována paměť víc procesů, buď by při každém přepínání muselo jádro přepisovat ringy v té mapě (což je hrozně pomalé, když stačí jen změnit ukazatel na mapu) nebo by procesy mohly lézt do stránek ostatních procesů vždy.
Z pohledu distra urcite ne, ale z pohledu uzivatelu to asi celkem ranec bude, protoze pokud to pustej vsechno najednou, bude to kompleti reinstal vseho. A pak je jeste otazka, jestli rekompilujou vsechny vydany verze, protoze nekdo muze pouzivat nejakou konkretni kvuli nejakymu chovani/bugu/... v novejsi.
Řešení v budoucnu?
Je otázkou, jestli spekulativní exekuce by měla pokračovat v případě, že je třeba čekat na data z main memory. Si říkám, že tam nějaké velké urychlení nebude. Jsou to stovky cyklů. Prostě pokud se při spekulativním provádění narazí na čtení z paměti, která nemá v cache, tak se fronta zastaví, dokud není rozhodnuto, jakým dalším směrem bude kód pokračovat. Tohle omezení lze software obejít tak, že spekulativní čtení paměti zařídím v kódu (prostě načtu i ty proměnné, které pak nejsou použity)
Jenomže právě tím spekulativní čtením dopředu se dosahuje značného zrychlení zpracování, a prostě se to v té cache nestačí zneplatnit nebo se to neřeší správně a ta data tam zůstávají, i když by tam zůstat neměla.
Obávám se, že jde o problém hluboko v návrhu a to nejde snadno odstranit/obejít patchem v mikrokodu
Ta data tam zustanou jednoduse proto, ze prepis = snizeni vykonu cache na 1/2. Coz ostatne ukazujou i ty workaroundy, ktery vedou na -30% vykonu celyho CPU prave tim, ze tu cache mazou. Coz je teda asi vyrazne pomalejsi nez kdyby si to resil primo CPU per zaznam, ale i tak bys mel trebas -10%.
RAM žádnou cache nemá, jede rychlostí FSB a rychleji už s procesorem komunikovat nemůže.
L3/L4 cache je už teď částečně transakční, stačí přidat možnost mít dočasně načtená data, která jiná jádra nevidí. Pokud by byla plně ACID, tak by tam byl side-channel, že při spekulativním běhu bude čtení z jiného jádra trvat trochu déle (než se spekulativní běh zahodí a paměťová transakce rollbackne), ale nemyslím si, že by to vůbec bylo měřitelné (musíte se trefit přesně do spekuliativního běhu a měřit extrémně přesně, jde o pár taktů, než CPU vyhodnotí ten if
, ne několik set jako u čtení RAM), a navíc to jde řešit ignorováním atomicity (každé jádro bude mít svoji kopii stejné stránky v dočasné L3 a při commitu se bez kontroly přepíší), konkurenční přístup bez memory barrier (které to celé serializují, takže to nejde měřit) stejně nemá zaručený výsledek.
Nebo to jde řešit tak, že se načítá přímo do L2 a do L3/L4 se to zapíše až při commitu.
Záleží na implementaci transakcí. Ono by mohlo stačit si spekulativně poznačit changelog a jen v případě commitu to zapsat do cache. Otázkou jsou race conditions ti sdílené cache (jádro X by načetlo kus paměti, jádro Y by na totéž místo zapsalo, jádro X by následně commitnulo "novou" hodnotu do sdílené cache), ale to by asi bylo řešitelné, třeba skrze nějaké monotoní hodiny.
Pořád to ale neřeší fakt, že se spekulativně přistupuje do RAM, jen to řeší cache. Mohl by tu být nějaký jiný side channel.
Ten konkurenční přístup jsem právě uváděl, bez memory barrier stejně není zaručené, jak se to poskládá, takže klidně může X načíst starou hodnotu a přepsat novou hodnotou Y, s memory barrierami se serializuje přístup k celé paměti, takže nejde detekovat konkrétní stránku, kam spekulace sáhla.
Hypoteticky můžete nějakým side-channelem přečíst, co se posílá z RAM do cache, a máte přístup ke všem datům na systému. Vymýšlet zabezpečení proti takovým hrozbám ale není moc přínosné, když chybí teorie, jak by se to mělo udělat, takže se nemáte proti čemu bránit.
> Ten konkurenční přístup jsem právě uváděl, bez memory barrier stejně není zaručené, jak se to poskládá
A jste si jist, že je na x86 OK nevidět vlastní zápis? Protože přesně to by se mohlo stát: něco zapíšu, ale protože při tom jiné jádro z téhož místa četlo, po čase (až to vypadne z cache, která je per-core) uvidím původní hodnotu, která bude ve sdílené cache. A po čase třeba uvidí zpět hodnotu, kterou zapsal.
Neznám do detailů memory model x86, ale intuitivně toto asi nebude korektní chování.
> Hypoteticky můžete nějakým side-channelem přečíst, co se posílá z RAM do cache
O přímém přečtení samotných dat jsem ale nepsal, „pouze“ o zjištění, kam se (spekulativně) přistupuje. To mi nepřijde až tak nereálné. Vzpomněl jsem si na bank conflict. OK, to není x86, ale Nvidia CUDA, ale se znalostí bližších detailů x86 by se mohlo něco najít i tady.
> Vymýšlet zabezpečení proti takovým hrozbám ale není moc přínosné, když chybí teorie, jak by se to mělo udělat, takže se nemáte proti čemu bránit.
Místo toho můžeme teoretické hrozby ignorovat a čekat, než se z nich stanou praktické. Jak se už nejednou stalo. Ostatně u branch prediction mě kdysi napadlo, že jde o teoretický problém, akorát jsem neměl nápad, jak útok převést do praxe. A je tu spousta jiných útoků, které byly původně označeny za čistě teotetické problémy. Třeba CRIME/BREACH byla často vytýkána teotetičnost, ale – zvlášt v kombinaci s HEIST – se to stává mocným nástrojem.
Je to OK, pokud do stejné cache line bude zapisovat více jader bez synchronizace, není výsledek definovaný. To, že neuvidíte svůj zápis, je ještě poměrně konzistentní stav, můžete se stát, že jedno číslo bude mít některé bajty z jednoho vlákna a zbylé z druhého (podle toho, jak se to zrovna seřadí na sběrnici).
Jaký teoretický problém vás napadl u branch prediction (při branch prediction by mohlo jít …)? Nebo to spíš obecné varování, že to může být nebezpečné (branch prediction by mohlo způsobovat problémy, ale nemám nejmenší tušení, jak a kde)? Proti tomu prvnímu se dá bránit a nepotřebujete praktický útok, velká část mechanizmů v kryptografii je založena na teoretických útocích. Proti tomu druhému se bránit nedá, protože je to tak obecné, že jediné řešení je tu technologii vůbec nepoužít.
Ale já nepsal o situaci, kdy na jedno místo zapisuje více vláken bez synchronizace. Já psal o situaci, kdy na to místo jedno vlákno zapisuje a jiné jedno (nebo více) čte.
Neříkám, že jde o typický scénář korektního kódu. Za současné situace může čtecí vlákno přečíst ledacos, což typicky není žádaný výsledek. Nově by to ale mohlo mít vliv i na zapisovací vlákno.
Kde by to mohlo mít vliv:
* Sběr entropie zahashováním paměti. OK, je to punkové řešení, které už teď má potenciální problémy, ale tímto byste přidal nové úplně jiného druhu.
* Buffer overflow při čtení by mohlo mít dopad i na obsah paměti.
* Memory dump neatomickým způsobem (za běhy programu) by mohl ovlivnit chod programu.
Uznávám, nic ze zmíněných příkladů se netýká vzorně napsaného kódu, ale zvyšuje to dopady některých implementačních nebo návrhových nedostatků mimo tradiční meze. A dost možná by to změnilo memory model na něco, co není kompatibilní se současnou specifikací.
K branch prediction: Napadlo mě, že spekulativní vykonání nějaké větve může mít vliv na cache, což by se dalo zneužít k nějakému side channelu. Nevěnoval jsem tomu pozornost. Teď ale někdo vymyslel, jak to dotáhnout do konce.
Tenhle poslední krok mi taky nebyl moc jasný. Princip je jednoduše vysvětlený pomocí pseudokódu v komentáři od Jendy na abíčku: http://www.abclinuxu.cz/zpravicky/spectre-a-meltdown-bezpecnostni-chyby-v-procesorech