Technical Article

Utrjevanje podpisovalnika PDF v Delphiju pred zlonamernim PKCS#12

Ko podpisujete PDF, običajno mislite na podpisni ključ kot na nekaj, kar nadzorujete. Nahaja se v datoteki .pfx, ki ste jo ustvarili in zaščitili z izbranim geslom. Koda, ki bere to datoteko, se zdi kot navadna napeljava in ne kot meja varnosti. Ta intuicija je napačna v trenutku, ko potrdilo ni več vaše. Namizno orodje, ki uporabniku omogoča izbiro poljubne datoteke .pfx, strežnik, ki sprejema naložene poverilnice, ali paketni podpisovalnik, ki prejema potrdila prek omrežja, vsi predajo bajte pod vplivom napadalca razčlenjevalniku, preden se ustvari en sam bajt podpisa. Bralnik PKCS#12 je napadalna površina v enakem smislu kot dekodirnik slik ali nalagalnik pisav.

Ta članek obravnava dve resnični napaki, ki sta bili prisotni v tem bralniku, obe na poti uvoza podpisnih poverilnic. Nobena ni eksotična. Obe izhajata iz istega izvornega vzroka, ki prizadene skoraj vsak dvojiški razčlenjevalnik, napisan v jeziku s fiksnimi širinami celih števil: dolžini ali številu iz datoteke se zaupa en korak dlje, kot bi se moralo. Ena vodi do branja izven meja, druga pa do procesa, ki se zaustavi, dokler ga ne prekinete.

Kam potujejo bajti

Uvoz datoteke .pfx za podpisovanje dokumenta ni ena sama operacija, temveč kratek cevovod in vsaka faza razčleni nekaj, kar je morda napisal napadalec. Vsebnik je struktura PKCS#12, kot jo določa RFC 7292, gnezdo vrečk AuthenticatedSafe, ovito okoli šifriranega ovoja, ki vsebuje zasebni ključ. Branje pomeni sprehod skozi ASN.1, izpeljavo ključa iz gesla, dešifriranje in nato predajo obnovljenega ključa RSA kodi, ki gradi podpis.

V knjižnici HotPDF se te faze preslikajo v ločene enote. Logika vsebnika PKCS#12 živi v HPDFPFX. Vsako značko, dolžino in vrednost, ki se je dotakne, dekodira bralnik ASN.1 v enoti HPDFASN1. Izpeljava ključa in dešifriranje PBES2 se nahajata v HPDFCrypt skupaj s PBKDF2HMACSHA256. Ko je ključ obnovljen, ga enota HPDFRSA in graditelj CMS SignedData v HPDFCMS spremenita v ločeni podpis, vgrajen v PDF. Javna vstopna točka, ki vodi celotno verigo, je en klic.

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

Vsak bajt datoteke signer.pfx steče skozi HPDFASN1 in HPDFPFX, preden se zgodi kakršno koli šifriranje. Če ti dve enoti nista previdni glede tega, kar trdi datoteka, šifriranje v nadaljevanju sploh ne bo dobilo priložnosti, da bi bilo pomembno.

Napaka ena: dolžina ASN.1, ki se ovije čez zaščito

ASN.1 v kodiranjih DER in BER kodira vsak element kot značko, dolžino in toliko bajtov vsebine. Dolžina je polje, ki mu morate zaupati, a ga hkrati preveriti, saj razčlenjevalniku pove, kako daleč mora brati, zapisal pa ga je tisti, ki je ustvaril datoteko. Standard X.690 §8.1.3 določa dve kodiranji. Kratka oblika stisne dolžino od 0 do 127 v en sam bajt. Dolga oblika, ki se uporablja za vse večje vrednosti, porabi en vodilni bajt, katerega spodnjih sedem bitov določa število bajtov dolžine, ki sledijo, nato pa toliko bajtov v formatu big-endian prenaša dejansko vrednost. Štirje bajti dolžine lahko tako deklarirajo velikost vsebine, ki se približuje štirim gigabajtom.

