EasyUniversal / DocsДокументация · SDK 0.3Подписка Universal ↗
Easy Universal / Справочник SDK

Process API 0.2: процессы, запись и профили

AngelScript · native runtime · локальная сборка

Process API 0.2 — процессы, запись, сканирование и профили

Новые разрешения process.handles_threads, process.write, process.scan
требуют manifest "api":"0.2". Старые пакеты 0.1 продолжают работать.
SDK имеет версию 0.3, формат IPC/выдачи остаётся 0.1. Все методы используют
обычные Windows-права для явно выбранного PID. Settings → Read through driver
или --allow-process PID --process-backend driver выбирает чтение через
установленный EasyUniversalBase с READ_INFO/READ. По умолчанию используется
Windows backend. Выбор драйвера выключен изначально, сбрасывается при смене
PID/пакета и фиксируется на время сессии; недоступный драйвер выдаёт ошибку
до вызовов скрипта, без автоматической подмены backend.

Существующие process::read_*, bytebuffer, RIP и pattern reads проходят через
тот же ProcessReader и прежние квоты: 256 операций / 1 MiB на callback,
1–65536 байт на запрос. Неполное чтение очищает весь результат скрипта и
возвращает false/last_error. PEB module reads также используют выбранный режим;
перечисление процессов/модулей system/регионов и отдельная запись остаются
обычными Windows-операциями. Повышение привилегий и переподключение после выхода
процесса не реализованы. Новый SYS собран, но не подписан/не загружен;
реальное чтение ядром пока не подтверждено. Установка драйвера — отдельный этап.

Потоки и handles

array<thread_info>@ process::enumerate_threads(uint pid);
array<handle_info>@ process::enumerate_handles(uint pid);
array<thread_info>@ process::find_orphan_threads(uint pid, const array<module_info> &in modules);

Нужны process.handles_threads и согласие Access selected PID. Потоки:
id, owner_pid (uint), priority (int), start_address (uint64),
start_known (bool). Toolhelp возвращает список; NtQueryInformationThread
запрашивает начальный адрес. Если его получить нельзя, start_known=false;
нулевой адрес не считается orphan. Максимум 2048 потоков, два обхода/callback,
150 мс между вызовами. Orphan — известный старт вне переданных 1–512 диапазонов
модулей; это не доказательство вредоносности. Проверяйте актуальность списка.

Handles: value (uint64), type_index, access, attributes (uint).
NtQuerySystemInformation/SystemExtendedHandleInformation фильтруется по PID.
Ядровые адреса объектов, имена и дублирование handles не предоставляются.
Тип — индекс Windows, не стабильная строка. Лимиты: один обход/callback,
8192 handles целевого процесса, системный буфер до 32 MiB и 50 мс между
вызовами. Внутренние NT-структуры могут меняться между версиями Windows.

Запись — отдельное согласие

bool process::write_enabled();
bool process::write_u8(uint pid, uint64 address, uint8 value);
bool process::write_u16(uint pid, uint64 address, uint16 value);
bool process::write_u32(uint pid, uint64 address, uint value);
bool process::write_u64(uint pid, uint64 address, uint64 value);
bool process::write_f32(uint pid, uint64 address, float value);
bool process::write_f64(uint pid, uint64 address, double value);
bool process::write_bytes(uint pid, uint64 address, const string &in bytes);
bool process::write_protected(uint pid, uint64 address, const string &in bytes, uint new_protect);

Требуется process.write, согласие на PID и дополнительная выключенная по
умолчанию кнопка Allow process writes (0.2). В CLI нужны оба флага:
--allow-process PID --allow-process-write PID. PID должны совпадать.
Смена PID/содержимого пакета сбрасывает согласие. Изменить permissions/API
в работающей сессии через Reload нельзя. Хост открывает отдельный writer handle
с VM_WRITE | VM_OPERATION | QUERY_INFORMATION | SYNCHRONIZE; обычный read handle
не получает права записи. write_enabled() безопасно проверяет возможность.

