Техническая статья

Защита подписи PDF в Delphi от вредоносного PKCS#12

Когда вы подписываете PDF, вы обычно думаете о ключе подписи как о чем-то, что вы полностью контролируете. Он находится в созданном вами файле .pfx, защищенном выбранном вами паролем. Код, считывающий этот файл, кажется лишь служебным каналом связи, а не границей безопасности. Эта интуиция ошибочна, как только сертификат перестает быть вашим. Настольное приложение, позволяющее пользователю выбрать любой .pfx, сервер, принимающий загруженные учетные данные, или пакетный модуль подписи, получающий сертификаты по сети - все они передают подконтрольные злоумышленнику байты парсеру еще до создания первого байта подписи. Читатель PKCS#12 является вектором атаки в той же степени, что и декодер изображений или загрузчик шрифтов

Эта статья проходит по двум ловушкам, от которых обязан защищаться каждый читатель PKCS#12, обе — на пути импорта signing credential, и по тому, как HotPDF их закрывает. Ни одна не экзотична. Обе растут из одной первопричины, бьющей почти по каждому бинарному парсеру на языке с целыми фиксированной ширины: длина или счёт из файла доверяются на один шаг дальше, чем следовало. Одна ведёт к чтению за пределами буфера, другая — к процессу, висящему, пока вы его не убьёте

Диаграмма HotPDF: ловушка числа итераций PBKDF2 — неограниченный поддельный счётчик в четыре миллиарда раундов закрепил бы CPU, тогда как нижняя и верхняя границы в HPDFCrypt отклоняют его
сто миллионов лежит выше любого легитимного файла, одновременно ограничивая худший случай — сначала декодируй «широко», затем огрань, затем используй
HotPDF: две полосы ловушки переполнения длины ASN.1 — поддельная длина, добавленная в 32 битах, проскальзывает мимо наивной проверки границ, тогда как HPDFASN1ParseNode читает в Int64 и отклоняет её
узкая защита работает с обрезанным призраком настоящего значения; чтение «широкого» поля первым делает поддельную длину видимой до любого SetLength

Куда передаются байты

Импорт файла .pfx для подписи документа не является одной операцией, это небольшой конвейер, и каждый его этап анализирует данные, которые мог подготовить злоумышленник. Контейнер представляет собой структуру PKCS#12, определенную в стандарте RFC 7292: набор блоков AuthenticatedSafe, обернутых вокруг зашифрованной структуры с закрытым ключом. Чтение этого контейнера включает обход ASN.1, получение ключа из пароля, расшифрование и последующую передачу восстановленного ключа RSA коду, создающему подпись

В HotPDF эти этапы сопоставлены с отдельными модулями. Логика контейнера PKCS#12 находится в HPDFPFX. Каждый тег, длина и значение, с которыми он работает, декодируются модулем чтения ASN.1 в HPDFASN1. Вычисление ключа и расшифрование PBES2 происходят в HPDFCrypt вместе с PBKDF2HMACSHA256. После восстановления ключа модули HPDFRSA и компоновщик CMS SignedData в HPDFCMS преобразуют его в отсоединенную подпись, внедряемую в PDF. Публичная точка входа, управляющая всей цепочкой, представляет собой один вызов

Конвейер импорта .pfx в HotPDF: байты, на которые влияет атакующий, проходят через HPDFPFX и HPDFASN1 до вывода ключа в HPDFCrypt и подписания HPDFRSA/HPDFCMS
читатель PKCS#12 — поверхность атаки; криптография ниже по течению уже не важна, если оба парсера небрежны
// Запускает весь конвейер: загружает PDF-заготовку, разбирает PFX,
// получает ключ, строит CMS SignedData, записывает подписанный результат.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // подпись внедрена
else
  // подпись не была завершена
;

Каждый байт файла signer.pfx проходит через HPDFASN1 и HPDFPFX до выполнения криптографических операций. Если эти два модуля не будут тщательно проверять данные из файла, надежность последующей криптографии уже не будет иметь значения

Ловушка первая: длина ASN.1, переполняющаяся мимо проверки

