Проверка электронной цифровой подписи Authenticode. Часть 2. Описание реализации

Dragokas

Angry & Scary Developer
Команда форума
Супер-Модератор
Разработчик
Клуб переводчиков
Сообщения
7,814
Реакции
6,593
Это продолжение. См. также другие части этой статьи:

Часть 1. Кусочек теории.
Часть 3. Программа проверки Authenticode ЭЦП

Часть 2. Описание реализации программы проверки подписей

Содержание:


2.1. Подготовка к проверке
2.2. Запуск процедуры проверки и обработка результатов
2.3. Очистка ресурсов.
2.4. Извлечение сертификатов и содержащейся в них информации
2.5. Извлечение атрибутов и крос-подписей
 
Последнее редактирование:
2.1. Подготовка к проверке

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

Прототип функции проверки в модуле 'modDigiSign' выглядит таким образом:
VB.NET / VBA:
Public Function SignVerify(
    sFilePath As String, _
    ByVal Flags As FLAGS_SignVerify, _
    SignResult As SignResult_TYPE) As Boolean
Сперва обнуляется пользовательская структура результатов проверки (SignResult), включается файловый редиректор, комбинируются флаги (Flags), подготавливается локальный кеш, проверяется возможность загрузки библиотеки Wintrust.dll, наличие проверяемого файла на диске, готовятся CLSID провайдеров проверки:

DRIVER_ACTION_VERIFY – для проверки WHQL подписи драйвера.
WINTRUST_ACTION_GENERIC_VERIFY_V2 – для проверки подписи остальных файлов.

Примечание: сами провайдеры состоят из файлов библиотек и записей реестра:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Defaults\Provider
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Providers\Trust

Далее получаем дескриптор контекста административного каталога, указав оптимальный алгоритм хеширования подписи:
VB.NET / VBA:
CryptCATAdminAcquireContext2 hCatAdmin, VarPtr(DRIVER_ACTION_VERIFY), StrPtr(BCRYPT_SHA256_ALGORITHM), 0&, 0&
Здесь и далее для Windows 8 и выше используются функции с постфиксом «2».

Получая хендл к файлу, временно отключаем редиректор:
VB.NET / VBA:
    RedirResult = ToggleWow64FSRedirection(False, sFilePath)

    hFile = CreateFile(StrPtr(sFilePath), GENERIC_READ, FILE_SHARE_READ Or FILE_SHARE_WRITE, ByVal 0&, OPEN_EXISTING, ByVal 0&, ByVal 0&)

    If RedirResult Then ToggleWow64FSRedirection True
Примечание: редиректор отключается только для конкретных путей.

Получаем размер файла. Если он превышает MAX_FILE_SIZE (100 MB.) и не указан флаг игнорирования (SV_NoFileSizeLimit), выходим из программы. Лимит взят навскидку для защиты от подвисаний.

Далее рассчитываем хеш файла:
VB.NET / VBA:
CryptCATAdminCalcHashFromFileHandle2(hCatAdmin, hFile, HashSize, aFileHash(0), 0&)

Проводится поиск хеша в каталогах безопасности:
VB.NET / VBA:
CatalogContext = CryptCATAdminEnumCatalogFromHash(hCatAdmin, aFileHash(0), HashSize, 0&, ByVal 0&)
Модуль modDigiSign ищет только первое совпадение. Есть ничтожно маленький шанс, что хеш этого файла попадётся в ещё одном каталоге. Если требуется перечислить все каталоги с заданным для поиска хешем, укажите последним параметром хендл результата предыдущего вызова функции.

Если файл был подписан через каталог, мы получим его контекст и затем можем запросить полный путь к файлу каталога безопасности (.cat) в структуру CATALOG_INFO:
VB.NET / VBA:
If CryptCATCatalogInfoFromContext(CatalogContext, CatalogInfo, 0&) Then

Если контекст не был получен, предполагаем, что проверяемая подпись – внутренняя (embedded).

Дальше мы заполняем структуры в зависимости от физического расположения подписи – внутренняя или внешняя (подписанная через каталог).
Файл может быть подписан одновременно обеими. По-умолчанию, в этом случае проверяется подпись через каталог. Чтобы поменять приоритет, установите флаг SV_DisableCatalogVerify. Такой флаг устанавливается автоматически при проверке вторичной подписи через флаг SV_CheckSecondarySignature, т.к. вторичная подпись может быть только внутренней.
Также, при проверке через каталог, не заполняется поле результатов проверки .isEmbedded. Чтобы принудительно заполнять это поле, укажите флаг SV_CheckEmbeddedPresence.

