Статья Детальный разбор цифровых подписей РЕ-файлов

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

Для этих целей был введён спец.интерфейс с публичным ключом PKI (public key interface), который описывают аж 15 стандартов криптографии PKCS#1..15 на все случаи жизни. В контексте данной статьи нас будет интересовать PKCS#7 - именно в нём оговариваются правила хранения цифровых сертификатов в исполняемых файлах.

Оглавление:

1. Основная идея​
2. Поиск подписи в файлах​
3. Внутренняя структура сертификата​
4. Разбор формата ASN.1 для хранения подписи​
5. Практическая часть - пишем парсер​
6. Заключение​



1. Основная идея

Секция SECURITY в исполняемом РЕ-файле служит для хранения цифровой подписи и атрибутов его безопасности. Основная цель - обеспечить механизм проверки подлинности программного обеспечения. Вот ключевые функции:

1. В этой секции размещаются сертификаты разработчика программы. Когда ОС запускает файл на исполнение, загрузчик образов Win может проверить эту подпись с помощью api WinVerifyTrust() из либы WinTrust.dll.​
2. Гарантия целостности. Если подпись валидна и выдана доверенным центром, это подтверждает, что содержимое файла не было модифицировано, и действительно принадлежит заявленному издателю.​
3. Репутация у антивирусного ПО. Наличие валидной цифровой подписи является важным флагом для систем защиты. Подписанные надёжным издателем файлы будут с меньшей вероятностью попадать под радары аверов, и смогут обходить защитные механизмы ОС.​

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

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

1.1. Расценки на цифровые штампы

Начиная с 64-битных систем в лице Висты, Microsoft ввела принудительную подпись для драйверов режима ядра. Сама корпорация не продаёт сертификаты напрямую - их выпускают аккредитованные удостоверяющие центры CA (Certificate Authorities). В общем случае схема здесь такая, причём отличается она для прикладного софта, и для драйверов режима ядра:

1. Чтобы подписать свой дров, для начала нужно купить у центра СА расширенный цифровой сертификат EV (Extended Validation). Он раздаётся только юридическим лицам, поставляется на USB-токене и стоит до $350 в год у провайдеров Sectigo, или до $600 у премиум-центров, например DigiCert. Ознакомиться с расценками можно тут.​
2. Подпись драйвера в Microsoft - бесплатно!​
Сертификат от центра СА в виде файла .pfx нужен лишь для того, чтобы подтвердить вашу личность при регистрации в Microsoft "Partner Center". Как только вы получите его на руки, подпись осуществляется бесплатно. Есть 2 варианта: "Attestation Signing" (автоматизированная подпись), или "WHQL" (полная сертификация), требует теста драйвера на реальном железе, так-что могут потребовать шекели.​

Зато с подписью пользовательских приложений EXE/DLL всё намного проще.
Она рекомендуется для коммерческих программ, чтобы избежать предупреждения "Неизвестный издатель" в окне UAC.

1. Здесь тоже нужно купить сертификат у центра, только на этот раз подойдёт обычный типа OV (Organization Vilidation) по более низкой цене. Сама процедура называется "Code Signing". К любому сертификату обязательно прикладывается пароль, который потребуется ввести на сл.этапе.​
2. После получения файла .pfx, мы сами подписываем свой экзе утилитой signtool.exe из пакета WinSDK.​
Синтаксис: signtool.exe sign /f "mycert.pfx" /p "Пароль" /tr http://timestamp.digicert.com /td sha256 /fd sha256 "app.exe"
Флаг /tr добавляет метку timestamp, чтобы подпись оставалась действительной даже после окончания срока службы сертификата.​

1.2. Самоподписанный сертификат "Self-Signed"

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

1. Создание сертификата для подписи кода.​
Для этого можно использовать api CryptMsgOpenToEncode().​
Другой вариант только для Win10+, и ориентирован на PowerShell:​
Код:
New-SelfSignedCertificate - Type Custom `
-KeyUsage DigitalSignature `
-Subject "CN=MyCompany" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3") `
-FriendlyName "MyCodeSigningCert"

2. Добавление сертификата в число доверенных на лок.компьютере.​
Установите созданный сертификат в "Доверенные корневые центры сертификации" на целевой машине через [Win+R-->certmgr.msc].​
3. Подпись самого .exe файла​
Используйте утилиту signtool.exe, как было предложено выше.​

CertMgr.webp


2. Поиск подписи в исполняемых файлах

