- COMP.SEC.100
- 16. Ohjelmistoturvallisuus
- 16.1 Ohjelmistoturvallisuus - Johdanto
Ohjelmistoturvallisuus - Johdanto¶
Tässä osuudessa tutustutaan erilaisiin ohjelmistojen haavoittuvuuksiin ja tekniikoihin, joilla niitä voidaan tunnistaa, estää tai vähentää haittavaikutuksia.
Ohjelmiston voi ajatella olevan turvallinen, kun se täyttää määritellyn tai ilmaistun tietoturvavaatimuksen. Tämä sisältää luottamuksellisuuden, eheyden ja saatavuuden vaatimukset järjestelmän toiminnalle ja datalle.
Kuvittele esimerkiksi sosiaalisen median ohjelmisto. Ohjelmistolle voidaan asettaa vaikka seuraavia vaatimuksia:
- Käyttäjä voi muokata ainoastaan omia tietojaan. (Eheys)
- Ainoastaan käyttäjän ystävät näkevät hänen tietonsa. (Luottamuksellisuus)
- Ohjelmiston on oltava käytettävissä 99.9 % ajassa. (Saatavuus)
Huomaa, että vaatimukset voivat olla myös toisiaan kumoavia: Esimerkiksi eheysvaatimus voi olla järjestelmän sulkeminen hyökkäyksen tapahtuessa, mutta tällöin järjestelmä ei ole saatavilla.
Tietoturvapoikkeama (security failure) on tilanne, jossa järjestelmä ei saavuta tietoturvatavoitetta. Haavoittuvuus eli tietoturva-aukko (vulnerability) puolestaan voidaan määritellä niin, että se aiheuttaa tietoturvapoikkeaman. Huomaa kuitenkin, että tässä on oletuksena, että tietoturvatavoite on määritelty. Käytännössä monissa järjestelmissä ei ole määritelty tietoturvatavoitteita tai ne ovat monitulkintaisia. Tieturvatavoitteita voidaan tarkoituksella myös heikentää esim. käytettävyyden ja saavutettavuuden lisäämiseksi.
Huomaa siis, että haavoittuvuuksia on myös silloin, kun niitä ei ole sidottu tietoturvatavoitteisiin. Tällöin ajatellaan, että haavoittuvuudet ovat toteutuskohtaisia haavoittuvuuksia (implementation vulnerability). Toki hyvin määritellyt tietoturvatavoitteet kattavat myös toteutuskohtaisia haavoittuvuuksia, mutta näin ei aina ole ja silti ajatellaan, että toteutuskohtaiset haavoittuvuudet ovat todellisia haavoittuvuuksia.
EU:n tietosuoja-asetuksen vaatimukset ohjelmistoille¶
On hyvä huomata, että tietosuojavaatimuksia tulee suoraan lainsäädännöstä. Erityisesti EU:n tietosuoja-asetus säätelee henkilötietoja käsittelevien ohjelmistojen vaatimuksia EU:n alueella. Siten on tärkeää ymmärtää, että ohjelmistokehityksessä on otettava huomioon EU:n tietosuoja-asetus. Uusia henkilötietoja käsitteleviä ohjelmia kehitettäessä sekä vanhoja ohjelmia korjattaessa on erittäin tärkeää, että ohjelmoijat ja heidän esihenkilönsä tietävät asetuksen vaatimukset tietosuojalle. Organisaation ylimmän johdonkin tulisi tietää, että EU:n tietosuoja-asetuksen täyttäminen edellyttää resursseja ja että organisaatio on taloudellisessa vastuussa mahdollisesta asetuksen laiminlyönnistä.
Jos ohjelma on käytettävissä EU:n alueella, EU:n tietosuoja-asetus asettaa ohjelmistolle lukuisia vaatimuksia, kuten
- Suostumuksen pyytäminen lain edellyttämällä tavalla
- Tietoturvaloukkausten raportointiin varautuminen sisältäen lokitietojen keräämisen ja hallinnan ja ilmoittamisen niille, joiden henkilötietoja on loukattu.
- Varautuminen käyttäjien tekemiin tietopyyntöihin. Tietopyyntöjä varten käyttäjän henkilötiedoista voi muodostaa automaattisen raportin.
- Osoitusvelvollisuuden täyttämiseen varautuminen
- Käsiteltävien henkilötietojen minimointi
- Henkilötietojen poistaminen, kun tiedolle ei ole enää tarvetta ja laillista perustetta säilyttää tietoa.
- Erityisten tietojen käsittelyyn varautuminen tai tietojen keräämisen välttäminen
- Vaikutustenarvioinnin tekeminen, jos tälle on asetuksen mukainen vaatimus (erityisen tietoryhmän tietojen käsittely tai suuri riski)
Eu:n tietosuoja-asetusta on käsitelty aiemmin tarkemmin Laki ja säädökset -luvussa.
Haavoittuvuuksien luokittelu¶
Haavoittuvuuksia voidaan luokitella eri tavoin, kuten yleisyyden, seurausten vakavuuden, seurausten luokittelun ja hyödynnettävyyden vaikeusasteen perusteella. Ennalta sovitulla luokittelulla saadaan haavoittuvuuksista yleisesti ymmärretty näkemys. CVE (Common Vulnerabilities and Exposures) on yksi yleinen tapa luokitella haavoittuvuuksia sekä samoin CWE (Common Weakness Enumeration). Suomessa haavoittuvuuksista tiedottaa ja niitä koordinoi Liikenne- ja viestintäviraston alaisuudessa toimiva Kyberturvallisuuskeskuksen CERT-toiminto.
Seuraavaan on tehty CYBOK-aineistoa mukaileva luokittelu haavoittuvuuksista. On hyvä huomata, että luokitteluja on erilaisia ja luokittelu ei ole kaiken kattava. Ajatus on kuitenkin kuvata yleisimmät toteutuskohtaiset haavoittuvuudet.
Muistinhallinnan haavoittuvuudet (syventävä)¶
Ohjelmoinnissa tarvitaan muistivarauksia ja osa haavoittuvuuksista liittyy niihin. Erityisesti kielet, joissa ei ole automaattista muistinvarausta ovat haavoittuvia, kuten C ja C++ -kielet. Jos ohjelmoija käyttää muistia varatun muistialueen ulkopuolelta, tapahtuu määrittelemätön toiminto, mikä voi johtaa haavoittuvuuteen. Tämänkaltaiset haavoittuvuudet ovat ylivuotohaavoittuvuuksia (buffer overflow vulnerability).
Esimerkiksi jos taulukolle on varattu tilaa 10 alkiolle, mutta ohjelma kirjoittaa 11 (tai enemmälle) alkiolle, tämä voi johtaa haavoittuvuuteen mainituissa ohjelmointikielissä. Haavoittuvuus perustuu taulukon perässä olevien muistiosoitteiden muokkaamiseen siten, että hyökkääjän oma koodi tulee suoritukseen. Esimerkiksi ohjelman suoritus siirtyy taulukon sisälle, mutta on myös kehittyneempiä hyökkäyksiä. Tilanne on vaarallinen, jos hyökkääjä pystyy täyttämään ylivuotavan muistin esim. verkkolomakkeesta, URL-osoitteesta tai muulla syötteellä. Kääntäjään tai järjestelmään on saatettu toteuttaa suojauksia, mutta näihin ei pidä luottaa, vaan ohjelma tulee toteuttaa turvallisesti.
Myös muistinvaraukseen voi liittyä haavoittuvuuksia. Muistinvarauksia tehdään, kun halutaan käyttää muistia dynaamisesti ohjelman suorituksen aikana. Toisin sanoen ennalta ei tiedetä käytettävän muistin määrää, joten muistia varataan ohjelman suorituksen aikana. Esimerkkinä haavoittuvuudesta on, että ohjelma poistaa muistialueen varauksen, mutta kuitenkin ohjelmasta edelleen viitataan samaan vapautettuun muistialueeseen, jota hyökkääjä voi päästä nyt muokkaamaan.
Muistinhallintaan liittyvä ongelma voi tulla myös siten, että ohjelma varaa muistin, mutta siihen on pääsy myös muilla resursseilla, kuten muilla ohjelmilla. Esimerkkinä tästä voi olla varatun muistin tyhjentämättä jättäminen, tilapäisen muistialueen käyttö, selaimen keksit (cookie), ympäristömuuttujat tai välimuisti (cache). Jokin toinen resurssi voi muuttaa tai lukea muistia. Huomaa myös, että muistinvaraus ja varauksen poisto voi tapahtua sinänsä oikein, mutta jos tallennettua tietoa ei ylikirjoiteta, luottamuksellista tietoa, kuten salasanoja, maksutietoja tai henkilötietoja voi paljastua.
Muistinhallinnan haavoittuvuuksia voi estää mm. koodikatselmoinneilla, testauksella ja ohjelman oikealla suunnittelulla, esim. käyttämällä taulukoita vain rajapintojen tai turvallisten ohjelmakutsujen kautta. Tämä tosin voi tapahtua suorituskyvyn kustannuksella.
Silloin, kun on mahdollista, voi myös ohjelmointikielen valinnalla estää haavoittuvuuksia. Esim. Java, Python, Perl, Rust ja Ruby sisältävät automaattisen muistinhallinnan, jolloin puskuriylivuodot ja muistivaraukset eivät aiheuta samalla tavalla ongelmia. Myös kääntäjän valinnalla ja asetuksilla voi estää haavoittuvuuksia, sillä kääntäjiinkin on asetettu suojauksia, jotka tunnistavat ja estävät haavoittuvuuksia. Täydellinen suoja tämä ei kuitenkaan ole, sillä kaikkia virheitä ei ole mahdollista tunnistaa.
Syötteen muodostamisen haavoittuvuudet (syventävä)¶
Ohjelmien täytyy usein muodostaa dynaamisesti syötteestä uusia rakenteita, kuten kyselyjä. Tästä esimerkkeinä ovat SQL-kyselyt ja html:n käsittely. Esimerkiksi käyttäjän kirjautuessa ohjelmaan käyttäjätunnus ja salasanan tiiviste saatetaan hakea SQL-kyselyillä tietokannasta. Vastaavasti verkkolomakkeella haettaessa tietoa, tiedot voidaan hakea SQL-kyselyillä tietokannasta. Ongelma tulee, jos käyttäjä pystyy syöttämään SQL-komentoja, jotka ohjelma tulkitsee osaksi SQL-kyselyä. SQL-komennot voivat olla monimutkaisia ja pahimmillaan tällainen haavoittuvuus voikin johtaa järjestelmän haltuunottoon, mutta lievemmilläänkin tiedon saaminen tietokannasta tai palvelunesto on mahdollista.
Ohjelmoijan tehtävä on huolehtia turvallisesta SQL-tietojen käsittelystä. Usein on mahdollista estää käyttäjän antaman syötteen tulkitseminen SQL-kyselyinä. Toisaalta, jollei estoa ole, haavoittuvuus on syntynyt.
Vastaavalla tavalla muissa syötteen muodostumisessa syntyvissä haavoittuvuuksissa ohjelmoijan on tunnettava suojautumismekanismit. Ne ovat usein yksinkertaisia, mutta ne on erikseen osattava rakentaa ohjelmakoodiin. Toisin sanoen ohjelmoijan on oltava valveutunut ja seurattava myös haavoittuvuuksien kehittymistä.
Yksi syötteisiin liittyvä haavoittuvuus on puutteellinen tiedonkulun hallinta (“incomplete mediation”). Ohjelma välittää jonkin tiedon, parametrin, toiselle ohjelmalle, joka jättää tekemättä sellaiset tarkistukset, joihin se kykenisi. Vaarana ovat paitsi tietoa välittävän ohjelman tekemät virheet myös se, että ohjelma onkin hyökkääjän hallussa. Yksinkertainen esimerkki on selaimen lähettämä tilaus web-kauppaan. Vaikka tilaaja olisi autentikoitukin, vastaanottajan eli palvelimen ei pidä uskoa esim. tilauksen mukana kerrottuun loppusummaan, vaan se pitää laskea ja estää virheellinen tilaus.
Ohjelmointirajapinnan haavoittuvuudet (syventävä)¶
Ohjelmointirajapinta tarkoittaa ohjelmistojen tapaa kommunikoida keskenään. Ohjelma esimerkiksi kutsuu toisen ohjelman rajapintaa, jolloin kutsuttu ohjelma suorittaa kutsuvan ohjelman antamien määritysten perusteella ennakkoon ohjelmoidut toiminnot. Esimerkiksi puhelimessa ohjelma voi haluta laitteen sijaintitiedot ja puhelimen ohjelmistorajapintaa kutsumalla saa tämän tiedon, jos ohjelmiston ja järjestelmän oikeudet riittävät tiedon välittämiseen. Muussa tapauksessa rajapinnasta saatu vastaus kertoo, että sijaintitietoa ei voi antaa sekä ehkä syyn tähän.
Lähes kaikki ohjelmat käyttävät yhtä tai useampaa ohjelmointirajapintaa. Ohjelmiston suunnittelija saattaa tehdä oletuksia rajapinnan toiminnasta eikä siten osaa varautua rajapinnan poikkeukselliseen toimintaan, mikä voi johtaa haavoittuvuuteen.
Myös ohjelmointirajapinnan toteutuksessa voi olla haavoittuvuus. Ohjelmoijan onkin tärkeää selvittää, että rajapinnat toimivat turvallisesti sekä varautua mahdollisuuteen päivittää kutsuttava rajapinta, siltä varalta, että käytetystä rajapinnasta löydetään haavoittuvuuksia.
Ohjelmoijan on huomioitava myös ohjelmointirajapinnan oikea käyttö. Tämä on erityisen tärkeää toteutettaessa kryptografiaa, jotta kryptografia tulee toteutetuksi kryptografisen toiminnon määrittelemän standardin edellyttämällä tavalla ja turvallisesti. Käytännössä ohjelmoijan on oltava selvillä turvallisista algoritmeista, niiden toteutuksesta ja alustuksesta, eri moodeista ja niiden turvallisuudesta sekä kutsujen oikeasta järjestyksestä. Valmiita ratkaisuja on tietenkin käytettävä, koska ne on jo toteutettu turvallisesti edellyttäen, että kaikki on tehty oikein. Jotkin rajapinnat osaavatkin huolehtia asiat suoraan oikein, mutta joissain jää paljon ohjelmoijan osaamisen varaan. Jotkin rajapinnat voivat kuitenkin sisältää virheitä tai käyttää heikkoa salausta, joten kryptografiaa käytettäessä on varmistettava rajapinnan ja toteutuksen turvallisuus.
Haavoittuvuudet kilpatilanteissa (syventävä)¶
Kun ohjelma käsittelee resursseja (esim. tiedostot, muisti ja tietokannat), jotka on jaettu muiden rinnakkaisten toimijoiden kesken, voi syntyä haavoittuvuus kilpatilanteessa (race condition vulnerability) Tätä kuvaa Tarkistuksen ja käytön välinen aika (TOCTTOU, time-of-check-to-time-of-use). Perinteinen esimerkki: “Jos asiakas laskee sinulle setelit ja joudut kääntymään toisaalle kirjoittaaksesi kuitin, niin lasketa tai laske itse uudestaan ennen kuin annat kuittia asiakkaalle.” Tätä vastaava tilanne on yleisempi eheysongelma pääsynvalvonnan yhteydessä: oikeuksien tarkistaminen tehdään tietyllä hetkellä ja niiden mukainen operaatio jonkin verran myöhemmin. Jos oikeudet tällä välillä muuttuvat, voi tapahtua vahinkoja. Tietokannan tapauksessa peruste tietyn arvon kirjoittamiselle johonkin kenttään voi häipyä, jos joku muu ehtii muuttaa kentän arvoa ensin. Yhtenä esimerkkinä ajoitusongelma tulee myös, kun tarkistetaan varmenteiden voimassaoloa esim. peruutuslistalta. Tämän vuoksi peruutuslistoja päivitetään melko tiuhaan.
Haavoittuvuuden kilpatilanteessa voi välttää ohjelman hyvällä suunnittelulla ja toteutuksella. Kilpatilanteet pitää ennakoida ja toteuttaa hyvien ohjelmointikäytäntöjen mukaisesti. Esimerkiksi jotkin ohjelmointikäskyt estävät kilpatilanteen syntymisen. Rinnakkaisuudessa oikeuksien hallinta tulee tärkeäksi, jotta kilpatilanne ei aiheuta ongelmia. Esimerkiksi muistialueen varauksen jälkeen mikään muu prosessi ei saa kirjoittaa muistialueelle.
Sivukanavahaavoittuvuudet (syventävä)¶
Sivukanavahaavoittuvuuksissa hyödynnetään varsinaisen ohjelmiston suorituksesta syntyvää ylimääräistä tietoa. Tämä tieto on tyypillisesti kohinaa, jota on mahdollista analysoida. Esimerkiksi prosessorin toimintaa on mahdollista analysoida laitteistolla tai ohjelmistolla. Prosessorin käsittelemää tietoa voi analysoida esim. tarkkailemalla suoritusaikaa tai virrankulutusta. Jos avaimen sisältö vaikuttaa seurattuun suoritusaikaan tai virrankulutukseen, on mahdollista saada selville avain tai sen osa.
Sivukanavahyökkäyksiltä suojaudutaan ohjelmiston oikealla suunnittelulla ja toteutuksella tekemällä analysointi vaikeaksi. Esimerkiksi suorituksen ajallisen analysoinnin voi estää toteuttamalla ohjelmiston käskyt niin, että salausavaimesta riippuvaa ajallista eroa ohjelman suorituksessa ei synny.
Sivukanavahyökkäyksien yhtenä muotona on tiedon lukemisen lisäksi ohjelman suoritukseen vaikuttaminen, näitä voidaan kutsua viansyöttöhyökkäyksiksi (fault injection attacks). Vian voi aiheuttaa esim. lämpötilamuutoksilla, kellon muutoksilla tai elektromagneettisella säteilyllä. Yhtenä esimerkkinä on ns. Row hammer -hyökkäys, jossa lukuisilla toistuvilla muistiviittauksilla muutetaan viereisten bittien paikkoja DRAM-muistista.
Kokonaisvaltaiseen turvallisuuteen tarvitaan monimutkaisia määrityksiä (syventävä)¶
Edellä on käsitelty toteutuskohtaisia haavoittuvuuksia. Järjestelmää tulee kuitenkin tarkastella kokonaisuutena. Esimerkiksi toteutuskohtainen haavoittuvuus ei aiheuta kokonaisuutena ongelmaa, jos siihen on varauduttu muualla järjestelmässä. Tällöin on rakennettu turvakerroksia, joilla estetään yksittäisen haavoittuvuuden aiheuttama ongelma.
Voi olla myös niin, että vaikka järjestelmässä ei ole toteutuskohtaisia haavoittuvuuksia, järjestelmän tietoturvatavoite ei silti täyty. Onkin tärkeää ottaa huomioon turvallisuus kokonaisuutena. Järjestelmä voidaan jakaa osajärjestelmiin, joiden turvallisuutta tarkastellaan myös osana kokonaisuutta. Esimerkiksi: Miten yhden osajärjestelmän ongelmaan varaudutaan kokonaisuutena ja miten osajärjestelmät tukevat turvallisuuden kokonaistavoitetta?
Toteutuskohtaisten haavoittuvuuksien turvallisuustavoite on helppo määritellä: joko se täyttyy tai ei täyty, esim. joko on haavoittuvuus tai sitä ei ole. On kuitenkin olemassa monimutkaisempia turvallisuusmäärityksiä. Näistä yksi on tietovuoturvallisuus (information flow security). Tällöin tietoturvallisuutta tarkastellaan tiedon siirtymisen näkökulmasta, jolloin kullekin tietovuolle määritellään julkisuus- ja luottamuksellisuusvaatimukset. Esimerkkinä käytännössä vaatimuksista on, että luottamuksellinen syöte (confidential input) ei vaikuta mitenkään julkiseen tulosteeseen (public output). Esimerkiksi hakemalla palvelusta myyntihintoja ei voi vaikuttaa siihen, mitä muut näkevät palvelujen julkisina myyntihintoina omissa hauissaan.
Tietovuomäärityksissä tulee arvioida myös syötteen toisteisuutta. Hyökkääjä voi esimerkiksi toistaa hakuja sekä tehdä niitä eri muodoissa. Näin saattaa olla mahdollista tehdä toistohyökkäys tai sitten saattaa olla mahdollista yhdistellä tietoa siten, että luottamuksellisuusvaatimus rikkoutuu.
Haavoittuvuudet ovat vikojen osajoukko (syventävä)¶
Haavoittuvuuden käsite on hyödyllinen, mutta ei kata kaikkea ohjelmistoturvallisuutta. Ohjelmistojen turvallisuutta voi edistää hyvillä ohjelmoinnin periaatteilla, testauksella ja erityisesti korkean laadun vaatimusten täyttämiseksi myös todistamisella. Haavoittuvuudet voidaan nähdä vikojen (fault) osajoukkona. Vika puolestaan on virhe suunnittelussa tai toteutuksessa. Vikojen taksonomiaa eli tieteellistä luokittelua on kehitetty dependable computing -alueen tutkimuksessa.