Tekninen artikkeli

Delphi PDF Signerin karkaisu haitallista PKCS#12:ta vastaan

Kun allekirjoitat PDF-tiedoston, ajattelet yleensä allekirjoitusavainta sellaisena mitä voit itse hallita. Se sijaitsee luomassasi tiedostossa .pfx, ja sitä suojaa valitsemasi salasana. Tiedostoa lukeva koodi tuntuu putkistolta, ei rajalta. Tämä intuitio on väärä sillä hetkellä, kun todistus lakkaa olemasta sinun. Työpöytätyökalu joka antaa käyttäjän valita minkä tahansa kohteen .pfx, ladatun tunnisteen hyväksyvä palvelin, verkon kautta varmenteita syöttävä eräallekirjoittaja, kaikki ojentavat hyökkääjien vaikuttamia tavuja jäsentäjälle ennen yhdenkään allekirjoitustavun tuottamista. PKCS#12-lukija on hyökkäyspinta, samassa merkityksessä kuin kuvadekooderi tai fontin lataaja

Tässä artikkelissa käydään läpi kaksi todellista kyseisessä lukijassa asunutta vikaa, molemmat polulla joka tuo allekirjoitustunnisteen. Kumpikaan ei ole eksoottinen. Molemmat johtuvat samasta perimmäisestä syystä joka koskettaa lähes jokaista kiinteäleveyksisillä kokonaisluvuilla kirjoitettua binaarijäsennintä: tiedoston pituuteen tai lukumäärään luotetaan askeleen verran pidemmälle kuin pitäisi. Toinen johtaa rajojen ulkopuoliseen luentaan, toinen prosessiin, joka roikkuu, kunnes tapat sen

Minne tavut matkustavat

Kohteen .pfx tuominen asiakirjan allekirjoittamiseen ei ole yksi toimenpide, se on lyhyt putkisto, ja jokaisessa vaiheessa jäsennetään jotain minkä hyökkääjä on saattanut kirjoittaa. Kontti on PKCS#12-rakenne, sellaisena kuin se on määritelty standardissa RFC 7292, pesä AuthenticatedSafe -pussukoita, jotka on kääritty yksityisen avaimen sisältävän salatun verhon ympärille. Sen lukeminen tarkoittaa ASN.1:n lukemista, avaimen johtamista salasanasta, salauksen purkamista ja sitten palautetun RSA-avaimen luovuttamista allekirjoituksen rakentavalle koodille

HotPDF:ssä kyseiset vaiheet osoittavat erillisiin yksiköihin. PKCS#12-säiliölogiikka asuu täällä HPDFPFX. ASN.1-lukija purkaa jokaisen sen koskettaman tunnisteen, pituuden ja arvon kohteessa HPDFASN1. Avaimen johdannainen ja PBES2-salauksen purku sijaitsevat kohteessa HPDFCrypt yhdessä PBKDF2HMACSHA256:n kanssa. Kun avain on palautettu, HPDFRSA ja CMS SignedData -rakennin kohteessa HPDFCMS muuttavat sen irrotetuksi allekirjoitukseksi joka on upotettu PDF-tiedostoon. Koko ketjua ohjaava julkinen sisääntulopiste on yksi kutsu

// Ohjaa koko putkea: lataa paikkamerkki-PDF, jäsennä PFX,
// johda avain, rakenna CMS SignedData, kirjoita allekirjoitettu tulos.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // allekirjoitus upotettu
else
  // allekirjoitus ei mennyt läpi
;

Kohteen signer.pfx jokainen tavu virtaa HPDFASN1:n ja HPDFPFX:n läpi ennen kryptografian tapahtumista. Jos nämä kaksi yksikköä eivät ole varovaisia sen suhteen, mitä tiedosto väittää, alavirran salaustekniikalla ei koskaan ole mahdollisuutta olla merkityksellinen

Vika yksi: ASN.1 pituus, joka kietoutuu vartijan ohi

