Technical Article

Защита подписи PDF в Delphi от вредоносных файлов PKCS#12

Когда вы подписываете 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

Почему оба исправления имеют одинаковую природу

Эти два дефекта выглядят по-разному (один представляет собой выход за границы буфера, а второй выражается в зависании процесса), но основаны на одной ошибке. В обоих случаях число из ненадежного файла передавалось в тип фиксированной ширины до проверки на соответствие реальности. Длина складывалась в 32-битном пространстве до проверки границ, а количество итераций усекалось до 32 бит до оценки диапазона. Оба случая требуют единого подхода: декодировать в максимальной ширине, проверить соответствие реальным лимитам и только после этого сузить тип. Промежуточный тип Int64 не является вопросом стиля, это единственная разрядность, в которой проверка видит реальное значение, подготовленное атакующему. Переполняемый предел не защищает код, а счетчик без верхней границы превращается в средство удаленного управления вашим процессором

Практические рекомендации для конвейера подписания

Урок состоит в том, чтобы проверять ненадежные файлы сертификатов так же тщательно, как и любые другие загружаемые данные. Ограничьте размер принимаемых файлов .pfx, поскольку настоящий сертификат занимает килобайты, а не мегабайты. Относитесь к ошибке разбора как к обычному отклонению некорректных данных, а не как к событию, требующему вывода трассировки стека пользователю. При подписи на сервере выполняйте импорт в изолированном процессе, чтобы зависший поток не привел к сбою всей службы, и установите тайм-аут для операции, чтобы ограничить время обработки сложного файла

Более широкий вывод выходит за рамки работы с сертификатами. Укрепление парсера не является разовым аудитом одного модуля, это качество должно быть присуще любому участку кода, где библиотека считывает внешние данные. Библиотека PDF обрабатывает много информации из ненадежных источников: шрифты, встроенные в документ, изображения в различных кодеках, потоковые фильтры и сертификаты на пути подписания. Каждый из этих элементов представляет собой вектор атаки и заслуживает одинаково строгой проверки любой длины и любого количества. HotPDF строит процесс импорта и подписания на защищенных модулях HPDFASN1, HPDFPFX, HPDFCrypt и HPDFCMS, гарантируя безопасный разбор учетных данных независимо от их происхождения

Процесс подписания, защищенный этими проверками, подробно описан в нашем руководстве по цифровым подписям PAdES в Delphi, а аналогичный защитный подход при шифровании документов (включая работу с ключами AES-256) рассматривается в статье о шифровании AES-256 и безопасности. Все эти функции поставляются в составе компонента HotPDF для Delphi и C++Builder вместе с API для загрузки, редактирования, шифрования и подписи документов