Каждый, кто запускал стандартный msfvenom-шеллкод на машине с актуальным Defender, знает: он живёт ровно до момента, пока агент не прочитает первые байты. Сигнатура
fc 48 83 e4 f0 (cld - сброс direction flag; and rsp, -0x10 - выравнивание стека на 16 байт - стандартный пролог x64 staged/stageless шеллкодов Metasploit Framework) давно в базах каждого вендора. Но проблема глубже: даже если вы зашифруете пейлоад, EDR перехватит вызовы через хуки в ntdll, а поведенческий движок соберёт цепочку VirtualAlloc → WriteProcessMemory → CreateRemoteThread и вынесет вердикт. Чтобы пройти мимо современной защиты, нужно понимать, как она работает на каждом уровне, и бить точечно по каждому из них.Здесь я разберу полный пайплайн разработки кастомного загрузчика: шифрование пейлоада, обфускацию шеллкода, техники in-memory execution и обход хуков EDR - с конкретным кодом на C и объяснением каждой строки.
Как EDR видит ваш пейлоад: четыре уровня детекта
Прежде чем писать загрузчик, стоит разобраться, с чем мы боремся. Современный EDR - не один механизм, а многоуровневая система. Каждый уровень требует отдельной техники обхода.Сигнатурный анализ на диске
Первый барьер - статический сканер. Он ищет известные последовательности байтов в файлах на диске: паттерны шеллкода, строки (имена функций WinAPI), характерные структуры PE-файла, подозрительные импорты. По данным исследования Elastic Security Labs, даже коммерческий фреймворк SHELLTER использует полиморфную обфускацию (Polymorphic Code, T1027.014, Defense Evasion) для изменения байтовой последовательности при каждой генерации - именно потому, что статические сигнатуры до сих пор остаются первой линией обороны.Поведенческий анализ в рантайме
Второй уровень - мониторинг поведения. EDR ставит хуки на функции ntdll.dll в user-mode, отслеживая вызовы вроде NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx. Плюс Event Tracing for Windows (ETW) генерирует телеметрию по .NET-загрузкам, сетевой активности, LDAP-запросам и куче всего ещё. По анализу SecurityVision, ETW - критический источник данных для EDR, и обход ETW - одна из приоритетных задач атакующего.Сканирование памяти
Третий уровень - периодическое сканирование памяти процессов. Даже если пейлоад расшифрован и крутится только в RAM, EDR может обнаружить его при следующем скане. Поэтому, как показывает исследование cirosec по разработке загрузчиков, мало просто выполнить код в памяти - нужно думать, в какой регион вы его кладёте и какие атрибуты у этого региона.Kernel-mode телеметрия
Четвёртый - и принципиально важный - уровень: kernel-mode драйверы EDR. CrowdStrike Falcon, SentinelOne, Microsoft Defender for Endpoint используют kernel-mode компоненты: PsSetCreateProcessNotifyRoutine для отслеживания создания процессов, ObRegisterCallbacks для контроля операций с хэндлами, minifilter-драйверы для файловых операций. Эти механизмы не обходятся техниками unhooking ntdll или direct syscalls - syscall всё равно попадает в ядро, где драйвер EDR его перехватывает. Это принципиальное ограничение всех user-mode техник из этой статьи, и его надо держать в голове: полный обход EDR на уровне ядра требует совершенно иных подходов (уязвимые драйверы - BYOVD, T1068 - или подписанные драйверы), которые выходят за рамки этого материала.Шифрование пейлоада антивирус: от XOR до AES-128 CBC
Шифрование - основа, без которой ваш шеллкод не доживёт до запуска. Задача не в криптографической стойкости, а в уничтожении сигнатур на диске. Три подхода - от простого к продвинутому.XOR с ключом: минимум для уничтожения паттернов
Простейший и самый компактный вариант. Согласно практическому руководству cirosec по разработке загрузчиков, XOR используется для обфускации строк прямо в лоадере:
C:
void xor_crypt(unsigned char* data, unsigned int data_length,
unsigned char* key, unsigned int key_length) {
for (unsigned int i = 0; i < data_length; i++) {
data[i] = data[i] ^ key[i % key_length];
}
}
Проблема XOR: при известном фрагменте открытого текста (а начало шеллкода предсказуемо) аналитик восстановит ключ за секунды. Для обхода автоматического сканера хватит, для противодействия ручному анализу - нет.
RC4: поточный шифр для основного пейлоада
RC4 - оптимальный баланс между компактностью и устойчивостью к тривиальному реверсу. Лично я использую реализацию из руководства cirosec:
C:
void rc4(unsigned char* key, unsigned long key_length,
unsigned char* input, unsigned long input_length) {
unsigned char S[256];
unsigned char tmp;
// Key Scheduling Algorithm (KSA)
for (int i = 0; i < 256; i++) {
S[i] = i;
}
for (int i = 0, j = 0; i < 256; i++) {
j = (j + S[i] + key[i % key_length]) % 256;
tmp = S[i];
S[i] = S[j];
S[j] = tmp;
}
// Pseudo-Random Generation Algorithm (PRGA)
for (int n = 0, j = 0, i = 0; n < input_length; n++) {
i = (i + 1) % 256;
j = (j + S[i]) % 256;
tmp = S[i];
S[i] = S[j];
S[j] = tmp;
int rnd = S[(S[i] + S[j]) % 256];
input[n] = rnd ^ input[n];
}
}
AES-128 CBC: промышленный стандарт
По данным Elastic Security Labs, SHELLTER (версия Elite 11.0) использует AES-128 CBC для шифрования финальных пейлоадов. Ключ и IV либо встроены в тело загрузчика, либо загружаются с удалённого сервера. Перед шифрованием пейлоад сжимается алгоритмом LZNT1.Для собственного загрузчика AES имеет смысл, когда важно усложнить ручной реверс. Реализация занимает больше кода, зато при серверном ключе аналитик не расшифрует пейлоад без доступа к инфраструктуре.
Практический совет: комбинируйте. XOR - для строк и имён функций в загрузчике, RC4 или AES - для основного шеллкода. По типу матрёшки, только без фанатизма. Это техника Obfuscated Files or Information (T1027, Defense Evasion) в терминологии MITRE ATT&CK.
Обфускация шеллкода: полиморфизм и API hashing
Шифрование убирает сигнатуры на диске, но после расшифровки код снова уязвим для сканирования памяти. Обфускация на уровне инструкций решает эту проблему.Полиморфный junk-код
По данным Elastic Security Labs, SHELLTER встраивает полиморфный самомодифицирующийся шеллкод в легитимные программы. Суть: между реальными инструкциями вставляются мусорные операции, которые не влияют на результат, но меняют байтовую последовательность при каждой генерации. Ломает статические дизассемблеры и эмуляторы.Пример мусорных инструкций, которые не меняют состояние:
Код:
; Junk block - пример для демонстрации концепции (x64)
push rax
mov rax, 0xDEADBEEFDEADBEEF
xor rax, rax
add rax, 0x41414141
sub rax, 0x41414141
pop rax
; Реальная инструкция загрузчика
call actual_function
API hashing вместо строковых имён
Вызов GetProcAddress с открытым именем функции - подарок для аналитика и сигнатурного движка. SHELLTER, по исследованию Elastic, использует time-based seeding для обфускации адресов API: хеш-значение функции вычисляется с привязкой к SystemTime из KUSER_SHARED_DATA, что делает хеши уникальными при каждом запуске.Базовый подход - классический хеш (DJB2, CRC32, ROR13) от имени функции. Загрузчик проходит по таблице экспорта ntdll.dll или kernel32.dll, вычисляет хеш каждого имени и сравнивает с заранее вычисленной константой:
C:
// Resolve по хешу - обходим строковые сигнатуры
FARPROC resolve_api(HMODULE module, DWORD target_hash) {
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)module;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)module + dos->e_lfanew);
PIMAGE_EXPORT_DIRECTORY exports = (PIMAGE_EXPORT_DIRECTORY)(
(BYTE*)module + nt->OptionalHeader.DataDirectory[0].VirtualAddress);
DWORD* names = (DWORD*)((BYTE*)module + exports->AddressOfNames);
WORD* ordinals = (WORD*)((BYTE*)module + exports->AddressOfNameOrdinals);
DWORD* functions = (DWORD*)((BYTE*)module + exports->AddressOfFunctions);
for (DWORD i = 0; i < exports->NumberOfNames; i++) {
char* name = (char*)((BYTE*)module + names[i]);
if (djb2_hash(name) == target_hash) {
return (FARPROC)((BYTE*)module + functions[ordinals[i]]);
}
}
return NULL;
}
Кастомный загрузчик шеллкода: пошаговая разработка
Собираем всё воедино. Загрузчик - минимальная программа, которая расшифровывает пейлоад в рантайме и передаёт ему управление.Шаг 1: Хранение зашифрованного шеллкода
Два основных варианта, каждый со своими плюсами.Встроенный пейлоад (.rsrc или массив байтов). Шеллкод помещается в секцию ресурсов PE-файла. Как отмечает cirosec, секция ресурсов может содержать данные с высокой энтропией в легитимных программах (иконки, изображения), поэтому привлекает меньше внимания со стороны защитных продуктов. Извлечение - через стандартные WinAPI: FindResource, LoadResource, LockResource.
Staged-пейлоад (загрузка с сервера). Загрузчик содержит только URL и логику расшифровки. Шеллкод скачивается в рантайме. Плюс: на диске нет зашифрованного блоба вообще, и можно проверять окружение на стороне сервера по аналогии с RedWarden. Минус: сетевая активность сама по себе может вызвать подозрения, и нужно маскировать трафик под легитимный.
Шаг 2: Расшифровка и выполнение
Минимальный загрузчик с RC4-расшифровкой и прямым исполнением. Примечание: этот код - упрощённый пример для демонстрации логики. В реальном загрузчике VirtualAlloc/VirtualProtect следует заменить на Nt-аналоги (NtAllocateVirtualMemory, NtProtectVirtualMemory), вызываемые через indirect syscalls или из чистой ntdll, как описано в секции об unhooking ниже. Без этого EDR перехватит вызовы через хуки:
📚 Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Шаг 3: Подготовка исполняемого файла
По рекомендациям cirosec, «одевание» исполняемого файла снижает уровень подозрительности: подпись (если есть доступ к сертификату), стандартные атрибуты (Company Name, File Description, Version Info), иконка. Инструменты вроде ScareCrow автоматизируют этот процесс. Голый файл без метаданных и иконки на 15 КБ - красный флаг для эвристики. Тут как с людьми: встречают по одёжке.In-memory execution техники: reflective DLL injection и process hollowing
Расшифровка и запуск шеллкода в собственном процессе - рабочий вариант, но EDR легко привязывает поведение к вашему исполняемому файлу. Техники инъекции позволяют исполнять код в контексте легитимного процесса.Reflective DLL injection
Ссылка скрыта от гостей
- загрузка DLL из памяти без обращения к LoadLibrary. Модуль содержит собственный минималистичный PE-загрузчик, который разрешает импорты, корректирует релокации и вызывает DllMain.Этапы (из анализа SecurityVision):
- Выделение памяти в целевом процессе через VirtualAllocEx
- Запись рефлективной DLL через WriteProcessMemory
- Запуск через CreateRemoteThread или NtCreateThreadEx - управление передаётся встроенному загрузчику
- Встроенный загрузчик находит адреса LoadLibraryA и GetProcAddress через PEB, разрешает импорты, выполняет релокации
- Управление передаётся основной логике
Process hollowing
Ссылка скрыта от гостей
- создание легитимного процесса в приостановленном состоянии с последующей заменой его кода на вредоносный.Цепочка вызовов:
- CreateProcess с флагом CREATE_SUSPENDED (например, svchost.exe)
- NtQueryInformationProcess - получение PEB и базового адреса образа
- NtUnmapViewOfSection - выгрузка оригинального кода
- VirtualAllocEx + WriteProcessMemory - размещение нового PE-образа
- SetThreadContext - обновление точки входа
- ResumeThread - запуск
Техники уклонения от EDR: unhooking и direct syscalls
Вот мы добрались до ключевого слоя - обхода поведенческого мониторинга. Без этого даже идеально зашифрованный и обфусцированный пейлоад будет пойман в момент исполнения.API unhooking через маппинг чистой ntdll
EDR ставит хуки на функции ntdll.dll - вставляет JMP-инструкцию в начало функции, перенаправляя вызов в свой обработчик. Решение: загрузить чистую копию ntdll.dll и использовать её вместо хукнутой.По данным Elastic Security Labs, SHELLTER реализует unhooking двумя способами:
- Маппинг свежей копии ntdll.dll через NtCreateSection + NtMapViewOfSection
- Открытие чистой ntdll.dll из директории KnownDLLs через NtOpenSection + NtMapViewOfSection
Концептуально процесс выглядит так:
📚 Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Direct и indirect syscalls
Вместо вызова функций ntdll.dll (даже нехукнутых) можно напрямую выполнить инструкцию syscall с нужным номером. Это Native API (T1106, Execution) на минимальном уровне: полностью обходим user-mode, переходя сразу в ядро.Инструменты вроде SysWhispers генерируют asm-стабы для прямых syscalls. Проблема: EDR начали проверять call stack, и если syscall выполнен не из ntdll.dll, а из неизвестного региона памяти - это аномалия. Тупо палится на стеке вызовов.
Решение - indirect syscalls: загрузчик находит адрес инструкции
syscall; ret внутри легитимной ntdll.dll и делает JMP на него. Call stack выглядит так, будто вызов пришёл из ntdll. По исследованию Elastic Security Labs, SHELLTER реализует именно indirect syscalls с corruption call stack для маскировки источника вызова.ETW patching
📚 Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
AMSI bypass
AMSI (
Ссылка скрыта от гостей
) перехватывает содержимое скриптов перед исполнением. Elastic Security Labs подтверждает, что SHELLTER включает встроенный AMSI bypass. Принцип аналогичен ETW patching - патч функции AmsiScanBuffer в amsi.dll, чтобы она всегда возвращала AMSI_RESULT_CLEAN.Обход песочниц и anti-analysis
Виртуализация и песочницы (Virtualization/Sandbox Evasion, T1497, Defense Evasion / Discovery) - последний рубеж, который ваш загрузчик должен учитывать.Keying - привязка к окружению
Загрузчик проверяет характеристики целевой машины перед расшифровкой: имя домена, имя пользователя, MAC-адрес, количество процессоров. Если среда не совпадает с ожидаемой - пейлоад не расшифровывается. В песочнице аналитик получит бессмысленный набор байтов. Как замок, который открывается только нужным ключом - и ключ этот сама машина жертвы.Эксплуатация ресурсных ограничений
Как отмечает cirosec, защитные продукты работают в условиях ограниченных ресурсов - не могут анализировать каждый файл бесконечно долго. Загрузчик может выделить и заполнить большой блок памяти (сотни мегабайт), выполнить длительный цикл вычислений или проверить минимальное количество процессоров и объём RAM. Песочницы обычно ограничены в ресурсах и могут пропустить такой файл или завершить анализ по таймауту. Тупо не хватает терпения.Unlinking модулей EDR
Отдельная интересная техника из SHELLTER: удаление «канареечных» DLL из PEB LDR. Некоторые EDR-вендоры внедряют в процессы DLL-«ловушки», которые отслеживают попытки ручного перебора списка модулей. SHELLTER обнаруживает и отвязывает такие модули (в исследовании Elastic упоминается kern3l32.dll - намеренная опечатка в имени для маскировки). Хитро, ничего не скажешь.Пентест обход средств защиты: практический чеклист
Собираем все техники в рабочий пайплайн. Вот порядок действий при разработке кастомного пейлоада.| Этап | Действие | MITRE ATT&CK |
|---|---|---|
| 1. Генерация | Сгенерировать raw shellcode (Cobalt Strike, Sliver, Havoc) | -- |
| 2. Шифрование | Зашифровать шеллкод RC4 или AES-128 CBC | T1027 |
| 3. Хранение | Поместить в .rsrc секцию или реализовать staged-загрузку | T1027 |
| 4. Обфускация загрузчика | API hashing, шифрование строк XOR, junk-код | T1027.014 |
| 5. Unhooking | Загрузить чистую ntdll из KnownDLLs или использовать indirect syscalls | T1562.001 |
| 6. ETW/AMSI patch | Пропатчить EtwEventWrite и AmsiScanBuffer | T1562.001 |
| 7. Anti-sandbox | Keying, проверка ресурсов, задержки | T1497 |
| 8. Расшифровка | Расшифровать пейлоад в RW-память | T1027 |
| 9. Исполнение | Сменить атрибуты на RX, передать управление или выполнить Process Injection | T1055 / T1620 |
| 10. Очистка | Обнулить расшифрованный шеллкод в памяти после исполнения | T1027 |
Каждый этап закрывает конкретный уровень детекта. Пропустите шифрование - попадётесь на статике. Пропустите unhooking - попадётесь на поведении. Пропустите anti-sandbox - пейлоад сдетонирует в песочнице аналитика. Одно слабое звено - и вся цепочка разваливается.
Разработка пейлоадов для обхода EDR: типичные ошибки
Несколько граблей, на которые наступают даже опытные операторы. Лично видел каждую из них на реальных проектах.RWX-регионы. Выделение памяти сразу с PAGE_EXECUTE_READWRITE - маркер для каждого EDR. Всегда двухэтапный подход: RW → запись → RX. Без вариантов.
Один процесс = все действия. Если ваш загрузчик расшифровывает, инжектит, патчит AMSI и ETW из одного процесса - поведенческая корреляция тривиальна. Разносите этапы по разным процессам, где это возможно.
Статические ключи шифрования в .data. Ключ, лежащий открытым текстом в секции данных, извлекается реверсером за 30 секунд. Используйте keying - деривацию ключа из параметров окружения - или серверную доставку ключа.
Игнорирование call stack. Direct syscalls из вашего .text региона оставляют аномальный call stack. Используйте indirect syscalls или call stack spoofing. На одном ассессменте именно по стеку вызовов нас и поймали - урок был дорогим.
Заключение
Обход антивируса и EDR - не одна серебряная пуля, а многоуровневая инженерная задача. Шифрование убивает сигнатуры, обфускация ломает статический анализ, unhooking и syscalls обходят поведенческий мониторинг, anti-analysis нейтрализует песочницы. Только комбинация всех техник в одном кастомном загрузчике даёт устойчивый результат на реальном ассессменте.Все описанные техники применимы исключительно в рамках авторизованного тестирования на проникновение. Детекты меняются еженедельно - то, что работает против Defender сегодня, может быть сломано завтрашним обновлением сигнатур. Тестируйте в изолированных лабах, документируйте результаты, обновляйте загрузчик под актуальные версии целевых EDR.
Попробуйте собрать загрузчик по этому пайплайну и прогнать через свой EDR в лабе. Если пролетел без алертов - поздравляю. Если нет - смотрите, на каком уровне поймали, и бейте точечно. Именно так и выглядит этот бесконечный процесс: они обновляют сигнатуры, мы обновляем загрузчик. Ну и далее в том-же духе..