ASN.1 formaateissa DER ja BER koodaa jokaisen elementin tunnisteena, pituutena ja kuinka monta sisältötavua niillä on. Pituus on kenttä, johon sinun on luotettava, mutta joka on tarkistettava, koska se kertoo jäsentäjälle, kuinka pitkälle lukea, ja sen kirjoitti kuka tahansa, joka on tuottanut tiedoston. X.690 §8.1.3 määrittelee kaksi koodausta. Lyhyt muoto pakkaa pituuden 0 - 127 yhdeksi tavuksi. Pitkä muoto, jota käytetään mihin tahansa suurempaan, vie yhden etutavun, jonka seitsemän pientä bittiä antavat perässä olevien pituustavujen määrän, jolloin niin moni big-endian -tavu kantaa todellista arvoa. Neljä pituista tavua voi siksi ilmoittaa sisällön koon, joka lähestyy neljää gigatavua

Tällaisen arvon dekoodauksen jälkeen jäsennystoiminnon on tarkistettava, että sisältö todella mahtuu puskurin sisään, ennen kuin se luottaa siihen. Luonnollinen tarkistus on vahvistaa, että nykyinen sijainti plus sisällön pituus ei kulje tietojen lopun ohi. Kirjoitettu ilmeisellä tavalla, asema, sisällön pituus ja 32-bittisissä etumerkillisissä kokonaisluvuissa oleva kokonaismäärä, kyseinen suojus on rikottu:

// Ansa: etumerkkinen 32-bittinen aritmetiikka. Jos ContentLen on lähellä numeroa MaxInt,
// Pos + ContentLen ylivuotaa NEGATIIVISEKSI arvoksi, joten vertailu
// on epätosi, ja taottu ~2 Gt pituus purjehtii suoraan läpi.
if Pos + ContentLen > Total then
  raise EHPDFASN1Error.Create('content overruns buffer');

Ongelma on lisäyksessä, ei vertailussa. Kun ContentLen on lähellä arvoa MaxInt (2147483647), Pos + ContentLen ylivuotaa etumerkillisen 32-bittisen alueen ja kietoutuu takaisin negatiiviseksi luvuksi. Negatiivinen summa ei ole koskaan suurempi kuin Total, joten vartija raportoi kaiken olevan kunnossa ja antaa jäsentimen jatkaa sisällön pituudella joka on karkeasti kaksi gigatavua jota puskuri ei sisällä. Seuraavaksi tapahtuu vahinko: lukija varaa puskurin kyseiselle väitetylle pituudelle ja kopioi siihen kohteeseen, kohde on SetLength jota seuraa Move luku lähteestä. Lähteessä on vain muutama sata tavua jäljellä, joten kopio lukee kaukana syötteen lopun ohi rajan ulkopuolisen luvun, joka parhaimmillaan kaatuu ja pahimmassa tapauksessa vuotaa viereisen prosessin muistin jäsennystoimintoon

Ainoa oikea vartija leventää välisummaa ennen vertailua, jotta lisäys ei voi ylivuotaa sitä tyyppiä, johon se on laskettu. Korjaus edistää kumpaakin operandia luokkaan Int64:

// Oikein: molemmat operandit levennettiin Int64:ksi ennen lisäystä, jotta summa
// ei pääse ylivuotamaan. Väärennetty 2 Gt pituus epäonnistuu nyt rajojen tarkistuksessa.
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');

Kohde Int64 sisältää kahden 32-bittisen arvon summan ilman häviötä, joten vertailu näkee todellisen numeron ja hylkää väärennetyn pituuden. Erillinen ei-negatiivinen ContentLen tarkistus sulkee yhtenevän tapauksen jossa dekoodattu arvo osuu itsestään negatiiviseksi. HotPDF:ssä tämä vartija sijaitsee HPDFASN1ParseNode:ssa, funktiossa, joka tuottaa solmun, jolle kaikki muut apuvälineet rakentuvat. Koska HPDFASN1Content mitoittaa kohteet SetLength ja Move suoraan solmun sisällön pituudesta, huonon suojuksen ohittanut solmu olisi myrkyttänyt jokaisen siitä otetun lukeman. Sidoksen kiinnittäminen dekoodauspisteessä tekee sen yläpuolella olevista apuvälineistä turvallisia

