Při podepisování PDF obvykle považujete podpisový klíč za něco, co plně ovládáte. Je uložen v souboru .pfx, který jste vygenerovali a chránili vámi zvoleným heslem. Kód, který tento soubor načítá, se zdá být jen technickým propojením, nikoli bezpečnostní hranicí. Tato intuice je chybná v okamžiku, kdy certifikát přestane být váš. Desktopový nástroj umožňující uživateli vybrat libovolný .pfx, server přijímající nahrané přihlašovací údaje nebo dávkový podpisový systém přijímající certifikáty přes síť – to vše předává bajty ovlivněné útočníkem analyzátoru (parseru) dříve, než se vygeneruje jediný bajt podpisu. Čtečka PKCS#12 představuje prostor pro útok (attack surface) ve stejném smyslu jako dekodér obrázků nebo načítač písem.
Tento článek prochází dvě skutečné chyby, které se v této čtečce nacházely, obě na cestě pro import podpisových údajů. Ani jedna z nich není exotická. Obě pramení ze stejné hlavní příčiny, která postihuje téměř každý binární parser napsaný v jazyce s pevnou šířkou celých čísel: délce nebo počtu ze souboru se důvěřuje o krok více, než by se mělo. Jedna vede k čtení mimo vyhrazenou paměť (out-of-bounds read), druhá k zablokování procesu, dokud jej neukončíte.
Kudy bajty putují
Import souboru .pfx za účelem podepsání dokumentu není jedinou operací. Je to krátká pipeline a každá fáze analyzuje něco, co mohl napsat útočník. Kontejner je struktura PKCS#12 definovaná v RFC 7292, což je spletitá struktura objektů AuthenticatedSafe obalující šifrovaný obal, který uchovává soukromý klíč. Čtení znamená procházení ASN.1, odvození klíče z hesla, dešifrování a následné předání obnoveného RSA klíče kódu, který sestavuje podpis.
V HotPDF se tyto fáze mapují na samostatné jednotky (unity). Logika kontejneru PKCS#12 se nachází v HPDFPFX. Každý tag, délka a hodnota, kterých se dotkne, jsou dekódovány ASN.1 čtečkou v HPDFASN1. Odvození klíče a dešifrování PBES2 sídlí v HPDFCrypt společně s PBKDF2HMACSHA256. Po obnovení klíče jej HPDFRSA a CMS sestavovač SignedData v HPDFCMS převedou na odpojený podpis vložený do PDF. Veřejným vstupním bodem, který řídí celý řetězec, je jediné volání.
// 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 souboru signer.pfx protéká přes HPDFASN1 a HPDFPFX předtím, než dojde k jakékoli kryptografické operaci. Pokud tyto dvě jednotky nejsou opatrné na to, co soubor deklaruje, kryptografie v dalších fázích nedostane šanci se vůbec projevit.
Chyba jedna: délka ASN.1, která přeteče přes ochranu
ASN.1 v DER a BER kóduje každý prvek jako tag, délku a příslušný počet bajtů obsahu. Délka je pole, kterému musíte důvěřovat, ale zároveň jej ověřovat, protože říká parseru, jak daleko má číst, a byla zapsána kýmkoli, kdo soubor vytvořil. Norma X.690 §8.1.3 definuje dvě kódování. Krátká forma balí délku 0 až 127 do jediného bajtu. Dlouhá forma, použité pro cokoli většího, vyžaduje jeden zaváděcí bajt, jehož spodních sedm bitů udává počet následujících bajtů délky, a poté příslušný počet bajtů v big-endian kódování nese samotnou hodnotu. Čtyři bajty délky tak mohou deklarovat velikost obsahu blížící se ke čtyřem gigabajtům.
Po dekódování takové hodnoty musí parser zkontrolovat, zda se obsah skutečně vejde do bufferu, než mu začne důvěřovat. Přirozenou kontrolou je ověření, zda aktuální pozice plus délka obsahu nepřesahují konec dat. Pokud je tato kontrola zapsána běžným způsobem, kdy pozice, délka obsahu i celková velikost jsou uloženy v 32bitových celých číslech se znaménkem, je tato 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ém je ve sčítání, nikoli v porovnání. Když se ContentLen blíží k MaxInt (2147483647), výraz Pos + ContentLen přeteče rozsah 32bitového celého čísla se znaménkem a otočí se na záporné číslo. Záporný součet není nikdy větší než Total, takže ochrana nahlásí, že je vše v pořádku, a umožní parseru pokračovat s délkou obsahu přibližně dva gigabajty, kterou buffer ve skutečnosti neobsahuje. Co se stane dále je poškození: čtečka alokuje buffer pro tuto deklarovanou délku a kopíruje do něj pomocí SetLength následovaného Move ze zdroje. Zdroj má k dispozici pouze několik stovek bajtů, takže kopírování čte daleko za konec vstupu. Toto čtení mimo vyhrazenou paměť (out-of-bounds read) v lepším případě způsobí pád programu, v horším uniknutí sousední paměti procesu do analyzovaných dat.
Jediná správná ochrana rozšiřuje mezisoučet před porovnáním, takže sčítání nemůže přetéct typ, ve kterém se počítá. Náprava 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á součet dvou 32bitových hodnot bez ztráty, takže porovnání vidí skutečné číslo a podvrženou délku odmítne. Samostatná kontrola nezápornosti u ContentLen ošetřuje případ, kdy dekódovaná hodnota sama o sobě vyjde jako záporná. V HotPDF se tato ochrana nachází v HPDFASN1ParseNode, což je funkce vytvářející uzel, na kterém staví všechny ostatní pomocné funkce. Vzhledem k tomu, že HPDFASN1Content určuje velikost svých operací SetLength a Move přímo podle délky obsahu uzlu, uzel, který by prošel špatnou ochranou, by znehodnotil každé čtení, které z něj vychází. Oprava této hranice v bodě dekódování je to, co činí pomocné funkce nad ní bezpečnými.
Chyba dvě: počet iterací PBKDF2 použitý jako zbraň
Druhá chyba není chybou paměti, ale případem, kdy soubor říká procesoru, jak tvrdě má pracovat. PKCS#12 chrání svůj klíčový materiál pomocí PBES2, což je schéma založené na hesle z PKCS#5 specifikované v RFC 8018. PBES2 spouští funkci pro odvození klíče, v tomto případě PBKDF2 s HMAC-SHA-256, a poté šifru, zde AES-256-CBC. PBKDF2 přijímá počet iterací a tento počet je parametr přenášený v souboru. Jeho celým účelem je být pomalý: více iterací znamená, že každý pokus o uhodnutí hesla stojí více výkonu, což je účinné proti offline útočníkovi. Norma RFC 8018 §4.2 výslovně uvádí, že vyšší počet je lepší pro bezpečnost, a záměrně nestanovuje žádný strop.
Tato otevřenost je v pořádku, pokud jste soubor vygenerovali vy. Stává se však zbraní, pokud jej vytvořil útočník. Počet iterací je pracovní faktor kontrolovaný útočníkem, což představuje odepření služby na základě algoritmické složitosti (denial of service). Podvržený soubor .pfx může zakódovat počet iterací v miliardách. Parser jej poslušně přečte a zavolá PBKDF2 pro příslušný počet kol HMAC-SHA-256, načež proces zmizí ve smyčce, ze které se u jednoho souboru nevrátí po celé minuty či hodiny. Na podpisovém serveru, který zpracovává jedny přihlašovací údaje na požadavek, jediné upravené nahrání zablokuje pracovní vlákno.
Hodnota počtu iterací zhoršuje problém s přetečením ještě předtím, než začne procesor naplno pracovat. Hodnota iterace je v souboru uložena jako ASN.1 INTEGER, který nemá pevnou šířku, zatímco pole, které PBKDF2 nakonec konzumuje, je 32bitový Integer. Pokud dekódujete INTEGER přímo do tohoto pole, velká hodnota se ořízne. Hodnota navržená tak, aby skončila na znaménkovém bitu, se vrátí jako záporná nebo jako nesouvisející malé číslo, takže ani rozsah práce již neodpovídá tomu, co soubor požadoval. Náprava načítá hodnotu v plné šířce a omezí ji před zúžením typu:
// 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
Načtení do Int64 znamená, že dekódovaná hodnota je skutečná, nikoli její oříznutý stín. Spodní hranice odmítá nulové a záporné počty, které nedávají smysl pro odvození klíče. Horní hranice, sto milionů, leží vysoko nad jakýmkoli legitimním souborem PKCS#12, který dnes používá desítky až nízké stovky tisíc iterací. Zároveň omezuje nejhorší případ na zvládnutelné množství práce. Teprve poté, co hodnota projde tímto pásmem, je zúžena na 32bitové pole, takže oříznutí již nikoho nepřekvapí. V HotPDF se toto omezení nachází v ParsePBES2Params, kde se dekódují parametry PBKDF2 na cestě k PBKDF2HMACSHA256.
Proč jsou obě opravy stejným řešením
Obě chyby vypadají odlišně, jedna jako přetečení bufferu a druhá jako zamrznutí procesu, ale jedná se o stejný omyl. V obou případech bylo číslo z nedůvěryhodného souboru přeneseno do typu s pevnou šířkou o krok dříve, než bylo zkontrolováno vůči realitě. Délka byla sečtena v 32 bitech před testem mezí; počet iterací byl zúžen na 32 bitů před testem rozsahu. Obě chyby ustupují stejné disciplíně: dekódovat v plné šířce, zkontrolovat vůči skutečnému limitu a teprve poté zúžit. Použití pomocného Int64 není otázkou stylu, ale jedinou šířkou, ve které může ochrana vidět hodnotu, kterou útočník skutečně zapsal. Hranice, která přeteče, není hranicí a počet bez stropu není parametrem, ale vzdáleným škrcením vašeho vlastního procesoru.
Praktická doporučení pro podpisovou pipeline
Úzkým ponaučením je validovat nedůvěryhodné vstupy certifikátů stejným způsobem, jakým byste validovali jakékoli nedůvěryhodné nahrané soubory. Omezte velikost přijímaného souboru .pfx, protože legitimní soubor má velikost v kilobajtech, nikoli v megabajtech. Považujte selhání analýzy za běžné odmítnutí vstupu, nikoli za chybu vyžadující zobrazení výpisu zásobníku (stack trace) uživateli. Pokud podepisujete na serveru, spouštějte import tam, kde zablokované pracovní vlákno nemůže shodit celou službu, a obalte operaci časovým limitem (timeout), aby neočekávaně náročný soubor byl omezen reálným časem i limitem iterací.
Širší ponaučení přesahuje oblast certifikátů. Zabezpečení parseru není jednorázový audit jedné jednotky, ale vlastnost každého místa, kde vaše knihovna čte bajty, které sama nezapsala. Knihovna PDF analyzuje mnoho dat z nedůvěryhodných zdrojů: písma vložená v dokumentu, obrázky v půl tuctu kodeků, filtry streamů a na podpisové cestě také certifikáty. Každý z nich představuje prostor pro útok a každý si zaslouží stejnou nedůvěru k jakékoli délce a počtu. HotPDF staví cestu pro import a podepisování na zabezpečených jednotkách HPDFASN1, HPDFPFX, HPDFCrypt a HPDFCMS popsaných zde, takže přihlašovací údaje, které jí předáte, jsou bez ohledu na svůj původ analyzovány defenzivně předtím, než jim začnete důvěřovat.
Podpisový proces chráněný těmito kontrolami je kompletně popsán v našem průvodci digitálními podpisy PAdES v Delphi a stejný defenzivní přístup aplikovaný na šifrování dokumentů, včetně cesty klíče AES-256, která sdílí tento kód, je popsán v článku o šifrování AES-256 a zabezpečení. Všechny tyto jednotky a šifrovací cesty jsou dodávány jako součást HotPDF Component pro Delphi a C++Builder spolu s rozhraními API pro načítání, úpravy, šifrování a podepisování popsanými jinde na tomto blogu.