Форматы ASN.1 в DER и BER кодируют каждый элемент в виде тега, длины и соответствующего количества байтов содержимого. Длина представляет собой поле, которому вы вынуждены доверять, но которое обязаны проверять, поскольку оно указывает парсеру глубину чтения и записывается создателем файла. Стандарт X.690 §8.1.3 определяет два способа кодирования. Короткая форма упаковывает длину от 0 до 127 в один байт. Длинная форма, используемая для больших объемов, задействует один начальный байт, младшие семь бит которого задают количество последующих байтов длины, после чего эти байты в формате big-endian содержат фактическое значение. Четыре байта длины могут объявить размер содержимого, приближающийся к четырем гигабайтам

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

// Ловушка: знаковая 32-битная арифметика. При ContentLen близком к MaxInt,
// Pos + ContentLen переполняется до ОТРИЦАТЕЛЬНОГО значения, поэтому сравнение
// ложно, и поддельная длина ~2 ГБ проходит проверку без препятствий.
if Pos + ContentLen > Total then
  raise EHPDFASN1Error.Create('content overruns buffer');

Проблема заключается в сложении, а не в сравнении. Когда ContentLen близок к MaxInt (2147483647), выражение Pos + ContentLen приводит к переполнению знакового 32-битного диапазона и превращается в отрицательное число. Отрицательная сумма никогда не бывает больше Total, поэтому проверка сообщает, что все в порядке, и позволяет парсеру продолжить работу с объявленной длиной около двух гигабайт, которых в буфере на самом деле нет. Дальнейшие действия приводят к повреждению данных: загрузчик выделяет буфер под эту длину и копирует в него данные с помощью SetLength и последующего вызова Move из источника. В источнике осталось всего несколько сотен байтов, поэтому копирование выходит далеко за пределы ввода. Это чтение за пределами буфера в лучшем случае вызывает аварийное завершение программы, а в худшем утекает конфиденциальные данные процесса в результаты разбора

Единственно правильная проверка расширяет промежуточную сумму до сравнения, чтобы сложение не могло переполнить тип, в котором вычисляется. HotPDF повышает оба операнда до Int64:

// Правильно: оба операнда расширены до Int64 перед сложением, поэтому сумма
// не может переполниться. Поддельная длина 2 ГБ теперь не проходит проверку границ.
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 как оружие

Вторая ловушка — не ошибка памяти, а файл, приказывающий вашему CPU, как усердно работать. 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, отправляет процесс в цикл, который не вернётся минуты или часы на одном поданном файле. На сервере подписания, обрабатывающем один credential на запрос, единственная подделка задерживает воркер

Счётчик усугубляет переполнение раньше, чем закручивает CPU. Значение итераций живёт в файле как ASN.1 INTEGER без фиксированной ширины, тогда как поле, которое PBKDF2 в итоге потребляет, — 32-битное Integer. Декодируйте INTEGER напрямую в это поле — большое значение усечётся, а значение, скроенное под знаковый бит, вернётся отрицательным или посторонним маленьким числом, так что даже размер работы перестаёт быть тем, который файл, казалось, запрашивал. HotPDF читает значение на полной ширине и ограничивает его до сужения:

// Сначала читаем количество итераций как Int64, затем ограничиваем разумным диапазоном
// ДО того, как оно будет сужено в 32-битное поле Iterations, используемое PBKDF2.
LIter := HPDFASN1ToInteger(Data, Node);          // возвращает 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);                    // безопасно: уже ограничено

Чтение в Int64 означает, что декодированное значение — настоящее, а не усечённый призрак самого числа. Нижняя граница отвергает нулевые и отрицательные счётчики, которые бессмысленны для выведения ключа. Верхняя граница, сто миллионов, лежит значительно выше любого легитимного файла PKCS#12, который сегодня использует от десятков до нескольких сотен тысяч итераций, одновременно ограничивая худший случай конечным, переживаемым объёмом работы. И лишь после того, как значение прошло эту полосу, оно сужается до 32-битного поля, так что усечение больше не сможет никого подловить. В HotPDF это ограничение находится в ParsePBES2Params, где параметры PBKDF2 декодируются на пути к PBKDF2HMACSHA256

Почему обе проверки — одна и та же проверка

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

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

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

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

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