Toinen vika: PBKDF2 iterointimäärä aseena

Toinen virhe ei ole muistivirhe, vaan tiedosto kertoo suorittimellesi kuinka lujasti pitää työskennellä. PKCS#12 suojaa avainmateriaaliaan PBES2:lla, standardissa RFC 8018 määritellyllä salasanapohjaisella järjestelmällä PBKDF5:stä. PBES2 suorittaa avaimen johdannaisfunktion, tässä PBKDF2:n ja HMAC-SHA-256:n, sitten salauksen, tässä AES-256-CBC. PBKDF2 ottaa iteraatiolaskurin, ja tämä on tiedostossa mukana oleva parametri. Sen koko tarkoitus on olla hidas: enemmän iteraatioita tarkoittaa jokaisen salasanan arvauksen maksavan enemmän, mikä on hyvä offline-hyökkääjää vastaan. RFC 8018 §4.2 sanoo nimenomaisesti, että suurempi määrä on parempi turvallisuuden kannalta, ja tarkoituksella ei aseta ylärajaa

Se avoimuus on hienoa kun itse loit tiedoston. Se on ase silloin kun hyökkääjä teki. Iterointimäärä on hyökkääjän ohjaama työtekijä, ja hyökkääjän hallinnassa oleva työtekijä on algoritmisen monimutkaisuuden palvelunesto. Väärennetty .pfx voi koodata miljardien iteraatioiden määrän; jäsennin lukee sen kuuliaisesti ja kutsuu PBKDF2:n noin monelle HMAC-SHA-256-kierrokselle, ja prosessi katoaa silmukkaan, joka ei palaa takaisin minuutteihin tai tunteihin yhden toimitetun tiedoston kohdalla. Yhtä tunnistetta per pyyntö käsittelevällä allekirjoituspalvelimella yksi luotu lataus pysäyttää työntekijän

Määrä tekee wraparoundista huonomman ennen kuin se saa suorittimen pyörimään. Iterointiarvo on tiedostossa ASN.1 INTEGER:inä, jolla ei ole kiinteää leveyttä, kun taas PBKDF2:n lopulta kuluttama kenttä on 32-bittinen Integer. Dekoodaa KOONNAISLUKU suoraan tuolle kentälle ja suuri arvo lyhenee, ja etumerkkibittiin muodostettu arvo tulee takaisin negatiivisena tai jonain toisiinsa liittymättömänä pienenä numerona, joten työn kokokaan ei enää ole se mitä tiedosto näytti pyytävän. Korjaus lukee arvon täysleveänä ja rajoittaa sitä ennen kaventamista:

// Lue iteraatiomäärä ensin Int64:nä ja kiinnitä sitten normaaliin kaistaan
// ENNEN kuin se kapenee 32-bittiseksi PBKDF2 iterointi kentäksi jota käyttää.
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);                    // turvallinen: jo rajattu

Lukeminen muotoon Int64 tarkoittaa, että dekoodattu arvo on todellinen arvo, ei sen katkaistu haamu. Alaraja hylkää nollat ja negatiiviset luvut, jotka ovat järjettömiä avainten johdannaisessa. Yläraja, satamiljoonaa, on reilusti yläpuolella minkä tahansa laillisen PKCS#12-tiedoston yläpuolella, joka käyttää nykyään kymmeniä - satoja tuhansia iteraatioita, samalla kun katkaisee pahimman mahdollisen tapauksen rajattuun, selviävään työmäärään. Vasta kun arvo on ohittanut tämän alueen, se supistuu 32-bittiseen kenttään, joten katkaiseminen ei voi enää yllättää ketään. HotPDF:ssä tämä kiinnike sijaitsee kohteessa ParsePBES2Params, jossa PBKDF2-parametrit puretaan matkalla sijaintiin PBKDF2HMACSHA256