Для проверки внутренней подписи нам нужно заполнить структуры:
где: WINTRUST_DATA -> dwUnionChoice устанавливаем как WTD_CHOICE_FILE (для проверки внутренней подписи), или WTD_CHOICE_CATALOG (для внешней).

Возводим флаг:
WINTRUST_DATA -> dwStateAction (WTD_STATEACTION_VERIFY или WTD_STATEACTION_IGNORE)
в зависимости от того, нужно или нет получать hWVTStateData, откуда мы сможем извлечь инфу о цепочке сертификатов в подписи.

WINTRUST_DATA -> dwProvFlags
Здесь устанавливаются специфические флаги проверки, такие как:
WTD_SAFER_FLAG – тихий режим
WTD_REVOCATION_CHECK_CHAIN – необходимость проверки на отзыв (управляется флагом SV_CheckRevocation)
WTD_LIFETIME_SIGNING_FLAG – разрешение проходить валидацию с просроченным сертификатам даже без подписи сервера времени (управляется флагом SV_AllowExpired) (подробнее см. раздел 1.3. Теория, п.3).

Для ОС Win8 и выше также запрашивается кол-во подписей. Для этого нужно заполнить структуру WINTRUST_SIGNATURE_SETTINGS и передать указатель на неё в поле WINTRUST_DATA -> pSignatureSettings.

Для проверки внешней подписи заполняем структуры:

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


2.2. Запуск процедуры проверки и обработка результатов

Проверка выполняется функцией WinVerifyTrust, экспортируемой WinTrust.dll, с передачей GUID провайдера политики проверки и указателя на WINTRUST_DATA.
В этот момент файловый переадресатор должен быть отключён!
VB.NET / VBA:
ReturnVal = WinVerifyTrust(INVALID_HANDLE_VALUE, ActionGuid, VarPtr(WintrustData))
INVALID_HANDLE_VALUE – обозначает тихий режим, без UI.
Возвращаемое значение – это результат проверки. Например, 0 – успешная проверка по всем политикам.
Описание части других значений можно посмотреть, например, по ссылкам:
MSDN. CERT_CHAIN_POLICY_STATUS structure
MSDN. TRUST Error Codes

На этом этапе программа преобразует код ошибки в описание (поля .ShortMessage и .FullMessage), заполняются поля .ReturnCode, .isLegit, .isSelfSigned, корректируется поле .isSigned, а также .isEmbedded (если было затребовано флагом SV_CheckEmbeddedPresence). В таком случае вызывается функция IsInternalSignPresent(), где в структуре PE проверяется наличие указателя на SecurityDir.

Дополнительно в прокси-обработчик ошибок WriteError заложен случай проверки признака повреждения бинарных данных подписи.

В результате проверки также заполняется структура WINTRUST_SIGNATURE_SETTINGS и поле WINTRUST_DATA -> hWVTStateData.

hWVTStateData – содержит указатель на данные состояния проверки (см. далее в разделе 2.4.), по которым можно извлечь информацию о сертификатах и крос-подписях.

Поле dwVerifiedSigIndex содержит индекс проверенной подписи.
Поле dwIndex нужно заполнить другим индексом, если мы хотим проверить следующую подпись у файла. Такую проверку нельзя выполнить под этим же контекстом административного каталога (когда уже вызван WinVerifyTrust), поэтому для проверки вторичной подписи контекст нужно «перезагрузить», очистив ресурсы и получив новый контекст. Даже если указан флаг SV_CheckSecondarySignature, модуль modDigiSign всегда выполняет первый вызов WinVerifyTrust для получения кол-ва подписей и индекса первой из проверенных, чтобы узнать какой индекс у вторичной подписи для её последующей проверки.


2.3. Очистка ресурсов.

Для освобождения ресурсов функцию WinVerifyTrust нужно вызвать повторно, заменив в поле dwStateAction флаг на WTD_STATEACTION_CLOSE.

Контекст административного каталога освобождается функцией CryptCATAdminReleaseContext.

Контекст каталога безопасности освобождается функцией CryptCATAdminReleaseCatalogContext.

После чего закрывается хендл файла, а результат проверки сохраняется в локальный кеш.

