DLL inject

Практика DLL injection через Memory API и manual mapping

DLL inject в Memory API

DLL inject нужен, когда код должен выполняться уже внутри target-процесса. Обычно это нужно для hook'ов, internal-логики, вызова внутренних функций и сценариев, где внешний reader уже неудобен.

Если нужен внешний фон, полезны DLL Injector, DLL Memory Hack и Code Caves & DLL's.

Примеры с LocalProcess рассчитаны на верхний уровень C#-скрипта в запущенном EyeAuras с загруженным модулем памяти. Log даёт хост скрипта. В обычном C#-классе передайте логгер и процесс явно. Одна ссылка на SDK не запускает модуль: во встраиваемом хосте сначала нужны запуск приложения, UseMemory() и ожидание EnsureReadyAsync(); подробнее в Embedded SDK.

LocalProcess.ByProcessId(...) и ByProcessName(...) возвращают новый ILocalProcess, который освобождает создавший его код. process.Memory используется вместе с этим reader'ом; отдельно закрывать заимствованный сервис хоста не нужно. Эти примеры не проверяют доступность нативных компонентов вашей сборки.

Ниже сохранены основные варианты существующего API. Для них нужен ваш тестовый процесс, в который вы можете загружать собственную DLL, полный путь к ней и совпадающая архитектура. Имена game и C:\Tools\MyHack.dll — заменяемые входы, а не поставляемые файлы.

Контракты сверены для EyeAuras 20fd3d4. Публичные обёртки доступны для чтения, но реализация нативного загрузчика в проверенный набор не входит. Поэтому подробности инициализации, поддерживаемые DLL и поведение конкретной Windows нужно проверять с соответствующей сборкой. Наличие метода не доказывает совместимость. NativeLocalProcess также объявляет эти возможности; LC здесь не рассматривается.

InjectDll

Это самый простой маршрут.

csharp
using EyeAuras.Memory;

{
    using var process = LocalProcess.ByProcessName("game");
    process.InjectDll(@"C:\Tools\MyHack.dll");
}

Видимая обёртка проверяет наличие файла и передаёт работу в RpmInjector. Сигнатура — void InjectDll(string fullDllPath): возвращаемого handle или адреса модуля здесь нет. Отсутствие исключения от вызова не заменяет проверку результата со стороны вашей DLL, например её собственного диагностического сообщения.

Историческое описание этого маршрута — загрузка через LoadLibraryW, с путём в UTF-16 и работой Windows loader. Проверка архитектуры, точный момент завершения, обработка импортов, DllMain и TLS не подтверждены одной публичной обёрткой. Используйте полный путь и DLL нужной архитектуры; права и ограничения процесса остаются отдельным условием. Reader принадлежит скрипту, но его Dispose не является командой выгрузить DLL. У вызова нет токена отмены.

CreateThread

Если InjectDll(...) это готовый high-level helper, то CreateThread(...) это уже более низкоуровневый строительный блок.

Memory API даёт его через IProcessControlApi. Ниже фрагмент для уже открытого ILocalProcess process с заранее определёнными IntPtr startAddress и IntPtr parameter:

Это фрагмент для уже открытого reader process. Вставляйте его в область владения reader, а не после закрытия блока предыдущего примера. startAddress и parameter должен подготовить вызывающий код.

var threadId = process.CreateThread(startAddress, parameter);

Это полезно, когда вы уже сами подготовили всё остальное:

  • нашли адрес функции внутри target-процесса
  • выделили память под аргументы
  • записали туда данные
  • хотите просто запустить код внутри процесса

Это схема интеграции, а не готовый повтор InjectDll: startAddress должен быть абсолютным VA подходящей функции в целевом процессе, а parameter — её согласованным указателем/значением. Возвращается uint ID потока, не handle ожидания и не результат функции. Сам возврат ID не подтверждает завершение кода.

В примере с LoadLibraryW дополнительно понадобились бы корректное разрешение экспорта (в том числе forwarded export), буфер UTF-16 с NUL и протокол ожидания завершения. Нельзя освобождать аргумент сразу после запуска. Здесь эти части не подменяются произвольным выбором экспорта или потока; для загрузки DLL выше есть публичный InjectDll.

Уникальные черты этого пути:

  • вы сами контролируете startAddress и parameter
  • можно вызывать не только LoadLibraryW, но и любую другую подходящую функцию
  • это базовый кирпич для более сложных сценариев

APC

Если нужен запуск через APC, в Memory API для этого есть QueueUserApc(...).

Здесь задача — поставить уже подготовленную функцию в очередь выбранного потока. Контракт IProcessSupportsUserApc имеет два варианта:

csharp
void QueueUserApc(int threadId, IntPtr apcRoutine, IntPtr apcArgument);
void QueueUserApc(IntPtr hThread, IntPtr apcRoutine, IntPtr apcArgument);

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

Уникальные черты этого пути:

  • код ставится в очередь существующему потоку
  • новый thread для запуска не создаётся
  • сценарий сильно зависит от того, как ведёт себя выбранный поток

Главная особенность APC:

  • вызов выполнится только тогда, когда поток войдёт в alertable wait

Из этого следуют и практические последствия:

  • APC нельзя считать "мгновенным запуском"
  • строку с путём к DLL нельзя освобождать раньше времени
  • выбирать поток "наугад" часто плохая идея

Этот маршрут нужен не для старта, а для случаев, когда вам действительно нужен запуск через уже существующий поток.

