Technical Article

Herding av 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 lever i en .pfx-fil du opprettet, beskyttet av et passord du valgte. Koden som leser den filen føles som rørlegging, ikke en grense. Den intuisjonen er feil i det øyeblikket sertifikatet slutter å være ditt. Et skrivebordsverktøy som lar en bruker velge hvilken som helst .pfx, en server som godtar opplastede legitimasjonsfiler, eller en batch-signerer som mates med sertifikater over nettverket, overleverer alle angrepspåvirkede byte til en tolker før en eneste signeringsbyte er produsert. En PKCS#12-innleser is attack surface, in the same sense that an image decoder or a font loader is.

Denne artikkelen går gjennom to reelle feil som fantes i denne innleseren, begge i banen som importerer en signeringslegitimasjon. Ingen av dem er eksotiske. Begge skyldes den samme underliggende årsaken som rammer nesten alle binærtolkere skrevet i et språk med heltall med fast bredde: En lengde eller et antall fra filen stoles på ett skritt lenger enn det burde. Den ene fører til en out-of-bounds-lesing, den andre til en prosess som henger til du avslutter den.

Hvor bytene reiser

Å importere en .pfx for å signere et dokument er ikke bare én operasjon, det er en kort rørledning, og hvert trinn tolker noe en angriper kan ha skrevet. Containeren er en PKCS#12-struktur som definert i RFC 7292, en samling av AuthenticatedSafe-pakker pakket rundt et kryptert hylster som inneholder den private nøkkelen. Å lese den innebærer å gå gjennom ASN.1, utlede en nøkkel fra passordet, dekryptere, og deretter overlevere den gjenvunne RSA-nøkkelen til koden som bygger signaturen.

I HotPDF er disse trinnene mappet to egne enheter. Beholderlogikken for PKCS#12 ligger i HPDFPFX. Hver tagg, lengde og verdi den berører, dekodes av ASN.1-innleseren i HPDFASN1. Nøkkelutledning og dekrypteringen av PBES2 ligger i HPDFCrypt sammen med PBKDF2HMACSHA256. Når nøkkelen er gjenvunnet, gjør HPDFRSA og CMS-SignedData-byggeren i HPDFCMS den om til den frittstående signaturen som bygges inn i PDF-en. Det offentlige inngangspunktet som driver hele kjeden, er ett enkelt kall.

// 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
;

Hver byte av signer.pfx flyter gjennom HPDFASN1 og HPDFPFX før noen kryptografi finner sted. Hvis disse to enhetene ikke er forsiktige med hva filen hevder, får kryptografien nedstrøms aldri sjansen til å bety noe.

Feil én: en ASN.1-lengde som går rundt forbi sikkerhetskontrollen

ASN.1 i DER og BER koder hvert element som en tagg, en lengde og det gitte antallet innholdsbyte. Lengden er feltet du må stole på, men verifisere, fordi den forteller tolkeren hvor langt den skal lese, og den ble skrevet av den som opprettet filen. X.690 §8.1.3 definerer to kodinger. Den korte formen pakker en lengde fra 0 til 127 inn i en enkelt byte. Den lange formen, som brukes til alt som er større, bruker én ledende byte der de laveste syv bitene angir antallet påfølgende lengdebyte, og deretter bærer dette antallet 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, tolkeren må sjekke at innholdet faktisk får plass i bufferen før den stoler på det. Den naturlige sjekken er å bekrefte at gjeldende posisjon pluss innholdets lengde ikke løper forbi slutten av dataene. Hvis denne kontrollen skrives på den opplagte måten, der posisjonen, innholdslengden og totalen holdes i signed 32-bit heltall, er sikkerhetskontrollen ødelagt:

// 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');

Problemet er addisjonen, ikke sammenligningen. Når ContentLen er nær MaxInt (2147483647), vil Pos + ContentLen overlaste det signed 32-bit området og slå rundt til et negativt tall. En negativ sum er aldri større enn Total, så kontrollen rapporterer at alt er i orden og lar tolkeren fortsette med en innholdslengde på rundt to gigabyte som bufferen ikke inneholder. Skaden skjer i neste trinn: Innleseren tildeler en buffer for denne påståtte lengden og kopierer inn i den, en SetLength etterfulgt av en Move som leser fra kilden. Kilden har bare noen få hundre byte igjen, så kopieringen leser langt forbi slutten av inndataene. Dette er en out-of-bounds-lesing som i beste fall krasjer og i verste fall lekker tilstøtende prosessminne inn i tolkningen.

Den eneste riktige kontrollen utvider den mellomliggende summen før sammenligningen, slik at addisjonen ikke kan overlaste typen den beregnes i. Løsningen oppgraderer begge operandene til Int64:

// 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');

