Tehnični članak

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

Diagram HotPDF napake in popravka števca ponovitev PBKDF2: ponarejen števec štirih milijard krogov je zasedel CPE, dokler ga spodnja in zgornja meja v HPDFCrypt nista zavrnili
sto milijonov leži nad vsako zakonito datoteko, medtem ko omeji najslabši primer — dekodiraj široko, nato omeji, nato uporabi

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

// Poganja celoten cevovod: naloži PDF z mestom za podpis, razčleni PFX,
// izpelje ključ, zgradi CMS SignedData, zapiše podpisani izhod.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // podpis je vgrajen
else
  // podpisovanje se ni dokončalo
;

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

HotPDF: pasova pred in po napaki prevrtitve dolžine ASN.1: ponarejena dolžina, prišteta v 32 bitih, je zdrsnila mimo preverjanja meja, dokler HPDFASN1ParseNode ni bral v Int64 in zavrnil
varovalka je tekla proti občesanemu duhu prave vrednosti; branje široke vrednosti najprej naredi ponarejeno dolžino vidno pred vsakim SetLength

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:

// Past: predznačena 32-bitna aritmetika. Ko je ContentLen blizu MaxInt,
// Pos + ContentLen prekorači obseg v NEGATIVNO vrednost, zato je primerjava
// neresnična in ponarejena dolžina ~2 GB gladko steče skozi.
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:

// Pravilno: oba operanda razširjena na Int64 pred seštevanjem, zato vsota
// ne more prekoračiti obsega. Ponarejena dolžina 2 GB zdaj ne prestane preverjanja meje.
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

Cevovod uvoza .pfx v HotPDF: napadalcu podvrženi bajti gredo skozi HPDFPFX in HPDFASN1, preden sledita izpeljava ključa HPDFCrypt in podpisovanje HPDFRSA/HPDFCMS
bralnik PKCS#12 je napadna površina — kriptografija navzdol nikoli ni pomembna, če sta razčlenjevalnika malomarna

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:

// Najprej preberi število iteracij kot Int64, nato ga omeji na razumen razpon
// PREDEN se zoži v 32-bitno polje Iterations, ki ga uporablja PBKDF2.
LIter := HPDFASN1ToInteger(Data, Node);          // vrne 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);                    // varno: že omejeno

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 Delphi Component za Delphi in C++Builder, skupaj z vmesniki API za nalaganje, urejanje, šifriranje in podpisovanje, ki so obravnavani drugje na tem blogu