Teknisk artikkel

Sikre en Delphi PDF-signerer mot ondsinnet PKCS#12

Når du signerer en PDF, tenker du vanligvis på signeringsnøkkelen som noe du kontrollerer. Den bor i en .pfx-fil du genererte, beskyttet av et passord du valgte. Koden som leser den filen, føles som rørlegging, ikke som en grense. Den intuisjonen er feil i det øyeblikket sertifikatet slutter å være ditt. Et skrivebordsverktøy som lar en bruker plukke en hvilken som helst .pfx, en server som tar imot et opplastet legitimasjonsbevis, en satsvis signerer som fôres med sertifikater over nettverket, alle gir angriperpåvirkede byte til en tolker før en eneste signaturbyte er laget. En PKCS#12-leser er angrepsflate, i samme forstand som en bildedekoder eller en skriftinnlaster er det

Denne artikkelen går gjennom to reelle defekter som levde i den leseren, begge i stien som importerer et signeringsbevis. Ingen av dem er eksotisk. Begge kommer fra den samme rotårsaken som treffer nesten enhver binærtolker skrevet i et språk med heltall av fast bredde: en lengde eller et antall fra filen blir stolt på ett steg lenger enn den burde. Den ene fører til en lesing utenfor grensene, den andre til en prosess som henger til du dreper den

HotPDF-diagram over defekten i PBKDF2-iterasjonsantallet og rettelsen: et forfalsket antall på fire milliarder runder pinte CPU-en til en nedre og øvre grense i HPDFCrypt avviste det
hundre millioner ligger over enhver legitim fil og setter samtidig tak på verste tilfelle — dekod bredt, sett så grenser, og bruk verdien først da
HotPDF: Før og etter i to baner for defekten med ASN.1-lengdeoverflyt: en forfalsket lengde lagt sammen i 32 bit slapp forbi grensesjekken til HPDFASN1ParseNode leste inn i Int64 og avviste den
vakten kjørte mot et avkortet spøkelse av den virkelige verdien; å lese bredt først gjør den forfalskede lengden synlig før noen SetLength

Hvor bytene reiser

Å importere en .pfx for å signere et dokument er ikke én operasjon, det er en kort løype, og hvert trinn tolker noe en angriper kan ha skrevet. Beholderen er en PKCS#12-struktur slik RFC 7292 definerer den, et rede av AuthenticatedSafe-poser lagt rundt et kryptert hylster som holder den private nøkkelen. Å lese den betyr å gå gjennom ASN.1, utlede en nøkkel fra passordet, dekryptere, og så gi den gjenvunne RSA-nøkkelen til koden som bygger signaturen

I HotPDF avbildes de trinnene på hver sine units. Logikken for PKCS#12-beholderen bor i HPDFPFX. Hver tagg, lengde og verdi den rører, dekodes av ASN.1-leseren i HPDFASN1. Nøkkelutledningen og PBES2-dekrypteringen ligger i HPDFCrypt ved siden av PBKDF2HMACSHA256. Når nøkkelen er gjenvunnet, gjør HPDFRSA og CMS-byggeren for SignedData i HPDFCMS den om til den frakoblede signaturen som bygges inn i PDF-en. Det offentlige inngangspunktet som driver hele kjeden, er ett kall

Løypa for import av .pfx i HotPDF: angriperpåvirkede byte går gjennom HPDFPFX og HPDFASN1 før nøkkelutledning i HPDFCrypt og signering i HPDFRSA/HPDFCMS
en PKCS#12-leser er angrepsflate — kryptografien nedstrøms betyr aldri noe hvis de to tolkerne er uforsiktige
// Driver hele løypa: last inn plassholder-PDF-en, tolk PFX-en,
// utled nøkkelen, bygg CMS SignedData, skriv den signerte utdatafilen.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // signaturen er bygd inn
else
  // signeringen ble ikke fullført
;

Hver byte av signer.pfx flyter gjennom HPDFASN1 og HPDFPFX før noen kryptografi skjer. Er de to unitene ikke nøye med hva filen påstår, får aldri kryptografien nedstrøms sjansen til å bety noe

Defekt én: en ASN.1-lengde som flyter over forbi vakten

ASN.1 i DER og BER koder hvert element som en tagg, en lengde og så mange innholdsbyte. Lengden er feltet du må stole på, men verifisere, for den forteller tolkeren hvor langt den skal lese, og den ble skrevet av den som lagde filen. X.690 §8.1.3 definerer to kodinger. Kortformen pakker en lengde fra 0 til 127 inn i én byte. Langformen, brukt for alt større, bruker én ledebyte der de sju laveste bitene gir antallet lengdebyte som følger, og deretter bærer så mange big-endian-byte den faktiske verdien. Fire lengdebyte kan derfor deklarere en innholdsstørrelse som nærmer seg fire gigabyte

