Когато подписвате PDF, обикновено мислите за ключа за подписване като за нещо, което контролирате. Той живее в генериран от вас .pfx файл, защитен с избрана от вас парола. Кодът, който чете този файл, се възприема като обикновена връзка, а не като граница. Тази интуиция е погрешна в момента, в който сертификатът престане да бъде ваш. Настолен инструмент, който позволява на потребителя да избере произволен .pfx, сървър, който приема качени идентификационни данни, пакетен модул за подписване, захранван със сертификати по мрежата, всички те предават повлияни от атакуващия байтове на анализатор, преди да бъде произведен дори един байт от подписа. Четецът на PKCS#12 е повърхност за атака, по същия начин, по който е декодерът на изображения или модулът за зареждане на шрифтове.
Тази статия разглежда два реални дефекта, които съществуваха в този четец, и двата в пътя, който импортира идентификационни данни за подписване. Нито един от тях не е екзотичен. И двата произтичат от една и съща коренна причина, която засяга почти всеки двоичен анализатор, написан на език с фиксирана ширина на целите числа: дължина или брой от файла се гласува доверие една стъпка по-далеч, отколкото трябва. Единият води до четене извън границите, а другият води до процес, който увисва, докато не го прекратите.
Къде пътуват байтовете
Импортирането на .pfx за подписване на документ не е одна операция, а кратък тръбопровод и всеки етап анализира нещо, което атакуващият може да е написал. Контейнерът е структура PKCS#12, дефинирана в RFC 7292, мрежа от AuthenticatedSafe обекти, обвити около шифрирана обвивка, която съдържа частния ключ. Четенето му означава обхождане на ASN.1, извличане на ключ от паролата, дешифриране и след това предаване на възстановения RSA ключ на кода, който изгражда подписа.
В HotPDF тези етапи се съпоставят с отделни модули. Логиката на контейнера PKCS#12 живее в HPDFPFX. Всеки маркер, дължина и стойност, които докосва, се декодират от четеца на ASN.1 в HPDFASN1. Извличането на ключове и дешифрирането по PBES2 се намират в HPDFCrypt заедно с PBKDF2HMACSHA256. Когато ключът е възстановен, HPDFRSA и съставителят на CMS SignedData в HPDFCMS го превръщат в отделен подпис, вграден в PDF файла. Публичната входна точка, която управлява цялата верига, е едно извикване.
// 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
;
Всеки байт от signer.pfx преминава през HPDFASN1 и HPDFPFX, преди да се извърши каквото и да е криптиране. Ако тези два модула не внимават с твърденията на файла, криптографията по веригата никога няма да получи шанс да има значение.
Дефект едно: дължина на ASN.1, която се превърта над предпазителя
ASN.1 в DER и BER кодира всеки елемент като маркер, дължина и съответния брой байтове със съдържание. Дължината е полето, на което трябва да се доверите, но и да проверите, защото тя казва на анализатора колко да прочете и е написана от този, който е създал файла. X.690 §8.1.3 дефинира две кодирания. Кратката форма пакетира дължина от 0 до 127 в един байт. Дългата форма, използвана за всичко по-голямо, изразходва един водещ байт, чиито долни седем бита дават броя на байтовете за дължина, които следват, а след това съответният брой байтове в big-endian формат пренасят действителната стойност. Следователно четири байта за дължина могат да декларират размер на съдържанието, приближаващ се до четири гигабайта.
След като декодира такава стойност, анализаторът трябва да провери дали съдържанието наистина се побира в буфера, преди да му се довери. Естествената проверка е да се потвърди, че текущата позиция плюс дължината на съдържанието не преминава границата на края на данните. Написан по очевидния начин, с позиция, дължина на съдържанието и обща сума, съхранявани в 32-битови цели числа със знак, този предпазител е негоден:
// 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');
Проблемът е в събирането, а не в сравнението. Когато ContentLen е близо до MaxInt (2147483647), Pos + ContentLen препълва подписания 32-битов диапазон и се превърта до отрицателно число. Отрицателна сума никога не е по-голяма от Total, така че предпазителят съобщава, че всичко е наред, и позволява на анализатора да продължи с дължина на съдържанието от приблизително два гигабайта, каквито буферът не съдържа. Това, което се случва след това, е повредата: четецът разпределя буфер за тази претендирана дължина и копира в него, SetLength, последвано от Move, четящо от източника. Източникът има само няколко стотици останали байтове, така че копирането чете далеч извън края на входните данни – четене извън границите, което в най-добрия случай се срива, а в най-лошия изтича съседна памет на процеса в анализатора.
Единственият правилен предпазител разширява междинната сума преди сравнението, така че събирането да не може да препълни типа, в който се изчислява. Корекцията превръща двата операнда в 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 съдържа сумата от две 32-битови стойности без загуба, така че сравнението вижда реалното число и отхвърля фалшивата дължина. Отделната проверка за неотрицателност на ContentLen затваря съответстващия случай, в който декодирана стойност сама по себе си се оказва отрицателна. В HotPDF този предпазител живее в HPDFASN1ParseNode, функцията, която създава възела, върху който се изгражда всеки друг помощник. Тъй като HPDFASN1Content оразмерява своите SetLength и Move директно от дължината на съдържанието на възела, възел, который е преминал през лош предпазител, би отровил всяко четене, взето от него. Фиксирането на границата в момента на декодиране е това, което прави помощниците над него безопасни.
Дефект две: брой итерации на PBKDF2, използван като оръжие
Втората уязвимост не е грешка в паметта, а файлът, който указва на процесора ви колко усилено да работи. PKCS#12 защитава ключовия си материал с PBES2 – схема, базирана на парола от PKCS#5, специфицирана в RFC 8018. PBES2 изпълнява функция за извличане на ключове, в случая PBKDF2 с HMAC-SHA-256, а след това шифър, в случая AES-256-CBC. PBKDF2 приема брой итерации и този брой е параметър, съдържащ се във файла. Цялата му цел е да бъде бавен: повече итерации означават, че всяко отгатване на парола струва по-скъпо, което е добре срещу офлайн атакуващ. RFC 8018 §4.2 изрично посочва, че по-големият брой е по-добър за сигурността, и умишлено не поставя таван.
Тази отвореност е добра, когато вие сте генерирали файла. Тя обаче е оръжие, когато атакуващият го е направил. Броят на итерациите е фактор на натоварване, контролиран от атакуващия, а фактор на натоварване, контролиран от атакуващия, е отказ от услуга поради алгоритмична сложност. Фалшифициран .pfx може да кодира брой итерации в милиарди; анализаторът надлежно го чете и извиква PBKDF2 за съответния брой кръгове от HMAC-SHA-256 и процесът изчезва в цикъл, който няма да се върне с минути или часове при един подаден файл. На сървър за подписване, който обработва по едни идентификационни данни на заявка, едно подправено качване спира работата на работната нишка.
Броят прави превъртането по-лошо, преди да накара процесора да се завърти в цикъл. Стойността на итерацията живее във файла като ASN.1 INTEGER, който няма фиксирана ширина, докато полето, което PBKDF2 в крайна сметка консумира, е 32-битов Integer. Декодирайте INTEGER директно в това поле и голямата стойност се съкращава, а стойност, създадена да попадне върху бита за знак, се връща като отрицателна или като някое несвързано малко число, така че дори размерът на работата вече не е това, което файлът изглежда е поискал. Корекцията чете стойността в пълна ширина и я ограничава, преди да я стесни:
// 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
Четенето в Int64 означава, че декодираната стойност е реалната, а не нейно съкратено копие. Долната граница отхвърля нулеви и отрицателни броеве, които са безсмислени за извличане на ключ. Горната граница от сто милиона е доста над всеки легитимен PKCS#12 файл, който днес използва от десетки до няколкостотин хиляди итерации, като същевременно ограничава най-лошия случай до ограничено, поносимо количество работа. Едва след като стойността е преминала този диапазон, тя се стеснява до 32-битовото поле, така че съкращаването вече не може да изненада никого. В HotPDF това ограничаване живее в ParsePBES2Params, където параметрите на PBKDF2 се декодират по пътя към PBKDF2HMACSHA256.
Защо и двете корекции са една и съща корекция
Двата дефекта изглеждат различни – единият е препълване на буфера, а другият е увиснал процес, но те са една и съща грешка. Във всеки от случаите на число от ненадежден файл е гласувано доверие в тип с фиксирана ширина една стъпка твърде рано, преди да бъде проверено спрямо реалността. Дължината беше събрана в 32 бита преди теста за граници; броят на итерациите беше стеснен до 32 бита преди теста за диапазон. И двете се подчиняват на една и съща дисциплина: декодиране при пълна ширина, проверка спрямо реалния лимит и едва тогава стесняване. Междинният Int64 не е избор на стил, той е единствената ширина, в която предпазителят може да види стойността, която атакуващият действително е написал. Граница, която се препълва, не е граница, а брой без таван не е параметър – това е отдалечено блокиране на собствения ви процесор.
Практически насоки за тръбопровод за подписване
Краткият урок е да валидирате ненадеждните входни данни от сертификати по съния начин, по който бихте валидирали всяко ненадеждно качване. Ограничете размера на .pfx, който приемате, тъй като легитимният е в килобайти, а не в мегабайти. Третирайте срива на анализа като рутинно отхвърлени входни данни, а не като грешка, заслужаваща проследяване на стека (stack trace) до потребителя. Ако подписвате на сървър, стартирайте импортирането там, където увиснала работна нишка не може да свали услугата със себе си, и задайте таймаут за операцията, така че неочаквано тежък файл да бъде ограничен както по астрономическо време, така и по лимит на итерациите.
По-широкият урок надхвърля рамките на сертификатите. Защитата на анализатора не е еднократен одит на един модул, тя е характеристика на всяко място, където вашата библиотека чете байтове, които не е написала. Една PDF библиотека анализира много неща от ненадеждни източници: вградени в документ шрифтове, изображения в половин дузина кодеци, филтри за потоци и, по пътя на подписването, сертификати. Всяко от тези неща е повърхност за атака и всяко заслужава същото подозрение към всяка дължина и всеки брой. HotPDF изгражда пътя на импортиране и подписване върху защитените модули HPDFASN1, HPDFPFX, HPDFCrypt и HPDFCMS, описани тук, така че идентификационните данни, които му предавате, откъдето и да са дошли, да бъдат анализирани защитно, преди да им се гласува каквото и да е доверие.
Работният процес за подписване, който тези проверки защитават, е разгледан от край до край в нашето ръководство за PAdES цифрови подписи в Delphi, а същата защитна позиция, приложена към шифрирането на документи, включително пътя на ключа за AES-256, който споделя тази кодова база, е описана в статията за шифриране с AES-256 и сигурност. Всичко това се доставя като част от HotPDF Component за Delphi и C++Builder, заедно с API за зареждане, редактиране, шифриране и подписване, разгледани на други места в този блог.