Po dekodiranju takšne vrednosti mora razčlenjevalnik preveriti, ali se vsebina dejansko prilega medpomnilniku, preden ji zaupa. Naravno preverjanje je potrditev, da trenutni položaj plus dolžina vsebine ne tečeta čez konec podatkov. Če je ta zaščita napisana na očiten način, kjer so položaj, dolžina vsebine in skupna dolžina shranjeni v 32-bitnih predznačenih celih številih, je neuporabna:

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

Težava je v seštevanju, ne v primerjavi. Ko je ContentLen blizu MaxInt (2147483647), seštevek Pos + ContentLen prekorači predznačeni 32-bitni obseg in se ovije v negativno število. Negativna vsota ni nikoli večja od Total, zato zaščita sporoči, da je vse v redu, in pusti razčlenjevalniku, da nadaljuje z dolžino vsebine približno dveh gigabajtov, ki jih medpomnilnik ne vsebuje. Kar sledi, je škoda: bralnik dodeli medpomnilnik za to trdeno dolžino in kopira vanj, pri čemer SetLength in nato Move bereta iz vira. Vir ima le še nekaj sto bajtov, zato kopiranje bere daleč čez konec vhoda. To je branje izven meja, ki v najboljšem primeru povzroči sesutje, v najslabšem pa razkrije sosednji pomnilnik procesa v razčlenjevanje.

Edina pravilna zaščita razširi vmesno vsoto pred primerjavo, tako da seštevanje ne more prekoračiti tipa, v katerem se izračunava. Popravek poviša oba operanda v 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');

Tip Int64 brez izgube zadrži vsoto dveh 32-bitnih vrednosti, zato primerjava vidi dejansko številko in zavrne ponarejeno dolžino. Ločeno preverjanje nenegativnosti za ContentLen zapre ustrezen primer, ko dekodirana vrednost sama po sebi postane negativna. V knjižnici HotPDF ta zaščita živi v funkciji HPDFASN1ParseNode, ki ustvari vozel, na katerem gradijo vsi ostali pomočniki. Ker HPDFASN1Content določa velikost svojih operacij SetLength in Move neposredno iz dolžine vsebine vozla, bi vozel, ki bi šel skozi slabo zaščito, zastrupil vsako branje iz njega. Popravek meje na točki dekodiranja je tisto, kar naredi pomočnike nad njim varne.

Napaka dve: število iteracij PBKDF2, uporabljeno kot orožje

Druga pomanjkljivost ni napaka pomnilnika, temveč datoteka, ki vašemu procesorju pove, kako trdo mora delati. PKCS#12 ščiti svoje ključne materiale s PBES2, shemo na podlagi gesla iz PKCS#5, določeno v RFC 8018. PBES2 zažene funkcijo izpeljave ključa, v tem primeru PBKDF2 s HMAC-SHA-256, nato pa šifro, tukaj AES-256-CBC. PBKDF2 sprejme število iteracij in to število je parameter, ki se prenaša v datoteki. Njegov namen je, da je počasen: več iteracij pomeni, da vsak poskus ugibanja gesla stane več, Portrait of an offline attacker. Standard RFC 8018 §4.2 izrecno določa, da je večje število boljše za varnost, in namerno ne določa zgornje meje.

Ta odprtost je v redu, ko ste datoteko ustvarili sami. Je pa orožje, ko jo ustvari napadalec. Število iteracij je dejavnik dela pod nadzorom napadalca, dejavnik dela pod nadzorom napadalca pa predstavlja zavrnitev storitve zaradi algoritmične kompleksnosti (algorithmic-complexity denial of service). Ponarejena datoteka .pfx lahko kodira število iteracij v milijardah; razčlenjevalnik jo vestno prebere in pokliče PBKDF2 for that many rounds of HMAC-SHA-256, in proces izgine v zanki, ki se ne bo vrnila nekaj minut ali ur zaradi ene same posredovane datoteke. Na podpisovalnem strežniku, ki obravnava eno poverilnico na zahtevo, en sam prirejen prenos zaustavi delovni proces.