Запись: 1–4096 байтов/вызов, 64 вызова и 64 KiB/callback; только committed,
не guard и в одном регионе. Простая запись требует writable-страницу.
write_protected ограничен одной страницей; new_protect — PAGE_READWRITE (4)
или PAGE_EXECUTE_READWRITE (0x40). Старые права восстанавливаются и после
неудачного WriteProcessMemory; executable-запись сбрасывает instruction cache.
Нет транзакции: частичная ошибка может оставить изменённые байты, откат значений
не выполняется. Ошибка восстановления защиты обозначается URGENT.
Согласие не делает произвольные адреса корректными: записывайте только проверенные
данные и только в процесс, состояние которого разрешено менять.

Сигнатуры и RIP-relative

uint64 process::find_pattern(uint pid, const string &in module, const string &in pattern, const string &in mask);
array<uint64>@ process::find_all_patterns(uint pid, const string &in module, const string &in pattern, const string &in mask);
uint64 process::resolve_rip_relative(uint pid, uint64 instruction, uint displacement_offset, uint instruction_length);

Нужны process.scan и согласие на PID. pattern — сырые байты AngelScript string,
НЕ строка вида 48 8B ??. mask такой же длины: x точный байт, ? wildcard;
1–4096 байтов, хотя бы один x. Имя модуля или полный путь сравниваются без
учёта ASCII-регистра. Сканируются только readable committed страницы модуля;
нечитаемые разрывы не склеиваются. Обрабатываются совпадения между блоками.
Один pattern scan/callback, модуль до 256 MiB, прочитано до 64 MiB,
50 мс между вызовами, максимум 4096 совпадений. Эти чтения имеют отдельный
scan budget, не 1-MiB лимит обычных read. Нет совпадения: 0/пустой массив и
пустая ошибка; сбой/лимит: тот же результат с last_error, без частичных списков.
Первый метод возвращает первое совпадение. RIP-helper читает signed disp32,
прибавляет к адресу следующей инструкции, проверяет переполнение; длина 1–15,
все четыре байта displacement должны находиться внутри неё. Это не дизассемблер.

JSON-профили и диагностика

bool process::save_profile(uint pid, const string &in filename);
string process::diff_profiles(const string &in filename_a, const string &in filename_b);
string process::diagnostics(uint pid);

Полный профиль требует process_read, process.memory_map,
process.handles_threads и согласие на PID; сохранение дополнительно требует
storage и согласие хранения. Профиль содержит метаданные процесса, регионы,
system/PEB модули, потоки, handles, timestamp и diagnostics. Это снимок доступных
метаданных, НЕ дамп всех байтов и НЕ атомарный снимок работающего процесса.

Файлы только в data/<package-id>/profiles: lowercase имя из букв/цифр/-_
с .json, до 80 символов. Обнаруженные reparse points и выход из каталога
отклоняются; конкурентная подмена файлов другим процессом вне модели runtime.
Существующий файл не перезаписывается: используйте новое имя.
Размер файла/результата diff до 8 MiB, два profile-вызова/callback;
полный снимок также расходует квоты карты/модулей/потоков/handles.

diff_profiles требует только storage: сравнивает ранее сохранённые файлы,
возвращает JSON с same_pid и changes по секциям: added, removed,
changed (before/after), сопоставляя address/base/id/value. PID reuse,
повторное использование handles и меняющиеся данные учитывайте самостоятельно.
diagnostics возвращает JSON-массив, не пишет файл и не требует storage.
Проверки: module_difference, rwx_region, thread_outside_modules,
unrecognized_handle_attributes (неизвестные биты attributes). Нормальная
программа также может иметь RWX или такие потоки: это эвристики, не вердикт.
Имена типов handles, их object names и универсальная классификация
«нестандартного handle» в этой версии отсутствуют.

Ошибки native операций: false/0/пустой результат и process::last_error();
отсутствующая permission/базовое согласие — исключение permission denied.
Проверяйте ошибку сразу. Native toolkit время учитывается отдельно от 20-мс
бюджета скрипта; квоты и внешний watchdog worker сохраняются. Windows вызовы
не прерываются посередине. Native x64 — проверяемая целевая архитектура
для модулей/сканирования/полных профилей; WOW64-модули не поддерживаются.