Manual mapping

Если LoadLibraryW вам не подходит, в Memory API есть InjectDllViaManualMapping(...).

Маппинг из файла

csharp
using EyeAuras.Memory;

{
    using var process = LocalProcess.ByProcessName("game");

    var mapping = process.InjectDllViaManualMapping(
        @"C:\Tools\MyHack.dll",
        LocalProcessMmapFlags.None);

    Log.Info($"Base=0x{mapping.BaseAddress.ToInt64():X}");
    Log.Info($"EntryPoint=0x{mapping.EntryPoint.ToInt64():X}");
}

Маппинг из памяти

csharp
using System.IO;
using EyeAuras.Memory;

{
    using var process = LocalProcess.ByProcessName("game");

    var dllBytes = File.ReadAllBytes(@"C:\Tools\MyHack.dll");
    var mapping = process.InjectDllViaManualMapping(dllBytes, LocalProcessMmapFlags.None);
    Log.Info($"Base=0x{mapping.BaseAddress.ToInt64():X}");
}

Уникальные черты этого пути:

  • DLL можно загрузить не только с диска, но и из byte[]
  • маршрут полностью контролируется mapper'ом
  • после загрузки вы получаете BaseAddress и EntryPoint

То есть это уже не "попросить Windows загрузить DLL", а отдельный loader-путь поверх Memory API.

Вход для обоих вариантов — ваша DLL и тестовый процесс, как в разделе InjectDll; примеры запускаются отдельно в хосте EyeAuras с модулем памяти. Перегрузка принимает string либо Memory<byte> и LocalProcessMmapFlags. Есть также вариант с bool erasePE = false; выше флаги указаны явно. В журнале появятся адреса из дескриптора. Обёртка передаёт работу LibraryMapper, внутреннее поведение которого здесь не проверено.

Что возвращается

InjectDllViaManualMapping(...) возвращает ManualMappedLibraryDescriptor:

  • BaseAddress
  • EntryPoint

Это значения адресов, а не владелец ресурса или доказательство завершения инициализации. ManualMappedLibraryDescriptor — структура без Dispose; нельзя вывести из неё протокол безопасной выгрузки.

Флаги mapper'а

  • LocalProcessMmapFlags.None — обычный маршрут
  • LocalProcessMmapFlags.DiscardHeaders — удалить PE-заголовки после маппинга
  • LocalProcessMmapFlags.SkipInitRoutines — не вызывать DllMain и TLS callbacks

Это назначение флагов по декларации. None — нулевое значение; само по себе оно не подтверждает совместимость с любыми импортами, TLS или кодом инициализации. При SkipInitRoutines интеграция должна отдельно определить корректный запуск и завершение инициализации. Упоминание такого сценария в тестовом исходнике не является выполненной проверкой загрузчика.

PIC

PIC это position-independent code, то есть код, который может выполняться не по фиксированному адресу, а из произвольного места в памяти.

В практическом RE-мире это обычно:

  • shellcode
  • маленький bootstrap stub
  • trampoline
  • self-contained payload, который сначала что-то подготавливает, а потом уже вызывает нужную функцию

Это уже самый продвинутый путь в этой статье, потому что здесь вы загружаете не DLL через loader и не готовую Windows API-функцию, а свой собственный исполняемый код.

Для законченного сценария нужен готовый машинный код, его архитектура, ABI, точка входа, аргументы и протокол завершения. Memory API не создаёт этот код. Без этих входов пример запуска был бы неполным.

Последовательность интеграции: выделить буфер, записать именно байты payload, установить подходящую защиту, запустить согласованную точку входа, дождаться завершения по протоколу интеграции и только затем освободить собственные буферы. Это описание границы ответственности, не готовый загрузчик. Байтовая запись подходит для машинного кода; обычные числовые аргументы читайте и пишите через Read<T>/Write<T>.

Здесь Memory API даёт вам базовые кирпичи:

  • AllocateMemory(...)
  • process.Memory.Write(...)
  • ProtectMemory(...)
  • CreateThread(...)

А вся ответственность за сам payload уже на вас:

  • код должен быть реально position-independent
  • адрес входа должен быть корректным
  • calling convention и аргументы должны совпадать
  • страница должна быть executable

Именно поэтому PIC лучше воспринимать не как "ещё один inject-метод", а как отдельный advanced-сценарий запуска своего кода внутри процесса.

На практике

При выборе существующего маршрута учитывайте:

  1. для загрузки собственной DLL в тестовый процесс начните с InjectDll(...)
  2. если нужен более сырой контроль над стартом кода, используйте CreateThread(...)
  3. если нужен запуск через существующий поток, идите в QueueUserApc(...)
  4. если нужен свой loader-путь или DLL из памяти, берите InjectDllViaManualMapping(...)
  5. если вы уже дошли до shellcode и bootstrap payload'ов, тогда имеет смысл идти в PIC

О чём легко забыть

  • x86 и x64 смешивать нельзя
  • InjectDll(...) любит полный и валидный путь на диске
  • CreateThread(...) требует, чтобы startAddress реально указывал на исполняемый код внутри target-процесса
  • QueueUserApc(...) требует корректного потока и корректного момента выполнения
  • manual mapping не делает DLL автоматически совместимой с любым случаем
  • PIC требует максимальной аккуратности, потому что здесь уже почти нет "страховочных" слоёв loader'а
  • чем ниже уровень маршрута, тем больше ответственности вы берёте на себя

Что читать дальше