«Он подписан, издатель известный» — этим аргументом закрывают вопрос про стороннюю программу, которую тянут в сборку или ставят на рабочий компьютер. Возразить с ходу нечем: вкладка «Цифровые подписи» в свойствах файла и правда показывает «Эта цифровая подпись действительна». Случаи, когда вредоносный файл подписан настоящим сертификатом, редки — на них и не рассчитывают. В таких разговорах я обычно зануда.

Подпись действительно кое-что подтверждает. Просто не то, что от неё ждут. Она говорит: «этот файл в момент подписи выпустил вот такой издатель, и после подписи содержимое не меняли». Она не говорит: «файл безопасен», «издатель — тот, за кого себя выдаёт прямо сейчас» и «ключ не украли полгода назад». Злоумышленники давно живут ровно в зазоре между тем, что подпись подтверждает, и тем, что в неё вчитывают.

Откуда у атакующего берётся чужой сертификат

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

Утечка ключа с узла сборки. Ключ лежит рядом с CI, потому что «так удобнее подписывать релизы», и уезжает вместе со всем остальным при компрометации билд-агента. Историю со Stuxnet, где вредонос был подписан сертификатами, украденными у Realtek и JMicron, разбирали публично ещё в 2010-м.

Кража в цепочке поставок. Атака на обновление ASUS Live Update в 2019 году (ShadowHammer): издатель в подписи был настоящий, ASUS. Подменили содержимое обновления, а не подпись. Пользователь видел легитимного издателя, потому что издатель и был легитимным.

Покупка через посредника со слабой проверкой. Реселлеры выпускают сертификаты на подставные юрлица. Формально издатель существует, но фактически это не так.

Вывод для тех, кто сам выпускает подписанные сборки: закрытый ключ не должен лежать на файловой системе узла сборки. Его место — в аппаратном модуле (HSM или токен), из которого он не извлекается, а узел сборки отправляет туда только хеш на подпись. Тогда компрометация агента даёт возможность подписывать, пока доступ жив, но не даёт унести ключ и подписывать после того, как доступ закрыли.

Почему просроченный сертификат не спасает

Здесь начинается неинтуитивная часть, и её стоит показать на командах.

Возьмём сертификат издателя, срок которого давно вышел, и проверим цепочку сегодня:

$ openssl x509 -in leaf.crt -noout -dates
notBefore=Jan  1 00:00:00 2023 GMT
notAfter=Jun  1 00:00:00 2023 GMT

$ openssl verify -CAfile ca.crt leaf.crt
CN=Example Software Publisher, O=Example
error 10 at 0 depth lookup: certificate has expired
error leaf.crt: verification failed

Истёк, проверка не прошла. Но проверка подписи спрашивает не «валиден ли сертификат сейчас», а «был ли он валиден в момент, когда подписывали». У openssl verify это выражается флагом -attime — проверить цепочку так, будто на дворе указанная дата:

$ openssl verify -attime $(date -u -d 2023-03-01 +%s) -CAfile ca.crt leaf.crt
leaf.crt: OK

Первого марта 2023 года сертификат был в порядке — и «на эту дату» цепочка сошлась. Флаг двигает время для всей цепочки сразу: если на выбранный момент был невалиден и корневой сертификат, проверка так же упадёт, только уже с ошибкой «not yet valid». То есть подпись живёт не только в окне листа, но и в окне УЦ над ним.

То же самое на контейнере подписи. Authenticode — это профиль PKCS#7, и цепочку в нём проверяют тем же способом; на лабораторном CMS-контейнере это видно так:

$ openssl cms -verify -binary -in app.p7s -inform DER -CAfile ca.crt -out /dev/null
CMS Verification failure
...:error:17000064:CMS routines:cms_signerinfo_verify_cert:...:Verify error: certificate has expired

$ openssl cms -verify -binary -in app.p7s -inform DER -CAfile ca.crt \
        -attime $(date -u -d 2023-03-01 +%s) -out /dev/null
CMS Verification successful

Вот и весь механизм. Подпись, сделанная просроченным сегодня сертификатом, остаётся валидной, если проверяющий согласится считать, что подписали её тогда, когда сертификат ещё был жив. Утечка сертификатов Nvidia в 2022-м — наглядная иллюстрация: оба ключа на момент утечки были уже просрочены, и это не помешало подписывать ими драйверы, которые Windows соглашалась загружать.

Остаётся вопрос: откуда проверяющий знает нужную дату и почему не берёт её из системных часов, которые атакующий переведёт куда угодно. Ответ — метка времени.

Как метка времени RFC 3161 фиксирует момент подписи