Подпись представляет собой структуру WIN_CERTIFICATE, которая содержит вложенную структуру SignedData в формате PKCS#7. Внутри этой структуры находится хеш-сумма подписываемых частей файла, и созданная с помощью закрытого ключа центра сертификации - сама цифровая подпись. Есть множество утилит, которые отображают содержимое подписи в том или ином виде. Например "PEAnatomist" дампить довольно скудную инфу, оставляя за бортом много интересного, а утилита "DiE" показывает весь сертификат в сыром формате ASN.1 (требует ручного разбора).

NtSecur.webp

Адрес и размер цифрового сертификата хранится в записи Security каталога IMAGE_DATA_DIRECTORY. Сертификат прошивается в уже готовый РЕ-файл как оверлей, а потому RVA всегда указывает на самый последний блок в файле, после всех имеющихся секций. Если посмотреть на достаточно большой размер 25 КБ становится очевидно, что утилита "PEAnatomist" от нас явно что-то скрывает, и в практической части мы узнаем, что именно. В природе встречаются и в 2 раза большие по объёму подписи, так-что 25 кило это вовсе не предел.

SecurDir.webp

Сертификат в бинарном виде начинается с выделенной на скрине структуры WIN_CERTIFICATE, где первый дворд определяет его общий размер 0x00006658, за ним идёт слово с версией 0x0200=v2.0, и в последнем слове кодируется тип 0x0002 (см.сишный хидер wintrust.h).

Код:
struct WIN_CERTIFICATE
  dwLength          dd  0
  wRevision         dw  0
  wCertificateType  dw  0   ;// WIN_CERT_TYPE_xxx
  bCertificate      db  ?   ;// от сюда начинается сама подпись!
ends

WIN_CERT_TYPE_X509              =  0x0001    ;// x.509 certificate
WIN_CERT_TYPE_PKCS_SIGNED_DATA  =  0x0002    ;// SignedData struct  <---------
WIN_CERT_TYPE_RESERVED          =  0x0003    ;// ....
WIN_CERT_TYPE_TS_STACK_SIGNED   =  0x0004    ;// Terminal Server Protocol


2.1. Потенциальные уязвимости цифровых подписей

Учитывая, что подпись находится в самом конце бинарника, можно изменить в каталоге поле с размером (в данном случае 0х6658), и дописать после подписи свой шелл-код. Как результат получим подписанный РЕ-файл с партизаном на борту. Правда начиная с Win10 баг в какой-то мере пофиксили - за это отвечает бюллетень MS13-098, а уязвимость известна как CVE-2013-3900.

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

1. Добавьте подраздел с именем Config в ветку реестра: HKLM\Software\Microsoft\Cryptography
2. Внутри "Config" создайте параметр REG_SZ с именем EnableCertPaddingCheck
3. Значение (1) включает защиту, (0) отключает.​
4. Ребут компьютера.​

Так почему-же уязвимость работает, и как удаётся ключам в реестре бороться с ней?
Дело в том, что функция проверки WinVerifyTrust() парсит подпись согласно формату хранения данных ASN.1, где вся информация разделена на блоки, а размер каждого хранится в байтах-префикса самого блока - такая схема известна как триплет TLV или "Type-Length-Value". Сертификаты РЕ-файлов имеют официальную свою структуру SignedData, где в качестве блоков выстапают её поля:

Код:
signedData ::= SEQUENCE {
   version           INTEGER
   digestAlgorithms  SET
   encapContentInfo  SEQUENCE
   certificateSet    SET
   signerInfos       SET
}

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


3. Внутренняя структура сертификатов

Для подписи РЕ-файлов используется технология "Microsoft Authenticode". В её основе лежат стандарты криптографии PKCS (publik-key-crypto-standart), и в первую очередь X.509 v3. Сертификат - это не просто файл, а структура данных, которая связывает открытый ключ с личностью разработчика ПО. Здесь нужно чётко представлять себе разницу между алгоритмом шифрования RSA, и родственным ему RSA_SIGN.
Главное отличие заключается в целях шифрования - RSA предназначен для скрытия информации от посторонних глаз, а RSA_sign - чтобы подтвердить подлинность отправителя. Несмотря на использование схожих математических принципов, эти алгоритмы работают прямо противоположным образом.

