- COMP.SEC.100
- 12. Käyttöjärjestelmät ja virtualisointi
- 12.3 Käyttöjärjestelmien rooli turvallisuudessa
Käyttöjärjestelmien rooli turvallisuudessa¶
Käyttöjärjestelmien ja hypervisorien tehtävänä on – ohjelmistopinon alimpina kerroksina – hallita tietojenkäsittelylaitteen resursseja ja luoda perusta, jolle voidaan rakentaa turvallisia sovelluksia. Tärkein tehtävä turvallisuuden kannalta on eristää suojausalueet ja välittää (tarkistaa) kaikki eristyksen läpäisyä mahdollisesti vaativat toiminnot. Ihannetapauksessa käyttöjärjestelmä suojaa kutakin prosessia kaikilta muilta prosesseilta. Esimerkiksi oheislaitteiden prosessit eivät saa päästä käsiksi ensisijaiseksi määrätylle prosessille P varattuun muistiin, oppia mitään P:hen liittyvistä toiminnoista paitsi niistä, jotka P päättää paljastaa, eikä rajoittaa P:tä käyttämästä sille allokoituja resursseja, kuten CPU-aikaa. Jotkin käyttöjärjestelmät voivat jopa säädellä tietovirtoja siten, että salaisiksi luokiteltuja tietoja ei koskaan pääse vuotamaan prosesseihin, joilla ei ole asianmukaisia oikeuksia, tai prosessit eivät voi muokata luokiteltuja tietoja ilman asianmukaisia oikeuksia. Nämä kaksi ovat muuten monitasoisen turvallisuuden (multilevel security, MLS) pääperiaatteet. Eristämisen yksityiskohtia tarkastellaan myöhemmässä luvussa. Tässä jatketaan siitä, miten käyttöjärjestelmän tyyppi vaikuttaa turvallisuuteen.
Käyttöjärjestelmän suunnitteluun on monia tapoja. Varhaiset käyttöjärjestelmät toimivat samalla ja ainoalla suojausalueella sovellusten kanssa, eikä minkäänlaista eristystä ollut. Monet sulautetut järjestelmät toimivat nykyään tällä tavalla. Sovellus voi vahingoittaa tiedostojärjestelmän, verkkopinon, ohjainten tai minkä tahansa muun järjestelmän osan toimintaa.
Nykyaikaisten yleiskäyttöisten käyttöjärjestelmien koodista suurin osa, mutta ei kaikki, sijaitsee yleensä yhdellä suojausalueella, tiukasti erillään sovelluksista. Jokainen sovellus on myös eristetty kaikista muista sovelluksista. Tällainen on esimerkiksi Windowsin, Linuxin, OS X:n ja monien alkuperäisen UNIXin jälkeläisten rakenne. Koska lähes kaikki käyttöjärjestelmän komponentit toimivat yhdellä suojausalueella, malli on erittäin tehokas. Komponentit ovat vuorovaikutuksessa yksinkertaisesti funktiokutsujen ja jaetun muistin avulla. Malli on myös turvallinen niin kauan kuin jokainen komponentti on kunnossa. Jos hyökkääjät onnistuvat vaarantamaan yhdenkin komponentin, kuten ohjaimen, kaikki suojaus mitätöityy. Yleensä laiteajurit ja muut käyttöjärjestelmän laajennukset (esim. Linuxin ydinmoduulit) ovat tärkeitä turvallisuuden kannalta. Usein kolmansien osapuolien kirjoittamat ja käyttöjärjestelmän ydinkoodia viallisemmat laajennukset voivat vaarantaa järjestelmän turvallisuuden kokonaan.
Tällaisissa järjestelmissä nykyään ytimen ja muiden suojausalueiden välinen raja on aiempaa sumeampi, koska käyttöjärjestelmän ytimen ulkopuoliset prosessit voivat ohittaa ytimen esimerkiksi nopeassa verkkoyhteydessä tai toteuttaa käyttäjäprosesseina suorituskyvyn kannalta ei-kriittisiä komponenttejaan. Esimerkkejä ovat File System in User Space (FUSE) UNIX-käyttöjärjestelmissä ja User Mode Driver Framework (UMDF) Windowsissa. Siitä huolimatta suurin osa käyttöjärjestelmän toiminnoista muodostaa edelleen yhden monoliittisen suojausalueen.
Hyvin erilainen suunnitteluvaihtoehto on monipalvelinkäyttöjärjestelmä, jonka kaikki komponentit eristetään omiin prosesseihinsa ja pohjalla on hyvin suppea mikroydin (lähinnä vuorontaja). Tällainen rakenne ei ole yhtä tehokas kuin monoliittinen malli, koska kaikki käyttöjärjestelmän komponenttien väliset vuorovaikutukset sisältävät aina prosessien välistä viestintää (Inter-Process Communication, IPC). Lisäksi monipalvelinjärjestelmä toimii kuin hajautettu järjestelmä, ja sellaisten ongelmat voivat muodostua hyvin monimutkaisiksi. Rakenteen etuna on kuitenkin se, että esimerkiksi haavoittuvainen ohjain ei voi niin helposti vaarantaa järjestelmän muuta osaa. Lisäksi, vaikka käsitteellisestä näkökulmasta katsottuna monipalvelin näyttää hajautetulta järjestelmältä, suuri osa todellisen hajautetun järjestelmän monimutkaisuudesta johtuu epäluotettavasta tiedonsiirrosta, jota ei ole olemassa monipalvelinjärjestelmissä. Yleinen näkemys on, että mikroydinpohjaisilla monipalvelinmalleilla on turvallisuus- ja luotettavuusetuja monoliittisiin ja yhden suojausalueen malleihin verrattuna, mutta turvallisuuden hintana on jonkin verran korkeammat yleiskustannukset.
On neljäskin mahdollisuus, “kirjastokäyttöjärjestelmä”. Siinä hyvin suppean ytimen päällä toimii kutakin sovellusta varten oma kokoelma (kirjasto) juuri sen sovelluksen tarvitsemia käyttöjärjestelmäpalveluja. Tämän rakenteen avulla sovellukset voivat jättää pois kaikki toiminnot, joita ne eivät kuitenkaan käyttäisi. Kirjastokäyttöjärjestelmiä ehdotettiin ensimmäisen kerran 1990-luvulla (esim. MIT:n Exokernel- ja Cambridgen Nemesis-projekteissa). Muutaman vuoden hiljaiselon jälkeen niistä on tulossa jälleen suosittuja – varsinkin virtualisoiduissa ympäristöissä, joissa niitä kutsutaan yleisesti Unikerneliksi. Turvallisuuden suhteen Unikerneleissä ei mikroydinpohjaisiin monipalvelinjärjestelmiin verrattuna ole yhtä hyvää käyttöjärjestelmän komponenttien eristystä. Toisaalta niissä (kirjasto-)käyttöjärjestelmän koodi voi olla paljon pienempi ja yksinkertaisempi. Lisäksi kirjasto ei voi vaarantaa eristystä: se on vain osa kunkin sovelluksen luotettua laskentapohjaa eikä mitään muuta.
Virtualisoitujen ympäristöjen tapausta voi verrata käyttöjärjestelmiin. Virtuaalikone, sovellukset ja riisuttu käyttöjärjestelmä voivat muodostaa yhden suojausalueen. Yleisempi tapaus on, että alimmalla tasolla on hypervisor, jonka muodostamassa virtuaalikoneessa voi pyöriä yksi tai useampi käyttöjärjestelmä, kuten Linux tai Windows. Tällainen hypervisor tarjoaa käyttöjärjestelmille illuusion, että ne toimivat omistamallaan laitteistolla. Spektrin toisesta päästä löydämme koko järjestelmän jakautuneena erillisiin, suhteellisen pieniin virtuaalikoneisiin. Itse asiassa jotkin käyttöjärjestelmät, kuten QubesOS, integroivat täysin virtualisoinnin ja käyttöjärjestelmien käsitteet sallimalla yksittäisten käyttäjäprosessien eristämisen omiin virtuaalikoneisiinsa. Lopuksi, kuten edellä jo nähtiin, Unikernelit ovat suosittuja virtualisoiduissa ympäristöissä hypervisorien lisäksi.
Yksi virtuaalikoneiden haitoista on se, että jokainen käyttöjärjestelmän levykuva (image) käyttää tallennustilaa ja lisää toisteisuutta. Jokainen järjestelmähän luulee olevansa laitteistokokonaisuuden ainut valtias, vaikka todellisuudessa se jakaa resursseja. Lisäksi jokainen virtuaalikoneen käyttöjärjestelmä tarvitsee erillisen ylläpidon: päivitykset, konfiguroinnin, testauksen jne. Sen vuoksi suosittu vaihtoehto on virtualisoida käyttöjärjestelmätasolla. Tässä lähestymistavassa useita ympäristöjä, joita kutsutaan konteiksi (container), toimivat yhden jaetun käyttöjärjestelmän päällä. Kontit on eristetty toisistaan mahdollisimman hyvin, ja niillä on omat ytimen nimiavaruudet, resurssirajoitukset jne., mutta lopulta ne jakavat taustalla olevan käyttöjärjestelmän ytimen ja usein käyttävät samoja ajettavia koodeja sekä kirjastoja.
Virtuaalikoneisiin verrattuna kontit ovat kevyempiä. Kuitenkin, jos ei oteta huomioon hallintanäkökohtia, virtuaalikoneita pidetään usein turvallisempina kuin kontit, koska virtuaalikoneet osastoivat resurssit melko tiukasti ja jakavat vain hypervisorin ohuena kerroksena laitteiston ja ohjelmiston välillä. Toisaalta jotkut uskovat, että kontit ovat turvallisempia kuin virtuaalikoneet, koska ne ovat niin kevyitä, että sovellukset voi jaotella “mikropalveluiksi”, joilla on konteissa tarkasti määritellyt rajapinnat. Lisäksi turvattavina olevien asioiden kokonaismäärä pienenee. Varhainen lähestymistapa kontteihin (eli “käyttöjärjestelmätason virtualisointiin”) löytyy jo chroot-komennosta, joka lisättiin Unix-versioon 7 vuonna 1979. Vuonna 2000 FreeBSD:n (eli Unix-projektin) julkaisema Jails meni paljon pidemmälle käyttöjärjestelmän virtualisoinnissa. Nykyään on monia konttitoteutuksia, joista tunnetuimpia on Docker.
On vielä otettava esille yksi käyttöjärjestelmien luokka, nimittäin sellaiset, jotka soveltuvat pieniin ja resursseiltaan rajoittuneisiin laitteisiin, varsinkin esineiden internetissä (IoT). Vaikka on eri mielipiteitä siitä, mitä IoT tarkoittaa ja tarkasteltavien laitteiden kirjo ulottuu älypuhelimista älypölyyn, yleinen käsitys on, että IoT sisältää ainakin kaikkein vähimpien resurssien laitteet. Tällaisille laitteille jopa minimiin riisutut yleiskäyttöiset käyttöjärjestelmät voivat viedä liikaa tilaa, kun käyttöjärjestelmän odotetaan toimivan vain muutamassa kilotavussa. Äärimmäisenä esimerkkinä ovat suositut IoT-käyttöjärjestelmät, kuten RIOT, jotka voivat olla kooltaan alle 10 kilotavua. Ne toimivat järjestelmissä, jotka vaihtelevat 8-bittisistä mikro-ohjaimista yleiskäyttöisiin 32-bittisiin prosessoreihin, joissa voi silti olla edistyneitä ominaisuuksia kuten muistinhallintayksikkö. Niistä saattavat puuttua Windowsin ja Linuxin kaltaisilta käyttöjärjestelmiltä vaaditut ominaisuudet ja sovellusten eristäminen. Sen sijaan ne voivat tukea monissa sulautetuissa järjestelmissä tärkeitä toimintoja kuten reaaliaikaisuutta tai vähän virtaa käyttäviä verkkoja.