Miksi molemmat korjaukset ovat sama korjaus

Viat näyttävät erilaisilta, toinen puskurin ylivuoto ja toinen jumissa oleva prosessi, mutta ne ovat sama virhe. Kummassakin tapauksessa ei-luotetusta tiedostosta peräisin oleva numero siirrettiin kiinteäleveyksiseen tyyppiin askeleen verran liian aikaisin, ennen kuin sen totuudenmukaisuus on tarkistettu. Pituus lisättiin 32-bitillä ennen rajojen testiä; iterointimäärä rajattiin 32-bittiin ennen aluetesitö. Molemmat asiat antautuvat samalle kurinalaisuudelle: dekoodaa täydellä leveydellä, tarkista todellista rajaa vasten ja kaventaa vasta sitten. Välituote Int64 ei ole tyylivalinta, se on ainoa leveys, jolla vartija näkee hyökkääjän todellisuudessa kirjoittaman arvon. Raja, joka ylivuotaa, ei ole raja, ja countti ilman ylärajaa ei ole parametri, se on oman CPU:n etäkuristin

Käytännön ohjeita allekirjoitusputkelle

Kapeana oppina on vahvistaa epäluotettava varmennesyöte tavalla jolla vahvistaisit minkä tahansa ei-luotetun lähetetyn koodin. Rajoita hyväksymäsi .pfx:n kokoa, sillä legitiimi sellainen on ennemminkin kilotavuja kuin megatavuja. Käsittele jäsennysvirhettä rutiininomaisena hylättynä syöttteenä, ei virheenä, joka on käyttäjälle pinon jäljityksen arvoinen. Jos allekirjoitat palvelimella, suorita tuonti sinne, missä jumiutunut työntekijä ei voi viedä palvelua mukanaan, ja aseta toiminnon ympärille aikakatkaisu, jotta odottamattoman kallis tiedosto on iteraatiokaton lisäksi myös kellon sitoma

Laajempi oppitunti ulottuu sertifikaattien ohi. Jäsennystoiminnon karkaisu (Parser hardening) ei ole yhden kerran tarkastus yhdestä yksiköstä, se on jokaisen paikan ominaisuus, missä kirjastosi lukee tavuja, joita se ei kirjoittanut. PDF-kirjasto jäsentää paljon epäluotettavista lähteistä: asiakirjaan upotetut fontit, kuvat puolessa tusinassa koodekissa, stream-suodattimet ja allekirjoituspolun varmenteet. Jokainen näistä on hyökkäyspinta ja jokainen ansaitsee saman epäluottamuksen jokaista pituutta ja jokaista numeroa kohtaan. HotPDF rakentaa tuonnin ja allekirjoituspolun tässä kuvattujen vahvistettujen HPDFASN1, HPDFPFX, HPDFCrypt ja HPDFCMS -yksiköiden varaan niin, että sille antamasi tunniste jäsennetään puolustavasti riippumatta siitä, mistä se tuli ennen kuin siihen koskaan luotetaan

Näiden tarkistusten suojaama allekirjoitustyönkulku katetaan päästä päähän PAdES-digitaaliallekirjoituksia Delphissä käsittelevässä ohjeessamme, ja sama puolustusasento pätee asiakirjojen salaukseen, mukaan lukien AES-256 avainpolku joka jakaa tämän koodikannan, on kuvattu artikkelissa AES-256:n salausta ja suojausta käsittelevässä artikkelissa. Kaikki tämä toimitetaan osana HotPDF Component -ominaisuuksia Delphille ja C++Builderille luomis-, lomake-, salaus- ja allekirjoitussovellusliittymien vieressä, joita käsitellään muualla tässä blogissa