Technical Article

Ojačavanje Delphi PDF potpisivača protiv zlonamernog PKCS#12 paketa

Kada potpisujete PDF, obično mislite o ključu za potpisivanje kao o nečemu što kontrolišete. On živi u .pfx datoteci koju ste generisali, zaštićen lozinkom koju ste izabrali. Kod koji čita tu datoteku deluje kao obična instalacija, a ne kao granica. Ta intuicija je pogrešna onog trenutka kada sertifikat prestane da bude vaš. Stoni alat koji korisniku omogućava da izabere bilo koji .pfx, server koji prihvata otpremljeni kredencijal, grupni potpisivač koji prima sertifikate preko mreže, svi oni predaju parseru bajtove pod uticajem napadača pre nego što se proizvede ijedan bajt potpisa. PKCS#12 čitač je površina napada, u istom smislu u kom su to dekoder slika ili učitavač fontova.

Ovaj članak prolazi kroz dva stvarna defekta koji su postojali u tom čitaču, oba na putanji koja uvozi kredencijal za potpisivanje. Nijedan nije egzotičan. Oba potiču iz istog korenskog uzroka koji pogađa skoro svaki binarni parser napisan u jeziku sa celobrojnim tipovima fiksne širine: dužini ili broju iz datoteke se veruje jedan korak više nego što bi trebalo. Jedan dovodi do čitanja van granica, a drugi do toga da proces visi sve dok ga ne ubijete.

Kuda putuju bajtovi

Uvoz .pfx datoteke radi potpisivanja dokumenta nije jedna operacija, to je kratak cevovod, i svaka faza analizira nešto što je napadač mogao da napiše. Kontejner je PKCS#12 struktura definisana u RFC 7292, gnezdo AuthenticatedSafe vreća obmotanih oko šifrovanog omotača koji drži privatni ključ. Čitanje toga podrazumeva prolazak kroz ASN.1, izvođenje ključa iz lozinke, dešifrovanje, a zatim predavanje oporavljenog RSA ključa kodu koji gradi potpis.

U HotPDF-u se ove faze mapiraju u posebne jedinice. Logika PKCS#12 kontejnera živi u HPDFPFX. Svaki tag, dužinu i vrednost koju dodirne dekodira ASN.1 čitač u HPDFASN1. Izvođenje ključa i PBES2 dešifrovanje nalaze se u HPDFCrypt zajedno sa PBKDF2HMACSHA256. Kada se ključ oporavi, HPDFRSA i graditelj CMS SignedData strukture u HPDFCMS pretvaraju ga u odvojeni potpis ugrađen u PDF. Javna ulazna tačka koja pokreće ceo lanac je jedan poziv.

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

Svaki bajt datoteke signer.pfx teče kroz HPDFASN1 i HPDFPFX pre nego što se dogodi bilo kakva kriptografija. Ako ove dve jedinice nisu pažljive u vezi sa onim što datoteka tvrdi, kriptografija nizvodno nikada neće dobiti priliku da postane važna.

Defekt jedan: ASN.1 dužina koja se obmotava mimo zaštite

ASN.1 u DER i BER formatu kodira svaki element kao tag, dužinu i toliko bajtova sadržaja. Dužina je polje kojem morate verovati ali ga i verifikovati, jer govori parseru koliko daleko da čita, a napisao ga je onaj ko je proizveo datoteku. X.690 §8.1.3 definiše dva kodiranja. Kratka forma pakuje dužinu od 0 do 127 u jedan bajt. Duga forma, koja se koristi za sve što je veće, troši jedan početni bajt čijih donjih sedam bitova daje broj bajtova dužine koji slede, a zatim toliko big-endian bajtova prenosi stvarnu vrednost. Četiri bajta dužine mogu stoga deklarisati veličinu sadržaja koja se približava cifri od četiri gigabajta.

Nakon dekodiranja takve vrednosti, parser mora da proveri da li sadržaj zaista staje u bafer pre nego što mu poveruje. Prirodna provera je da se potvrdi da trenutna pozicija plus dužina sadržaja ne prelaze kraj podataka. Napisana na očigledan način, sa pozicijom, dužinom sadržaja i ukupnim brojem koji se čuvaju u 32-bitnim označenim celim brojevima, ta zaštita je neupotrebljiva:

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

Problem je sabiranje, a ne poređenje. Kada je ContentLen blizu MaxInt (2147483647), Pos + ContentLen prepunjava označeni 32-bitni opseg i obmotava se na negativan broj. Negativan zbir nikada nije veći od Total, tako da zaštita izveštava da je sve u redu i omogućava parseru da nastavi sa dužinom sadržaja od oko dva gigabajta koju bafer zapravo ne sadrži. Ono što se dešava sledeće je šteta: čitač alocira bafer za tu deklarisanu dužinu i kopira u njega, sa SetLength praćenim sa Move čitajući iz izvora. Izvor ima samo nekoliko stotina bajtova preostalih, pa kopiranje čita daleko preko kraja ulaza, što je čitanje van granica koje u najboljem slučaju ruši program, a u najgorem prepušta memoriju susednog procesa u analizu.

Jedina ispravna zaštita proširuje međuzbir pre poređenja, tako da sabiranje ne može prepuniti tip u kom se izračunava. Rešenje promoviše oba operanda u 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');

Int64 drži zbir dve 32-bitne vrednosti bez gubitka, tako da poređenje vidi stvarni broj i odbacuje lažnu dužinu. Posebna provera nenegativnosti na ContentLen zatvara odgovarajući slučaj gde dekodirana vrednost sama po sebi ispadne negativna. U HotPDFu ova zaštita živi u HPDFASN1ParseNode, funkciji koja proizvodi čvor na kome se gradi svaki drugi pomoćni element. Pošto HPDFASN1Content dimenzioniše svoje SetLength i Move direktno iz dužine sadržaja čvora, čvor koji bi prošao lošu zaštitu otrovao bi svako čitanje preuzeto iz njega. Popravljanje granice na mestu dekodiranja je ono što čini pomoćne funkcije iznad njega bezbednim.