Если ваша программа больше не нуждается в проверке ЭЦП, разумно будет выполнить принудительную очистку памяти, занятой кешем. Для этого возведите флаг SV_CacheFree, передав пустое имя файла:
VB.NET / VBA:
SignVerify "", SV_CacheFree, SignResult
 
Последнее редактирование:
2.4. Извлечение сертификатов и содержащейся в них информации

2.4.0.Cert_properties.png


1. State data -> provider data.
VB.NET / VBA:
pProvData = WTHelperProvDataFromStateData(StateData)
Указатель на данные состояния проверки (WINTRUST_DATA -> hWVTStateData) передаётся в функцию WTHelperProvDataFromStateData.
В результате получаем данные о провайдере проверки (структура CRYPT_PROVIDER_DATA).

2. Provider data -> Provider signer
VB.NET / VBA:
pCPSigner = WTHelperGetProvSignerFromChain(pProvData, idxSign, 0&, 0&)
Получаем данные о подписантах и кросс-подписантах (структура CRYPT_PROVIDER_SGNR).
Кросс-подпись, например, может накладываться сервером отметки времени (см. раздел 2.5.).

3.1. Provider signer -> psSigner -> HashAlgorithm -> pszObjId
VB.NET / VBA:
memcpy MsgSigner, ByVal CPSigner(0).psSigner, LenB(MsgSigner)

.Algorithm = StringFromPtrA(MsgSigner.HashAlgorithm.pszObjId)
Получаем содержание подписанного сообщения PKCS #7 (CMSG_SIGNER_INFO), а из него извлекаем параметр HashAlgorithm (структура CRYPT_ALGORITHM_IDENTIFIER), в которой хранится алгоритм хеша выборки (digest).


2.4.3.1.1.Hash_of_Signature.jpg

2.4.3.1.2.Hash_of_Signature2.jpg


Прим.: чтобы получить алгоритма хеша подписи сертификата, с которым был подписан файл, см. пункт 5.2.1.

Алгоритм помечен в виде идентификатора объекта (OID), список и расшифровку которых можно увидеть по ссылке: MSDN. CRYPT_ALGORITHM_IDENTIFIER structure.

3.2. Provider signer -> Provider cert.[]
VB.NET / VBA:
ReDim CPCERT(CPSigner(idxSign).csCertChain - 1)

GetMem4 ByVal ArrPtr(CPCERT()), lpSA
GetMem4 ByVal lpSA + 12, lpOldPt
GetMem4 CPSigner(idxSign).pasCertChain, ByVal lpSA + 12
Получаю массив сертификатов провайдера (CRYPT_PROVIDER_CERT) подписанта (в VB я это делаю через подмену указателя на данные массива).
Если Вам нужны сертификаты кросс-подписанта сделайте тоже самое над массивом CRYPT_PROVIDER_SGNR -> pasCounterSigners (см. раздел 2.5.). Он содержит указатели на другие CRYPT_PROVIDER_SGNR.

4. Provider cert -> Cert. context
VB.NET / VBA:
For i = 0 To CPSigner(idxSign).csCertChain - 1
    Signature(idxSign).Certificate(i) = CertDuplicateCertificateContext(CPCERT(i).pCert)
Next
Получаем и дублируем контекст сертификата.

Далее выполняю операции над двумя сертификатами:
1) корневым (сертификат центра сертификации):
VB.NET / VBA:
idxRoot = UBound(Signature(0).Certificate)
pCertificate = Signature(0).Certificate(idxRoot)
2) финальным (сертификат подписанта)
VB.NET / VBA:
idxSigner = 0
pCertificate = Signature(0).Certificate(idxSigner)

Извлекать параметры сертификата, имея его контекст, можно по-разному. Через:
1) CertGetCertificateContextProperty
2) CertNameToStr -> CertGetNameString

Рассмотрим их:

5.1. Cert. context -> Property by ID
VB.NET / VBA:
CertGetCertificateContextProperty (pCertContext, ID, buf(0), bufSize)
Функция может принимать такие идентификаторы.

В данном случае я передаю CERT_HASH_PROP_ID, чтобы получить SHA256 хеш корневого сертификата.
Также, этим способом я получаю e-mail адрес подписанта, т.к. функция имеет полезный флаг CERT_NAME_STR_ENABLE_PUNYCODE_FLAG для декодирования строк формата IA5_STRING, в котором может быть закодирована часть с хостом e-mail адреса.