RFC 3161 задаёт TimeStampToken — отдельную структуру, подписанную ключом службы меток времени (TSA). Она заверяет: «хеш такой-то существовал не позднее такого-то момента». В Authenticode этот токен кладут рядом с подписью файла как отдельный атрибут (старый способ, через контрподпись PKCS#9, — другой; их иногда путают). Внутри токена — заверенное службой время:

$ openssl ts -reply -in resp.tsr -text
Status info:
Status: Granted.

TST info:
Version: 1
Policy OID: tsa_policy1
Hash Algorithm: sha256
Serial number: 0x02
Time stamp: Mar  1 12:00:00 2023 GMT
TSA: DirName:/CN=Example TSA/O=Example

Сертификат такой службы отличается от обычного одним обязательным расширением — без него он не имеет права заверять время, и RFC 3161 требует, чтобы расширение было критичным:

$ openssl x509 -in tsa.crt -noout -ext extendedKeyUsage
X509v3 Extended Key Usage: critical
    Time Stamping

Проверяющий берёт время из этого токена, а не из локальных часов, и оценивает сертификат издателя на этот момент. Задумано ради честной цели: чтобы легально выпущенная программа не переставала считаться подписанной, когда срок её сертификата истечёт. Побочный эффект в том, что подпись переживает свой сертификат, — и украденный ключ с истёкшим сертификатом остаётся пригодным для подписи, которую проверяющий примет, пока момент метки попадает в окно действия сертификата.

Подпись покрывает не весь файл

Ещё одно заблуждение: будто хеш считается со всего содержимого файла. Не совсем. Формат Authenticode при подсчёте хеша исключает три области PE: поле контрольной суммы, запись о таблице сертификатов в каталоге данных заголовка и саму таблицу атрибутных сертификатов, где лежит подпись. Всё остальное, включая данные, дописанные в конец файла, в хеш входит — поэтому наивно приклеить полезную нагрузку в хвост .exe не выйдет, подпись сломается.

Место, которое в хеш не попадает, — внутри самой структуры подписи. Там есть поле, куда исторически клали, например, идентификатор партнёра, чтобы дистрибутив можно было чуть-чуть пометить, не переподписывая. Раз эти байты не хешируются, туда можно положить не только идентификатор, и старая реализация WinVerifyTrust не проверяла, что именно там лежит. Эта конкретная брешь — CVE-2013-3900; исправление к ней Microsoft выпустила ещё тогда (Security Advisory 2915720, ключ реестра EnableCertPaddingCheck), но включается оно вручную — жёсткая проверка ломает те самые легальные сценарии с пометкой дистрибутива.

Практический вывод: «подпись целая» не равно «файл целиком тот, что подписывал издатель». Процесс приёмки, который останавливается на факте валидной подписи, не отличит исходный файл от дополненного.

Почему отзыв сертификата не закрывает проблему

Логичное возражение: украли ключ — издатель отзовёт сертификат, и подписи перестанут проходить. На практике мешают две причины.

Первая: проверка отзыва для подписи кода мягкая. Если список отзыва или OCSP-ответчик недоступны, проверяющий чаще всего трактует это как «отозвать не смогли, считаем валидным». Недоступность точки распространения списка превращает проверку отзыва в формальность — и это то, что стоит мониторить у себя, а не считать, что отзыв сработает сам.

Вторая тоньше. Отзыв можно выпустить с датой компрометации — тогда невалидными становятся подписи, заверенные меткой времени после этой даты, а честные подписи, сделанные раньше, продолжают проходить. Для легального софта это правильно. Но означает, что «просто отозвать» недостаточно: если дата компрометации проставлена позже реального момента кражи, окно, в котором подпись злоумышленника всё ещё принимается, остаётся открытым.

Зачем это атакующему

Подпись — не способ обойти защиту в лоб. Это способ снизить подозрительность на каждом шаге. Подписанный известным именем файл получает меньше предупреждений SmartScreen и охотнее проходит правила, где «подписан — значит, из доверенного источника».

На первой линии это видно по тому, как расставляют приоритеты. Когда за смену через аналитика проходит сотня срабатываний, подписанный известным издателем файл уходит в глубокий разбор реже неподписанного — не по злому умыслу, а потому что очередь конечна, а подпись выглядит как аргумент в пользу «скорее чисто». Именно за этот сдвиг приоритета атакующий и платит, покупая или воруя сертификат.

Что посмотреть у себя

Проверять не факт подписи, а чем и когда подписано. «Действительна» в свойствах файла — не признак доверия. Признак — совпадение отпечатка сертификата и имени издателя с тем, что вы ожидаете от этого софта. Список разрешённых издателей с конкретными отпечатками (в WDAC или AppLocker) работает; правило «пропускать всё подписанное» не работает.

Смотреть на метку времени как на данные. Есть ли токен TSA, чей он и попадает ли момент подписи в окно сертификата. Подпись с меткой времени у самого края срока действия сертификата — повод присмотреться, а не расслабиться.

Блокировать по отпечатку известные скомпрометированные сертификаты. После утечки Nvidia рабочая рекомендация была именно такой: не ждать отзыва, а внести отпечатки в запрещающие правила WDAC. Для драйверов это особенно важно — там подпись служит условием загрузки кода в ядро, и цена ошибки выше.

Не полагаться на мягкую проверку отзыва. Если у вас есть контроль над приёмкой, проверку стоит делать строгой хотя бы для критичных категорий — понимая, что это даст ложные срабатывания при недоступности точки распространения списка.

Со стороны выпуска — держать ключ в HSM, вести журнал того, что и кто подписал, и выдавать доступ к подписи как отдельное чувствительное действие. Большинство этих историй начиналось не с криптографии, а с того, что ключ лежал там, где его смогли взять.

Коротко

Цифровая подпись отвечает на вопрос «кто и когда это выпустил и не меняли ли файл после». Она не отвечает на вопрос «можно ли этому доверять сейчас». Просроченный сертификат остаётся опасным, потому что подпись живёт по времени метки, а не по текущей дате. Украденный опасен потому, что отзыв слаб и запаздывает. А сам факт подписи ценен атакующему не как обход защиты, а как снижение бдительности на каждом рубеже. Проверять поэтому нужно не наличие подписи, а конкретного издателя по конкретному отпечатку — и относиться к «файл подписан» как к началу проверки, а не как к её итогу.

Что почитать

  • RFC 3161 — формат запроса и токена метки времени.

  • Microsoft Security Advisory 2915720 и CVE-2013-3900 — про неохваченную подписью область и EnableCertPaddingCheck.

  • «Windows Authenticode Portable Executable Signature Format» — что именно входит в хеш.

  • Разбор Operation ShadowHammer у Kaspersky (Securelist) — пример подписи настоящим сертификатом издателя.