1. RSA (encryption) - данные шифруются открытым ключом PublicKey, который может быть у любого из нас. Однако расшифровать их сможет только обладатель закрытого ключа PrivateKey, который хранится в секрете у получателя. Эти 2 ключа всегда создаются парой, и привязаны друг к другу логикой.​
2. RSA_signing - алго просто доказывает, что сообщение отправлено конкретным лицом и гарантирует, что по пути в него не вносились изменения. Здесь подпись создаётся наоборот с помощью закрытого ключа центра сертификации, а проверить её может любой, у кого есть открытый ключ отправителя.​

Для организации подписей используются сл.стандарты PKCS и типы файлов, где непосредственно подпись является частью сертификата (хранится в массиве sertificateSet структуры SignedData):

1. PKCS#12 (файл .pfx, или .p12)
Контейнер является личным хранилищем СА, и содержит всё для процедуры подписи, включая закрытый ключ.​
Именно этот файл ожидает на входе утилита signtool.exe.​
2. PKCS#7 (ныне CMS, файл .p7s)
Содержит сертификаты всех доверенных центров СА, но без закрытого ключа, а только с публичными.​
Этот формат используется для передачи сертификатов по линиям связи до пункта назначения.​
3. X.509 v3 (DER, CRT)
Это сам цифровой сертификат! Он содержит открытый ключ и информацию о владельце.​
Обычно используется для проверки уже существующей подписи, или как часть цепочки сертификатов.​

Таким образом, внутри подписанного PE-файла находится сам сертификат (ваш паспорт как разработчика), и цифровая подпись, которая создана для конкретного файла с помощью закрытого ключа.

Issuer_Subject.webp

Рассмотрим эту схему подробней..
Значит получив файл, центр СА вычисляет хэш отдельных его участков (например только секции-кода), при этом запись Security в каталоге РЕ-файла исключается из подсчёта хэша, что позволяет изменить второй дворд с размером для внедрения своего шелла. Из выхлопной трубы хэширования выходит т.н. дайджест фиксированной длинны, который для пущей надёжности шифруется закрытым ключом центра сертификации.

Алго шифрования выбирает центр на своё усмотрение, причём RSA_SIGN считается уже устаревшим, в пользу более современного ECDSA-256. Дело в том, что ключ последнего вдвое меньше, при этом он выигрывает RSA по устойчивости ко взлому, и имеет аппаратную поддержку шифрования TPM на уровне процессора (trusted platform module).

SignAlgo.webp

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

На заключительном этапе, всё это хозяйство центр СА заворачивает в контейнер PKCS#7, и отправляет разработчику программного обеспечения, который утилитой signtool.exe прошивает сертификат в свой исполняемый файл. Как результат, в РЕ-файле оказываются данные в формате X.509 ASN.1 (Abstract Syntax Notation).


4. Разбор формата хранения сертификатов ASN.1

На схеме ниже представлены основные структуры сертификата. Проблема в том, что поля в них имеют произвольный размер, и если мы хотим написать свой парсер, придётся вычислять смещения динамически. На первый взгляд всё кажется сложно, однако ASN.1 разрабатывали отнюдь не глупые люди, и здесь всё продумано до мелочей. Так он выглядит на бумажке:

pkcs_7.webp

Во-первых в контейнере ASN.1 можно хранить информацию любого типа, и этот тип указывается всегда в первом байте уже знакомой нам триады Type-Length-Value или коротко TLV. Байт "Type" играет важную роль в стандарте X.509 и логически разделён на 3 части.

1. Под тэг выделяются биты[4:0] - именно в них кодируется непосредственно тип данных, а остальные биты можно считать массовкой. Например в старших [7:6] указывается класс тэга - обычно это дефолтный "Universal", и в редких случаях "Context-Specific", т.е. зависит от настроения того, кто создавал этот контейнер. Аналогично и бит[5] с дефолтным значением нуль.​
2. Второй байт в TLV задаёт размер ячейки данного типа, и кодируется он весьма своеобразно. Это "байт со-знаком", а потому макс его размер 127=0x7F. Если-же для ячейки этого мало, то в байте "Length" взводится старший бит, а младшая его тетрада будет хранить число следующих за ним байт, где и найдём фактический размер. Обратите внимание, что 2-байтные размеры ячеек хранятся уже в сетевом порядке Big-Endial, а не как обычно в памяти Little-Endian. Поэтому прочитав нужно обязательно поменять байты местами через rol ax,8 или xchg ah,al.​

В таблице ниже перечислены все возможные значения тега, при этом тёмным выделены те, которые встречаются на практике часто.

TagFormat.webp