5.2.1. Cert. context -> Cert. info
VB.NET / VBA:
memcpy Certificate, ByVal pCertificate, LenB(Certificate)
pCertInfo = Certificate.pCertInfo
memcpy out_CertInfo, ByVal pCertInfo, LenB(out_CertInfo)
Получаем CERT_INFO.

Теперь из нее мы можем получить алгоритм хеша подписи сертификата:
VB.NET / VBA:
CERT_INFO -> SignatureAlgorithm -> pszObjId

2.4.5.2.1.Hash_of_Certificate.jpg


Ещё в этой структуре хранится такая полезная информация как: серийный номер сертификата, данные об издателе и подписанте, дата начала и дата истечения срока действия сертификата, публичный ключ и прочее.

5.2.2. Cert info subject (CRYPTOAPI_BLOB) -> Subject X.500 string
VB.NET / VBA:
CertNameToStr X509_ASN_ENCODING Or PKCS_7_ASN_ENCODING, VarPtr(Blob), CERT_X500_NAME_STR, StrPtr(sName), BufferSize
Происходит декодирование строки формата X.500, либо PKCS_7.

5.2.3. Subject X.500 string -> CN
Извлекаем CN или по желанию другие параметры, с помощью ручного парсинга строки Distinguished Names (DN).
Строка записана по стандарту rfc2253.
Пример строки:
C=RU, PostalCode=115093, S=Moscow, L=Moscow, STREET="Street Serpukhovsko B, 44", O=RIVER SOLUTIONS, CN=RIVER SOLUTIONS
Детальнее см. раздел 1.4.5.

Этим способом я получаю имя подписанта и имя организации, которая выдала сертификат.

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


2.5. Извлечение атрибутов и крос-подписей

Атрибуты являются частью цифровой подписи. Присутствие некоторых из них – опционально.
Существует 2 вида атрибутов:
– authenticated (удостоверенные, ещё называются – проверенные)
– unauthenticated (неудостоверенные)

2.5.1.Attribs.png


Проверенные атрибуты – это часть данных, которые входят в выборку хеша и легитимность которых подтверждается в процессе проверки подписи.

Неудостоверенные атрибуты не входят в выборку и их легитимность не подтверждается на первом этапе проверки основной подписи.

Однако, существует такое понятие, как крос-подпись. Она накладывается поверх основной подписи. И храниться она как раз в виде неудостоверенного атрибута. Так что её легитимность проверяется отдельно, точно в таком же порядке, как и для основной подписи.

Атрибут состоит из:
– идентификатора атрибута (OID)
– значения атрибута

Неполный перечень атрибутов можно посмотреть в приложенном к статье файле OIDs.txt
Так, в примере выше:
Удостоверенные:
1.3.6.1.4.1.311.2.1.12 – это spcSpOpusInfo
1.2.840.113549.1.9.3 – это contentType (тип содержимого)
1.2.840.113549.1.9.4 – это messageDigest (выборка сообщения)
Неудостоверенные:
1.3.6.1.4.1.311.3* – крос-подпись сервера штампа времени (подробнее о назначении см. раздел 1.3, п.3.)

Рассмотрим пример с этой крос-подписью. Она также имеет собственные атрибуты:

2.5.2.TimeStamp.png


Посмотрим на OID 1.2.840.113549.1.9.5 (на рисунке уже расшифрован) – это время подписания файла.

Значение любого атрибута кодируется в формате ASN.1, как:
C++:
{
    byte TypeData – тип данных
    byte cbData – размер данных в байтах
    byte[] RawData – сырые данные
}

Список типов данных можно посмотреть здесь.
Рассматриваемый OID представлен в стандарте RFC5652 (см. п.11.3).
Его значение может быть закодировано в форматах:
– UTCTime, как «YYMMDDHHMMSSZ» (для дат от 01.01.1950 до 31.12.2049)
– GeneralizedTime, как «YYYYMMDDHHMMSSZ» (для всех остальных дат).

При работе через CryptoAPI, это значение автоматически попадает в поле sftVerifyAsOf структуры CRYPT_PROVIDER_SGNR крос-подписи (в формате FILETIME с UTC+0).

К этой крос-подписи может быть «прицеплена» ещё одна и так рекурсивно сколько угодно.

Доступ к атрибутам из-под CryptoAPI можно получить через поля AuthAttrs и UnauthAttrs структуры CMSG_SIGNER_INFO, содержащие масив атрибутов CRYPT_ATTRIBUTE.
 
Последнее редактирование:
Назад
Сверху Снизу