Technical Article

Härdning av en Delphi PDF-signerare mot skadlig PKCS#12

När du signerar en PDF tänker du vanligtvis på signeringsnyckeln som något du kontrollerar. Den lever i en .pfx-fil som du har genererat, skyddad av ett lösenord du har valt. Koden som läser den filen känns som rörledningar, inte en gräns. Den intuitionen är felaktig så fort certifikatet slutar vara ditt. Ett skrivbordsverktyg som låter en användare välja vilken .pfx som helst, en server som tar emot en uppladdad inloggningsuppgift eller en batch-signerare som matas med certifikat över nätverket, lämnar alla över attacker-påverkade bytes till en tolkningsmodul innan en enda signeringsbyte har skapats. En PKCS#12-läsare är en attackerbar yta, på samma sätt som en bildavkodare eller en teckensnittsladdare är

Denna artikel går igenom två verkliga defekter som fanns i den läsaren, båda i sökvägen som importerar en signeringsuppgift. Ingen av dem är exotisk. Båda härstammar från samma grundorsak som drabbar nästan alla binära tolkar skrivna i ett språk med heltalsvariabler med fast bredd: en längd eller ett antal från filen betros ett steg längre än vad det borde. Den ena leder till en läsning utanför gränserna, den andra till en process som hänger sig tills du avslutar den

Vart bytes färdas

Att importera en .pfx för att signera ett dokument är inte en enda operation, det är en kort pipeline, och varje steg tolkar något som en angripare kan ha skrivit. Behållaren är en PKCS#12-struktur enligt definitionen i RFC 7292, ett näste av AuthenticatedSafe-påsar lindade runt ett krypterat hölje som håller den privata nyckeln. Att läsa den innebär att stega igenom ASN.1, härleda en nyckel från lösenordet, dekryptera och sedan lämna över den återställda RSA-nyckeln till koden som bygger signaturen

I HotPDF mappas dessa steg till olika enheter. PKCS#12-behållarlogiken ligger i HPDFPFX. Varje tagg, längd och värde den berör avkodas av ASN.1-läsaren i HPDFASN1. Nyckelhärledning och PBES2-dekryptering ligger i HPDFCrypt tillsammans med PBKDF2HMACSHA256. När nyckeln har återställts förvandlar HPDFRSA och CMS SignedData-byggaren i HPDFCMS den till den fristående signaturen som bäddas in i PDF-filen. Den offentliga startpunkten som driver hela kedjan är ett enda anrop

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

Varje byte i signer.pfx strömmar genom HPDFASN1 och HPDFPFX innan någon kryptografi sker. Om dessa två enheter inte är försiktiga med vad filen påstår får kryptografin nedströms aldrig chansen att spela någon roll

Defekt ett: en ASN.1-längd som spiller förbi skyddet

ASN.1 i DER och BER kodar varje element som en tagg, en längd och så många innehållsbytes. Längden är fältet som du måste lita på men verifiera, eftersom det talar om för tolken hur långt den ska läsa, och det skrevs av den som producerade filen. X.690 §8.1.3 definierar två kodningar. Den korta formen packar en längd på 0 till 127 i en enda byte. Den långa formen, som används för allt som är större, använder en inledande byte vars lägsta sju bitar anger antalet längdbytes som följer, varpå det antalet big-endian-bytes bär det faktiska värdet. Fyra längdbytes kan därför deklarera en innehållsstorlek som närmar sig fyra gigabyte

Efter att ha avkodat ett sådant värde måste tolken kontrollera att innehållet faktiskt får plats i bufferten innan den litar på det. Den naturliga kontrollen är att bekräfta att den aktuella positionen plus innehållslängden inte sträcker sig förbi slutet av data. Om det skrivs på det uppenbara sättet, med positionen, innehållslängden och summan i 32-bitars signerade heltal, fungerar inte detta skydd

// 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 är additionen, inte jämförelsen. När ContentLen är nära MaxInt (2147483647) spiller Pos + ContentLen över det signerade 32-bitarsintervallet och slår runt till ett negativt tal. En negativ summa är aldrig större än Total, så skyddet rapporterar att allt är bra och låter tolken fortsätta med en innehållslängd på ungefär två gigabyte som bufferten inte innehåller. Det som händer sedan är skadan: läsaren tilldelar en buffert för den påstådda längden och kopierar till den, en SetLength följt av en Move som läser från källan. Källan har bara några hundra bytes kvar, så kopieringen läser långt förbi slutet av indata, en läsning utanför gränserna som i bästa fall kraschar och i värsta fall läcker intilliggande processminne till tolkningen

Det enda korrekta skyddet utvidgar den mellanliggande summan innan jämförelsen, så att additionen inte kan spilla över den typ den beräknas i. Lösningen upphöjer båda operanderna till 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 rymmer summan av två 32-bitars värden utan förlust, så jämförelsen ser det verkliga talet och avvisar den förfalskade längden. Den separata kontrollen för icke-negativa värden på ContentLen stänger det matchande fallet där ett avkodat värde blir negativt i sig självt. I HotPDF finns detta skydd i HPDFASN1ParseNode, funktionen som producerar noden som alla andra hjälpare bygger på. Eftersom HPDFASN1Content dimensionerar sin SetLength och Move direkt från nodens innehållslängd, skulle en nod som passerade ett trasigt skydd ha förgiftat varje läsning som gjordes från den. Att korrigera gränsen vid avkodningspunkten är det som gör hjälparna ovanför säkra

Defekt två: ett PBKDF2-iterationsantal som används som vapen

Den andra bristen är inte ett minnesfel, det är filen som talar om för din processor hur hårt den ska arbeta. PKCS#12 skyddar sitt nyckelmaterial med PBES2, det lösenordsbaserade schemat från PKCS#5 som specificeras i RFC 8018. PBES2 kör en nyckelhärledningsfunktion, här PBKDF2 med HMAC-SHA-256, och sedan en chiffer, här AES-256-CBC. PBKDF2 tar ett antal iterationer, och det antalet är en parameter som finns i filen. Hela dess syfte är att vara långsam: fler iterationer innebär att varje lösenordsgissning kostar mer, vilket är bra mot en offline-angripare. RFC 8018 §4.2 is explicit att ett större antal är bättre för säkerheten, och sätter medvetet inget tak

Den öppenheten är bra när du genererade filen. Den är ett vapen när angriparen gjorde det. Iterationsantalet är en angriparkontrollerad arbetsfaktor, och en angriparkontrollerad arbetsfaktor är en överbelastningsattack via algoritmisk komplexitet. En förfalskad .pfx kan koda ett antal iterationer i miljardklassen; tolken läser pliktskyldigt av det och anropar PBKDF2 för så många rundor av HMAC-SHA-256, och processen försvinner in i en loop som inte kommer att returnera på minuter eller timmar för en enda angiven fil. På en signeringsserver som hanterar en inloggningsuppgift per begäran kan en enda preparerad uppladdning stänga ner en arbetsprocess

Antalet gör omslaget värre innan det får processorn att spinna. Iterationsvärdet finns i filen som ett ASN.1 INTEGER, vilket inte har någon fast bredd, medan fältet som PBKDF2 slutligen konsumerar är en 32-bitars Integer. Avkoda INTEGER direkt till det fältet och ett stort värde trunkeras, och ett värde som är skapat för att landa på teckenbiten kommer tillbaka som negativt eller som något orelaterat litet tal, så att inte ens storleken på arbetet längre är vad filen verkade be om. Lösningen läser värdet i full bredd och begränsar det innan det smalnas av

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

Att läsa in i en Int64 innebär att det avkodade värdet är det verkliga, inte en trunkerad spökbild av det. Den nedre gränsen avvisar noll och negativa antal, vilket är orimligt för en nyckelhärledning. Den övre gränsen, hundra miljoner, ligger långt över alla legitima PKCS#12-filer, vilka idag använder tiotals till låga hundratals tusen iterationer, samtidigt som det värsta scenariot begränsas till en hanterbar mängd arbete. Först efter att värdet har passerat det intervallet smalnas det av till 32-bitarsfältet, så att trunkeringen inte längre kan överraska någon. I HotPDF finns denna begränsning i ParsePBES2Params, där PBKDF2-parametrarna avkodas på vägen till PBKDF2HMACSHA256

Varför båda lösningarna är samma lösning

De två defekterna ser olika ut, den ena ett buffertspill och den andra en låst process, men de är samma misstag. I båda fallen fördes ett tal från en opålitlig fil in i en typ med fast bredd ett steg för tidigt, innan det hade kontrollerats mot verkligheten. Längden adderades i 32 bitar före gränstestet; iterationsantalet smalnades av till 32 bitar före intervalltestet. Båda ger vika för samma disciplin: avkoda i full bredd, kontrollera mot den verkliga gränsen och först därefter smalna av. Det mellanliggande Int64-värdet är inte ett stilval, det är den enda bredd där skyddet kan se det värde som angriparen faktiskt skrev. En gräns som spiller över är ingen gräns, och ett antal utan tak är inte en parameter, det är ett fjärrstyrd strypning av din egen processor

Praktisk vägledning för en signeringspipeline

Den enkla lärdomen är att validera opålitliga certifikatindata på samma sätt som du skulle validera vilken opålitlig uppladdning som helst. Sätt ett tak för storleken på en .pfx du accepterar, eftersom en legitim fil är i kilobyte, inte megabyte. Behandla ett tolkningsfel som rutinmässigt avvisad indata, inte som ett fel som är värt en stackspårning för användaren. Om du signerar på en server, kör importen där en stängd arbetsprocess inte kan ta ner tjänsten med sig, och sätt en tidsgräns runt operationen så att en oväntat krävande fil begränsas av klocktid såväl som av iterationstaket

Den bredare lärdomen sträcker sig bortom certifikat. Härdning av tolkningsmoduler är inte en engångsrevision av en enhet, det är en egenskap på varje plats där ditt bibliotek läser bytes det inte har skrivit. Ett PDF-bibliotek tolkar mycket från opålitliga källor: teckensnitt inbäddade i ett dokument, bilder i ett halvt dussin kodekar, strömfilter och, på signeringssökvägen, certifikat. Var och en av dessa är en attackerbar yta, och var och en förtjänar samma misstänksamhet mot varje längd och varje antal. HotPDF bygger import- och signeringssökvägen på de härdade enheterna HPDFASN1, HPDFPFX, HPDFCrypt och HPDFCMS som beskrivs här, så att inloggningsuppgifterna du ger det, varifrån de än kom, tolkas defensivt innan de någonsin betros

Signeringsarbetsflödet som dessa kontroller skyddar beskrivs från början till slut i vår genomgång av digitala PAdES-signaturer i Delphi, och samma defensiva hållning tillämpad på dokumentkryptering, inklusive AES-256-nyckelsökvägen som delar denna kodbas, beskrivs i artikeln om AES-256-kryptering och säkerhet. Allt detta levereras som en del av HotPDF Component för Delphi och C++Builder, tillsammans med de API:er för laddning, redigering, kryptering och signering som beskrivs på andra ställen i denna blogg