Defekt dva: broj iteracija PBKDF2 koji se koristi kao oružje

Druga mana nije memorijska greška, već situacija u kojoj datoteka govori vašem procesoru koliko naporno treba da radi. PKCS#12 štiti svoj ključni materijal pomoću PBES2, šeme zasnovane na lozinci iz PKCS#5, navedene u RFC 8018. PBES2 pokreće funkciju izvođenja ključa, ovde PBKDF2 sa HMAC-SHA-256, a zatim šifru, ovde AES-256-CBC. PBKDF2 uzima broj iteracija, a taj broj je parametar koji se prenosi u datoteci. Njegova jedina svrha je da bude spor: više iteracija znači da svaki pokušaj pogađanja lozinke košta više, što je dobro protiv oflajn napadača. RFC 8018 §4.2 izričito navodi da je veći broj bolji za bezbednost, i namerno ne postavlja gornju granicu.

Ta otvorenost je u redu kada ste sami generisali datoteku. Ali ona je oružje kada to uradi napadač. Broj iteracija je faktor rada koji kontroliše napadač, a faktor rada pod kontrolom napadača je uskraćivanje usluge (DoS) usled algoritamske složenosti. Lažni .pfx može kodirati broj iteracija u milijardama; parser ga poslušno čita i poziva PBKDF2 za toliko rundi HMAC-SHA-256, a proces nestaje u petlji koja se neće vratiti minutima ili satima sa jednom isporučenom datotekom. Na serveru za potpisivanje koji obrađuje jedan kredencijal po zahtevu, jedno modifikovano otpremanje blokira radnu nit (worker).

Broj iteracija pogoršava situaciju sa obmotavanjem pre nego što natera procesor da se beskonačno vrti. Vrednost iteracije živi u datoteci kao ASN.1 INTEGER, koji nema fiksnu širinu, dok je polje koje PBKDF2 na kraju troši 32-bitni Integer. Dekodirajte INTEGER direktno u to polje i velika vrednost se odseca, a vrednost napravljena tako da padne na bit znaka vraća se kao negativna ili kao neki nepovezani mali broj, tako da čak ni količina posla više nije ono što je datoteka naizgled tražila. Rešenje čita vrednost u punoj širini i ograničava je pre sužavanja:

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

Zašto su obe popravke zapravo ista popravka

Dva defekta izgledaju različito, jedan je prekoračenje bafera, a drugi blokiran proces, ali to je ista greška. U svakom slučaju, broj iz nepouzdane datoteke je prenet u tip fiksne širine jedan korak prerano, pre nego što je proveren u odnosu na realnost. Dužina je sabrana u 32 bita pre testa granica; broj iteracija je sužen na 32 bita pre testa opsega. Oba se pokoravaju istoj disciplini: dekodirajte u punoj širini, proverite u odnosu na stvarnu granicu i tek onda suzite. Međuvrednost Int64 nije stilski izbor, to je jedina širina u kojoj zaštita može da vidi vrednost koju je napadač zaista napisao. Granica koja se prepunjava nije granica, a broj bez gornjeg limita nije parametar, već daljinski regulator na vašem sopstvenom procesoru.

Praktični saveti za cevovod potpisivanja

Uža lekcija je da validirate nepouzdani ulaz sertifikata na isti način na koji biste validirali bilo koje nepouzdano otpremanje. Ograničite veličinu .pfx datoteke koju prihvatate, pošto je legitimna u kilobajtima, a ne megabajtima. Tretirajte neuspeh analize kao rutinski odbijen ulaz, a ne kao grešku vrednu prikaza niza poziva (stack trace) korisniku. Ako potpisujete na serveru, pokrenite uvoz tamo gde blokirana radna nit ne može da obori uslugu sa sobom, i postavite vremensko ograničenje (timeout) oko operacije tako da neočekivano skupa datoteka bude ograničena realnim vremenom, kao i ograničenjem iteracija.

Šira lekcija seže dalje od sertifikata. Ojačavanje parsera nije jednokratna revizija jedne jedinice, to je svojstvo svakog mesta gde vaša biblioteka čita bajtove koje nije sama napisala. PDF biblioteka analizira mnogo toga iz nepouzdanih izvora: fontove ugrađene u dokument, slike u pola tuceta kodeka, filtere tokova i, na putanji potpisivanja, sertifikate. Svaki od njih je površina napada, i svaki zaslužuje istu sumnju prema svakoj dužini i svakom broju. HotPDF gradi putanju uvoza i potpisivanja na ojačanim jedinicama HPDFASN1, HPDFPFX, HPDFCrypt i HPDFCMS koje su ovde opisane, tako da se kredencijal koji mu predate, odakle god da je došao, analizira defanzivno pre nego što mu se ikada poveruje.

Tok rada za potpisivanje koji ove provere štite pokriven je od početka do kraja u našem vodiču kroz PAdES digitalne potpise u Delphi-ju, a isti defanzivni stav primenjen na šifrovanje dokumenata, uključujući putanju AES-256 ključa koja deli ovu bazu koda, opisan je u članku o AES-256 šifrovanju i bezbednosti. Sve to se isporučuje kao deo HotPDF komponente za Delphi i C++Builder, zajedno sa API-jima za učitavanje, uređivanje, šifrovanje i potpisivanje koji su pokriveni na drugim mestima na ovom blogu.