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

Основная идея контроля целостности – вычислить хэш от содержимого файла или же его части, а затем сравнить его с эталонным.

Можно выделить три основные способа контроля целостности:

  • Вычисление хэша исполняемого файла вручную

  • Задание контрольной суммы в заголовке исполняемого файла

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

  • Проверка цифровой подписи исполняемого файла при запуске

В приведенных примерах для наглядности и упрощения кода проверка ошибок не приведена.

Вычисление хэша исполняемого файла вручную (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-файла.

Комментарии (17)


  1. vilgeforce
    08.08.2023 15:23
    +2

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


    1. qw1
      08.08.2023 15:23

      Причём, это буквально 16-битная сумма всех 16-битных слов файла + 32-битный размер файла. PE checksum никогда не превышает 0x10000 + размер файла.


    1. freeExec
      08.08.2023 15:23
      +2

      От повреждения файла защитит.


  1. Apoheliy
    08.08.2023 15:23

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

    Например, вызов GetModuleFileName(NULL, fileName, MAX_PATH); очень легко может заполнить fileName строкой, не закрытой нулевым символом. И последующий вызов CreateFile может улететь в крэш.


  1. xi-tauw
    08.08.2023 15:23
    +5

    При помощи всех перечисленных способов, кроме последнего, можно проверить целостность не только исполняемого файла текущего процесса

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

    Давайте я просто перечисли самые простые способы обходов:

    1) Затереть проверки

    2) Затереть хэши-эталоны

    3) Пересчитать хэши-эталоны на новые значения

    4) Пропатчить в памяти

    5) Пропатчить загружаемую библиотеку, а не сам файл

    6) Пропатчить виндовые функции, которые вы дергаете для проверки

    Тут я, скажем так, иссяк, потому что еще 5 способов это просто разные варианты четвертого.


    1. vilgeforce
      08.08.2023 15:23

      А сколько веселья можно устроить с загруженным драйвером! :-)


      1. KernelCore Автор
        08.08.2023 15:23

        А сколько веселья можно устроить с загруженным драйвером! :-)

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

        Не самый простой путь.


        1. freeExec
          08.08.2023 15:23

          Разве нельзя добавить свой сертификат в корневой доверенный?


          1. qw1
            08.08.2023 15:23
            +1

            Windows при загрузке драйверов доверяет только сертификатам Microsoft.


            1. KernelCore Автор
              08.08.2023 15:23
              +1

              Либо сертификатом от доверенного удостоверяющего центра, если драйвер подписан до 30 июня (точную дату не помню) 2021.


              1. qw1
                08.08.2023 15:23

                Хочется уточнить: это любой доверенный центр, например, VeriSign, или строго по списку партнёров Microsoft?


                1. KernelCore Автор
                  08.08.2023 15:23

                  Насколько я помню, не любого.
                  Но от VeriSign будет принят.

                  Нашел такой линк:

                  Deprecation of Software Publisher Certificates


                  1. qw1
                    08.08.2023 15:23

                    Про сторонние центры там явно написано:


                    Starting in 2021, will Microsoft be the sole provider of production kernel mode code signatures?

                    Yes.


                    1. KernelCore Автор
                      08.08.2023 15:23

                      Ну да, с 2021 года только Microsoft


    1. KernelCore Автор
      08.08.2023 15:23
      -1

      Целостность работающего процесса собрались проверять.

      Целостность исполняемого файла

      Давайте я просто перечисли самые простые способы обходов:

      Безусловно, все эти проверки могут быть обойдены и абсолютной защиты не существует. Что, однако, не делает проверки бесполезными, особенно если использовать несколько подходов, делать проверки в разных местах и т.д.


  1. funca
    08.08.2023 15:23
    +1

    Для подписания исполняемого файла сначала необходимо создать или приобрести сертификат.

    Правильнее конечно приобрести, и кроме подписи необходимо проверять валидность сертификата, которым был подписан файл (функции CertGetCertificateChain и CertVerifyCertificateChainPolicy).

    Использовать самоподписный сертификат, добавляя его в Trusted Root Certification Authorities, как в статье, стоит лишь для экспериментов.


    1. boldape
      08.08.2023 15:23
      +3

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