Število iteracij poslabša prekoračitev, preden sploh zavrti procesor. Vrednost iteracije živi v datoteki kot ASN.1 INTEGER, ki nima fiksne širine, medtem ko je polje, ki ga PBKDF2 na koncu porabi, 32-bitni Integer. Če dekodirate INTEGER neposredno v to polje, se velika vrednost skrajša, vrednost, oblikovana tako, da pristane na prednačenem bitu, pa se vrne kot negativna ali kot nepovezana majhna številka, tako da celo velikost dela ni več tisto, kar se je zdelo, da datoteka zahteva. Popravek prebere vrednost v polni širini in jo omeji pred krčenjem:

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

Branje v Int64 pomeni, da je dekodirana vrednost resnična in ne skrajšana utvara. Spodnja meja zavrne ničelno in negativno število, kar je nesmiselno za izpeljavo ključa. Zgornja meja, sto milijonov, je precej nad katero koli legitimno datoteko PKCS#12, ki danes uporablja od nekaj deset do nekaj sto tisoč iteracij, hkrati pa omejuje najslabši primer na omejeno, preživetveno količino dela. Šele ko vrednost prestane to preverjanje, se zoži na 32-bitno polje, tako da skrajšanje ne more več nikogar presenetiti. In HotPDF this clamp lives in ParsePBES2Params, where the PBKDF2 parameters are decoded on the way to PBKDF2HMACSHA256.

Zakaj sta oba popravka isti popravek

Obe napaki sta videti različni, ena kot prekoračitev medpomnilnika in druga kot zaustavljen proces, vendar gre za isto napako. V obeh primerih je bila številka iz nezaupljive datoteke prenesena v tip s fiksno širino en korak prezgodaj, preden je bila preverjena glede na realnost. Dolžina je bila sešteta v 32 bitih pred preizkusom meja; število iteracij je bilo zoženo na 32 bitov pred preizkusom obsega. Obe sledita isti disciplini: dekodiraj v polni širini, preveri glede na realno mejo in šele nato zoži. Vmesni Int64 ni slogovna izbira, temveč edina širina, v kateri lahko zaščita vidi vrednost, ki jo je napadalec dejansko napisal. Meja, ki se prekorači, ni meja, in število brez zgornje meje ni parameter, temveč daljinski regulator za vaš lastni procesor.

Praktična navodila za podpisovalni cevovod

Ozkogledna lekcija je validacija nezaupljivih vhodov potrdil na enak način, kot bi validirali kateri koli nezaupljiv prenos. Omejite velikost datoteke .pfx, ki jo sprejemate, saj ima legitimna velikost v kilobajtih in ne megabajtih. Obravnavajte neuspeh razčlenjevanja kot običajen zavrnjen vhod in ne kot napako, vredno sledenja zlaganja (stack trace) uporabniku. Če podpisujete na strežniku, zaženite uvoz tam, kjer zaustavljen delovni proces ne more onemogočiti storitve, in dodajte časovno omejitev (timeout) okoli operacije, da bo nepričakovano draga datoteka omejena tako s časom kot tudi z mejo iteracij.

Širša lekcija sega preko potrdil. Utrjevanje razčlenjevalnika ni enkratna revizija ene enote, temveč lastnost vsakega mesta, kjer vaša knjižnica bere bajte, ki jih ni sama zapisala. Knjižnica PDF razčleni veliko podatkov iz nezaupljivih virov: pisave, vgrajene v dokument, slike v pol ducata kodekih, filtre tokov in, na podpisni poti, potrdila. Vsak od teh predstavlja napadalno površino in vsak si zasluži enak sum glede vsake dolžine in vsakega števila. 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.

Podpisovalni delovni tok, ki ga ti pregledi ščitijo, je v celoti zajet v našem vodniku o digitalnih podpisih PAdES v Delphiju, enaka obrambna drža, uporabljena pri šifriranju dokumentov, vključno s potjo ključa AES-256, ki si deli to kodo, pa je opisana v članku o šifriranju AES-256 in varnosti. Vse to se dostavlja kot del komponente HotPDF Component za Delphi in C++Builder, skupaj z vmesniki API za nalaganje, urejanje, šifriranje in podpisovanje, ki so obravnavani drugje na tem blogu.