Technical Article

Hærdning af en Delphi PDF-signerer mod skadelig PKCS#12

Når du signerer en PDF, tænker du normalt på signeringsnøglen som noget, du kontrollerer. Den ligger i en .pfx-fil, du har genereret, beskyttet af en adgangskode, du har valgt. Koden, der læser den fil, føles som rørføring, ikke en grænse. Den intuition er forkert i det øjeblik, certifikatet ikke længere er dit. Et skrivebordsværktøj, der lader en bruger vælge en hvilken som helst .pfx, en server, der accepterer et uploadet legitimationsbevis, en batch-signerer, der fodres med certifikater over netværket, sender alle angriber-påvirkede bytes til en fortolker, før en eneste signeringsbyte produceres. En PKCS#12-læser er en angrebsflade, på samme måde som en billeddekoder eller en skrifttypeindlæser er.

Denne artikel gennemgår to reelle fejl, der fandtes i den læser, begge i den sti, der importerer et signeringslegitimationsbevis. Ingen af dem er eksotiske. Begge stammer fra den samme grundårsag, som rammer næsten enhver binær fortolker skrevet i et sprog med heltæltyper af fast bredde: en længde eller et antal fra filen betros et skridt længere, end det burde. Den ene fører til en out-of-bounds-læsning, den anden til en proces, der hænger, indtil du stopper den.

Hvor bytes rejser hen

Import af en .pfx til at signere et dokument er ikke én handling, det er en kort pipeline, og hvert trin fortolker noget, som en angriber kan have skrevet. Containeren er en PKCS#12-struktur som defineret i RFC 7292, en rede af AuthenticatedSafe-poser pakket ind i et krypteret hylster, der indeholder den private nøgle. At læse den betyder at gennemgå ASN.1, udlede en nøgle fra adgangskoden, dekryptere og derefter overdrage den gendannede RSA-nøgle til koden, der bygger signaturen.

I HotPDF kortlægges disse trin til separate enheder. PKCS#12-containerlogikken lever i HPDFPFX. Hvert mærke, længde og værdi, den berører, dekodes af ASN.1-læseren i HPDFASN1. Nøgleudledning og PBES2-dekryptering ligger i HPDFCrypt sammen med PBKDF2HMACSHA256. Når nøglen er gendannet, omdanner HPDFRSA og CMS-SignedData-byggeren i HPDFCMS den til den fritstående signatur, der er indlejret i PDF'en. Det offentlige indgangspunkt, der driver hele kæden, is ét kald.

// 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 af signer.pfx flyder gennem HPDFASN1 og HPDFPFX, før nogen kryptografi finder sted. Hvis disse to enheder ikke er forsigtige med, hvad filen hævder, får kryptografien længere nede i strømmen aldrig chancen for at gøre en forskel.

Fejl ét: en ASN.1-længde, der wrapper forbi beskyttelsen

ASN.1 i DER og BER koder hvert element som et mærke (tag), en længde og så mange indholdsbytes. Længden er det felt, du skal stole på, men verificere, fordi det fortæller fortolkeren, hvor langt den skal læse, og det blev skrevet af den, der oprettede filen. X.690 §8.1.3 definerer to kodninger. Den korte form pakker en længde på 0 til 127 ind i en enkelt byte. Den lange form, der bruges til alt større, bruger én startbyte, hvis laveste syv bits angiver antallet af efterfølgende længdebytes, hvorefter så mange big-endian bytes bærer den faktiske værdi. Fire længdebytes kan derfor erklære en indholdsstørrelse, der nærmer sig fire gigabytes.

Efter dekodning af en sådan værdi skal fortolkeren kontrollere, at indholdet faktisk passer ind i bufferen, før den stoler på det. Den naturlige kontrol er at bekræfte, at den aktuelle position plus indholdslængden ikke løber forbi slutningen af dataene. Skrevet på den indlysende måde, med positionen, indholdslængden og totalen alle indeholdt i 32-bit heltal med fortegn, er den beskyttelse defekt:

// 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 additionen, ikke sammenligningen. Når ContentLen er tæt på MaxInt (2147483647), overløber Pos + ContentLen det signerede 32-bit område og wrapper rundt til et negativt tal. En negativ sum er aldrig større end Total, så beskyttelsen rapporterer, at alt er fint og lader fortolkeren fortsætte med en indholdslængde på omkring to gigabytes, som bufferen ikke indeholder. Det, der sker bagefter, er skaden: læseren allokerer en buffer til denne påståede længde og kopierer ind i den, en SetLength efterfulgt af en Move, der læser fra kilden. Kilden har kun et par hundrede bytes tilbage, så kopieringen læser langt forbi slutningen af inputtet, en out-of-bounds-læsning, der i bedste fald crasher og i værste fald lækker tilstødende proceshukommelse ind i fortolkningen.

Den eneste korrekte beskyttelse udvider den mellemliggende sum før sammenligningen, så additionen ikke kan overløbe den type, den beregnes i. Løsningen forfremmer begge operander 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 indeholder summen af to 32-bit værdier uden tab, så sammenligningen ser det rigtige tal og afviser den forfalskede længde. Den separate ikke-negative kontrol af ContentLen lukker det matchende tilfælde, hvor en dekodet værdi lander negativt i sig selv. In HotPDF denne beskyttelse lever i HPDFASN1ParseNode, funktionen, der producerer den knude, alle andre hjælpere bygger på. Fordi HPDFASN1Content dimensionerer sin SetLength og Move direkte fra knudens indholdslængde, ville en knude, der bestod en dårlig beskyttelse, have forgiftet hver læsning taget fra den. At rette grænsen på dekodningstidspunktet er det, der gør hjælperne over den sikre.

Fejl to: et PBKDF2-iterationsantal brugt som våben

Den anden fejl er ikke en hukommelsesfejl, det er filen, der fortæller din CPU, hvor hårdt den skal arbejde. PKCS#12 beskytter sit nøglemateriale med PBES2, det adgangskodebaserede skema fra PKCS#5, specificeret i RFC 8018. PBES2 kører en nøgleudledningsfunktion, her PBKDF2 med HMAC-SHA-256, og derefter en cipher, her AES-256-CBC. PBKDF2 tager et iterationsantal, og dette antal er en parameter, der bæres i filen. Formålet er netop at være langsom: flere iterationer betyder, at hvert adgangskodegæt koster mere, hvilket er godt mod en offline-angriber. RFC 8018 §4.2 er eksplicit om, at et større antal er bedre for sikkerheden, og sætter bevidst intet loft.

Denne åbenhed er fin, når du selv har genereret filen. Den er et våben, når angriberen gjorde det. Iterationsantallet er en angriberkontrolleret arbejdsfaktor, og en angriberkontrolleret arbejdsfaktor er et algoritmisk-kompleksitets-denial-of-service-angreb. En forfalsket .pfx kan kode et iterationsantal i milliardklassen; fortolkeren læser det pligtskyldigt og kalder PBKDF2 i så mange runder af HMAC-SHA-256, og processen forsvinder ind i en løkke, der ikke vender tilbage i minutter eller timer for én leveret fil. På en signeringsserver, der håndterer ét legitimationsbevis pr. anmodning, standser en enkelt manipuleret upload en proces.

Antallet gør wraparound værre, før det får CPU'en to spinne. Iterationsværdien findes i filen som et ASN.1 INTEGER, der ikke har nogen fast bredde, mens feltet, PBKDF2 i sidste ende forbruger, er et 32-bit Integer. Hvis man afkoder INTEGER direkte ind i dette felt, afkortes en stor værdi, og en værdi skabt til at lande på fortegnsbitten kommer tilbage som negativ eller som et urelateret lille tal, så selv arbejdets størrelse ikke længere er den, filen så ud til at anmode om. Løsningen læser værdien i fuld bredde og begrænser den før afkortning:

// 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øsninger er den samme løsning

De to fejl ser forskellige ud, den ene er en bufferoverløb og den anden en ophængt proces, men de er den samme fejl. I begge tilfælde blev et tal fra en upålidelig fil overført til en type med fast bredde et trin for tidligt, før det var blevet kontrolleret mod virkeligheden. Længden blev lagt sammen i 32 bits før grænsetesten; iterationsantallet blev afkortet til 32 bits før områdestesten. Begge underkaster sig den samme disciplin: dekod i fuld bredde, kontroller mod den reelle grænse, og afkort først derefter. Den mellemliggende Int64 er ikke et stilvalg, det er den eneste bredde, hvor beskyttelsen kan se den værdi, angriberen faktisk skrev. En grænse, der overløber, er ikke en grænse, og et antal uden loft er ikke en parameter, det er en fjernstyret drossel på din egen CPU.

Praktisk vejledning til en signeringspipeline

Valider upålidelige certifikat-input på samme måde, som du ville validere ethvert upålideligt upload. Sæt loft over størrelsen af en .pfx, du accepterer, da en legitim en er på kilobytes, ikke megabytes. Behandl et fortolkningssvigt som rutinemæssigt afvist input, ikke en fejl, der er værd at vise en stacktrace for til brugeren. Hvis du signerer på en server, skal du køre importen der, hvor en standset proces ikke kan trække tjenesten ned med sig, og lægge en timeout omkring handlingen, så en uventet ressourcekrævende fil begrænses af væguret såvel som af iterationsloftet.

Den bredere lektie rækker ud over certifikater. Hærdning af fortolkere er ikke en engangsrevision af en enkelt enhed, det er en egenskab ved hvert sted, dit bibliotek læser bytes, det ikke selv har skrevet. Et PDF-bibliotek fortolker en stor del fra upålidelige kilder: skrifttyper indlejret i et dokument, billeder i et halvt dusin codecs, strømfiltre og, på signeringsstien, certifikater. Hver af disse er en angrebsflade, og hver fortjener den samme mistænksomhed over for enhver længde og ethvert antal. HotPDF bygger import- og signeringsstien på de hærdede enheder HPDFASN1, HPDFPFX, HPDFCrypt og HPDFCMS, der er beskrevet her, så det legitimationsbevis, du giver det, uanset hvor det kom fra, fortolkes defensivt, før der nogensinde stoles på det.

Signeringsarbejdsgangen, som disse kontroller beskytter, er dækket fra ende til anden i vores gennemgang af PAdES digitale signaturer i Delphi, og den samme defensive holdning anvendt på dokumentkryptering, inklusive AES-256-nøglestien, der deler denne kodebase, er beskrevet i artiklen om AES-256-kryptering og sikkerhed. Alt dette leveres som en del af HotPDF Component til Delphi og C++Builder sammen med API'erne til indlæsning, redigering, kryptering og signering, der er dækket andre steder på denne blog.