Карта памяти (RC20, API 0.1 совместим)

array<memory_region>@ process::enumerate_memory_regions(uint pid);

Метод выполняет VirtualQueryEx в изолированном worker-процессе по унаследованному дескриптору выбранного процесса. Он возвращает карту адресного пространства, а не содержимое памяти. Сам этот метод не записывает память и не открывает другой PID.

Разрешение и запуск

Добавьте "process.memory_map" в permissions файла package.json. Выберите PID в клиенте и явно включите Map selected PID (либо Read + map selected PID, если пакет также объявляет process_read). По умолчанию согласие выключено; смена PID сбрасывает его. В CLI эквивалент — run <package> --allow-process <PID>.

Разрешение независимо от process_read. Пакет только с process.memory_map получает PROCESS_QUERY_INFORMATION | SYNCHRONIZE, без PROCESS_VM_READ. Для чтения байтов дополнительно нужны process_read и соответствующее согласие. Windows может отклонить доступ; API не обходит ограничения защищённых процессов.

Переданный pid должен точно совпадать с согласованным PID. process::attached(), alive(), pid(), name() и last_error() доступны для обоих разрешений. Изменение разрешений при Reload запрещено: необходима новая сессия.

Структура memory_region

ПолеТипЗначение
addressuint64Начало региона
sizeuint64Размер в байтах
stateuintWindows: commit `0x1000`, reserve `0x2000`, free `0x10000`
typeuintWindows: image `0x1000000`, mapped `0x40000`, private `0x20000`; у free — `0`
protectionuintНеизменённые флаги `MEMORY_BASIC_INFORMATION.Protect`
readable / writable / executableboolЭффективные R/W/X для committed-региона
guardboolУстановлен PAGE_GUARD

Для free/reserve, PAGE_NOACCESS и guard-страниц эффективные R/W/X выключены. Write-copy считается доступной для записи. Для PAGE_EXECUTE включён X, но не R. Поле protection сохраняет исходные флаги, включая guard и модификаторы кеширования. Поля описывают страницы целевого процесса, а не права скрипта: writable не даёт API записи.

Пример

void on_load() {
    if (!process::attached()) return;
    array<memory_region>@ regions = process::enumerate_memory_regions(process::pid());
    if (process::last_error().length() != 0) {
        runtime::log(process::last_error());
        return;
    }
    runtime::log("Regions: " + regions.length());
    for (uint i = 0; i < regions.length(); ++i) {
        if (regions[i].state == 0x1000 && regions[i].readable)
            runtime::log("Readable: " + regions[i].address + " / " + regions[i].size);
        if (i >= 8) break;
    }
}

Ограничения и ошибки

Не более одного сканирования за callback, 16 384 регионов и 10 мс на сканирование. Вызов входит в общий лимит операций Process API. При ошибке PID, завершении процесса, ошибке Windows или превышении лимита возвращается пустой массив и устанавливается process::last_error(); частичная карта не выдаётся. Без объявленного разрешения или согласия возникает исключение permission denied. При успехе last_error() очищается.

Результат — самостоятельный снимок, который можно сохранить в скрипте. Изменение его полей не влияет на процесс. Карта не атомарна: другой поток целевого процесса может изменить регионы во время обхода. Для обновления запускайте новый запрос; не сканируйте карту каждый кадр без необходимости. API рассчитан на Windows x64, включая согласованные WOW64-процессы в пределах их доступного адресного пространства.

RC21 — модули и сравнение источников

array<module_info>@ process::enumerate_modules(uint pid, const string &in source);
array<module_difference>@ process::diff_modules(uint pid);

Оба метода требуют process_read и явного согласия на тот же PID. Одного
process.memory_map недостаточно: PEB и метаданные модулей требуют чтения.
Хост запрашивает PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | SYNCHRONIZE.
Если полные query-права недоступны и пакет не требует карты памяти, хост может
сохранить старый доступ с QUERY_LIMITED_INFORMATION для обычных чтений; RC21
в этом случае может вернуть ошибку Windows. Скрипт не открывает новый процесс.