Etter å ha dekodet en slik verdi må tolkeren sjekke at innholdet faktisk får plass i bufferen før den stoler på det. Den naturlige sjekken er å bekrefte at gjeldende posisjon pluss innholdslengden ikke løper forbi enden av dataene. Skrevet på den opplagte måten, med posisjonen, innholdslengden og totalen alle holdt i fortegnede 32-bits heltall, er den vakten ødelagt:

// Fellen: fortegnet 32-bits aritmetikk. Med ContentLen nær MaxInt
// flyter Pos + ContentLen over til en NEGATIV verdi, så sammenligningen
// er usann og en forfalsket lengde på cirka 2 GB seiler rett gjennom.
if Pos + ContentLen > Total then
  raise EHPDFASN1Error.Create('content overruns buffer');

Problemet er addisjonen, ikke sammenligningen. Når ContentLen er nær MaxInt (2147483647), flyter Pos + ContentLen over det fortegnede 32-bits området og vikler seg rundt til et negativt tall. En negativ sum er aldri større enn Total, så vakten melder at alt er i orden og lar tolkeren gå videre med en innholdslengde på rundt to gigabyte som bufferen ikke inneholder. Det som skjer så, er skaden: leseren allokerer en buffer for den påståtte lengden og kopierer inn i den, en SetLength etterfulgt av et Move som leser fra kilden. Kilden har bare noen hundre byte igjen, så kopien leser langt forbi enden av inndataene, en lesing utenfor grensene som i beste fall krasjer og i verste fall lekker tilstøtende prosessminne inn i tolkingen

Den eneste korrekte vakten utvider den mellomliggende summen før sammenligningen, slik at addisjonen ikke kan flyte over i typen den regnes ut i. Rettelsen løfter begge operandene til Int64:

// Riktig: begge operandene utvidet til Int64 før addisjonen, så summen
// kan ikke vikle seg rundt. En forfalsket lengde på 2 GB feiler nå grensesjekken.
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');

En Int64 holder summen av to 32-bits verdier uten tap, så sammenligningen ser det virkelige tallet og avviser den forfalskede lengden. Den separate sjekken på at ContentLen ikke er negativ, lukker det tilsvarende tilfellet der en dekodet verdi lander negativt på egen hånd. I HotPDF bor denne vakten i HPDFASN1ParseNode, funksjonen som lager noden hver eneste andre hjelper bygger på. Fordi HPDFASN1Content dimensjonerer sin SetLength og sitt Move rett fra nodens innholdslengde, ville en node som slapp forbi en dårlig vakt ha forgiftet hver lesing tatt fra den. Å fikse grensen på dekodingspunktet er det som gjør hjelperne over den trygge

Defekt to: et PBKDF2-iterasjonsantall brukt som våpen

Den andre feilen er ikke en minnefeil, det er filen som forteller CPU-en din hvor hardt den skal jobbe. PKCS#12 beskytter nøkkelmaterialet sitt med PBES2, det passordbaserte opplegget fra PKCS#5, spesifisert i RFC 8018. PBES2 kjører en nøkkelutledningsfunksjon, her PBKDF2 med HMAC-SHA-256, og deretter en chiffer, her AES-256-CBC. PBKDF2 tar et iterasjonsantall, og det antallet er en parameter som bæres i filen. Hele hensikten er at det skal være tregt: flere iterasjoner betyr at hver passordgjetting koster mer, noe som er bra mot en angriper som jobber frakoblet. RFC 8018 §4.2 sier uttrykkelig at et større antall er bedre for sikkerheten, og setter med vilje ingen øvre grense

Den åpenheten er grei når du lagde filen. Den er et våpen når angriperen gjorde det. Iterasjonsantallet er en arbeidsfaktor angriperen styrer, og en arbeidsfaktor angriperen styrer er et tjenestenektangrep basert på algoritmisk kompleksitet. En forfalsket .pfx kan kode et iterasjonsantall i milliardklassen; tolkeren leser det pliktoppfyllende og kaller PBKDF2 for så mange runder med HMAC-SHA-256, og prosessen forsvinner inn i en løkke som ikke kommer tilbake på minutter eller timer for én levert fil. På en signeringsserver som håndterer ett legitimasjonsbevis per forespørsel, stanser én uthengt opplasting en arbeider