Чтобы не возникало лишних вопросов, рассмотрим их бегло, а подробное описание всех тегов можно найти здесь.
Значит имеем большой зоопарк типов строк, хотя в РЕ-сертификатах можно встретить только Octet(0x04), Printable(0x13), и UTF8(0x0C). Отличаются они тем, что первый тип "Octet" включает в себя весь диапазон расширенной таблицы ASCII 0x00..FF, что позволяет описывать массивы hex-данных произвольной длины.

А вот второй тип строк "Printable" используется для хранения только печатных символов латиницы ASCII 0x20..7F. В редких случаях можно нарваться и на тип "UTF8" (кириллица, польский, китайский, и прочее), и чтобы вывести такие строки в окно Win, сначала нужно расширить их до UTF16 функцией MultiByteToWideChar(), и лишь потом печатать в Unicode. В чистом виде WinAPI не понимает UTF8 - он предусмотрен для Linux и МАС.

Тип Sequence(0x30) определяет последовательность данных разного типа, которые логически связаны между собой. Эта самый распространённый тэг во всём сертификате. Например на втором скрине выше видно, что почти все поля в структуре "tbsCertificate" имеют тип "Sequence", т.к. содержимое этих полей описывают одну логическую сущность.

asn1.webp

Время и дата кодируются тегами 0x17-18. Тип UTC является укороченным вариантом и способен хранить дату только до 2050-года, при этом первые 2 знака столетия(20) отбрасываются, а остальные просто строка в формате YYMMDDhhmmssZ (в качестве нуль-терминала выступает символ Z). В свою очередь тип GeneralizedTime(0x18) уже не ограничивает год и кодируется полностью YYYYMMDDhhmmssZ, что на 2 байта длиннее. TimeStamp используется в сертификатах чтобы указать, когда он был выдан центром СА, и до какого года действителен.

Особняком стоит тег Object-Identifier(0x06) или просто OID. Если все теги описывают отдельные поля сертификата, то OID однозначно идентифицирует конкретно взятый объект. Например на предыдущем скрине видно, что сертификат начинается с поля "ContentType" со значением OID=1.2.840.113549.1.7.2, и это объект в лице корневой структуры SignedData. Объекты могут быть вложенными друг в друга, а OID как-раз определяет их границы.

При создании сертификатов, OID указывается как строка с разделёнными точкой цифрами, но в файле он хранится уже в виде hex-значений. Поэтому для самописного парсера ASN.1 важно определить константы в секции-данных по такому сценарию - это позволит искать информацию в сертификатах по двоичной маске.

Код:
;// Паттерны поиска: Type(6) + Length(3) + OID + Type(13.String)
;//-------------------------------------------------------------
align 8
commonName       db   0x06,0x03,0x55,0x04,0x03,0x13,0,0   ;//  2.5.4.3    Владелец
countryName      db   0x06,0x03,0x55,0x04,0x06,0x13,0,0   ;//  2.5.4.6    Государство
localityName     db   0x06,0x03,0x55,0x04,0x07,0x13,0,0   ;//  2.5.4.7    Город
provinceName     db   0x06,0x03,0x55,0x04,0x08,0x13,0,0   ;//  2.5.4.8    Область / Штат офиса
streetAddress    db   0x06,0x03,0x55,0x04,0x09,0x13,0,0   ;//  2.5.4.9    Улица / адрес
organization     db   0x06,0x03,0x55,0x04,0x0A,0x14,0,0   ;//  2.5.4.10   Организация
orgUnitName      db   0x06,0x03,0x55,0x04,0x0B,0x13,0,0   ;//  2.5.4.11   Цент выдачи сертификата
description      db   0x06,0x03,0x55,0x04,0x0D,0x13,0,0   ;//  2.5.4.13   Описание
userCertificate  db   0x06,0x03,0x55,0x04,0x24,0x13,0,0   ;//  2.5.4.36   Публичный ключ
subjectAltName   db   0x06,0x03,0x55,0x1D,0x11,0x04,0,0   ;//  2.5.29.17  Владелец
issuerAltName    db   0x06,0x03,0x55,0x1D,0x12,0x04,0,0   ;//  2.5.29.18  Издатель
crlDistribution  db   0x06,0x03,0x55,0x1D,0x1F,0x04,0,0   ;//  2.5.29.31  Сайт для отзыва сертификата X.509 v2

4.1. Онлайн декодер ASN.1