source принимает только точные строки system и peb:

  • system: EnumProcessModulesEx (LIST_MODULES_64BIT), GetModuleInformation,

GetModuleFileNameExW. Не закрывает возвращённые HMODULE — это не owned handles.

  • peb: динамически полученный NtQueryInformationProcess, ProcessBasicInformation,

затем ReadProcessMemory для PEB, Ldr, InMemoryOrderModuleList, префикса
LDR_DATA_TABLE_ENTRY и строки FullDllName. Проверяет диапазоны, циклы,
обратные связи и голову списка после обхода. Не изменяет список загрузчика.

RC21 поддерживает только native x64 процессы из x64 runtime. WOW64 отклоняется
с явной ошибкой: native и 32-битный loader domains нельзя считать одним списком.
PEB/Ldr — внутренние структуры Windows; текущие x64 offsets проверены при
сборке относительно общих полей winternl, но совместимость будущих версий
Windows не гарантируется. Подробнее: [NtQueryInformationProcess](https://learn.microsoft.com/en-us/windows/win32/api/winte…),
[PEB_LDR_DATA](https://learn.microsoft.com/en-us/windows/win32/api/winte…).

Результаты

module_info — самостоятельная value-структура:

ПолеТипЗначение
baseuint64Базовый адрес модуля
sizeuint64SizeOfImage, байты
pathstringПолный путь UTF-8

Массив упорядочен по base; содержит максимум 512 модулей. Пути ограничены
4095 UTF-16 символами. Лимит одного обхода — 10 мс между native вызовами,
не более четырёх обходов за callback. Каждый обход входит в общий лимит
256 Process-операций; чтения PEB расходуют общий byte budget 1 MiB/callback.
diff_modules расходует два обхода. Native Windows-вызовы не прерываются
этим таймером; остаётся внешний watchdog worker.

module_difference содержит:

ПолеТипЗначение
baseuint64Адрес, по которому сопоставлялись записи
kindstringmissing_system / missing_peb / metadata_mismatch
system_size / peb_sizeuint64Размеры в соответствующих списках
system_path / peb_pathstringПути; пустая строка, если записи нет

Размер сравнивается точно; пути — без учёта регистра через CompareStringOrdinal
после нормализации / в \. Символьные ссылки, device paths и альтернативные
имена файла не разрешаются в канонический путь. Пустой diff при пустом
last_error означает отсутствие расхождений, а не ошибку.

Любая ошибка возвращает пустой массив и process::last_error(); частичные
списки не выдаются. Отсутствие разрешения/согласия вызывает permission denied.
Проверяйте last_error сразу после каждого вызова: следующий успешный вызов
очищает ошибку. Изменения loader list во время запроса могут приводить к ошибкам
или расхождениям — это не доказательство вмешательства. Источник system также
опирается на состояние loader и не является независимым доверенным оракулом;
см. [ограничения EnumProcessModulesEx](https://learn.microsoft.com/en-us/windows/win32/api/psapi…).
LOAD_LIBRARY_AS_DATAFILE и произвольные private mappings не считаются гарантированно
перечисляемыми модулями. Методы не ищут PE во всей памяти и не обходят защиту.

void on_load() {
    if (!process::attached()) return;
    uint pid = process::pid();
    array<module_info>@ modules = process::enumerate_modules(pid, "system");
    if (process::last_error().length() != 0) {
        runtime::log(process::last_error()); return;
    }
    if (modules.length() > 0)
        runtime::log(modules[0].path + " / " + modules[0].base + " / " + modules[0].size);
    array<module_difference>@ differences = process::diff_modules(pid);
    if (process::last_error().length() != 0) {
        runtime::log(process::last_error()); return;
    }
    runtime::log("Differences: " + differences.length());
}
Easy Universal — отдельный проект. Easy External не изменён.