Kun allekirjoitat PDF-tiedoston, ajattelet yleensä allekirjoitusavainta jonakin hallitsemanasi asiana. Se elää luomassasi .pfx-tiedostossa, suojattuna valitsemallasi salasanalla. Koodi, joka lukee tuon tiedoston, tuntuu pelkältä putkitukselta, ei rajalta. Tämä intuitio on väärä heti, kun varmenne lakkaa olemasta sinun. Työpöytätyökalu, jonka avulla käyttäjä voi valita minkä tahansa .pfx-tiedoston, palvelin, joka hyväksyy ladatun valtuustiedon, tai eräallekirjoittaja, jolle syötetään varmenteita verkon yli - kaikki nämä luovuttavat hyökkääjän vaikuttamia tavuja jäsentimelle ennen kuin yhtäkään allekirjoitustavua tuotetaan. PKCS#12-lukija on hyökkäyspinta, aivan samassa mielessä kuin kuvadekooderi tai fonttilataaja on.
Tämä artikkeli käy läpi kaksi todellista vikaa, jotka olivat tuossa lukijassa, molemmat polulla, joka tuo allekirjoitusvaltuustietoja. Kumpikaan ei ole eksoottinen. Molemmat johtuvat samasta juussyystä, joka iskee lähes jokaiseen binaarijäsentimeen, joka on kirjoitettu kiinteäleveyksisiä kokonaislukuja käyttävällä kielellä: tiedostosta saatavaan pituuteen tai määrään luotetaan askeleen pidemmälle kuin pitäisi. Toinen johtaa rajojen ulkopuoliseen lukemiseen (out-of-bounds read) ja toinen prosessiin, joka jumiutuu, kunnes se tapetaan.
Minne tavut matkustavat
.pfx-tiedoston tuominen dokumentin allekirjoittamiseksi ei ole yksi operaatio, se on lyhyt putki, ja jokainen vaihe jäsentää jotakin, mitä hyökkääjä on saattanut kirjoittaa. Säiliö on PKCS#12-rakenne, kuten RFC 7292:ssa on määritelty - AuthenticatedSafe-laukkujen pesä, joka on kääritty salatun suojuksen ympärille, joka pitää sisällään yksityisen avaimen. Sen lukeminen tarkoittaa ASN.1-rakenteen läpikäyntiä, avaimen johtamista salasanasta, salaustekstin purkamista ja sitten palautetun RSA-avaimen luovuttamista allekirjoituksen rakentavalle koodille.
HotPDF-komponentissa nämä vaiheet kartoittuvat erillisiin yksiköihin. PKCS#12-säiliölogiikka elää yksikössä HPDFPFX. Jokainen sen koskettama tagi, pituus ja arvo puretaan ASN.1-lukijalla yksikössä HPDFASN1. Avaimen johtaminen ja PBES2-salauksenpurku sijaitsevat yksikössä HPDFCrypt yhdessä PBKDF2HMACSHA256-rutiinin kanssa. Kun avain on palautettu, HPDFRSA ja CMS SignedData -rakentaja yksikössä HPDFCMS muuttavat sen erilliseksi allekirjoitukseksi (detached signature), joka upotetaan PDF-tiedostoon. Koko ketjua ajava julkinen aloituspiste on yksi kutsu.
// Drives the full pipeline: load the placeholder PDF, parse the PFX,
// derive the key, build CMS SignedData, write the signed output.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// signature embedded
else
// signing did not complete
;
Jokainen signer.pfx-tiedoston tavu virtaa yksiköiden HPDFASN1 ja HPDFPFX läpi ennen minkään kryptografian tapahtumista. Jos nämä kaksi yksikköä eivät ole varovaisia sen suhteen, mitä tiedosto väittää, alavirran kryptografialla ei koskaan ole tilaisuutta vaikuttaa.
Ensimmäinen vika: ASN.1-pituus, joka kiertyy suojauksen ohi
ASN.1 DER- ja BER-muodoissa koodaa jokaisen elementin tagina, pituutena ja kyseisenä määränä sisältötavuja. Pituus on kenttä, johon on luotettava mutta joka on todennettava, koska se kertoo jäsentimelle kuinka pitkälle lukea, ja sen on kirjoittanut se, joka tiedoston loi. X.690 §8.1.3 määrittelee kaksi koodausta. Lyhyt muoto pakkaa pituuden 0-127 yhteen tavuun. Suuremmille arvoille käytettävä pitkä muoto varaa yhden johtotavun, jonka alimmat seitsemän bittiä ilmoittavat sitä seuraavien pituustavujen määrän, ja sitten kyseinen määrä big-endian-tavuja kantaa varsinaista arvoa. Neljä pituustavua voi siten ilmoittaa sisällön kooksi lähes neljä gigatavua.
Purettuaan tällaisen arvon jäsentimen on tarkistettava, että sisältö todella mahtuu puskurin sisään, ennen kuin se luottaa siihen. Luonnollinen tarkistus on varmistaa, ettei nykyinen sijainti plus sisällön pituus ylitä datan loppua. Jos tämä suojaus kirjoitetaan ilmeisellä tavalla, jossa sijainti, sisällön pituus ja kokonaismäärä ovat kaikki 32-bittisiä etumerkillisiä kokonaislukuja, se rikkoutuu:
// The trap: signed 32-bit arithmetic. With ContentLen near MaxInt,
// Pos + ContentLen overflows to a NEGATIVE value, so the comparison
// is false and a forged ~2 GB length sails straight through.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
Ongelma on yhteenlaskussa, ei vertailussa. Kun ContentLen on lähellä MaxInt-arvoa (2147483647), Pos + ContentLen ylivuotaa etumerkillisen 32-bittisen alueen ja kiertyy negatiiviseksi luvuksi. Negatiivinen summa ei ole koskaan suurempi kuin Total, so suojarutiini ilmoittaa kaiken olevan kunnossa ja antaa jäsentimen jatkaa noin kahden gigatavun sisältöpituudella, jota puskuri ei sisällä. Seuraavaksi tapahtuu vahinko: lukija varaa puskurin tälle väitetylle pituudelle ja kopioi siihen - SetLength, jota seuraa lähteestä lukeva Move. Lähteessä on jäljellä vain muutamia satoja tavuja, joten kopiointi lukee kauas syötteen lopun ohi. Kyseessä on rajojen ulkopuolinen luku (out-of-bounds read), joka parhaimmillaan kaataa ohjelman ja pahimmillaan vuotaa viereistä prosessimuistia jäsennysprosessiin.
Ainoa oikea suojarutiini laajentaa välisumman ennen vertailua, jotta yhteenlasku ei voi ylivuotaa sitä tyyppiä, jossa se lasketaan. Korjaus korottaa molemmat operandit Int64-tyyppiin:
// Correct: both operands widened to Int64 before the add, so the sum
// cannot wrap. A forged 2 GB length now fails the bounds check.
if ContentLen < 0 then
raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
raise EHPDFASN1Error.Create('content overruns buffer');
Int64 säilyttää kahden 32-bittisen arvon summan häviöttömästi, joten vertailu näkee todellisen luvun ja hylkää väärennetyn pituuden. Erillinen ei-negatiivisuuden tarkistus ContentLen-arvolle sulkee vastaavan tapauksen, jossa purettu arvo päätyy itsestään negatiiviseksi. HotPDF-komponentissa tämä suojarutiini elää funktiossa HPDFASN1ParseNode, joka tuottaa solmun, johon jokainen muu apuri perustuu. Koska HPDFASN1Content mitoittaa SetLength- ja Move-kutsujen koot suoraan solmun sisältöpituudesta, solmu joka läpäisi huonon suojauksen olisi myrkyttänyt jokaisen siitä otetun lukutoiminnon. Rajan korjaaminen purkupisteessä tekee sen yläpuolisista apureista turvallisia.
Toinen vika: PBKDF2-iteraatiomäärä aseena
Toinen vika ei ole muistivirhe, vaan se on tiedosto, joka käskee suoritintasi tekemään liikaa töitä. PKCS#12 suojaa avainmateriaaliaan PBES2-menetelmällä, joka on salasanapohjainen skeema PKCS#5-standardista ja määritelty RFC 8018 -dokumentissa. PBES2 ajaa avaimenjohtamisfunktion (tässä PBKDF2 ja HMAC-SHA-256), ja sitten salaimen (tässä AES-256-CBC). PBKDF2 ottaa iteraatiomäärän, ja tämä määrä on tiedostossa kuljetettava parametri. Sen ainoa tarkoitus on olla hidas: enemmän iteraatioita tarkoittaa, että jokainen salasanan arvaus maksaa enemmän, mikä on hyvä offline-hyökkääjää vastaan. RFC 8018 §4.2 sanoo nimenomaisesti, että suurempi määrä on parempi turvallisuudelle, eikä aseta tarkoituksella mitään ylärajaa.
Tämä avoimuus on hienoa, kun loit tiedoston itse. Se on ase, kun hyökkääjä loi sen. Iteraatiomäärä on hyökkääjän hallitsema työkerroin, ja hyökkääjän hallitsema työkerroin on algoritmi-kompleksisuuteen perustuva palvelunestohyökkäys (denial of service). Väärennetty .pfx-tiedosto voi koodata miljardien laajuisen iteraatiomäärän; jäsennin lukee sen uskollisesti ja kutsuu PBKDF2-funktiota kyseisen monen HMAC-SHA-256-kierroksen ajan, ja prosessi katoaa silmukkaan, joka ei palaa minuutteihin tai tunteihin yhden toimitetun tiedoston takia. Allekirjoituspalvelimella, joka käsittelee yhden valtuustiedon per pyyntö, yksittäinen manipuloitu lataus pysäyttää työntekijäprosessin.
Määrä pahentaa ylivuotoa ennen kuin se saa suorittimen pyörimään. Iteraatioarvo elää tiedostossa ASN.1 INTEGER -tyyppinä, jolla ei ole kiinteää leveyttä, kun taas PBKDF2:n lopulta kuluttama kenttä on 32-bittinen Integer. Pura INTEGER suoraan kyseiseen kenttään, ja suuri arvo katkaisutuu, ja arvo, joka on suunniteltu päätymään merkkibitille, palaa negatiivisena tai jonakin asiaankuulumattomana pienenä lukuna. Tällöin työn määrä ei enää edes vastaa sitä, mitä tiedosto näytti pyytävän. Korjaus lukee arvon täydessä leveydessään ja rajaa sen ennen kaventamista:
// Read the iteration count as Int64 first, then clamp to a sane band
// BEFORE it is narrowed into the 32-bit Iterations field PBKDF2 uses.
LIter := HPDFASN1ToInteger(Data, Node); // returns Int64
if (LIter < 1) or (LIter > 100000000) then
raise EHPDFPFXError.CreateFmt(
'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
[LIter]);
Iterations := Integer(LIter); // safe: already bounded
Miksi molemmat korjaukset ovat sama korjaus
Nämä kaksi vikaa näyttävät erilaisilta, toinen puskurin ylivuodolta ja toinen jumiutuneelta prosessilta, mutta ne ovat sama virhe. Kummassakin tapauksessa epäluotettavasta tiedostosta saatu luku siirrettiin kiinteäleveyksiseen tyyppiin askeleen liian aikaisin, ennen kuin se oli tarkistettu todellisuutta vasten. Pituus laskettiin yhteen 32 bitissä ennen rajatarkistusta; iteraatiomäärä kavennettiin 32 bittiin ennen aluetarkistusta. Molemmat taipuvat saman kurinalaisuuden alle: pura täydessä leveydessä, tarkista todellista rajaa vasten ja kavenna vasta sitten. Välivaiheen Int64 ei ole tyylivalinta, se on ainoa leveys, jossa suojarutiini voi nähdä arvon, jonka hyökkääjä todella kirjoitti. Ylivuotava raja ei ole raja, ja määrä ilman kattoa ei ole parametri vaan oman suorittimesi etäkuristin.
Käytännön ohjeita allekirjoitusputkelle
Kapea opetus on validoida epäluotettava sertifikaattisyöte samalla tavalla kuin validoisit minkä tahansa epäluotettavan latauksen. Rajoita hyväksymäsi .pfx-tiedoston kokoa, koska laillinen tiedosto on kilotavuja, ei megatavuja. Kohtele jäsennysvirhettä rutiininomaisena hylättynä syötteenä, ei virheenä, joka vaatisi pinonjäljityksen (stack trace) näyttämistä käyttäjälle. Jos allekirjoitat palvelimella, suorita tuonti siellä, missä jumiutunut työntekijäprosessi ei voi kaataa palvelua mukanaan, ja aseta toiminnolle aikakatkaisu, jotta odottamattoman kallis tiedosto rajataan seinäkellon ajan sekä iteraatiokaton mukaan.
Laajempi opetus ulottuu sertifikaattien ohi. Jäsentimen karkaisu ei ole kerrallinen tarkastus yhdessä yksikössä, se on ominaisuus jokaisessa paikassa, jossa kirjastosi lukee tavuja, joita se ei ole kirjoittanut. PDF-kirjasto jäsentää valtavasti tietoa epäluotettavista lähteistä: dokumenttiin upotettuja fontteja, kuvia puolessa tusinassa eri koodekissa, virtasuodattimia ja allekirjoituspolulla sertifikaatteja. Jokainen niistä on hyökkäyspinta, ja jokainen ansaitsee saman epäilyn jokaista pituutta ja määrää kohtaan. HotPDF rakentaa tuonti- ja allekirjoituspolun tässä kuvattujen karkaistujen HPDFASN1-, HPDFPFX-, HPDFCrypt- ja HPDFCMS-yksiköiden varaan, jotta sille annettava valtuustieto - mistä tahansa se onkin peräisin - jäsennetään puolustuksellisesti ennen kuin siihen koskaan luotetaan.
Allekirjoituksen työnkulku, jota nämä tarkistukset suojaavat, käsitellään alusta loppuun läpikäynnissämme PAdES-digitaalisista allekirjoituksista Delphissä, ja sama dokumenttien salaukseen sovellettu puolustuksellinen asenne - mukaan lukien tämän koodikannan jakama AES-256-avainpolku - kuvaillaan artikkelissa AES-256-salauksesta ja -turvallisuudesta. Kaikki tämä toimitetaan osana Delphi- ja C++Builder-ohjelmille tarkoitettua HotPDF Component -komponenttia yhdessä muiden tässä blogissa käsiteltyjen lataus-, muokkaus-, salaus- ja allekirjoitusrajapintojen kanssa.