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
Это самый простой маршрут.
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 имеет два варианта:
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(...).
Маппинг из файла
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}");
}
Маппинг из памяти
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:
BaseAddressEntryPoint
Это значения адресов, а не владелец ресурса или доказательство завершения инициализации. 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-сценарий запуска своего кода внутри процесса.
На практике
При выборе существующего маршрута учитывайте:
- для загрузки собственной DLL в тестовый процесс начните с
InjectDll(...) - если нужен более сырой контроль над стартом кода, используйте
CreateThread(...) - если нужен запуск через существующий поток, идите в
QueueUserApc(...) - если нужен свой loader-путь или DLL из памяти, берите
InjectDllViaManualMapping(...) - если вы уже дошли до shellcode и bootstrap payload'ов, тогда имеет смысл идти в
PIC
О чём легко забыть
x86иx64смешивать нельзяInjectDll(...)любит полный и валидный путь на дискеCreateThread(...)требует, чтобыstartAddressреально указывал на исполняемый код внутри target-процессаQueueUserApc(...)требует корректного потока и корректного момента выполненияmanual mappingне делает DLL автоматически совместимой с любым случаемPICтребует максимальной аккуратности, потому что здесь уже почти нет "страховочных" слоёв loader'а- чем ниже уровень маршрута, тем больше ответственности вы берёте на себя