Keď podpisujete PDF, zvyčajne si myslíte, že podpisový kľúč je niečo, čo máte pod kontrolou. Nachádza sa v súbore .pfx, ktorý ste vygenerovali, chránený vami zvoleným heslom. Kód, ktorý číta tento súbor, sa zdá byť len potrubím, nie bezpečnostnou hranicou. Táto intuícia je nesprávna vo chvíli, keď certifikát prestane byť váš. Desktopový nástroj, ktorý umožňuje používateľovi vybrať akýkoľvek súbor .pfx, server prijímajúci nahrané prihlasovacie údaje, dávkový podpisovač kŕmený certifikátmi cez sieť – to všetko odovzdáva útočníkom ovplyvnené bajty parseru skôr, ako sa vyprodukuje čo i len jediný bajt podpisu. Čítačka PKCS#12 je útočnou plochou v rovnakom zmysle ako dekodér obrázkov alebo loader písiem.
Tento článok prechádza dvoma reálnymi chybami, ktoré žili v tejto čítačke, obe v ceste, ktorá importuje podpisové údaje. Ani jedna nie je exotická. Obe pochádzajú z rovnakej hlavnej príčiny, ktorá postihuje takmer každý binárny parser napísaný v jazyku s celými číslami s pevnou šírkou: dĺžke alebo počtu zo súboru sa dôveruje o krok viac, než by sa malo. Jedna vedie k čítaniu mimo rozsah, druhá k zaseknutiu procesu, kým ho neukončíte.
Kam putujú bajty
Importovanie súboru .pfx na podpísanie dokumentu nie je jedna operácia, je to krátka pipeline a každá fáza analyzuje niečo, čo mohol napísať útočník. Kontajner je štruktúra PKCS#12 definovaná v RFC 7292, hniezdo balíkov AuthenticatedSafe obalených okolo šifrovaného obalu, ktorý drží súkromný kľúč. Čítanie znamená prechod cez ASN.1, odvodenie kľóča z hesla, dešifrovanie a následné odovzdanie obnoveného kľúča RSA kódu, ktorý zostavuje podpis.
V HotPDF sa tieto fázy mapujú na odlišné jednotky. Logika kontajnera PKCS#12 žije v HPDFPFX. Každý tag, dĺžka a hodnota, ktorej sa dotkne, je dekódovaná čítačkou ASN.1 v HPDFASN1. Odvodenie kľúča a dešifrovanie PBES2 sedia v HPDFCrypt vedľa PBKDF2HMACSHA256. Po obnovení kľúča ho HPDFRSA a CMS SignedData builder v HPDFCMS premenia na odpojený podpis vložený v PDF. Verejný vstupný bod, ktorý riadi celý reťazec, je jedno volanie.
// 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
;
Každý bajt súboru signer.pfx preteká cez HPDFASN1 a HPDFPFX predtým, ako dôjde k akejkoľvek kryptografii. Ak tieto dve jednotky nie sú opatrné na to, čo súbor tvrdí, kryptografia ďalej v reťazci nikdy nedostane šancu na tom záležať.
Chyba jedna: dĺžka ASN.1, ktorá sa pretečením dostane za ochranu
ASN.1 v kódovaniach DER a BER kóduje každý prvok ako tag, dĺžku a príslušný počet bajtov obsahu. Dĺžka je pole, ktorému musíte veriť, ale overovať ho, pretože hovorí parseru, ako ďaleko má čítať, a bolo zapísané tým, kto súbor vytvoril. Norma X.690 §8.1.3 definuje dve kódovania. Krátka forma balí dĺžku 0 až 127 do jedného bajtu. Dlhá forma, používaná pre čokoľvek väčšie, využíva jeden úvodný bajt, ktorého dolných sedem bitov udáva počet nasledujúcich bajtov dĺžky, a potom tento počet bajtov v big-endian formáte nesie skutočnú hodnotu. Štyri bajty dĺžky preto môžu deklarovať veľkosť obsahu blížiacu sa k štyrom gigabajtom.
Po dekódovaní takejto hodnoty musí parser skontrolovať, či sa obsah skutočne zmestí do vyrovnávacej pamäte, skôr než mu dôveruje. Prirodzenou kontrolou je potvrdiť, že aktuálna pozícia plus dĺžka obsahu neprekročí koniec dát. Prirodzeným spôsobom zápisu s 32-bitovými znamienkovými celými číslami pre pozíciu, dĺžku aj celok je táto ochrana nefunkčná:
// 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');
Problémom je sčítanie, nie porovnanie. Keď je ContentLen blízko k MaxInt (2147483647), výraz Pos + ContentLen pretečie znamienkový 32-bitový rozsah a zmení sa na záporné číslo. Záporný súčet nie je nikdy väčší ako Total, takže ochrana nahlási, že všetko je v poriadku, a dovolí parseru pokračovať s dĺžkou obsahu zhruba dva gigabajty, ktorú vyrovnávacia pamäť neobsahuje. To, čo nasleduje potom, je poškodenie: čítačka alokuje pamäť pre túto deklarovanú dĺžku a kopíruje do nej pomocou SetLength a následného Move čítajúceho zo zdroja. Zdroj má k dispozícii iba niekoľko stoviek bajtov, takže kopírovanie číta ďaleko za koniec vstupu, čo je čítanie mimo rozsah, ktoré v najlepšom prípade spadne a v najhoršom unikne susednú pamäť procesu do analýzy.
Jediná správna ochrana rozširuje medzisúčet pred porovnaním, takže sčítanie nemôže pretiecť typ, v ktorom sa počíta. Oprava povyšuje oba operandy na 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');
Typ Int64 uchováva súčet dvoch 32-bitových hodnôt bez straty, takže porovnanie vidí skutočné číslo a odmietne sfalšovanú dĺžku. Samostatná kontrola nezápornosti pre ContentLen uzatvára zodpovedajúci prípad, kedy dekódovaná hodnota sama osebe skončí ako záporná. V HotPDF táto ochrana žije v HPDFASN1ParseNode, čo je funkcia, ktorá produkuje uzol, na ktorom stavia každý ďalší pomocník. Keďže HPDFASN1Content nastavuje veľkosť pre SetLength a Move priamo z dĺžky obsahu uzla, uzol, ktorý by prešiel zlou ochranou, by otrávil každé čítanie z neho. Oprava hranice v bode dekódovania je to, čo robí pomocníkov nad ňou bezpečnými.
Chyba dve: počet iterácií PBKDF2 použitý ako zbraň
Druhá chyba nie je chybou pamäte, ale tým, že súbor hovorí vášmu procesoru, ako tvrdo má pracovať. PKCS#12 chráni svoj kľúčový materiál pomocou PBES2, čo je schéma založená na hesle z PKCS#5 špecifikovaná v RFC 8018. PBES2 spúšťa funkciu odvodenia kľúča, v tomto prípade PBKDF2 s HMAC-SHA-256, a potom šifru, tu AES-256-CBC. PBKDF2 prijíma počet iterácií a tento počet je parameter prenášaný v súbore. Jeho celým účelom je byť pomalý: viac iterácií znamená, že každý pokus o uhádnutie hesla stojí viac, čo je dobré proti offline útočníkovi. RFC 8018 §4.2 výslovne uvádza, že väčší počet je lepší pre bezpečnosť a zámerne nestanovuje žiadny strop.
Táto otvorenosť je v poriadku, keď ste súbor vygenerovali vy. Je to však zbraň, keď ho vytvoril útočník. Počet iterácií je faktor zaťaženia riadený útočníkom a faktor zaťaženia riadený útočníkom predstavuje denial of service (odmietnutie služby) s algoritmickou zložitosťou. Sfalšovaný súbor .pfx môže kódovať počet iterácií v miliardách; parser ho svedomito prečíta a zavolá PBKDF2 pre toľko kôl HMAC-SHA-256, pričom proces zmizne v slučke, ktorá sa nevráti minúty alebo hodiny na jednom dodanom súbore. Na podpisovom serveri, ktorý spracováva jedny prihlasovacie údaje na požiadavku, jediné pripravené nahranie ochromí pracovníka (worker).
Počet zhoršuje pretečenie ešte predtým, ako roztočí CPU. Hodnota iterácie žije v súbore ako ASN.1 INTEGER, ktorý nemá pevnú šírku, zatiaľ čo pole, ktoré PBKDF2 nakoniec spotrebováva, je 32-bitový Integer. Dekódujte INTEGER priamo do tohto poľa a veľká hodnota sa oreže, pričom hodnota navrhnutá tak, aby pristála na znamienkovom bite, sa vráti ako záporná alebo ako nejaké nesúvisiace malé číslo, takže ani veľkosť práce už nezodpovedá tomu, čo súbor zdanlivo požadoval. Oprava číta hodnotu v plnej šírke a ohraničuje ju pred zúžením:
// 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
Prečo sú obe opravy rovnakou opravou
Tieto dve chyby vyzerajú odlišne, jedna ako pretečenie vyrovnávacej pamäte a druhá ako zamrznutý proces, ale ide o rovnaký omyl. V oboch prípadoch sa číslo z nedôveryhodného súboru prenieslo do typu s pevnou šírkou o krok skôr, než bolo skontrolované voči realite. Dĺžka sa sčítala v 32 bitoch pred testom hraníc; počet iterácií bol zúžený na 32 bitov pred testom rozsahu. Obe chyby podliehajú rovnakej disciplíne: dekódovať v plnej šírke, skontrolovať voči skutočnému limitu a až potom zúžiť. Pomocný typ Int64 nie je otázkou štýlu, je to jediná šírka, v ktorej môže ochrana vidieť hodnotu, ktorú útočník skutočne zapísal. Hranica, ktorá pretečie, nie je hranicou, a počet bez stropu nie je parameter, je to vzdialený škrtiaci ventil pre vaše vlastné CPU.
Praktické rady pre podpisovú pipeline
Úzkym ponaučením je validovať nedôveryhodný vstup certifikátu rovnako, ako by ste validovali akékoľvek nedôveryhodné nahrávanie. Obmedzte veľkosť prijímaného súboru .pfx, keďže legitímny má kilobajty, nie megabajty. Spracujte zlyhanie analýzy ako bežný odmietnutý vstup, nie ako chybu stojacu za zobrazenie stack trace používateľovi. Ak podpisujete na serveri, spúšťajte import tam, kde zablokovaný worker nemôže stiahnuť celú službu so sebou, a nastavte časový limit (timeout) pre túto operáciu, aby neočakávane náročný súbor bol ohraničený reálnym časom rovnako ako limitom iterácií.
Širšie ponaučenie presahuje rámec certifikátov. Zabezpečenie parsera nie je jednorazový audit jednej jednotky, je to vlastnosť každého miesta, kde vaša knižnica číta bajty, ktoré sama nezapísala. A PDF knižnica analyzuje veľa vecí z nedôveryhodných zdrojov: písma vložené v dokumente, obrázky v pol tucte kodekov, filtre streamov a na podpisovej ceste aj certifikáty. Každá z týchto oblastí predstavuje útočnú plochu a každá si zaslúži rovnaké podozrenie voči každej dĺžke a každému počtu. HotPDF stavia cestu importu a podpisovania na zabezpečených jednotkách HPDFASN1, HPDFPFX, HPDFCrypt a HPDFCMS popísaných tu, takže prihlasovacie údaje, ktoré mu odovzdáte, nech už prišli odkiaľkoľvek, sú analyzované defenzívne pred tým, ako sa im začne dôverovať.
Podpisový proces, ktorý tieto kontroly chránia, je popísaný od začiatku do konca v našom návode na digitálne podpisy PAdES v Delphi a rovnaký defenzívny prístup aplikovaný na šifrovanie dokumentov, vrátane cesty kľúča AES-256, ktorá zdieľa túto codebase, je opísaný v článku o šifrovaní AES-256 a bezpečnosti. To všetko sa dodáva ako súčasť HotPDF Component pre Delphi a C++Builder spolu s API na načítanie, úpravu, šifrovanie a podpisovanie, ktoré sú popísané inde na tomto blogu.