Чтобы отточить навигацию внутри РЕ-сертификатов, имеет смысл отправиться на сайт ASN.1 JavaScript decoder, где лежит декодер формата ASN.1. Ресурс предлагает даже офлайн версию своего инструмента - просто распакуйте архив в любую папку, и запустите файл index-local.html. Так, помимо самого исходника декодера получим подмножество файлов, среди которых будет и oids.js с описанием всех id объектов.

При запуске, декодер запрашивает файл в формате ASN.1 ..только где его взять?
Для этого мы должны создать файл сертификата сами, вскрыв консервным ножом исполняемый РЕ. Вот последовательность шагов:

1. Откройте интересующий вас РЕ-файл в любом вьювере - у меня "PEAnatomist".​
2. Перейдите в каталог IMAGE_DATA_DIRECTORY, и найдите в нём запись Security.​
3. Открой этот-же файл в двоичном редакторе "HxD", и по Ctrl+G перейдите по указанному в Security адресу.​
4. Выделите в HxD все байты до конца файла, и скопируйте их в новый файл по Ctrl+N --> Ctrl+V.​
5. Внимание! Удалите первые 8 байт из нового файла, и сохраните его с расширением PeFileName.der.​
6. Откройте новоиспечённый файл сертификата *.der в декодере ASN.1​

Так получим подробное описание полей сертификата, и рядом готовый HEX-дамп, в котором будут подсвечиваться триплеты TLV каждого объекта. Синим цветом - байты "Type", а зелёным байты "Length" (не забывайте про порядок BigEndian). Это позволит наглядно увидеть, с чем придётся иметь дело нашему парсеру.

asnJs.webp


5. Практическая часть - пишем парсер

Под занавес напишем приложение на ассемблере fasm, которое заглянет в сертификат указанного приложения, и отобразит в элемент ListView нарытую информацию. Не нужно воспринимать его как готовый инструмент - это просто демка для тех, кто хочет разобраться в формате хранения данных ASN.1 DER.

Чтобы не загромождать код, я упустил здесь много нюансов, например перечисление всей цепочки доверенных центров СА от корневого и последнего в списке. Поэтому в некоторых случаях в поле "Владелец" будет отображаться первый доверенный центр, хотя корневой всегда валидный.

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

1. Списки отзыва "Certificate Revocation List", CRL
Это самый старый и распространённый метод, и представляет собой файл со списком серийных номеров сертификатов, которые были отозваны центром до истечения их срока действия. Проверяющая сторона в лице Win обращается к этому адресу, скачивает актуальный список CRL, и ищет в нём серийник проверяемого сертификата. Если номер найден - дело труба и значит сертификат отозван! Недостаток способа в том, что списки могут быть ОЧЕНЬ большими, и обновляются они с некоторой периодичностью, а не в реальном времени.

2. Протокол OCSP или "Online Certificate Status Protocol"
Более современный и быстрый способ, который позволяет в режиме реального времени запросить у сервера статус конкретного сертификата по его SN. Для этого в сертификате имеется поле с адресом OCSP-респондера - клиент отправляет короткий запрос на этот сервер, и тут-же получает однозначный ответ: Good, Revoked (отозван), или Unknown. Чтобы ускорить процесс, Win кэширует у себя скачанные списки CRL и ответы OCSP на определённое время, чтобы не запрашивать их повторно при каждом запуске приложения.

В общем вот, что получилось у меня в итоге..
Отмечу, что это далеко не вся информация, которую можно выжать из сертификата.

Наши китайские друзья раскошелились на расширенный серт EV,
который выдаётся обычно драйверам режима ядра
resWise.webp



А этот от премиум-центра DigiSert,
причём владельцем является Российская компания из Новосиба.
resMovavi.webp


Центры сертификации есть даже в Западной Африке,
однако мой парсер нарвался здесь на внутреннюю ошибку, о которой я упомянул выше.

resMi.webp


6. Выводы

В заключении хотелось-бы сказать, что подписывать имеет смысл только драйверы Win, и скачанные из Интернета установщики пользовательских программ, поскольку именно в момент доступа к системной папке ProgramFiles можно получить окно UAC с предупреждением об "Неизвестном издателе". Однако как показывают тесты, довольно много прогеров покупают сертификаты и к обычным своим прожкам, а потому решать уже лично вам - хотите вы завоевать дополнительное доверие у системы, или пустите всё на самотёк. В скрепке найдёте исходник моей демки на fasm'e, парочку доков pdf по теме, а так-же готовый исполняемый файл для тестов. Всем удачи, пока!
 

Вложения

Последнее редактирование:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab