Technisch artikel

Delphi PDF-ondertekenaar harden tegen kwaadaardige PKCS#12

Wanneer u een PDF ondertekent, ziet u de ondertekeningssleutel doorgaans als iets wat u beheert. Zij woont in een .pfx-bestand dat u hebt gegenereerd, beschermd door een wachtwoord dat u hebt gekozen. De code die dat bestand leest voelt als leidingwerk, niet als een grens. Die intuïtie klopt niet zodra het certificaat niet meer van u is. Een desktoptool waarmee een gebruiker elke willekeurige .pfx kan kiezen, een server die een geüploade credential accepteert, een batchondertekenaar die via het netwerk certificaten krijgt aangereikt, alle drie geven bytes die door een aanvaller zijn beïnvloed aan een parser voordat er ook maar één handtekeningbyte wordt geproduceerd. Een PKCS#12-lezer is aanvalsoppervlak, in dezelfde zin als een beelddecoder of een lettertypelader dat is

Dit artikel loopt twee echte defecten langs die in die lezer leefden, beide in het pad dat een ondertekeningscredential importeert. Geen van beide is exotisch. Beide komen voort uit dezelfde hoofdoorzaak die vrijwel elke binaire parser treft die in een taal met vaste-breedte gehele getallen is geschreven: een lengte of een aantal uit het bestand wordt één stap verder vertrouwd dan zou mogen. De ene leidt tot een leesactie buiten de grenzen, de andere tot een proces dat hangt tot u het afschiet

HotPDF-diagram van het defect en de oplossing rond het PBKDF2-iteratieaantal: een vervalst aantal van vier miljard ronden hield de CPU bezet totdat een onder- en bovengrens in HPDFCrypt het afwees
honderd miljoen ligt boven elk legitiem bestand en begrenst tegelijk het slechtste geval — decodeer breed, begrens dan, gebruik daarna
HotPDF: voor- en nabanen van het ASN.1-lengteoverloopdefect: een vervalste lengte die in 32 bits werd opgeteld glipte langs de grenscontrole totdat HPDFASN1ParseNode in Int64 las en haar afwees
de bewaking liep tegen een afgekapte schim van de echte waarde; eerst breed lezen maakt de vervalste lengte zichtbaar vóór enige SetLength

Waar de bytes reizen

Een .pfx importeren om een document te ondertekenen is geen enkele bewerking, het is een korte pijplijn, en elke fase parseert iets wat een aanvaller kan hebben geschreven. De container is een PKCS#12-structuur zoals gedefinieerd in RFC 7292, een nest van AuthenticatedSafe-bags rondom een versleutelde omhulling die de privésleutel bevat. Haar lezen betekent ASN.1 doorlopen, een sleutel afleiden uit het wachtwoord, ontsleutelen, en dan de herstelde RSA-sleutel doorgeven aan de code die de handtekening bouwt

In HotPDF komen die fasen overeen met afzonderlijke units. De logica voor de PKCS#12-container woont in HPDFPFX. Elke tag, lengte en waarde die zij aanraakt wordt gedecodeerd door de ASN.1-lezer in HPDFASN1. Sleutelafleiding en de PBES2-ontsleuteling zitten in HPDFCrypt, naast PBKDF2HMACSHA256. Wanneer de sleutel is hersteld, maken HPDFRSA en de CMS-SignedData-bouwer in HPDFCMS er de losgekoppelde handtekening van die in de PDF wordt ingesloten. Het publieke instappunt dat de hele keten aanstuurt is één aanroep

Pijplijn van .pfx-import in HotPDF: door een aanvaller beïnvloede bytes gaan door HPDFPFX en HPDFASN1 voordat HPDFCrypt sleutelafleiding doet en HPDFRSA/HPDFCMS ondertekenen
een PKCS#12-lezer is aanvalsoppervlak — de cryptografie verderop doet er nooit toe als de twee parsers onzorgvuldig zijn
// Stuurt de volledige pijplijn aan: laad de placeholder-PDF, parseer de PFX,
// leid de sleutel af, bouw CMS SignedData, schrijf de ondertekende uitvoer.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // handtekening ingesloten
else
  // ondertekening niet voltooid
;

Elke byte van signer.pfx stroomt door HPDFASN1 en HPDFPFX voordat er enige cryptografie plaatsvindt. Als die twee units niet zorgvuldig omgaan met wat het bestand beweert, krijgt de cryptografie verderop nooit de kans om ertoe te doen

Defect één: een ASN.1-lengte die langs de bewaking wrapt

ASN.1 codeert in DER en BER elk element als een tag, een lengte en zoveel contentbytes. De lengte is het veld dat u moet vertrouwen maar verifiëren, want het vertelt de parser hoe ver hij moet lezen, en het is geschreven door wie het bestand ook produceerde. X.690 §8.1.3 definieert twee coderingen. De korte vorm perst een lengte van 0 tot 127 in één byte. De lange vorm, gebruikt voor alles wat groter is, besteedt één kopbyte waarvan de lage zeven bits het aantal volgende lengtebytes geven, waarna zoveel big-endian bytes de werkelijke waarde dragen. Vier lengtebytes kunnen dus een contentgrootte declareren die tegen vier gigabyte aanloopt

Na het decoderen van zo'n waarde moet de parser controleren dat de content werkelijk binnen de buffer past voordat hij haar vertrouwt. De natuurlijke controle is bevestigen dat de huidige positie plus de contentlengte niet voorbij het einde van de data loopt. Op de voor de hand liggende manier geschreven, met de positie, de contentlengte en het totaal alle drie in 32-bits ondertekende gehele getallen, is die bewaking kapot:

// De valstrik: ondertekende 32-bits rekenkunde. Met ContentLen dicht bij MaxInt
// loopt Pos + ContentLen over naar een NEGATIEVE waarde, dus de vergelijking
// is onwaar en een vervalste lengte van ~2 GB glijdt er zo doorheen.
if Pos + ContentLen > Total then
  raise EHPDFASN1Error.Create('content overruns buffer');

Het probleem is de optelling, niet de vergelijking. Wanneer ContentLen dicht bij MaxInt (2147483647) ligt, loopt Pos + ContentLen over het ondertekende 32-bits bereik heen en wrapt naar een negatief getal. Een negatieve som is nooit groter dan Total, dus de bewaking meldt dat alles in orde is en laat de parser doorgaan met een contentlengte van ruwweg twee gigabyte die de buffer niet bevat. Wat daarna gebeurt is de schade: de lezer alloceert een buffer voor die beweerde lengte en kopieert erin, een SetLength gevolgd door een Move die uit de bron leest. De bron heeft nog maar een paar honderd bytes over, dus de kopie leest ver voorbij het einde van de invoer, een leesactie buiten de grenzen die in het beste geval crasht en in het slechtste geval aangrenzend procesgeheugen de parse in lekt

De enige juiste bewaking verbreedt de tussensom vóór de vergelijking, zodat de optelling niet kan overlopen in het type waarin zij wordt berekend. De oplossing promoveert beide operanden naar Int64:

// Correct: beide operanden verbreed naar Int64 vóór de optelling, zodat de som
// niet kan wrappen. Een vervalste lengte van 2 GB zakt nu voor de grenscontrole.
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');

Een Int64 houdt de som van twee 32-bits waarden zonder verlies vast, dus de vergelijking ziet het echte getal en wijst de vervalste lengte af. De aparte niet-negatieve controle op ContentLen sluit het bijpassende geval waarin een gedecodeerde waarde op zichzelf negatief uitkomt. In HotPDF woont deze bewaking in HPDFASN1ParseNode, de functie die de knoop produceert waarop elke andere helper voortbouwt. Omdat HPDFASN1Content haar SetLength en Move rechtstreeks op de contentlengte van de knoop dimensioneert, zou een knoop die langs een slechte bewaking kwam elke daaruit genomen leesactie hebben vergiftigd. De grens vastzetten op het punt van decoderen is wat de helpers erboven veilig maakt

Defect twee: een PBKDF2-iteratieaantal als wapen gebruikt

De tweede fout is geen geheugenfout, het is het bestand dat uw CPU vertelt hoe hard hij moet werken. PKCS#12 beschermt zijn sleutelmateriaal met PBES2, het op wachtwoorden gebaseerde schema uit PKCS#5, gespecificeerd in RFC 8018. PBES2 draait een sleutelafleidingsfunctie, hier PBKDF2 met HMAC-SHA-256, en daarna een cipher, hier AES-256-CBC. PBKDF2 neemt een iteratieaantal, en dat aantal is een parameter die in het bestand wordt meegedragen. Het hele doel ervan is traag te zijn: meer iteraties betekent dat elke wachtwoordgok meer kost, wat goed is tegen een offline aanvaller. RFC 8018 §4.2 is expliciet dat een groter aantal beter is voor de beveiliging, en stelt bewust geen plafond

Die openheid is prima wanneer u het bestand hebt gegenereerd. Zij is een wapen wanneer de aanvaller dat deed. Het iteratieaantal is een door de aanvaller beheerste werkfactor, en een door de aanvaller beheerste werkfactor is een denial of service op basis van algoritmische complexiteit. Een vervalste .pfx kan een iteratieaantal in de miljarden coderen; de parser leest het plichtsgetrouw en roept PBKDF2 aan voor zoveel ronden HMAC-SHA-256, en het proces verdwijnt in een lus die op één aangeleverd bestand minuten of uren niet terugkeert. Op een ondertekeningsserver die per verzoek één credential verwerkt, legt één geprepareerde upload een worker stil

Het aantal maakt de overloop erger nog voordat het de CPU laat draaien. De iteratiewaarde staat in het bestand als een ASN.1 INTEGER, dat geen vaste breedte heeft, terwijl het veld dat PBKDF2 uiteindelijk verbruikt een 32-bits Integer is. Decodeer de INTEGER rechtstreeks in dat veld en een grote waarde kapt af, en een waarde die zo is geprepareerd dat zij op de tekenbit landt komt negatief terug of als een of ander onverwant klein getal, zodat zelfs de omvang van het werk niet meer is wat het bestand leek te vragen. De oplossing leest de waarde op volle breedte en begrenst haar voordat zij wordt versmald:

// Lees het iteratieaantal eerst als Int64 en klem het dan in een verstandige band
// VOORDAT het wordt versmald naar het 32-bits veld Iterations dat PBKDF2 gebruikt.
LIter := HPDFASN1ToInteger(Data, Node);          // retourneert 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);                    // veilig: al begrensd

In een Int64 lezen betekent dat de gedecodeerde waarde de echte is en geen afgekapte schim ervan. De ondergrens wijst nul en negatieve aantallen af, die voor een sleutelafleiding onzinnig zijn. De bovengrens, honderd miljoen, ligt ruim boven elk legitiem PKCS#12-bestand, dat tegenwoordig tienduizenden tot enkele honderdduizenden iteraties gebruikt, en begrenst tegelijk het slechtste geval tot een begrensde, overleefbare hoeveelheid werk. Pas nadat de waarde door die band is gekomen, wordt zij versmald naar het 32-bits veld, zodat de afkapping niemand meer kan verrassen. In HotPDF woont deze klem in ParsePBES2Params, waar de PBKDF2-parameters worden gedecodeerd onderweg naar PBKDF2HMACSHA256

Waarom beide oplossingen dezelfde oplossing zijn

De twee defecten zien er verschillend uit, het ene een bufferoverschrijding en het andere een hangend proces, maar ze zijn dezelfde fout. In beide gevallen werd een getal uit een niet-vertrouwd bestand één stap te vroeg in een type met vaste breedte gedragen, voordat het tegen de werkelijkheid was gecontroleerd. De lengte werd in 32 bits opgeteld vóór de grenstest; het iteratieaantal werd naar 32 bits versmald vóór de bereiktest. Beide zwichten voor dezelfde discipline: decodeer op volle breedte, controleer tegen de echte limiet, en versmal pas daarna. De tussenliggende Int64 is geen stijlkeuze, het is de enige breedte waarin de bewaking de waarde kan zien die de aanvaller werkelijk heeft geschreven. Een grens die overloopt is geen grens, en een aantal zonder plafond is geen parameter, het is een remote gaspedaal op uw eigen CPU

Praktische richtlijnen voor een ondertekeningspijplijn

De smalle les is om niet-vertrouwde certificaatinvoer te valideren zoals u elke niet-vertrouwde upload zou valideren. Begrens de omvang van een .pfx die u accepteert, aangezien een legitieme kilobytes groot is en geen megabytes. Behandel een parsefout als routinematig afgewezen invoer, niet als een fout die een stack trace naar de gebruiker verdient. Ondertekent u op een server, draai de import dan waar een vastgelopen worker de dienst niet mee omlaag kan trekken, en zet een time-out om de bewerking zodat een onverwacht duur bestand ook door de klok wordt begrensd en niet alleen door het iteratieplafond

De bredere les reikt voorbij certificaten. Parserharding is geen eenmalige audit van één unit, het is een eigenschap van elke plek waar uw bibliotheek bytes leest die zij niet zelf heeft geschreven. Een PDF-bibliotheek parseert heel wat uit niet-vertrouwde bronnen: lettertypen die in een document zijn ingesloten, afbeeldingen in een half dozijn codecs, streamfilters en, op het ondertekeningspad, certificaten. Elk daarvan is aanvalsoppervlak, en elk verdient hetzelfde wantrouwen jegens elke lengte en elk aantal. HotPDF bouwt het import- en ondertekeningspad op de hier beschreven geharde units HPDFASN1, HPDFPFX, HPDFCrypt en HPDFCMS, zodat de credential die u het aanreikt, waar zij ook vandaan komt, defensief wordt geparseerd voordat zij ooit wordt vertrouwd

De ondertekeningsworkflow die deze controles beschermen wordt van begin tot eind behandeld in onze doorloop van PAdES-handtekeningen in Delphi, en dezelfde defensieve houding toegepast op documentversleuteling, inclusief het AES-256-sleutelpad dat deze codebase deelt, wordt beschreven in het artikel over AES-256-versleuteling en beveiliging. Dit alles wordt geleverd als onderdeel van het HotPDF Delphi Component voor Delphi en C++Builder, naast de API's voor laden, bewerken, versleutelen en ondertekenen die elders op deze blog aan bod komen