Как работает техника, минующая стандартные механизмы ОС Windows.
На диске нет подозрительной библиотеки, в списке модулей процесса тоже ничего необычного, а посторонний код уже выполняется. Для Windows такая ситуация вполне возможна. Программа может разместить DLL непосредственно в памяти и самостоятельно подготовить библиотеку к работе, пропустив привычную загрузку через системный механизм.
Reflective DLL Injection, или рефлективное внедрение DLL, использует именно такой подход. Техника интересна не обещанием «невидимости», а тем, как меняет привычную картину расследования. Искать один вредоносный файл недостаточно, приходится разбираться, какие области памяти появились внутри процесса, кто создал исполняющийся код и как управление попало по соответствующему адресу.
При этом рефлективная загрузка не отменяет защиту памяти, не выдаёт административные права и не гарантирует отсутствие следов. Чтобы понимать возможности и ограничения метода, полезнее разобрать обязанности загрузчика и наблюдаемые последствия, чем собирать универсальный инструмент из нескольких коротких примеров на C.
Что делает загрузку рефлективной
DLL представляет собой библиотеку динамической компоновки. Приложения подключают такие библиотеки, чтобы пользоваться готовыми функциями. При обычной загрузке через LoadLibrary Windows находит файл, отображает образ в адресное пространство процесса, подготавливает зависимости и выполняет предусмотренную инициализацию. Простого копирования байтов недостаточно, поскольку библиотека хранится в файле и располагается в памяти по разным правилам.
В классической рефлективной схеме часть работы берёт на себя код внутри самой DLL. Небольшой загрузчик начинает выполняться из исходного буфера, разбирает собственный образ и подготавливает полноценную рабочую копию библиотеки. Отсюда слово «рефлективная», библиотека участвует в собственной загрузке. Известная реализация Стивена Фьюэра использует экспортируемую функцию ReflectiveLoader, но название функции не относится к обязательным правилам Windows.
Нужно разделять два действия. Рефлективная загрузка отвечает за подготовку библиотеки к исполнению, а внедрение в другой процесс отвечает за доставку кода и передачу управления. Библиотека способна загрузить себя внутри уже работающего процесса без обращения к соседнему. Если же код переносят в чужое адресное пространство, появляются дополнительные требования к правам доступа и дополнительные следы.
| Подход | Кто подготавливает библиотеку | Что отличает метод |
|---|---|---|
| Обычная загрузка DLL | Загрузчик Windows | Приложение обращается к штатному механизму загрузки образа из файла |
| Классическая рефлективная загрузка | Загрузчик внутри самой DLL | Библиотека содержит код, который подготавливает её рабочую копию из буфера памяти |
| Ручная загрузка, или manual mapping | Отдельный загрузчик | Внешний код размещает образ и выполняет необходимые операции вместо системного загрузчика |
| Внедрение в другой процесс | Зависит от выбранной техники | Код исполняется в другом адресном пространстве, способ загрузки может различаться |
Границы терминов в инструментах и публикациях иногда размыты. Для анализа полезнее установить конкретный механизм, чем спорить о названии. Наличие DLL в памяти ещё не доказывает рефлективную загрузку, а отсутствие вызова LoadLibrary само по себе не объясняет, как появился работающий модуль.
Почему байты DLL ещё не являются работающей библиотекой
Обычные исполняемые файлы и DLL Windows используют формат PE, Portable Executable. Заголовки описывают архитектуру, размеры образа, секции и служебные таблицы. Секции содержат машинный код, данные и другие части программы. Их положение в исходном файле может отличаться от положения в загруженном образе.
Главная ловушка связана с адресами. Файловое смещение показывает положение байта от начала файла. Относительный виртуальный адрес, RVA, показывает положение относительно начала образа после загрузки. Виртуальный адрес, VA, указывает конкретное место в адресном пространстве процесса. Подставлять одно значение вместо другого нельзя.
Представим секцию, которая начинается в файле со смещения 0x400, а в образе должна располагаться по RVA 0x2000. Если функция находится внутри секции на расстоянии 0x120 от начала, её файловое смещение равно 0x520, а RVA равен 0x2120. Прибавив 0x2120 к адресу буфера с исходным файлом, программа попадёт не туда. Для участков, представленных в файле, перевод адреса требует учесть расположение соответствующей секции.
Размеры тоже различаются. SizeOfRawData описывает объём данных секции в файле, VirtualSize относится к секции в памяти, а SizeOfImage задаёт размер всего образа с учётом выравнивания. Часть памяти может требовать заполнения нулями без копирования соответствующих байтов из файла. Поэтому загрузчик должен проверять границы, размеры и арифметику смещений, а не доверять каждому полю заголовка.
Как библиотека подготавливает себя к работе
У рефлективного загрузчика есть необычная стартовая проблема. Код должен воспользоваться функциями Windows до того, как система подготовила его собственные импорты. Импорты перечисляют внешние функции, необходимые библиотеке. В обычной DLL адреса таких функций заполняет загрузчик Windows, но исходный буфер рефлективной библиотеки ещё не прошёл соответствующую обработку.
Поэтому начальный участок должен работать с минимальными предположениями об окружении. В классических реализациях загрузчик находит необходимые системные функции через уже присутствующие в процессе модули. Произвольный вызов библиотечной функции C или привычной функции Windows в начале процедуры может оказаться преждевременным. Даже отладочный вывод способен создать зависимость, которую ещё некому обслужить.
Дальнейшую работу можно представить как несколько обязанностей. Конкретный порядок отдельных операций зависит от реализации, но результат должен удовлетворять требованиям загружаемой библиотеки.
- Проверить исходный образ. Установить архитектуру, границы заголовков и секций, наличие нужных служебных таблиц и достаточность буфера.
- Подготовить память. Разместить заголовки и секции по адресам, предусмотренным для загруженного образа, обработать области без исходных данных.
- Исправить адреса при переносе. Применить записи таблицы базовых перемещений, если фактический адрес отличается от предпочтительного.
- Подготовить зависимости. Найти необходимые библиотеки и заполнить адреса импортируемых функций.
- Настроить окружение. Учесть права страниц, локальные данные потоков, обработку исключений и другие возможности, которые использует конкретная DLL.
- Выполнить инициализацию. Передать управление предусмотренным процедурам, проверить результат и корректно обработать отказ.
Такой загрузчик воспроизводит необходимую часть работы Windows, но не становится полной заменой системного механизма. Чем сложнее библиотека и чем больше особенностей среды выполнения использует программа, тем выше требования к совместимости. Успешный показ окна с сообщением проверяет лишь небольшой участок этой работы.
Где обычно ломаются упрощённые реализации
Самые неприятные ошибки проявляются после внешне успешного запуска. Библиотека уже выполнила простую функцию, однако падает при первом исключении, обращении к локальной переменной потока или загрузке дополнительной зависимости. Причина часто лежит в обязанностях, которые короткий учебный загрузчик вообще не выполнял.
При переносе образа нужно исправлять адреса, обозначенные записями релокаций, с учётом типа каждой записи. Для распространённых образов x86 и x64 используются, в частности, HIGHLOW и DIR64 с разной шириной исправляемого значения. Универсальная запись через указатель машинного размера способна повредить соседние данные. Обход таблицы обязан останавливаться на её границе, а не ждать случайного нулевого поля за пределами допустимого диапазона.
При обработке импортов нельзя безусловно считать FirstThunk исходным перечнем имён. Формат различает таблицу поиска импортов и таблицу адресов, которую загрузчик заполняет результатами. Нужно учитывать OriginalFirstThunk, импорт по имени и порядковому номеру, отсутствие таблиц и ошибки поиска функций. Отдельные сложности создают отложенные импорты и зависимости, для которых требуется дополнительное разрешение имён.
TLS в контексте PE означает Thread Local Storage, локальное хранилище потоков, и не связано с одноимённым протоколом шифрования. Поддержка TLS не сводится к вызову функций обратного вызова. Библиотеке могут понадобиться выделение и подготовка локальных данных, индекс доступа к ним и корректное поведение при работе нескольких потоков.
На x64 дополнительного внимания требуют таблицы, описывающие восстановление стека при исключениях. Для кода, загруженного вне штатного механизма, может потребоваться отдельная регистрация таких данных. Иначе обычный вызов проходит успешно, а исключение превращается в аварийное завершение. Проверка «функция вернула число» такую проблему не обнаруживает.
Наконец, точка входа образа не обязательно совпадает с пользовательской DllMain. Компилятор может поставить перед ней служебный код среды выполнения, а у некоторых DLL точка входа отсутствует. Экспортированная прикладная функция тоже не запускается автоматически после загрузки. Успех инициализации, успешный вызов нужной функции и корректная выгрузка требуют отдельных проверок.
Почему права на исполнение не означают обход защиты
DEP, механизм предотвращения выполнения данных, запрещает исполнять инструкции из страниц, которым не разрешено выполнение. Если программа законно получила исполняемую область памяти, запуск кода из такой области не нарушает правил DEP. Вызов VirtualProtect, который меняет разрешения страницы, нельзя сам по себе называть обходом защиты.
Постоянные права RWX одновременно разрешают чтение, запись и исполнение. Для большинства обычного кода такая свобода избыточна. Разделение записываемых данных и исполняемого кода уменьшает риск случайного или злонамеренного изменения инструкций. Однако переход от записываемой памяти к исполняемой тоже не делает загрузку незаметной и может ограничиваться политиками процесса.
Более прямое препятствие для подобных схем создаёт Arbitrary Code Guard, ACG. При соответствующей конфигурации механизм запрещает выделять динамическую исполняемую память и превращать уже выделенную память в исполняемую. Если загрузчик рассчитывает именно на такой путь, операция может завершиться отказом. Ограничение зависит от настроек процесса и не означает, что любая Windows одинаково блокирует любую рефлективную DLL.
Включать жёсткие ограничения без проверки совместимости тоже нельзя. Некоторые приложения создают машинный код во время работы, например с помощью JIT-компиляции. Для них запрет динамического кода может нарушить нормальную работу. Защитные политики проверяют на конкретных приложениях и сценариях, а не применяют ко всем процессам по принципу «чем больше запретов, тем лучше».
Отдельная граница проходит по правам доступа. Возможность загрузить библиотеку в собственный процесс не даёт права записать код в любой соседний. Доступ к чужой памяти, создание потоков и взаимодействие с защищёнными процессами ограничивает операционная система. Рефлективный загрузчик не заменяет получение необходимых прав и сам по себе не повышает привилегии.
Что показывают реальные инструменты
Практический пример рефлективной загрузки встречается в нативной реализации Meterpreter для Windows. Компонент metsrv.dll содержит основную функциональность агента и может запускаться через рефлективный механизм. Но у Meterpreter есть реализации для разных сред, поэтому переносить свойства одной Windows-библиотеки на весь инструмент некорректно.
Показателен другой момент. Код, который в итоге исполняется из памяти, может попасть на компьютер внутри обычного исполняемого файла. Следовательно, рефлективная загрузка отдельного компонента ничего не доказывает о способе первоначальной доставки. В одной цепочке могут соседствовать файл на диске, сценарий, сетевой обмен и библиотека, существующая только как загруженный образ.
В экосистеме Cobalt Strike также нужно различать Beacon, его загрузчик и дополнительные модули. Классический Beacon Object File, BOF, представляет собой объектный файл COFF, который подготавливает среда Beacon. Такой модуль не равнозначен самостоятельной рефлективной DLL. Знакомая функция go в исходном коде ещё не определяет формат готового файла и способ его исполнения.
Для анализа из этого следует практическое правило. Названия продукта или расширения файла недостаточно. Нужно установить, какой компонент перед нами, кто его загружает, в каком процессе выполняется код и какие зависимости требуются. Ошибка на этом уровне приводит к неверным ожиданиям и при отладке, и при поиске атаки.
Какие следы остаются в памяти
Обычная загрузка образа связывает исполняемые области с файловым образом и структурами загрузчика. У классического рефлективного варианта библиотека часто располагается в частной памяти процесса, обозначаемой как MEM_PRIVATE, и не появляется в обычных списках модулей. Несоответствие между реально исполняемым кодом и зарегистрированными библиотеками становится полезным признаком.
Однако частная исполняемая память встречается и у легитимных программ. Среды с JIT-компиляцией создают машинные инструкции во время работы. Защитное ПО и некоторые упакованные приложения тоже способны усложнять картину. Поэтому правило «MEM_PRIVATE плюс право исполнения означает вирус» даст ложные срабатывания.
- Происхождение области памяти. Связана ли область с файловым образом, какие права имеет и соответствует ли обычному поведению приложения.
- Адреса исполнения. Начинается ли поток в известном модуле, встречаются ли в стеке вызовов адреса из подозрительной области.
- Содержимое. Сохранились ли PE-заголовки, характерные структуры, строки и машинный код, похожие на самостоятельный модуль.
- Окружающие события. Как запускался процесс, какие обращения к нему предшествовали появлению кода, возникла ли необычная сетевая активность.
Сигнатуры MZ и PE помогают найти некоторые образы, но отсутствие заголовков ничего не гарантирует. Аналогично, поиск только страниц RWX пропустит код в областях с правами чтения и исполнения. Проверять нужно сочетание структуры памяти и поведения, учитывая возможности конкретного инструмента.
Для исследования сохранённой памяти подходит Volatility 3. Плагины позволяют сопоставлять процессы, списки DLL и области виртуальной памяти, а поиск подозрительных областей помогает выбрать участки для дальнейшего разбора. Результат malfind служит поводом проверить область, а не готовым заключением о заражении. Отсутствие находок также не доказывает чистоту системы.
Что можно увидеть через Sysmon
Sysmon помогает восстановить последовательность событий вокруг внедрения, но не заменяет анализ памяти. При подходящей конфигурации полезны события создания процессов, доступа к другим процессам, создания удалённых потоков и загрузки модулей. Перед расследованием нужно понимать, какие категории действительно собирались и что отбрасывали фильтры.
Для сопоставления действий пригодятся события Sysmon с идентификаторами 7, 8 и 10. Каждое отвечает на свой вопрос, а отдельная запись не устанавливает вредоносность происходящего.
| Событие | Что показывает | Чего не доказывает |
|---|---|---|
| 7, ImageLoad | Загрузку модуля в процесс | Не гарантирует видимость образа, вручную подготовленного в частной памяти. Категория по умолчанию отключена |
| 8, CreateRemoteThread | Создание потока другим процессом и сведения о стартовом адресе | Не охватывает все способы передачи управления и не означает автоматически атаку |
| 10, ProcessAccess | Открытие одним процессом другого с определёнными правами | Не подтверждает, что после открытия действительно произошли запись памяти или исполнение кода |
При рефлективной загрузке внутри собственного процесса события межпроцессного доступа могут вообще не возникнуть. У вручную загруженной библиотеки может отсутствовать ожидаемое событие ImageLoad. Пустые поля StartModule или StartFunction в событии удалённого потока заслуживают внимания, но требуют сопоставления с адресом и содержимым памяти.
Поэтому полезная логика обнаружения объединяет несколько признаков. Необычное обращение к процессу, исполнение из частной области и последующее несвойственное приложению поведение вместе дают гораздо больше оснований для проверки, чем одно название функции. При этом отсутствие части событий может объясняться настройками сбора, а не действиями атакующего.
Как проверять совместимость и работу защиты
Для учебной проверки достаточно библиотеки с безвредной функцией, которая возвращает фиксированное значение или выполняет простой расчёт. Такой пример позволяет проверить загрузку и вызов без перехвата клавиатуры, сетевого управления и других возможностей, не относящихся к механизму. Испытания проводят в изолированной виртуальной машине с собственными тестовыми процессами.
Архитектуру следует определять отдельно для библиотеки, целевого процесса и системы. Разрядность программы, которая выполняет проверку, ничего не доказывает о соседнем процессе. В современных Windows сведения об архитектуре и работе через WOW64 можно получить через IsWow64Process2, но результат нужно правильно интерпретировать и сопоставить с типом PE-образа. Для ARM64 нельзя ограничиваться вопросом «32 или 64 бита».
Полезный набор испытаний проверяет несколько разных условий. Сначала библиотека работает через штатную загрузку, затем исследуемый загрузчик должен обеспечить ожидаемый результат в заявленной конфигурации. Отдельно проверяют используемые библиотекой импорты, TLS, исключения, повторные вызовы и освобождение ресурсов. Повреждённый образ или отсутствующая зависимость должны приводить к контролируемому отказу, а не к продолжению работы с неверными адресами.
Проверка защитных средств требует другого критерия успеха. Недостаточно увидеть уведомление «угроза заблокирована». Нужно установить, какой этап остановлен, какие события сохранены и сможет ли аналитик восстановить цепочку. Если запуск прошёл, следует проверить видимость подозрительной памяти и поведения. Отсутствие всплывающего предупреждения не означает отсутствие записанной телеметрии.
Что делать при подозрении на внедрение
При реальном инциденте действия зависят от происходящего на компьютере. Если продолжаются утечка или разрушительные операции, приоритет получает сдерживание атаки. Когда обстановка позволяет сохранить доказательства, полезно до завершения подозрительного процесса собрать его сведения, журналы и доступные данные памяти. Перезапуск может уничтожить сведения, которые уже нельзя будет восстановить по файлам.
Дальше расследование должно выйти за пределы найденной DLL. Нужно выяснить, какой компонент доставил библиотеку, как получил возможность исполнить код, какие ресурсы стали доступны и существует ли механизм повторного запуска. Завершение процесса убирает текущий экземпляр кода из его адресного пространства, но не устраняет причину компрометации.
Reflective DLL Injection хорошо показывает слабость проверки, ограниченной файлами. Библиотека способна работать без обычной загрузки с диска, однако для работы по-прежнему нужны память, исполняющийся поток и взаимодействие с операционной системой. Именно связи между этими объектами позволяют искать посторонний код там, где список файлов уже ничего не объясняет.