Antallet gjør overflyten verre før det får CPU-en til å spinne. Iterasjonsverdien ligger i filen som et ASN.1-INTEGER, som ikke har fast bredde, mens feltet PBKDF2 til slutt bruker er et 32-bits Integer. Dekod INTEGER-et rett inn i det feltet, og en stor verdi avkortes, og en verdi laget for å lande på fortegnsbiten kommer tilbake negativ eller som et urelatert lite tall, så selv størrelsen på arbeidet er ikke lenger det filen så ut til å be om. Rettelsen leser verdien i full bredde og setter grenser før den snevres inn:

// Les iterasjonsantallet som Int64 først, og klem det så inn i et fornuftig
// bånd FØR det snevres inn i det 32-bits Iterations-feltet PBKDF2 bruker.
LIter := HPDFASN1ToInteger(Data, Node);          // returnerer 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);                    // trygt: allerede avgrenset

Å lese inn i en Int64 betyr at den dekodede verdien er den virkelige, ikke et avkortet spøkelse av den. Den nedre grensen avviser null og negative antall, som er meningsløse for en nøkkelutledning. Den øvre grensen, hundre millioner, ligger godt over enhver legitim PKCS#12-fil, som i dag bruker titusener til lave hundretusener av iterasjoner, samtidig som den setter tak på verste tilfelle til en avgrenset og overkommelig mengde arbeid. Først etter at verdien har passert det båndet, snevres den inn til 32-bits feltet, så avkortingen kan ikke lenger overraske noen. I HotPDF bor denne klemmen i ParsePBES2Params, der PBKDF2-parametrene dekodes på veien til PBKDF2HMACSHA256

Hvorfor begge rettelsene er den samme rettelsen

De to defektene ser forskjellige ut, én bufferoverskridelse og én hengt prosess, men de er den samme feilen. I begge tilfeller ble et tall fra en fil man ikke kan stole på, båret inn i en type med fast bredde ett steg for tidlig, før det var sjekket mot virkeligheten. Lengden ble lagt sammen i 32 bit før grensetesten; iterasjonsantallet ble snevret inn til 32 bit før områdetesten. Begge gir etter for den samme disiplinen: dekod i full bredde, sjekk mot den virkelige grensen, og snevre først da inn. Den mellomliggende Int64 er ikke et stilvalg, den er den eneste bredden der vakten kan se verdien angriperen faktisk skrev. En grense som flyter over, er ingen grense, og et antall uten tak er ingen parameter, det er en fjernstyrt strupeventil på din egen CPU

Praktiske råd for en signeringsløype

Den smale lærdommen er å validere sertifikatinndata du ikke kan stole på, slik du ville validert enhver annen opplasting du ikke kan stole på. Sett tak på størrelsen på en .pfx du tar imot, siden en legitim fil er kilobyte, ikke megabyte. Behandle en tolkefeil som rutinemessig avvist inndata, ikke som en feil verdt en stakksporing til brukeren. Signerer du på en server, kjør importen der en hengt arbeider ikke kan ta ned tjenesten med seg, og legg et tidsavbrudd rundt operasjonen så en uventet dyr fil avgrenses av klokketid i tillegg til iterasjonstaket

Den bredere lærdommen rekker forbi sertifikater. Herding av tolkere er ikke en engangsrevisjon av én unit, det er en egenskap ved hvert eneste sted biblioteket ditt leser byte det ikke skrev selv. Et PDF-bibliotek tolker en hel del fra kilder man ikke kan stole på: skrifter innebygd i et dokument, bilder i et halvt dusin kodeker, strømfiltre, og på signeringsstien sertifikater. Hver av dem er angrepsflate, og hver fortjener den samme mistenksomheten mot hver lengde og hvert antall. HotPDF bygger import- og signeringsstien på de herdede unitene HPDFASN1, HPDFPFX, HPDFCrypt og HPDFCMS som er beskrevet her, slik at legitimasjonsbeviset du gir den, uansett hvor det kom fra, tolkes defensivt før det i det hele tatt blir stolt på

Signeringsarbeidsflyten disse sjekkene beskytter, er dekket fra ende til ende i gjennomgangen vår av PAdES-digitalsignaturer i Delphi, og den samme defensive holdningen anvendt på dokumentkryptering, inkludert AES-256-nøkkelstien som deler denne kodebasen, er beskrevet i artikkelen om AES-256-kryptering og sikkerhet. Alt sammen følger med HotPDF Delphi Component for Delphi og C++Builder, ved siden av API-ene for innlasting, redigering, kryptering og signering som er dekket andre steder på denne bloggen