En Int64 holder summen av to 32-bit verdier uten tap, så sammenligningen ser det faktiske tallet og avviser den forfalskede lengden. Den separate ikke-negative sjekken på ContentLen dekker det tilsvarende tilfellet der en dekodet verdi blir negativ av seg selv. I HotPDF ligger denne kontrollen i HPDFASN1ParseNode, funksjonen som produserer noden alle andre hjelpefunksjoner bygger på. Fordi HPDFASN1Content fastsetter størrelsen på SetLength og Move direkte fra nodens innholdslengde, ville en node som passerte en dårlig kontroll ha forgiftet alle lesinger som ble tatt fra den. Å rette opp grensen på dekodingstidspunktet er det som gjør hjelpefunksjonene over trygge.

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

Den andre feilen er ikke en minnefeil; det er filen som forteller prosessoren din hvor hardt den skal jobbe. PKCS#12 beskytter sitt nøkkelmateriale med PBES2, den passordbaserte metoden 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 dette antallet er en parameter som følger med i filen. Formålet er nettopp å være treg: flere iterasjoner betyr at hvert passordgjett koster mer, noe som er bra mot en offline-angriper. RFC 8018 §4.2 er eksplisitt på at et høyere antall er bedre for sikkerheten, og setter bevisst intet tak.

Denne åpenheten er grei når det var du som opprettet filen. Den er et våpen når det var angriperen som gjorde det. Iterasjonsantallet er en angriperkontrollert arbeidsfaktor, og en angriperkontrollert arbeidsfaktor 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 vil returnere på minutter eller timer for én enkelt innsendt fil. På en signeringsserver som håndterer én legitimasjonsfil per forespørsel, vil en enkelt spesielt utformet opplasting lamme en arbeiderprosess.

Antallet gjør talloverflyten verre før det får prosessoren til å spinne

Iterasjonsverdien ligger i filen som en ASN.1 INTEGER, som ikke har fast bredde, mens feltet PBKDF2 til slutt forbruker er en 32-bit Integer. Dekodes INTEGER rett inn i det feltet, vil en stor verdi avkortes, og en verdi som er utformet for å treffe fortegnsbiten kommer tilbake som negativ eller som et urelatert lite tall, slik at selv omfanget av arbeidet ikke lenger er det filen så ut til å be om. Løsningen leser verdien i full bredde og begrenser den før den snevres inn:

// 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

Hvorfor begge løsningene er den samme løsningen

De to feilene ser forskjellige ut, den ene en bufferoverflyt og den andre en låst prosess, men de er den samme feilen. I begge tilfeller ble et tall fra en upålitelig fil ført inn i en type med fast bredde ett trinn for tidlig, før det hadde blitt sjekket mot virkeligheten. Lengden ble addert i 32 bits før grensetesten; iterasjonsantallet ble snevret inn til 32 bits før områdestesten. Begge underkaster seg den samme disiplinen: Dekod i full bredde, sjekk mot den faktiske grensen, og snevre inn først etterpå. Den mellomliggende Int64 er ikke et stilvalg; det er den eneste bredden der sikkerhetskontrollen kan se verdien angriperen faktisk skrev. En grense som overlaster er ikke en grense, og et antall uten tak er ikke en parameter, det er en ekstern struping av din egen prosessor.

Praktisk veiledning for en signeringsrørledning

Den konkrete lærdommen er å validere upålitelige sertifikatinndata på samme måte som du ville validert enhver upålitelig opplasting. Sett et tak på størrelsen på en .pfx du godtar, siden en legitim fil er i kilobyte, ikke megabyte. Behandle en tolkingsfeil som rutinemessig avvist inndata, ikke en feil som er verdt å vise en stack-trace til brukeren for. Hvis du signerer på en server, kjør importen der en låst arbeiderprosess ikke kan ta ned hele tjenesten, og legg en tidsavbruddsgrense rundt operasjonen slik at en uventet tung fil begrenses av faktisk tid i tillegg til iterasjonstaket.

Den bredere lærdommen strekker seg forbi sertifikater. Herding av tolkeren er ikke en engangsrevisjon av én enhet, det er en egenskap ved alle steder der biblioteket ditt leser byte det ikke har skrevet selv. Et PDF-bibliotek tolker mye fra upålitelige kilder: fonter innebygd i et dokument, images in half a dozen codecs, stream filters, and, on the signing path, certificates. Each of those is attack surface, and each deserves the same suspicion of every length and every count. HotPDF builds the import and signing path on the hardened HPDFASN1, HPDFPFX, HPDFCrypt, and HPDFCMS units described here, so that the credential you hand it, wherever it came from, is parsed defensively before it is ever trusted.

Signeringsarbeidsflyten disse sjekkene beskytter er dekket fra start til slutt i vår gjennomgang av PAdES digitale signaturer i Delphi, og den samme defensive holdningen brukt på dokumentkryptering, inkludert AES-256-nøkkelbanen som deler denne kodebasen, er beskrevet i artikkelen om AES-256-kryptering og sikkerhet. Alt dette leveres som en del av HotPDF-komponenten for Delphi og C++Builder, sammen med API-ene for lasting, redigering, kryptering og signering som er dekket andre steder på denne bloggen.