Иногда возникает необходимость удостовериться в том, что исполняемый файл приложения не был изменен (поврежден при передаче или пропатчен третьим лицом). В деле контроля целостности нам помогут хэш-функции.
Основная идея контроля целостности – вычислить хэш от содержимого файла или же его части, а затем сравнить его с эталонным.
Можно выделить три основные способа контроля целостности:
Вычисление хэша исполняемого файла вручную
Задание контрольной суммы в заголовке исполняемого файла
Проверка встроенной цифровой подписи исполняемого файла
Проверка цифровой подписи исполняемого файла при запуске
В приведенных примерах для наглядности и упрощения кода проверка ошибок не приведена.
Вычисление хэша исполняемого файла вручную (CryptoAPI)
Разумеется, хэш-функцию можно реализовать и вручную. Мы же для подсчета хэша файла воспользуемся CryptoAPI. В CryptoAPI реализован ряд алгоритмов хэширования, в том числе MD5, SHA256, SHA384, SHA512 и т.д. В качестве примера будем вычислять хэш по алгоритму MD5. Длина MD5-хэша составляет 128 бит (16 байт).
В начале с помощью функции GetModuleFileName получаем путь к исполняемому файлу текущего процесса. Далее открываем дескриптор данного исполняемого файла вызовом функции CreateFile. Затем необходимо выполнить подготовительные действия для использования алгоритма хэширования из CryptoAPI. Получение дескриптора криптопровайдера осуществляется вызовом функции CryptAcquireContext. Последующий вызов функции CryptCreateHash осуществляет инициализацию хэширования потока данных и сохранение дескриптора объекта хэширования. Далее читаем файл небольшими частями (в данной примере по 1024 байта) и добавляем считанные данные к созданному объекту хэширования при помощи функции CryptHashData. Итоговое значение хэша получается функцией CryptGetHashParam. Функция GetStoredFileHash получает эталонный хэш файла, который может хранится в файле или ключе реестра, получен по сети и т.д. Далее вычисленное значение хэша сравнивается с эталонным. При совпадении данных значений файл будет считаться неизмененным. В завершении необходимо освободить используемые ресурсы, закрыв дескрипторы объекта хэширования, криптопровайдера и исполняемого файла.
#define BUFFER_SIZE 1024
#define MD5_LENGTH 16
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
HANDLE hFile = NULL;
BYTE buffer[BUFFER_SIZE] = { 0 };
BYTE hash[MD5_LENGTH] = { 0 };
DWORD hashLength = MD5_LENGTH;
TCHAR fileName[MAX_PATH] = { 0 };
GetModuleFileName(NULL, fileName, MAX_PATH);
hFile = CreateFile(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
CryptCreateHash(hProv, CALG_MD5, NULL, 0, &hHash);
DWORD bytesRead = 0;
BOOL readRes = FALSE;
do
{
readRes = ReadFile(hFile, buffer, BUFFER_SIZE, &bytesRead, NULL);
if (bytesRead == 0)
{
break;
}
CryptHashData(hHash, buffer, bytesRead, 0);
} while (readRes);
CryptGetHashParam(hHash, HP_HASHVAL, hash, &hashLength, 0);
BYTE storedHash[MD5_LENGTH] = { 0 };
GetStoredFileHash(storedHash, MD5_LENGTH);
SIZE_T hashCmpRes = RtlCompareMemory(hash, storedHash, MD5_LENGTH);
if (hashCmpRes == MD5_LENGTH)
{
printf("Integrity check successful\n");
}
else
{
printf("Integrity check failed\n");
}
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
CloseHandle(hFile);
Однако, функции CryptoAPI считаются устаревшими и в новых приложениях рекомендуется использовать Cryptography Next Generation APIs.
Вычисление хэша исполняемого файла (Cryptography Next Generation API)
Вычисление хэша с использованием Cryptography Next Generation APIs аналогично предыдущему случаю с использованием CryptoAPI. Инициализация криптопровайдера для выбранного алгоритма хэширования осуществляется вызовом функции BCryptOpenAlgorithmProvider. Далее выделяется память под объект функции хэширования. Объем памяти, необходимый для хранения данного объекта, определяется при помощи функции BCryptGetProperty. Объект функции хэширования создается вызовом функции BCryptCreateHash. Далее читаем файл небольшими частями (в данной примере по 1024 байта) и добавляем считанные данные к созданному объекту хэширования при помощи функции BCryptHashData. Итоговое значение хэша получается функцией BCryptFinishHash. Далее вычисленное значение хэша сравнивается с эталонным. В завершении необходимо освободить используемые ресурсы, закрыв дескрипторы объекта хэширования, криптопровайдера и исполняемого файла и освободив память, выделенную для объекта функции хэширования.
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_HASH_HANDLE hHash = NULL;
PBYTE hashObject = NULL;
ULONG bytesRead = 0;
DWORD hashObjectSize = 0;
BYTE hash[MD5_LENGTH] = { 0 };
DWORD hashLength = MD5_LENGTH;
TCHAR fileName[MAX_PATH] = { 0 };
GetModuleFileName(NULL, fileName, MAX_PATH);
HANDLE hFile = CreateFile(fileName, GENERIC_READ,
FILE_SHARE_READ, NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, NULL, 0);
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH,
(PBYTE)&hashObjectSize, sizeof(DWORD),
&bytesRead, 0);
hashObject = (PBYTE)HeapAlloc(GetProcessHeap(),0,hashObjectSize);
BCryptCreateHash(hAlg, &hHash, hashObject, hashObjectSize, NULL, 0, 0);
BOOL readRes = FALSE;
BYTE buffer[BUFFER_SIZE] = { 0 };
do
{
readRes = ReadFile(hFile, buffer, BUFFER_SIZE, &bytesRead, NULL);
if (bytesRead == 0)
{
break;
}
BCryptHashData(hHash, buffer, bytesRead, 0);
} while (readRes);
BCryptFinishHash(hHash, hash, hashLength, 0);
BYTE storedHash[MD5_LENGTH] = { 0 };
GetStoredFileHash(storedHash, hashLength);
SIZE_T hashCmpRes = RtlCompareMemory(hash, storedHash, hashLength);
if (hashCmpRes == hashLength)
{
printf("Integrity check success\n");
}
else
{
printf("Integrity check failed\n");
}
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
HeapFree(GetProcessHeap(), 0, hashObject);
CloseHandle(hFile);
Задание контрольной суммы в заголовке исполняемого файла
В заголовке PE-файла, к коим относится .exe, .dll и .sys, в опциональном (на самом деле нет) заголовке (раздел Nt Headers → Optional Header) имеется поле CheckSum. По умолчанию данное поле не инициализировано и имеет нулевое значение.
Для задания контрольной суммы в заголовке PE-файла на этапе компоновки исполняемого файла необходимо выставить опцию «Set Checksum» компоновщика в значение «Yes /RELEASE» (Project → Properties → Linker → Advanced → Set Checksum).
После включения данной опции при сборке исполняемого файла в поле CheckSum будет помещено вычисленное значение хэша.
Функция MapFileAndCheckSum позволяет вычислить значение контрольной суммы заданного исполняемого файла и получить значение из поля CheckSum его заголовка. При совпадении вычисленного значения хэша файла со значением из заголовка исполняемый файл считается неизмененным.
TCHAR fileName[MAX_PATH] = {0};
GetModuleFileName(NULL, fileName, MAX_PATH);
DWORD headerSum = 0;
DWORD checkSum = 0;
MapFileAndCheckSum(fileName, &headerSum, &checkSum);
if (headerSum == checkSum)
{
printf("Integrity check successful\n");
}
else
{
printf("Integrity check failed\n");
}
Проверка встроенной цифровой подписи исполняемого файла
Для подписания исполняемого файла сначала необходимо создать или приобрести сертификат.
Для создания сертификата воспользуемся утилитой makecert, запущенной от имени администратора в Visual Studio Developer Command Prompt:
makecert -r -pe -n "CN=Integrity Test" -ss MY IntegrityTestCert.cer
После выполнения этой команды в текущей директории будет создан файл сертификата с именем IntegrityTestCert.cer. Integrity Test – название организации, выдавшей сертификат. MY – название хранилища, куда будет помещен сертификат. В данном случае это приватное хранилище текущего пользователя. Увидеть сертификат можно запустив приложение certmgr: Personal → Certificates.
Далее необходимо добавить сертификат в хранилище сертификатов доверенных корневых центров сертификации. Это можно сделать из командной строки:
certmgr -add IntegrityTestCert.cer -s -r LocalMachine Root
Того же можно добиться, используя графический интерфейс установщика сертификатов, дважды кликнув мышью на файле сертификата:
Далее необходимо выбрать Install Certificate → Local Machine и выбрать хранилище, куда поместить сертификат: «Trusted Root Certification Authorities» («Доверенные корневые центры сертификации»):
Теперь осталось только подписать исполняемый файл созданным сертификатом при помощи утилиты signtool:
signtool sign /v /s MY /n "Integrity Test" /t http://timestamp.digicert.com /fd SHA256 "IntegrityTest.exe"
После подписывания исполняемого файла в его свойствах появится вкладка «Digital Signatures», в которой будет отображена информация об имеющихся цифровых подписях:
При запуске подписанного исполняемого файла от имени администратора будет отображено окно UAC со сведениями об издателе (организации, чьей цифровой подписью подписан исполняемый файл).
При патчинге файла, его хэш не сойдется со значением, указанным в цифровой подписи. При этом будет отображено желтое окно UAC с предупреждением о запуске файла от неизвестного издателя.
Для получения информации о цифровой подписи файла можно воспользоваться функциями WinVerifyTrust / WinVerifyTrustEx или MsiGetFileSignatureInformation.
Основные возвращаемые значения данных функций:
ERROR_SUCCESS – проверка цифровой подписи завершилась успешно, хэш файла совпал с хэшем, указанным в цифровой подписи
TRUST_E_NOSIGNATURE – файл не содержит цифровой подписи
TRUST_E_BAD_DIGEST – хэш файла не совпал с хэшем, указанным в цифровой подписи. Обычно это означает, что файл был изменен (например, пропатчен)
Функции WinVerifyTrust необходимо передать адрес переменной, содержащей GUID выполняемого действия и используемого провайдера, и адрес структуры WINTRUST_DATA, необходимой криптопровайдеру для проверки цифровой подписи исполняемого файла. Для проверки цифровой подписи исполняемого файла необходимо указать значение GUID WINTRUST_ACTION_GENERIC_VERIFY_V2. В структуре WINTRUST_DATA необходимо присвоить полю dwUnionChoice значение WTD_CHOICE_FILE. При этом в поле pFile необходимо указать адрес структуры WINTRUST_FILE_INFO, в которой хранится путь к проверяемому исполняемому файлу.
WCHAR fileName[MAX_PATH] = { 0 };
GetModuleFileNameW(NULL, fileName, MAX_PATH);
WINTRUST_FILE_INFO fileInfo = { 0 };
fileInfo.cbStruct = sizeof(WINTRUST_FILE_INFO);
fileInfo.pcwszFilePath = fileName;
fileInfo.hFile = NULL;
fileInfo.pgKnownSubject = NULL;
GUID winTrustPolicy = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA winTrustData = { 0 };
winTrustData.cbStruct = sizeof(WINTRUST_DATA);
winTrustData.pPolicyCallbackData = NULL;
winTrustData.pSIPClientData = NULL;
winTrustData.dwUIChoice = WTD_UI_NONE;
winTrustData.fdwRevocationChecks = WTD_REVOKE_NONE;
winTrustData.dwUnionChoice = WTD_CHOICE_FILE;
winTrustData.dwStateAction = WTD_STATEACTION_VERIFY;
winTrustData.hWVTStateData = NULL;
winTrustData.pwszURLReference = NULL;
winTrustData.dwProvFlags = 0;
winTrustData.dwUIContext = WTD_UICONTEXT_EXECUTE;
winTrustData.pFile = &fileInfo;
LONG res = WinVerifyTrust(NULL, &winTrustPolicy, &winTrustData);
switch(res)
{
case ERROR_SUCCESS:
printf("Signature verified\n");
break;
case TRUST_E_NOSIGNATURE:
printf("No signature\n");
break;
case TRUST_E_BAD_DIGEST:
printf("File corrupted\n");
break;
default:
printf("Other result: 0x%X\n", res);
}
Функция MsiGetFileSignatureInformation упрощает получения информации о цифровой подписи файла. При этом данная функция для получения контекста сертификата цифровой подписи и хэша файла вызывает WinVerifyTrust.
TCHAR fileName[MAX_PATH] = { 0 };
GetModuleFileName(NULL, fileName, MAX_PATH);
PCCERT_CONTEXT pContext = NULL;
HRESULT res = MsiGetFileSignatureInformation(fileName, MSI_INVALID_HASH_IS_FATAL,
&pContext, NULL, NULL);
switch(res)
{
case ERROR_SUCCESS:
printf("Signature verified\n");
break;
case TRUST_E_NOSIGNATURE:
printf("No signature\n");
break;
case TRUST_E_BAD_DIGEST:
printf("File corrupted\n");
break;
default:
printf("Other result: 0x%X\n", res);
}
При использовании первого способа с вызовом WinVerifyTrust осуществляется проверка наличия сертификата, с помощью которого был подписан исполняемый файл, в хранилище «Trusted Root Certification Authorities» («Доверенные корневые центры сертификации»). При отсутствии сертификата в данном хранилище будет возвращена ошибка CERT_E_UNTRUSTED_ROOT (0x800B0109). Функция MsiGetFileSignatureInformation не проверяет наличие сертификата, с помощью которого был подписан исполняемый файл, в хранилище сертификатов.
Проверка цифровой подписи исполняемого файла при запуске
Еще одним способом контроля целостности исполняемого файла является выставление в поле DllCharacteristics опционального заголовка исполняемого файла флага IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY. При этом при запуске исполняемого файла ОС проверит валидность его цифровой подписи и факт выдачи сертификата, с помощью которого подписан файл, одним из доверенных корневых центров сертификации. При невалидности цифровой подписи или подписания исполняемого файла тестовым сертификатом (как в предыдущем примере) будет отображено следующее сообщение об ошибке:
Флаг IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY необходимо выставлять для DLL, используемых определенными компонентами Windows. Также данный флаг рекомендуется выставить для драйверов. Помимо этого, драйверы, использующие API для фильтрации запуска и завершения процессов и т.д., должны быть собраны с опцией /INTEGRITYCHECK.
Для подписания исполняемого файла с флагом IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY «настоящей» цифровой подписью в настоящее время необходимо воспользоваться Azure Code Signing.
Для выставления флага IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY при сборке исполняемого файла в разделе Project → Properties → Linker → Command Line → Additional Options необходимо добавить опцию /INTEGRITYCHECK.
Для того, чтобы иметь возможность запускать исполняемые файлы, собранные с опцией /INTEGRITYCHECK и подписанные тестовым сертификатом, необходимо включить режим тестовой подписи, обычно используемый при разработке и тестировании драйверов, выполнив от имени администратора команду:
bcdedit /set TESTSIGNING ON
Режим принятия тестовых подписей будет активирован после перезагрузки ОС. Разумеется, выполнять такие действия лучше в виртуальной машине.
Если сбросить флаг IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY, то проверки цифровой подписи при запуске исполняемого файла производиться не будет. Однако, цифровая подпись файла в данном случае станет невалидной.
Заключение
Каждый из рассмотренных способов обладает своими достоинствами и недостатками. При помощи всех перечисленных способов, кроме последнего, можно проверить целостность не только исполняемого файла текущего процесса, но и произвольного PE-файла.