PE basics
Базовые PE-понятия в контексте Memory API и практической reverse engineering работы
PE basics для Memory API
PE это формат .exe и .dll в Windows. В контексте Memory API это важно потому, что большая часть удобной работы идёт не с "произвольным адресом", а с конкретным модулем: его секциями, импортами, экспортами и RVA. Для внешнего контекста полезны Game Fundamentals, DLL Injector и PE/COFF Spec.
Минимум терминов
- модуль — загруженный
.exeили.dll RVA— адрес относительно базы модуляVA— абсолютный адрес в памяти процесса- секция — кусок PE-образа вроде
.text,.rdata,.data - экспорт — то, что модуль отдаёт наружу
- импорт — то, что модуль подтягивает из других DLL
Если база модуля 0x7FF700000000, а RVA равен 0x1234, то VA будет BaseAddress + 0x1234.
Главное правило
Примеры с LocalProcess рассчитаны на верхний уровень C#-скрипта в запущенном EyeAuras с загруженным модулем памяти. Log даёт хост скрипта. В обычном C#-классе передайте логгер и процесс явно. Одна ссылка на SDK не запускает модуль: во встраиваемом хосте сначала нужны запуск приложения, UseMemory() и ожидание EnsureReadyAsync(); подробнее в Embedded SDK.
LocalProcess.ByProcessId(...) возвращает новый ILocalProcess, который освобождает создавший его код. process.Memory используется вместе с этим reader'ом; отдельно закрывать заимствованный сервис хоста не нужно. Эти примеры не проверяют доступность нативных компонентов вашей сборки.
Нормальный маршрут почти всегда такой:
- открыть процесс
- взять модуль
- работать уже внутри этого модуля
Для этого и нужен MemoryOfModule(...):
using System.Diagnostics;
using System.Linq;
using EyeAuras.Memory;
using EyeAuras.Memory.Scaffolding;
{
using var process = LocalProcess.ByProcessId(Process.GetCurrentProcess().Id);
using var kernel32 = process.MemoryOfModule("kernel32.dll");
Log.Info($"Base: 0x{kernel32.BaseAddress:X}, размер: {kernel32.MemorySize}");
}
Внутри блока kernel32 описывает базу и размер загруженного PE-образа. Это не копия DLL и не проверка границ при каждом чтении. Прямые адреса у этого backend абсолютные; процесс держим открытым до конца работы с представлением.
Что умеет Memory API
Для PE-практики чаще всего нужны:
ReadSections()ReadExports()ReadImports()
Чтобы увидеть результат, выполните следующий отдельный скрипт в EyeAuras с модулем памяти. Вход — текущий процесс и его загруженный kernel32.dll; внешние адреса и файлы не нужны.
using System.Diagnostics;
using EyeAuras.Memory;
using EyeAuras.Memory.Scaffolding;
{
using var process = LocalProcess.ByProcessId(Process.GetCurrentProcess().Id);
using var module = process.MemoryOfModule("kernel32.dll");
foreach (var section in module.ReadSections())
{
Log.Info($"{section.Name}: RVA=0x{section.VirtualAddress:X}, " +
$"VA=0x{section.StartVA:X}, end=0x{section.EndVA:X}");
}
foreach (var export in module.ReadExports())
{
if (export.IsForwarder)
Log.Info($"{export.ExportName}: forwarder={export.ForwarderName}");
else
Log.Info($"{export.ExportName}: ordinal={export.Ordinal}, " +
$"VA=0x{module.BaseAddress + (long)export.RVA:X}");
}
var imports = module.ReadImports();
Log.Info($"Imports={imports.Count}");
foreach (var import in imports)
{
Log.Info($"{import.DllName}!{import.FunctionName}: " +
$"IAT slot VA=0x{module.BaseAddress + (long)import.IatEntryRva:X}");
}
}
Журнал покажет секции с RVA и VA, экспорты и записи импортов. Списки могут быть пустыми. Имена ordinal-only экспортов могут отсутствовать; forwarded export указывает на строку перенаправления, поэтому BaseAddress + RVA для него нельзя считать адресом исполняемой функции. IatEntryRva у импорта — место ячейки IAT, а не адрес импортированной функции. Импорт по ordinal не обязан иметь FunctionName.
Это синхронное чтение живого образа без токена отмены и без гарантии неизменности процесса. PE-helper'ы могут бросить исключение при недоступных данных или неподдерживаемом заголовке. Они не выполняют вызов функции и не меняют IAT.
Этого обычно хватает, чтобы:
- понять layout модуля
- найти публичную функцию
- посмотреть внешние зависимости
- подготовить
IAT-patching,pattern scanningилиcode cave
RVA и смещение в файле
ReadSections, ReadExports и ReadImports работают с загруженным образом по IMemory.BaseAddress. Нельзя просто передать им массив байтов DLL с диска и считать его тем же layout: загрузчик размещает секции по RVA, а файл хранит их по смещениям raw data.
К примеру, секция начинается с RVA 0x1000, её PointerToRawData в файле равен 0x400. Для RVA 0x1234:
- в загруженном модуле адрес будет
BaseAddress + 0x1234 - в файле байт будет по
0x400 + (0x1234 - 0x1000) = 0x634, если он входит в raw data секции
Виртуальный хвост секции может вообще не иметь байтов в файле. VirtualSize и RawSize различаются; у ModuleSectionEntry нет PointerToRawData, поэтому это пояснение формата, а не готовый файловый конвертер. Для разбора файла нужен PE-reader с таблицей секций. Ручное декодирование байтов здесь уместно именно как разбор формата, но для загруженного модуля уже есть перечисленные helper'ы.
В этой версии StartVA/EndVA у секции — абсолютные адреса, верхняя граница исключающая; вычисляемая длина использует максимум VirtualSize и RawSize. Не принимайте её за доказательство, что все страницы доступны для чтения. Размер ячеек import thunk определяется PE32/PE32+, а не размером IntPtr вашего helper-класса.
Где это пригодится
PE-база нужна почти везде, где вы хотите уйти от мышления "вот случайный адрес" к более устойчивой модели:
- модуль
- секция
- экспорт
- импорт
RVA
Именно эта модель потом лучше всего стыкуется с сигнатурами, инжектом и patching-сценариями.
Контракты на этой странице сверены по EyeAuras 20fd3d4; это чтение исходников, без запуска примеров. Полные сигнатуры и ограничения своей версии проверяйте по карте API памяти и SDK/XML reference.