Kai pasirašote PDF dokumentą, paprastai manote, kad pasirašymo raktą valdote patys. Jis saugomas jūsų sugeneruotame .pfx faile, apsaugotame jūsų pasirinktu slaptažodžiu. Kodas, nuskaitantis tą failą, atrodo kaip paprastas techninis kanalas, o ne saugumo riba. Ši intuicija yra klaidinga, kai sertifikatas nebėra jūsų pačių. Darbalaukio įrankis, leidžiantis naudotojui pasirinkti bet kurį .pfx failą, serveris, priimantis įkeltus kredencialus, arba paketinis pasirašymo modulis, gaunantis sertifikatus per tinklą – visi jie perduoda užpuoliko paveiktus baitus parseriui dar prieš sugeneruojant pirmąjį parašo baitą. PKCS#12 skaitytuvas yra puolimo paviršius, lygiai taip pat kaip vaizdų dekoderis ar šriftų įkėlimo programa.
Šiame straipsnyje apžvelgiami du tikri defektai, kurie egzistavo tame skaitytuve, abu susiję su pasirašymo kredencialų importavimo keliu. Nei vienas iš jų nėra egzotiškas. Abu kyla iš tos pačios pagrindinės priežasties, kuri kamuoja beveik kiekvieną dvejetainį parserį, parašytą kalba su fiksuoto pločio sveikaisiais skaičiais: failo ilgis arba skaičius yra pasitikimas vienu žingsniu labiau nei turėtų būti. Vienas iš jų sukelia skaitymą už ribų (out-of-bounds read), o kitas – procesą, kuris pakimba, kol jį nutraukiate.
Kur keliauja baitai
.pfx failo importavimas dokumento pasirašymui nėra viena operacija – tai trumpas konvejeris, ir kiekvienas jo etapas analizuoja tai, ką galėjo parašyti užpuolikas. Konteineris yra PKCS#12 struktūra, apibrėžta RFC 7292 standarte, t. y. AuthenticatedSafe paketų rinkinys, apgaubiantis šifruotą apvalkalą, kuriame saugomas privatusis raktas. Jo skaitymas reiškia ASN.1 analizę, rakto gavimą iš slaptažodžio, dešifravimą ir atkurto RSA rakto perdavimą parašą kuriančiam kodui.
HotPDF komponente šie etapai yra susieti su atskirais moduliais. PKCS#12 konteinerio logika saugoma HPDFPFX modulyje. Kiekvieną žymę, ilgį ir reikšmę, kurias jis paliečia, dekoduoja ASN.1 skaitytuvas, esantis HPDFASN1 modulyje. Rakto išvestis (key derivation) ir PBES2 dešifravimas atliekami HPDFCrypt modulyje kartu su PBKDF2HMACSHA256. Kai raktas atkuriamas, HPDFRSA ir CMS SignedData kūrėjas HPDFCMS modulyje paverčia jį atskiru parašu, įterptu į PDF failą. Viešasis įėjimo taškas, kuris valdo visą grandinę, yra vienas iškvietimas.
// 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
;
Kiekvienas signer.pfx baitas pereina per HPDFASN1 ir HPDFPFX modulius dar prieš prasidedant bet kokiam kriptografiniam procesui. Jei šie du moduliai nėra atsargūs su tuo, kas nurodyta faile, žemiau esanti kriptografija net neturės progos suveikti.
Pirmasis defektas: ASN.1 ilgis, kuris viršija apsaugą
ASN.1 formatas DER ir BER kodavimuose kiekvieną elementą užkoduoja kaip žymę (tag), ilgį (length) ir atitinkamą turinio baitų skaičių. Ilgis yra laukas, kuriuo privalote pasitikėti, bet patikrinti, nes jis nurodo parseriui, kiek skaitytai, ir jį įrašė tas, kas sukūrė failą. X.690 §8.1.3 apibrėžia du kodavimo būdus. Trumpoji forma supakuoja ilgį nuo 0 iki 127 į vieną baitą. Ilgoji forma, naudojama didesniems dydžiams, naudoja vieną pradinį baitą, kurio žemiausi septyni bitai nurodo po jo einančių ilgio baitų skaičių, o tada atitinkamas baitų skaičius didėjančia tvarka (big-endian) nurodo tikrąją reikšmę. Keturi ilgio baitai gali deklaruoti turinio dydį, siekiantį beveik keturis gigabaitus.
Dekodavęs tokią reikšmę, parseris turi patikrinti, ar turinys telpa buferyje, prieš juo pasitikėdamas. Natūralus patikrinimas yra įsitikinti, kad dabartinė pozicija plius turinio ilgis neviršija duomenų pabaigos. Parašius tai paprastu būdu, kur pozicija, turinio ilgis ir bendras dydis saugomi 32 bitų sveikųjų skaičių kintamuosiuose su ženklu (signed integers), ši apsauga neveikia:
// 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 yra sudėtis, o ne palyginimas. Kai ContentLen yra arti MaxInt (2147483647), suma Pos + ContentLen viršija 32 bitų su ženklu rėžius ir virsta neigiama reikšme. Neigiama suma niekada nėra didesnė už Total, so the guard reports that everything is fine and lets the parser proceed with a content length of roughly two gigabytes that the buffer does not contain. What happens next is the damage: the reader allocates a buffer for that claimed length and copies into it, a SetLength followed by a Move reading from the source. The source has only a few hundred bytes left, so the copy reads far past the end of the input, an out-of-bounds read that at best crashes and at worst leaks adjacent process memory into the parse.
Vienintelė teisinga apsauga išplečia tarpinę sumą prieš palyginimą, kad sudėtis negalėtų perpildyti tipo, kuriame ji skaičiuojama. Sprendimas paaukština abu operandus iki Int64 tipo:
// 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');
Tipas Int64 saugo dviejų 32 bitų reikšmių sumą be praradimų, todėl palyginimas mato tikrąjį skaičių ir atmeta suklastotą ilgį. Atskiras neigiamų reikšmių tikrinimas lauke ContentLen išsprendžia atvejį, kai dekoduota reikšmė pati savaime tampa neigiama. HotPDF komponente ši apsauga yra HPDFASN1ParseNode funkcijoje, kuri sukuria mazgą (node), kuriuo remiasi visi kiti pagalbiniai įrankiai. Kadangi HPDFASN1Content nustato savo SetLength ir Move dydžius tiesiogiai iš mazgo turinio ilgio, mazgas, praėjęs blogą apsaugą, būtų sugadinęs kiekvieną iš jo paimtą nuskaitymą. Rėžio pataisymas dekodavimo momentu yra tai, kas padaro virš jo esančius įrankius saugius.
Antrasis defektas: PBKDF2 iteracijų skaičius kaip ginklas
Antrasis trūkumas yra ne atminties klaida – tai failas, nurodantis jūsų procesoriui (CPU), kaip sunkiai dirbti. PKCS#12 saugo savo rakto medžiagą naudodamas PBES2 – slaptažodžiu pagrįstą schemą iš PKCS#5, aprašytą RFC 8018 standarte. PBES2 paleidžia rakto išvesties funkciją, šiuo atveju PBKDF2 su HMAC-SHA-256, o tada šifrą – AES-256-CBC. PBKDF2 priima iteracijų skaičių, ir šis skaičius yra parametras, perduodamas faile. Visas jo tikslas yra lėtas veikimas: daugiau iteracijų reiškia, kad kiekvienas slaptažodžio spėjimas kainuoja brangiau, o tai naudinga prieš neprisijungusį užpuoliką. RFC 8018 §4.2 aiškiai nurodo, kad didesnis skaičius yra geresnis saugumui, ir sąmoningai nenustato jokių lubų.
Šis atvirumas yra tinkamas, kai failą sugeneravote patys. Tačiau tai tampa ginklu, kai jį sukuria užpuolikas. Iteracijų skaičius yra užpuoliko valdomas darbo faktorius, o užpuoliko valdomas darbo faktorius sukelia algoritminio sudėtingumo paslaugos trikdymą (denial of service). Suklastotas .pfx failas gali nurodyti iteracijų skaičių milijardais; parseris sąžiningai jį perskaitys ir iškvies PBKDF2 atitinkamam skaičiui HMAC-SHA-256 ciklų, o procesas pakibs cikle, kuris negrąžins rezultato kelias minutes ar valandas. Pasirašymo serveryje, kuris apdoroja vieną užklausą vienu metu, vienas suklastotas įkėlimas gali visiškai sustabdyti apdorojimo procesą.
Šis skaičius dar labiau pablogina situaciją prieš procesoriui pradedant suktis cikle. Iteracijos reikšmė faile saugoma kaip ASN.1 INTEGER tipas, kuris neturi fiksuoto pločio, o laukas, kurį galiausiai naudoja PBKDF2, yra 32 bitų Integer. Dekoduokite INTEGER tiesiai į šį lauką, ir didelė reikšmė bus sutrumpinta (truncate), o reikšmė, parinkta taip, kad paveiktų ženklo bitą, grįš kaip neigiama arba kaip koks nors nesusijęs mažas skaičius, todėl netgi atliekamo darbo apimtis nebeatitiks tos, kurios prašė failas. Sprendimas nuskaito pilno pločio reikšmę ir apriboja ją prieš sutrumpinant:
// 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
Nuskaitymas į Int64 kintamąjį reiškia, kad dekoduota reikšmė yra tikroji, o ne sutrumpinta jos versija. Apatinė riba atmeta nulį ir neigiamus skaičius, kurie neturi prasmės rakto išvedimui. Viršutinė riba – šimtas milijonų – yra gerokai didesnė už bet kurį tikrą PKCS#12 failą, kuriame šiandien naudojama nuo dešimčių iki šimtų tūkstančių iteracijų, tačiau apriboja blogiausią scenarijų iki priimtino darbo kiekio. Tik po to, kai reikšmė praeina šį patikrinimą, ji yra sumažinama iki 32 bitų lauko, todėl sutrumpinimas nebegali nieko nustebinti. HotPDF komponente šis ribojimas atliekamas ParsePBES2Params procedūroje, kur dekoduojami PBKDF2 parametrai pakeliui į PBKDF2HMACSHA256.
Kodėl abu sprendimai yra tokie patys
Šie du defektai atrodo skirtingi – vienas iš jų yra buferio perpildymas, o kitas – pakibęs procesas – tačiau jie yra ta pati klaida. Kiekvienu atveju skaičius iš nepatikimo failo buvo perkeltas į fiksuoto pločio tipą vienu žingsniu per anksti, dar prieš jį patikrinant su tikrove. Ilgis buvo pridėtas 32 bitų režimu prieš rėžių testą; iteracijų skaičius buvo susiaurintas iki 32 bitų prieš diapazono testą. Abu sprendimai paklūsta tai pačiai taisyklei: dekoduoti pilnu pločiu, patikrinti su tikrąja riba ir tik tada susiaurinti. Tarpinis Int64 nėra stiliaus pasirinkimas, tai vienintelis plotis, kuriame apsauga gali matyti reikšmę, kurią iš tikrųjų įrašė užpuolikas. Perpildomas rėžis nėra apsauga, o skaičius be jokių ribų nėra parametras – tai nuotolinis jūsų procesoriaus stabdis.
Praktiniai patarimai pasirašymo grandinei
Svarbiausia pamoka yra patikrinti nepatikimus sertifikatų duomenis taip pat, kaip tikrintumėte bet kokį nepatikimą įkėlimo. Apribokite priimamo .pfx failo dydį, nes tikras failas užima kilobaitus, o ne megabaitus. Apdorokite analizės klaidą kaip įprastą atmestą įvestį, o ne kaip klaidą, dėl kurios naudotojui reikia rodyti dėklo pėdsaką (stack trace). Jei pasirašote serveryje, vykdykite importavimą ten, kur pakibęs procesas negali sustabdyti visos paslaugos veikimo, ir nustatykite operacijos vykdymo laiko limitą (timeout), kad neįprastai brangus failas būtų apribotas laiku bei iteracijų limitu.
Platesnė pamoka siekia toliau nei sertifikatai. Parserio apsauga nėra vienkartinis vieno modulio auditas – tai yra savybė, kuri tai pat turi būti taikoma kiekvienoje vietoje, kur jūsų biblioteka nuskaito baitus, kurių pati neįrašė. PDF biblioteka analizuoja daugybę duomenų iš nepatikimų šaltinių: dokumente įterptus šriftus, vaizdus, srautų filtrus ir pasirašymo kelyje – sertifikatus. Kiekvienas iš jų yra puolimo paviršius, ir kiekvienas nusipelno to paties įtarumo dėl bet kokio ilgio ir kiekvieno skaičiaus. HotPDF kuria importavimo ir pasirašymo kelią naudodama apsaugotus HPDFASN1, HPDFPFX, HPDFCrypt ir HPDFCMS modulius, aprašytus čia, kad kredencialai, kuriuos jam pateikiate, iš kur jie beateitų, būtų analizuojami gynybiškai prieš pradedant jais pasitikėti.
Pasirašymo eiga, kurią saugo šios patikros, yra išsamiai aprašyta mūsų parašų apžvalgoje Delphi aplinkoje, o ta pati gynybinė pozicija, taikoma dokumentų šifravimui, įskaitant AES-256 rakto kelią, kuris dalijasi šiuo kodu, yra aprašyta straipsnyje apie AES-256 šifravimą ir saugumą. Visa tai yra platinama kaip dalis HotPDF Component, skirto Delphi ir C++Builder, kartu su įkėlimo, redagavimo, šifravimo ir pasirašymo API, aprašytais kitose šio tinklaraščio dalyse.