selvom jeg i disse dage mest er kendt for netværk på applikationsniveau og distribuerede systemer, tilbragte jeg den første del af min karriere med at arbejde med operativsystemer og hypervisorer. Jeg opretholder en dyb fascination af de lave detaljer om, hvordan moderne processorer og systemprogrammer fungerer. Da den nylige nedsmeltning og Spectre-sårbarheder blev annonceret, Jeg gravede ind i de tilgængelige oplysninger og var ivrig efter at lære mere.
sårbarhederne er forbløffende; Jeg vil hævde, at de er en af de vigtigste opdagelser inden for datalogi i de sidste 10-20 år. Afbødningerne er også vanskelige at forstå, og nøjagtige oplysninger om dem er svære at finde. Dette er ikke overraskende i betragtning af deres kritiske karakter. Afbødning af sårbarhederne har krævet måneder med hemmelighedsfuldt arbejde af alle de store CPU -, Operativsystem-og cloud-leverandører. Det faktum, at problemerne blev holdt under omslag i 6 måneder, hvor bogstaveligt talt hundreder af mennesker sandsynligvis arbejdede på dem, er forbløffende.
selvom der er skrevet meget om nedsmeltning og Spectre siden deres meddelelse, har jeg ikke set en god introduktion på mellemniveau til sårbarhederne og afbødningerne. I dette indlæg vil jeg forsøge at rette op på det ved at give en blid introduktion til den udstyrs-og programbaggrund, der kræves for at forstå sårbarhederne, en diskussion af sårbarhederne selv, samt en diskussion af de aktuelle afbødninger.
vigtig note: fordi jeg ikke har arbejdet direkte på afbødningerne og ikke arbejder hos Intel, Microsoft, Google, , Red Hat osv. nogle af de detaljer, som jeg vil give, er muligvis ikke helt nøjagtige. Jeg har samlet dette indlæg baseret på min viden om, hvordan disse systemer fungerer, offentligt tilgængelig dokumentation og patches/diskussion sendt til LKML og KSEN-devel. Jeg vil meget gerne blive rettet, hvis noget af dette indlæg er unøjagtigt, selvom jeg tvivler på, at det snart vil ske, hvor meget af dette emne der stadig er dækket af NDA.
i dette afsnit vil jeg give nogle baggrund kræves for at forstå sårbarhederne. Afsnittet dækker over en stor mængde detaljer og er rettet mod læsere med en begrænset forståelse af computerudstyr og systemprogrammer.
virtuel hukommelse
virtuel hukommelse er en teknik, der bruges af alle operativsystemer siden 1970 ‘ erne. Det giver et lag af abstraktion mellem hukommelsesadresselayoutet, som de fleste programmer ser, og de fysiske enheder, der understøtter den hukommelse (RAM, diske osv.). På et højt niveau giver det applikationer mulighed for at bruge mere hukommelse, end maskinen faktisk har; dette giver en kraftig abstraktion, der gør mange programmeringsopgaver lettere.
figur 1 viser en forenklet computer med 400 bytes hukommelse lagt ud i “sider” på 100 bytes (rigtige computere bruger kræfter på to, typisk 4096). Computeren har to processer, hver med 200 bytes hukommelse på 2 sider hver. Processerne kører muligvis den samme kode ved hjælp af faste adresser i 0-199 byte-området, men de understøttes af diskret fysisk hukommelse, så de ikke påvirker hinanden. Selvom moderne operativsystemer og computere bruger virtuel hukommelse på en væsentligt mere kompliceret måde end hvad der præsenteres i dette eksempel, gælder den grundlæggende forudsætning, der er præsenteret ovenfor, i alle tilfælde. Operativsystemer abstraherer de adresser, som applikationen ser fra de fysiske ressourcer, der understøtter dem.
oversættelse af virtuelle til fysiske adresser er en så almindelig operation i moderne computere, at hvis operativsystemet skulle være involveret i alle tilfælde, ville computeren være utrolig langsom. Moderne CPU-udstyr giver en enhed kaldet en oversættelse Lookaside Buffer (TLB), der cacher nyligt anvendte kortlægninger. Dette gør det muligt for CPU ‘ er at udføre adresseoversættelse direkte i udstyr det meste af tiden.
figur 2 viser adresseoversættelsesstrømmen:
et program henter en virtuel adresse.
CPU ‘ en forsøger at oversætte den ved hjælp af TLB. Hvis adressen findes, bruges oversættelsen.
hvis adressen ikke findes, konsulterer CPU ‘ en et sæt “sidetabeller” for at bestemme kortlægningen. Sidetabeller er et sæt fysiske hukommelsessider, der leveres af operativsystemet et sted, hvor udstyret kan finde dem (f.eks. Sidetabeller kortlægger virtuelle adresser til fysiske adresser og indeholder også metadata såsom tilladelser.
hvis sidetabellen indeholder en kortlægning, returneres den, cachelagres i TLB ‘ en og bruges til opslag. Hvis sidetabellen ikke indeholder en kortlægning, hæves en” sidefejl ” til operativsystemet. En sidefejl er en særlig form for afbrydelse, der gør det muligt for operativsystemet at tage kontrol og bestemme, hvad der skal gøres, når der mangler eller ugyldig kortlægning. For eksempel kan operativsystemet afslutte programmet. Det kan også tildele nogle fysiske hukommelse og kortlægge det i processen. Hvis en sidefejlbehandler fortsætter udførelsen, vil den nye kortlægning blive brugt af TLB.
figur 3 viser et lidt mere realistisk billede af, hvordan virtuel hukommelse ser ud i en moderne computer (pre-nedsmeltning — mere om dette nedenfor). I denne opsætning har vi følgende funktioner:
Kernelhukommelse vises med rødt. Det er indeholdt i fysisk adresseområde 0-99. Kernelhukommelse er speciel hukommelse, som kun operativsystemet skal have adgang til. Brugerprogrammer bør ikke kunne få adgang til det.
brugerhukommelse vises i gråt.
ikke-allokeret fysisk hukommelse vises med blåt.
i dette eksempel begynder vi at se nogle af de nyttige funktioner i virtuel hukommelse. Primært:
brugerhukommelse i hver proces er i det virtuelle interval 0-99, men bakkes op af forskellige fysiske hukommelse.
Kernelhukommelse i hver proces er i det virtuelle interval 100-199, men bakket op af den samme fysiske hukommelse.
som jeg kort nævnt i det foregående afsnit, har hver side tilknyttede tilladelsesbit. Selvom kernehukommelse er kortlagt i hver brugerproces, når processen kører i brugertilstand, kan den ikke få adgang til kernehukommelsen. Hvis en proces forsøger at gøre det, vil det udløse en sidefejl, på hvilket tidspunkt operativsystemet afslutter det. Men når processen kører i kernetilstand (for eksempel under et systemopkald), tillader processoren adgangen.
på dette tidspunkt vil jeg bemærke, at denne type dobbelt kortlægning (hver proces, der har kernen kortlagt direkte i den) har været standardpraksis i operativsystemdesign i over tredive år af ydeevneårsager (systemopkald er meget almindelige, og det ville tage lang tid at omforme kernen eller brugerrummet ved hver overgang).
CPU cache topologi
det næste stykke baggrundsinformation, der kræves for at forstå sårbarhederne, er CPU og cache-topologi for moderne processorer. Figur 4 viser en generisk topologi, der er fælles for de fleste moderne CPU ‘ er. Den består af følgende komponenter:
den grundlæggende enhed for udførelse er “CPU-tråden” eller “maskintråd” eller “hyper-tråd.”Hver CPU-tråd indeholder et sæt registre og evnen til at udføre en strøm af maskinkode, ligesom en programtråd.
CPU-tråde er indeholdt i en “CPU-kerne.”De fleste moderne CPU’ er indeholder to tråde pr.
moderne CPU ‘ er indeholder generelt flere niveauer af cachehukommelse. Cache-niveauerne tættere på CPU-tråden er mindre, hurtigere og dyrere. Jo længere væk fra CPU ‘ en og tættere på hovedhukommelsen cachen er, jo større, langsommere og billigere er den.
typisk moderne CPU-design bruger en L1 / L2-cache pr. Det betyder, at hver CPU tråd på kernen gør brug af de samme caches.
flere CPU-kerner er indeholdt i en “CPU-pakke.”Moderne CPU’ er kan indeholde op til 30 kerner (60 tråde) eller mere pr.
alle CPU-kernerne i pakken deler typisk en L3-cache.
CPU-pakker passer ind i ” stikkontakter.”De fleste forbrugercomputere er single socket, mens mange datacenter-servere har flere stikkontakter.
spekulativ udførelse
det sidste stykke baggrundsinformation, der kræves for at forstå sårbarhederne, er en moderne CPU-teknik kendt som “spekulativ udførelse.”Figur 5 viser et generisk diagram over eksekveringsmotoren inde i en moderne CPU.
den primære afhentning er, at moderne CPU ‘ er er utroligt komplicerede og ikke blot udfører maskininstruktioner i rækkefølge. Hver CPU-tråd har en kompliceret rørledningsmotor, der er i stand til at udføre instruktioner ude af drift. Årsagen til dette har at gøre med caching. Som jeg diskuterede i det foregående afsnit, bruger hver CPU flere niveauer af caching. Hver cache-miss tilføjer en betydelig delay-tid til programudførelse. For at afbøde dette er processorer i stand til at udføre fremad og ude af drift, mens de venter på hukommelsesbelastninger. Dette er kendt som spekulativ udførelse. Følgende kodestykke viser dette.
if (x < array1_size) { y = array2 * 256]; }
forestil dig, atarray1_size ikke er tilgængelig i cache, men adressen tilarray1 er. CPU ‘ en kan gætte (spekulere), at x er mindre end array1_size og gå videre og udfør beregningerne inde i if-sætningen. Når array1_size læses fra hukommelsen, kan CPU ‘ en bestemme, om den gættede korrekt. Hvis det gjorde det, kan det fortsætte med at have sparet en masse tid. Hvis det ikke gjorde det, kan det smide de spekulative beregninger og starte forfra. Dette er ikke værre, end hvis det havde ventet i første omgang.
en anden type spekulativ udførelse er kendt som indirekte gren forudsigelse. Dette er ekstremt almindeligt i moderne programmer på grund af virtuel forsendelse.
class Base { public: virtual void Foo() = 0; };class Derived : public Base { public: void Foo() override { … } };Base* obj = new Derived; obj->Foo();
(kilden til det forrige uddrag er dette indlæg)
den måde, hvorpå det forrige uddrag implementeres i maskinkode, er at indlæse “v-table” eller “virtual dispatch table” fra hukommelsesplaceringen, somobj peger på og derefter kalder det. Fordi denne operation er så almindelig, har moderne CPU ‘ er forskellige interne caches og vil ofte gætte (spekulere), hvor den indirekte gren vil gå og fortsætte udførelsen på det tidspunkt. Igen, hvis CPU ‘ en gætter korrekt, kan den fortsætte med at have sparet en masse tid. Hvis det ikke gjorde det, kan det smide de spekulative beregninger og starte forfra.
nedsmeltningssårbarhed
Når vi nu har dækket alle baggrundsoplysningerne, kan vi dykke ned i sårbarhederne.
Rogue data cache load
den første sårbarhed, kendt som nedsmeltning, er overraskende enkel at forklare og næsten triviel at udnytte. Udnyttelseskoden ser stort set ud som følgende:
1. uint8_t* probe_array = new uint8_t; 2. // ... Make sure probe_array is not cached 3. uint8_t kernel_memory = *(uint8_t*)(kernel_address); 4. uint64_t final_kernel_memory = kernel_memory * 4096; 5. uint8_t dummy = probe_array; 6. // ... catch page fault 7. // ... determine which of 256 slots in probe_array is cached
lad os tage hvert trin ovenfor, beskrive hvad det gør, og hvordan det fører til at kunne læse hukommelsen på hele computeren fra et brugerprogram.
i den første linje tildeles et “probe array”. Dette er hukommelse i vores proces, der bruges som en sidekanal til at hente data fra kernen. Hvordan dette gøres, vil snart blive tydeligt.
efter tildelingen sørger angriberen for, at ingen af hukommelsen i probearrayet er cachelagret. Der er forskellige måder at opnå dette på, hvoraf den enkleste inkluderer CPU-specifikke instruktioner til at rydde en hukommelsesplacering fra cache.
angriberen fortsætter derefter med at læse en byte fra kernens adresserum. Husk fra vores tidligere diskussion om virtuel hukommelse og sidetabeller, at alle moderne kerner typisk kortlægger hele kernens virtuelle adresserum i brugerprocessen. Operativsystemer er afhængige af, at hver sidetabelindgang har tilladelsesindstillinger, og at brugertilstandsprogrammer ikke har adgang til kernehukommelse. Enhver sådan adgang vil resultere i en sidefejl. Det er faktisk, hvad der i sidste ende vil ske i trin 3.
moderne processorer udfører imidlertid også spekulativ udførelse og vil udføre forud for fejlinstruktionen. Således kan trin 3-5 udføres i CPU ‘ ens rørledning, før fejlen hæves. I dette trin multipliceres byten af kernehukommelse (som varierer fra 0-255) med systemets sidestørrelse, som typisk er 4096.
i dette trin bruges den multiplicerede byte af kernehukommelse derefter til at læse fra sondearrayet til en dummy-værdi. Multiplikationen af byte med 4096 er at undgå en CPU-funktion kaldet “prefetcher” fra at læse flere data, end vi ønsker i cachen.
ved dette trin har CPU ‘ en indset sin fejl og rullet tilbage til trin 3. Resultaterne af de spekulerede instruktioner er dog stadig synlige i cache. Angriberen bruger operativsystemfunktionalitet til at fange den fejlagtige instruktion og fortsætte udførelsen (f.eks.
i trin 7 gentager angriberen sig og ser, hvor lang tid det tager at læse hver af de 256 mulige bytes i sondearrayet, der kunne have været indekseret af kernehukommelsen. CPU ‘ en har indlæst en af placeringerne i cachen, og denne placering indlæses væsentligt hurtigere end alle de andre placeringer (som skal læses fra hovedhukommelsen). Denne placering er værdien af byte i kernehukommelsen.
Ved hjælp af ovenstående teknik og det faktum, at det er standardpraksis for moderne operativsystemer at kortlægge al fysisk hukommelse i kernens virtuelle adresserum, kan en angriber læse hele computerens fysiske hukommelse.
nu undrer du dig måske: “du sagde, at sidetabeller har tilladelsesbit. Hvordan kan det være, at brugertilstandskoden var i stand til spekulativt at få adgang til kernehukommelse?”Årsagen er, at dette er en fejl i Intel-processorer. Efter min mening er der ingen god grund, ydeevne eller på anden måde, for at dette er muligt. Husk, at al virtuel hukommelsesadgang skal ske via TLB. Det er let muligt under spekulativ udførelse at kontrollere, at en cachelagret kortlægning har tilladelser, der er kompatible med det aktuelle kørende privilegieniveau. Intel gør det simpelthen ikke. Andre processorleverandører udfører en tilladelseskontrol og blokerer spekulativ udførelse. Så vidt vi ved, er nedsmeltning således en Intel-sårbarhed.
Rediger: det ser ud til, at mindst en ARM-processor også er modtagelig for nedsmeltning som angivet her og her.
nedsmeltningsbegrænsninger
nedsmeltning er let at forstå, trivielt at udnytte og har heldigvis også en relativt ligetil afbødning (i det mindste konceptuelt — kerneludviklere er muligvis ikke enige om, at det er ligetil at implementere).
kernel page table isolation (KPTI)
Husk at i afsnittet om virtuel hukommelse beskrev jeg, at alle moderne operativsystemer bruger en teknik, hvor kernelhukommelse er kortlagt i hver brugertilstandsproces virtuel hukommelsesadresserum. Dette er for både ydeevne og enkelhed grunde. Det betyder, at når ET program foretager et systemopkald, er kernen klar til brug uden yderligere arbejde. Løsningen til nedsmeltning er ikke længere at udføre denne dobbelte kortlægning.