Kada potpisujete PDF, obično mislite o ključu za potpisivanje kao o nečemu što kontrolirate. On živi u .pfx datoteci koju ste generirali, zaštićen lozinkom koju ste odabrali. Kod koji čita tu datoteku čini se kao obična cijev, a ne granica. Ta je intuicija pogrešna u trenutku kada certifikat prestane biti vaš. Stolni alat koji korisniku omogućuje odabir bilo kojeg .pfx-a, poslužitelj koji prihvaća prenesenu vjerodajnicu, skupni potpisivač koji prima certifikate preko mreže - svi oni predaju parseru bajtove pod utjecajem napadača prije nego što se proizvede ijedan bajt potpisa. Čitač PKCS#12 je napadačka površina, u istom smislu u kojem je to dekoder slika ili učitavač fontova
Ovaj članak prolazi kroz dva stvarna nedostatka koja su živjela u tom čitaču, oba na putanji koja uvozi vjerodajnicu za potpisivanje. Nijedan nije egzotičan. Oba proizlaze iz istog korijenskog uzroka koji pogađa gotovo svaki binarni parser napisan u jeziku s cijelim brojevima fiksne širine: duljini ili broju iz datoteke vjeruje se korak više nego što bi trebalo. Jedan vodi do čitanja izvan granica, a drugi do procesa koji visi dok ga ne prekinete
Kuda bajtovi putuju
Uvoz .pfx-a radi potpisivanja dokumenta nije jedna operacija, to je kratki cjevovod, a svaka faza analizira nešto što je napadač mogao napisati. Spremnik je struktura PKCS#12 kako je definirano u RFC 7292, gnijezdo AuthenticatedSafe vrećica omotanih oko šifriranog omotača koji drži privatni ključ. Njezino čitanje znači prolazak kroz ASN.1, izvođenje ključa iz lozinke, dešifriranje, a zatim predavanje oporavljenog RSA ključa kodu koji gradi potpis
U HotPDF-u se te faze mapiraju na zasebne jedinice. Logika spremnika PKCS#12 živi u HPDFPFX. Svaku oznaku, duljinu i vrijednost koju dotakne dekodira ASN.1 čitač u HPDFASN1. Izvođenje ključa i dešifriranje PBES2 nalaze se u HPDFCrypt zajedno s PBKDF2HMACSHA256. Kada se ključ oporavi, HPDFRSA i graditelj CMS SignedData u HPDFCMS pretvaraju ga u izdvojeni potpis ugrađen u PDF. Javna ulazna točka koja pokreće cijeli lanac je jedan poziv
// Pokreće cijeli cjevovod: učitava pripremljeni PDF, raščlanjuje PFX,
// izvodi ključ, gradi CMS SignedData i zapisuje potpisanu izlaznu datoteku.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// potpis je ugrađen
else
// potpisivanje nije dovršeno
;
Svaki bajt datoteke signer.pfx teče kroz HPDFASN1 i HPDFPFX prije nego što se dogodi bilo kakvo šifriranje. Ako te dvije jedinice nisu oprezne oko onoga što datoteka tvrdi, kriptografija nizvodno nikada neće dobiti priliku da bude važna
Nedostatak jedan: duljina ASN.1 koja se omotava preko zaštite
ASN.1 u DER-u i BER-u kodira svaki element kao oznaku, duljinu i toliko bajtova sadržaja. Duljina je polje kojem morate vjerovati, ali ga i provjeriti, jer ono govori parseru koliko daleko treba čitati, a napisao ga je onaj tko je proizveo datoteku. X.690 §8.1.3 definira dva kodiranja. Kratki oblik pakira duljinu od 0 do 127 u jedan bajt. Dugi oblik, koji se koristi za sve veće od toga, troši jedan početni bajt čijih donjih sedam bitova daje broj bajtova duljine koji slijede, a zatim toliko big-endian bajtova nosi stvarnu vrijednost. Četiri bajta duljine mogu stoga deklarirati veličinu sadržaja koja se približava četirima gigabajtima
Nakon dekodiranja takve vrijednosti, parser mora provjeriti stane li sadržaj doista unutar spremnika prije nego što mu povjeruje. Prirodna provjera je potvrda da trenutna pozicija plus duljina sadržaja ne prelaze kraj podataka. Napisana na očit način, s pozicijom, duljinom sadržaja i ukupnim zbrojem koji se čuvaju u 32-bitnim označenim cijelim brojevima, ta je zaštita slomljena
// Zamka: označena 32-bitna aritmetika. Uz ContentLen blizu MaxInt,
// Pos + ContentLen se omotava na NEGATIVNU vrijednost, pa usporedba
// daje lažan rezultat i lažna duljina od ~2 GB prolazi nekažnjeno.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
Problem je u zbrajanju, a ne u usporedbi. Kada je ContentLen blizu MaxInt (2147483647), Pos + ContentLen prelijeva označeni 32-bitni raspon i omotava se na negativan broj. Negativan zbroj nikada nije veći od Total, pa zaštita javlja da je sve u redu i dopušta parseru da nastavi s duljinom sadržaja od otprilike dva gigabajta koje spremnik ne sadrži. Ono što se događa sljedeće je šteta: čitač dodjeljuje spremnik za tu traženu duljinu i kopira u njega, uz SetLength praćen s Move koji čita iz izvora. Izvor ima samo još nekoliko stotina bajtova, pa kopiranje čita daleko izvan kraja ulaza, što je čitanje izvan granica koje u najboljem slučaju ruši program, a u najgorem otkriva susjednu memoriju procesa u analizu
Jedina ispravna zaštita proširuje međuzbroj prije usporedbe, tako da zbrajanje ne može preliti tip u kojem se računa. Ispravak promovira oba operanda u Int64
// Ispravno: oba operanda proširena u Int64 prije zbrajanja, pa se zbroj
// ne može omotati. Lažna duljina od 2 GB sada pada na provjeri granica.
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');
Int64 drži zbroj dviju 32-bitnih vrijednosti bez gubitka, pa usporedba vidi stvarni broj i odbacuje lažnu duljinu. Zasebna provjera nenegativnosti na ContentLen zatvara odgovarajući slučaj u kojem dekodirana vrijednost sama po sebi ispadne negativna. U HotPDF-u ova zaštita živi u HPDFASN1ParseNode, funkciji koja proizvodi čvor na kojem se grade svi ostali pomoćnici. Budući da HPDFASN1Content određuje veličinu svojih SetLength i Move izravno iz duljine sadržaja čvora, čvor koji je prošao lošu zaštitu zatrovao bi svako čitanje uzeto iz njega. Ispravljanje granice na točki dekodiranja je ono što čini pomoćnike iznad njega sigurnima
Nedostatak dva: broj ponavljanja PBKDF2 korišten kao oružje
Drugi nedostatak nije memorijska pogreška, to je datoteka koja govori vašem procesoru koliko naporno mora raditi. PKCS#12 štiti svoj ključni materijal s PBES2, shemom temeljenom na lozinki iz PKCS#5, specificiranom u RFC 8018. PBES2 pokreće funkciju izvođenja ključa, ovdje PBKDF2 s HMAC-SHA-256, a zatim šifru, ovdje AES-256-CBC. PBKDF2 uzima broj iteracija, a taj je broj parametar koji se prenosi u datoteci. Cijela mu je svrha biti spor: više iteracija znači da svaki pokušaj pogađanja lozinke košta više, što je dobro protiv izvanmrežnog napadača. RFC 8018 §4.2 izričito navodi da je veći broj bolji za sigurnost i namjerno ne postavlja gornju granicu
Ta je otvorenost u redu kada ste sami generirali datoteku. Ona je oružje kada ju je generirao napadač. Broj iteracija je faktor rada pod kontrolom napadača, a faktor rada pod kontrolom napadača je uskraćivanje usluge algoritamske složenosti. Lažni .pfx može kodirati broj iteracija u milijardama; parser ga poslušno čita i poziva PBKDF2 za toliko krugova HMAC-SHA-256, a proces nestaje u petlji koja se neće vratiti minutama ili satima s jednom isporučenom datotekom. Na poslužitelju za potpisivanje koji obrađuje jednu vjerodajnicu po zahtjevu, jedan izrađeni prijenos zaustavlja radnika
Broj čini omotavanje još gorim prije nego što natjera procesor da se vrti. Vrijednost iteracije živi u datoteci kao ASN.1 INTEGER, koji nema fiksnu širinu, dok je polje koje PBKDF2 u konačnici troši 32-bitni Integer. Dekodirajte INTEGER izravno u to polje i velika vrijednost se skraćuje, a vrijednost stvorena da sleti na bit predznaka vraća se kao negativna ili kao neki nepovezani mali broj, pa čak ni veličina rada više nije ono što se činilo da datoteka traži. Ispravak čita vrijednost na punoj širini i ograničava je prije sužavanja
// Broj iteracija prvo pročitaj kao Int64, pa ograniči u razuman raspon
// PRIJE nego što se sužava u 32-bitno polje Iterations koje PBKDF2 koristi.
LIter := HPDFASN1ToInteger(Data, Node); // vraća 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); // sigurno: već ograničeno
Čitanje u Int64 znači da je dekodirana vrijednost stvarna, a ne njezin skraćeni duh. Donja granica odbacuje nulte i negativne brojeve, koji su besmisleni za izvođenje ključa. Gornja granica, sto milijuna, nalazi se znatno iznad bilo koje legitimne PKCS#12 datoteke, koja danas koristi desetke do niske stotine tisuća iteracija, dok ograničava najgori slučaj na ograničenu količinu rada koja se može preživjeti. Tek nakon što vrijednost prođe taj raspon, sužava se na 32-bitno polje, tako da skraćivanje više nikoga ne može iznenaditi. U HotPDF-u ova stezaljka živi u ParsePBES2Params, gdje se parametri PBKDF2 dekodiraju na putu do PBKDF2HMACSHA256
Zašto su oba ispravka isti ispravak
Dva nedostatka izgledaju različito, jedan je prekoračenje spremnika, a drugi obješeni proces, ali to je ista pogreška. U svakom slučaju, broj iz nepouzdane datoteke prenesen je u tip fiksne širine jedan korak prerano, prije nego što je provjeren u odnosu na stvarnost. Drugo, duljina je dodana u 32 bita prije provjere granica; broj iteracija je sužen na 32 bita prije provjere raspona. Obje se pokoravaju istoj disciplini: dekodirajte na punoj širini, provjerite stvarna ograničenja i tek tada suzite. Tip Int64 nije stilski izbor, to je jedina širina u kojoj zaštita može vidjeti vrijednost koju je napadač stvarno napisao. Granica koja se prelijeva nije granica, a broj bez gornje granice nije parametar, to je daljinski regulator na vašem vlastitom procesoru
Praktične smjernice za cjevovod potpisivanja
Uža lekcija je da provjerite nepouzdani unos certifikata na isti način na koji biste provjerili bilo koji nepouzdani prijenos. Ograničite veličinu .pfx-a koji prihvaćate, budući da je legitimni u kilobajtima, a ne megabajtima. Tretirajte neuspjeh analize kao uobičajeni odbijeni unos, a ne kao pogrešku vrijednu praćenja stoga korisniku. Ako potpisujete na poslužitelju, pokrenite uvoz tamo gdje zaustavljeni radnik ne može srušiti uslugu s njim, i postavite vremensko ograničenje oko operacije tako da neočekivano skupa datoteka bude ograničena zidnim satom kao i kapom iteracije
Šira lekcija seže izvan certifikata. Učvršćivanje parsera nije jednokratna revizija jedne jedinice, to je svojstvo svakog mjesta na kojem vaša knjižnica čita bajtove koje nije sama napisala. PDF knjižnica analizira mnogo toga iz nepouzdanih izvora: fontove ugrađene u dokument, slike u pola tuceta kodeka, filtre tokova i, na putanji potpisivanja, certifikate. Svaki od njih je napadačka površina i svaki zaslužuje istu sumnju za svaku duljinu i svaki broj. HotPDF gradi putanju uvoza i potpisivanja na učvršćenim jedinicama HPDFASN1, HPDFPFX, HPDFCrypt i HPDFCMS koje su ovdje opisane, tako da se vjerodajnica koju mu predate, odakle god dolazila, analizira obrambeno prije nego što joj se ikada povjeruje
Tijek rada potpisivanja koji ove provjere štite pokriven je od početka do kraja u našem vodiču kroz PAdES digitalne potpise u Delphi-ju, a isti obrambeni stav primijenjen na šifriranje dokumenata, uključujući putanju ključa AES-256 koja dijeli ovu bazu koda, opisan je u članku o AES-256 šifriranju i sigurnosti. Sve se to isporučuje kao dio softvera HotPDF Delphi Component za Delphi i C++Builder, zajedno s API-jima za učitavanje, uređivanje, šifriranje i potpisivanje koji su obrađeni drugdje na ovom blogu