Atunci când semnați un PDF, de obicei vă gândiți la cheia de semnare ca la ceva ce controlați. Aceasta se află într-un fișier .pfx pe care l-ați generat, protejat de o parolă pe care ați ales-o. Codul care citește acel fișier pare a fi o simplă conductă, nu o frontieră. Această intuiție este greșită în momentul în care certificatul nu mai este al dvs. Un instrument desktop care permite unui utilizator să aleagă orice .pfx, un server care acceptă o acreditare încărcată, un semnatar în lot (batch) alimentat cu certificate prin rețea, toate acestea transmit octeți influențați de atacator unui analizor (parser) înainte de a fi produs un singur octet de semnătură. Un cititor PKCS#12 este o suprafață de atac, în același sens în care este un decodor de imagini sau un încărcător de fonturi.
Acest articol prezintă două defecte reale care existau în acel cititor, ambele pe calea care importă o acreditare de semnare. Niciunul nu este exotic. Ambele provin din aceeași cauză principală care afectează aproape orice analizor binar scris într-un limbaj cu întregi de lățime fixă: o lungime sau un număr din fișier este de încredere cu un pas mai mult decât ar trebui. One duce la o citire în afara limitelor (out-of-bounds read), celălalt la un proces care se blochează până când îl opriți forțat.
Unde călătoresc octeții
Importul unui fișier .pfx pentru a semna un document nu este o singură operațiune, ci o conductă scurtă, iar fiecare etapă analizează ceva ce este posibil să fi fost scris de un atacator. Containerul este o structură PKCS#12 definită în RFC 7292, un cuib de pachete AuthenticatedSafe înfășurate în jurul unui înveliș criptat care deține cheia privată. Citirea sa înseamnă parcurgerea ASN.1, derivarea unei chei din parolă, decriptarea, apoi predarea cheii RSA recuperate către codul care construiește semnătura.
În HotPDF, acele etape sunt mapate pe unități distincte. Logica containerului PKCS#12 se află în HPDFPFX. Fiecare etichetă, lungime și valoare pe care o atinge este decodificată de cititorul ASN.1 din HPDFASN1. Derivarea cheii și decriptarea PBES2 se află în HPDFCrypt, alături de PBKDF2HMACSHA256. Când cheia este recuperată, HPDFRSA și constructorul CMS SignedData din HPDFCMS o transformă în semnătura detașată încorporată în PDF. Punctul public de intrare care conduce întregul lanț este un singur apel.
// 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
;
Fiecare octet din signer.pfx trece prin HPDFASN1 și HPDFPFX înainte de a avea loc orice criptografie. Dacă aceste două unități nu sunt atente la ceea ce pretinde fișierul, criptografia din aval nu va mai conta niciodată.
Defectul unu: o lungime ASN.1 care trece de protecție prin depășire
ASN.1 în DER și BER codifică fiecare element ca o etichetă, o lungime și acel număr de octeți de conținut. Lungimea este câmpul pe care trebuie să-l verificați chiar dacă aveți încredere în el, deoarece indică analizorului cât de mult să citească și a fost scris de cel care a produs fișierul. X.690 §8.1.3 definește două codificări. Forma scurtă împachetează o lungime de la 0 la 127 într-un singur octet. Forma lungă, folosită pentru orice valoare mai mare, utilizează un octet de început ale cărui ultime șapte biți indică numărul de octeți de lungime care urmează, iar apoi acei octeți în format big-endian transportă valoarea reală. Prin urmare, patru octeți de lungime pot declara o dimensiune a conținutului care se apropie de patru gigaocteți.
După decodificarea unei astfel de valori, analizorul trebuie să verifice dacă conținutul se potrivește efectiv în buffer înainte de a avea încredere în el. Verificarea firească este de a confirma că poziția curentă plus lungimea conținutului nu depășesc sfârșitul datelor. Scrisă în mod evident, cu poziția, lungimea conținutului și totalul păstrate în întregi cu semn pe 32 de biți, acea protecție este ineficientă:
// 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');
Problema este adunarea, nu compararea. Când ContentLen este aproape de MaxInt (2147483647), Pos + ContentLen depășește intervalul cu semn pe 32 de biți și se transformă într-un număr negativ. O sumă negativă nu este niciodată mai mare decât Total, așa că protecția raportează că totul este în regulă și permite analizorului să continue cu o lungime a conținutului de aproximativ doi gigaocteți pe care bufferul nu îi conține. Ceea ce urmează este dauna: cititorul alocă un buffer pentru acea lungime declarată și copiază în el, un SetLength urmat de un Move care citește din sursă. Sursa mai are doar câteva sute de octeți, așa că operațiunea de copiere citește mult dincolo de sfârșitul datelor de intrare, o citire în afara limitelor care în cel mai bun caz blochează programul, iar în cel mai rău caz divulgă memoria procesului adiacent în analiză.
Singura protecție corectă extinde suma intermediară înainte de comparare, astfel încât adunarea să nu poată depăși tipul în care este calculată. Remedierea promovează ambii operanzi la 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');
Un Int64 reține suma a două valori pe 32 de biți fără pierderi, astfel încât compararea vede numărul real și respinge lungimea falsificată. Verificarea separată a caracterului non-negativ pentru ContentLen acoperă cazul corespunzător în care o valoare decodificată ajunge să fie negativă de la sine. În HotPDF, această protecție se află în HPDFASN1ParseNode, funcția care produce nodul pe care se bazează orice altă funcție ajutătoare. Deoarece HPDFASN1Content își stabilește dimensiunile pentru SetLength și Move direct din lungimea conținutului nodului, un nod care ar fi trecut de o protecție defectuoasă ar fi compromis fiecare citire efectuată din el. Corectarea limitei în momentul decodificării este ceea ce face sigure funcțiile ajutătoare de deasupra ei.
Defectul doi: un număr de iterații PBKDF2 folosit ca armă
A doua defecțiune nu este o eroare de memorie, ci este reprezentată de fișierul care îi spune procesorului cât de mult să lucreze. PKCS#12 își protejează materialul cheii cu PBES2, schema bazată pe parolă din PKCS#5, specificată în RFC 8018. PBES2 rulează o funcție de derivare a cheii, în acest caz PBKDF2 cu HMAC-SHA-256, apoi un cifru, aici AES-256-CBC. PBKDF2 acceptă un număr de iterații, iar acel număr este un parametru transportat în fișier. Scopul său principal este să fie lent: mai multe iterații înseamnă că fiecare încercare de ghicire a parolei costă mai mult, ceea ce este util împotriva unui atacator offline. RFC 8018 §4.2 precizează în mod explicit că un număr mai mare este mai bun pentru securitate și nu stabilește în mod deliberat niciun plafon.
Această deschidere este în regulă atunci când ați generat fișierul. Devine o armă când atacatorul a făcut-o. Numărul de iterații este un factor de lucru controlat de atacator, iar un factor de lucru controlat de atacator este un atac de tip refuz al serviciului prin complexitate algoritmică. Un fișier .pfx falsificat poate codifica un număr de iterații de ordinul miliardelor; analizorul îl citește cu diligență și apelează PBKDF2 pentru acel număr de runde de HMAC-SHA-256, iar procesul dispare într-o buclă care nu va returna nimic timp de minute sau ore la un singur fișier furnizat. Pe un server de semnare care gestionează o singură acreditare per solicitare, o singură încărcare creată cu rea-intenție blochează un proces de execuție (worker).
Numărul de iterații agravează problema depășirii înainte de a face procesorul să ruleze bucle la nesfârșit. Valoarea iterației se află în fișier ca un tip INTEGER ASN.1, care nu are o lățime fixă, în timp ce câmpul pe care PBKDF2 îl consumă în cele din urmă este un Integer pe 32 de biți. Decodificați tipul INTEGER direct în acel câmp și o valoare mare este trunchiată, iar o valoare concepută să ajungă pe bitul de semn revine ca negativă sau ca un număr mic necorelat, astfel încât nici dimensiunea efortului de calcul nu mai este cea pe care fișierul părea să o ceară. Remedierea citește valoarea la lățime completă și o limitează înainte de a o reduce:
// 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
Citirea într-un Int64 înseamnă că valoarea decodificată este cea reală, nu o proiecție trunchiată a acesteia. Limita inferioară respinge numerele egale cu zero și cele negative, care nu au sens pentru o derivare de cheie. Limita superioară, de o sută de milioane, se situează cu mult peste orice fișier PKCS#12 legitim, care astăzi folosește de la zeci la câteva sute de mii de iterații, limitând în același timp cel mai rău caz la un volum de muncă delimitat și controlabil. Numai după ce valoarea a trecut de acel interval este redusă la câmpul pe 32 de biți, astfel încât trunchierea să nu mai poată surprinde pe nimeni. În HotPDF, această limitare se află în ParsePBES2Params, unde parametrii PBKDF2 sunt decodificați în drum spre PBKDF2HMACSHA256.
De ce ambele remedieri reprezintă de fapt aceeași remediere
Cele două defecte par diferite, unul fiind o depășire de buffer, iar celălalt un proces blocat, dar reprezintă aceeași greșeală. În fiecare caz, un număr dintr-un fișier nesigur a fost transferat într-un tip cu lățime fixă cu un pas prea devreme, înainte de a fi fost verificat în raport cu realitatea. Lungimea a fost adunată pe 32 de biți înainte de testul limitelor; numărul de iterații a fost redus la 32 de biți înainte de testul intervalului. Ambele se supun aceleiași discipline: decodificare la lățime completă, verificare în raport cu limita reală și abia apoi reducerea dimensiunii. Tipul intermediar Int64 nu este o alegere de stil, ci este singura lățime în care protecția poate vedea valoarea pe care atacatorul a scris-o de fapt. O limită care se depășește nu este o limită, iar un număr fără plafon nu este un parametru, ci un control de la distanță asupra propriului procesor.
Ghid practic pentru o conductă de semnare
Lecția specifică este de a valida datele de intrare nesigure ale certificatului în modul în care ați valida orice încărcare nesigură. Limitați dimensiunea unui fișier .pfx pe care îl acceptați, deoarece unul legitim are kiloocteți, nu megaocteți. Tratați o eroare de analiză ca pe o respingere obișnuită a datelor de intrare, nu ca pe o eroare care merită o urmărire a stivei (stack trace) prezentată utilizatorului. Dacă semnați pe un server, rulați importul acolo unde un worker blocat nu poate opri serviciul cu totul și puneți o limită de timp (timeout) în jurul operațiunii, astfel încât un fișier neșteptat de costisitor ca resurse să fie delimitat atât de ceasul real, cât și de limita de iterații.
Lecția mai largă depășește certificatele. Securizarea analizorului nu este un audit unic al unei singure unități, ci este o proprietate a fiecărui loc în care biblioteca citește octeți pe care nu i-a scris ea. O bibliotecă PDF analizează foarte mult din surse nesigure: fonturi încorporate într-un document, imagini în o jumătate de duzină de codec-uri, filtre de flux și, pe calea de semnare, certificate. Fiecare dintre acestea este o suprafață de atac și fiecare merită aceeași suspiciune cu privire la fiecare lungime și fiecare număr. HotPDF construiește calea de import și semnare pe unitățile securizate HPDFASN1, HPDFPFX, HPDFCrypt și HPDFCMS descrise aici, astfel încât acreditarea pe care o furnizați, indiferent de unde provine, să fie analizată defensiv înainte de a fi vreodată de încredere.
Fluxul de lucru de semnare pe care îl protejează aceste verificări este acoperit cap-la-cap în ghidul nou despre semnăturile digitale PAdES în Delphi, iar aceeași postură defensivă aplicată criptării documentelor, inclusiv calea cheii AES-256 care partajează această bază de cod, este descrisă în articolul despre criptarea AES-256 și securitate. Toate acestea sunt livrate ca parte a HotPDF Component pentru Delphi și C++Builder, alături de API-urile de încărcare, editare, criptare și semnare acoperite în alte părți ale acestui blog.